From 0e4bb4513199466761f422edba899f70a202fb92 Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Wed, 7 Feb 2024 12:36:52 +0100 Subject: [PATCH] refactor: Use dataclasses for all SQLAlchemy database classes With dataclasses, a `__init__` function is created for all database classes. This adds autocomplete and code recommendations and type checkers like mypy can check the passed types. Since the `__hash__` method was removed during this change, the intersection method to get common projects doesn't work anymore. I changed it to a proper database join, which is faster. In addition, many type annotations were added. Some mypy and pylint errors and warnings were fixed in the tests. --- .../capellacollab/core/database/__init__.py | 2 +- .../capellacollab/core/database/migration.py | 34 +-- backend/capellacollab/events/crud.py | 8 +- backend/capellacollab/events/models.py | 41 ++-- backend/capellacollab/health/routes.py | 2 +- backend/capellacollab/notices/models.py | 4 +- backend/capellacollab/projects/crud.py | 30 +++ backend/capellacollab/projects/models.py | 21 +- backend/capellacollab/projects/routes.py | 1 - .../projects/toolmodels/backups/crud.py | 4 +- .../toolmodels/backups/injectables.py | 2 +- .../projects/toolmodels/backups/models.py | 15 +- .../projects/toolmodels/backups/routes.py | 4 +- .../toolmodels/backups/runs/models.py | 17 +- .../projects/toolmodels/backups/validation.py | 2 +- .../capellacollab/projects/toolmodels/crud.py | 61 ++--- .../toolmodels/diagrams/validation.py | 2 +- .../projects/toolmodels/injectables.py | 2 +- .../toolmodels/modelbadge/validation.py | 2 +- .../projects/toolmodels/models.py | 37 +-- .../toolmodels/modelsources/git/crud.py | 4 +- .../modelsources/git/injectables.py | 4 +- .../toolmodels/modelsources/git/models.py | 14 +- .../toolmodels/modelsources/git/routes.py | 4 +- .../toolmodels/modelsources/git/validation.py | 4 +- .../toolmodels/modelsources/t4c/crud.py | 4 +- .../modelsources/t4c/injectables.py | 2 +- .../toolmodels/modelsources/t4c/models.py | 12 +- .../toolmodels/modelsources/t4c/routes.py | 4 +- .../toolmodels/restrictions/injectables.py | 8 +- .../toolmodels/restrictions/models.py | 10 +- .../toolmodels/restrictions/routes.py | 8 +- .../projects/toolmodels/routes.py | 27 +-- .../projects/toolmodels/validation.py | 2 +- .../projects/toolmodels/workspace.py | 2 +- .../capellacollab/projects/users/models.py | 4 +- .../capellacollab/sessions/hooks/jupyter.py | 4 +- .../sessions/hooks/pure_variants.py | 2 +- backend/capellacollab/sessions/models.py | 31 ++- .../capellacollab/sessions/operators/k8s.py | 26 ++- backend/capellacollab/sessions/routes.py | 64 ++++-- .../settings/configuration/models.py | 2 +- .../integrations/purevariants/models.py | 4 +- .../settings/modelsources/git/models.py | 2 +- .../settings/modelsources/t4c/models.py | 33 +-- .../modelsources/t4c/repositories/crud.py | 8 +- .../modelsources/t4c/repositories/models.py | 12 +- .../settings/modelsources/t4c/routes.py | 5 +- backend/capellacollab/tools/crud.py | 10 +- .../tools/integrations/models.py | 6 +- .../tools/integrations/routes.py | 1 + backend/capellacollab/tools/models.py | 35 +-- backend/capellacollab/tools/routes.py | 6 +- backend/capellacollab/users/models.py | 25 ++- backend/capellacollab/users/routes.py | 34 +-- backend/capellacollab/users/tokens/crud.py | 7 +- backend/capellacollab/users/tokens/models.py | 6 +- backend/capellacollab/users/tokens/routes.py | 2 +- backend/pyproject.toml | 3 +- backend/tests/conftest.py | 14 +- backend/tests/projects/toolmodels/conftest.py | 6 +- .../toolmodels/test_toolmodel_routes.py | 4 +- .../k8s_operator/test_session_k8s_operator.py | 2 +- backend/tests/sessions/test_session_hooks.py | 93 +++++--- .../tests/sessions/test_sessions_routes.py | 211 ++++++++++-------- backend/tests/settings/conftest.py | 2 +- 66 files changed, 635 insertions(+), 429 deletions(-) diff --git a/backend/capellacollab/core/database/__init__.py b/backend/capellacollab/core/database/__init__.py index 12fdc4be6..581e427b2 100644 --- a/backend/capellacollab/core/database/__init__.py +++ b/backend/capellacollab/core/database/__init__.py @@ -17,7 +17,7 @@ SessionLocal = orm.sessionmaker(autocommit=False, autoflush=False, bind=engine) -class Base(orm.DeclarativeBase): +class Base(orm.MappedAsDataclass, orm.DeclarativeBase): type_annotation_map = { dict[str, str]: postgresql.JSONB, dict[str, t.Any]: postgresql.JSONB, diff --git a/backend/capellacollab/core/database/migration.py b/backend/capellacollab/core/database/migration.py index 66ddbc149..cf78b3f74 100644 --- a/backend/capellacollab/core/database/migration.py +++ b/backend/capellacollab/core/database/migration.py @@ -84,7 +84,7 @@ def migrate_db(engine, database_url: str): create_coffee_machine_model(session) -def initialize_admin_user(db): +def initialize_admin_user(db: orm.Session): LOGGER.info("Initialized adminuser %s", config["initial"]["admin"]) admin_user = users_crud.create_user( db=db, @@ -94,7 +94,7 @@ def initialize_admin_user(db): events_crud.create_user_creation_event(db, admin_user) -def initialize_default_project(db): +def initialize_default_project(db: orm.Session): LOGGER.info("Initialized project 'default'") projects_crud.create_project( db=db, @@ -104,7 +104,7 @@ def initialize_default_project(db): ) -def initialize_coffee_machine_project(db): +def initialize_coffee_machine_project(db: orm.Session): LOGGER.info("Initialize project 'Coffee Machine'") projects_crud.create_project( db=db, @@ -114,7 +114,7 @@ def initialize_coffee_machine_project(db): ) -def create_tools(db): +def create_tools(db: orm.Session): LOGGER.info("Initialized tools") registry = config["docker"]["registry"] if os.getenv("DEVELOPMENT_MODE", "").lower() in ("1", "true", "t"): @@ -131,12 +131,12 @@ def create_tools(db): ) tools_crud.create_tool(db, papyrus) - tools_crud.create_version(db, papyrus.id, "6.1") - tools_crud.create_version(db, papyrus.id, "6.0") + tools_crud.create_version(db, papyrus, "6.1") + tools_crud.create_version(db, papyrus, "6.0") - tools_crud.create_nature(db, papyrus.id, "UML 2.5") - tools_crud.create_nature(db, papyrus.id, "SysML 1.4") - tools_crud.create_nature(db, papyrus.id, "SysML 1.1") + tools_crud.create_nature(db, papyrus, "UML 2.5") + tools_crud.create_nature(db, papyrus, "SysML 1.4") + tools_crud.create_nature(db, papyrus, "SysML 1.1") else: # Use public Github images per default @@ -154,21 +154,22 @@ def create_tools(db): docker_image_template=f"{registry}/jupyter-notebook:$version", ) tools_crud.create_tool(db, jupyter) + assert jupyter.integrations integrations_crud.update_integrations( db, jupyter.integrations, integrations_models.PatchToolIntegrations(jupyter=True), ) - default_version = tools_crud.create_version(db, capella.id, "6.0.0", True) - tools_crud.create_version(db, capella.id, "5.2.0") - tools_crud.create_version(db, capella.id, "5.0.0") + default_version = tools_crud.create_version(db, capella, "6.0.0", True) + tools_crud.create_version(db, capella, "5.2.0") + tools_crud.create_version(db, capella, "5.0.0") - tools_crud.create_version(db, jupyter.id, "python-3.11") - tools_crud.create_nature(db, jupyter.id, "notebooks") + tools_crud.create_version(db, jupyter, "python-3.11") + tools_crud.create_nature(db, jupyter, "notebooks") - default_nature = tools_crud.create_nature(db, capella.id, "model") - tools_crud.create_nature(db, capella.id, "library") + default_nature = tools_crud.create_nature(db, capella, "model") + tools_crud.create_nature(db, capella, "library") for model in toolmodels_crud.get_models(db): toolmodels_crud.set_tool_for_model(db, model, capella) @@ -184,6 +185,7 @@ def create_t4c_instance_and_repositories(db): version = tools_crud.get_version_by_tool_id_version_name( db, tool.id, "5.2.0" ) + assert version default_instance = settings_t4c_models.DatabaseT4CInstance( name="default", license="placeholder", diff --git a/backend/capellacollab/events/crud.py b/backend/capellacollab/events/crud.py index b63df31c2..12954b949 100644 --- a/backend/capellacollab/events/crud.py +++ b/backend/capellacollab/events/crud.py @@ -26,14 +26,16 @@ def create_event( raise ValueError( f"Event type must of one of the following: {allowed_types}" ) + event = models.DatabaseUserHistoryEvent( - user_id=user.id, + user=user, event_type=event_type, execution_time=datetime.datetime.now(datetime.UTC), - executor_id=executor.id if executor else None, - project_id=project.id if project else None, + executor=executor, + project=project, reason=reason, ) + db.add(event) db.commit() diff --git a/backend/capellacollab/events/models.py b/backend/capellacollab/events/models.py index b74186bfe..23b91da1a 100644 --- a/backend/capellacollab/events/models.py +++ b/backend/capellacollab/events/models.py @@ -3,7 +3,6 @@ import datetime import enum -import typing as t import pydantic import sqlalchemy as sa @@ -14,10 +13,6 @@ from capellacollab.projects import models as projects_models from capellacollab.users import models as users_models -if t.TYPE_CHECKING: - from capellacollab.projects.models import DatabaseProject - from capellacollab.users.models import DatabaseUser - class EventType(enum.Enum): CREATED_USER = "CreatedUser" @@ -55,27 +50,37 @@ class HistoryEvent(BaseHistoryEvent): class DatabaseUserHistoryEvent(database.Base): __tablename__ = "user_history_events" - id: orm.Mapped[int] = orm.mapped_column(primary_key=True, index=True) + id: orm.Mapped[int] = orm.mapped_column( + init=False, primary_key=True, index=True + ) - user_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("users.id")) - user: orm.Mapped["DatabaseUser"] = orm.relationship( + user_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("users.id"), + init=False, + ) + user: orm.Mapped[users_models.DatabaseUser] = orm.relationship( back_populates="events", foreign_keys=[user_id] ) + event_type: orm.Mapped[EventType] + reason: orm.Mapped[str | None] = orm.mapped_column(default=None) + executor_id: orm.Mapped[int | None] = orm.mapped_column( - sa.ForeignKey("users.id") + sa.ForeignKey("users.id"), + init=False, ) - executor: orm.Mapped["DatabaseUser"] = orm.relationship( - foreign_keys=[executor_id] + executor: orm.Mapped[users_models.DatabaseUser | None] = orm.relationship( + default=None, foreign_keys=[executor_id] ) project_id: orm.Mapped[int | None] = orm.mapped_column( - sa.ForeignKey("projects.id") - ) - project: orm.Mapped["DatabaseProject"] = orm.relationship( - foreign_keys=[project_id] + sa.ForeignKey("projects.id"), + init=False, ) + project: orm.Mapped[ + projects_models.DatabaseProject | None + ] = orm.relationship(default=None, foreign_keys=[project_id]) - execution_time: orm.Mapped[datetime.datetime] - event_type: orm.Mapped[EventType] - reason: orm.Mapped[str | None] + execution_time: orm.Mapped[datetime.datetime] = orm.mapped_column( + default=datetime.datetime.now(datetime.UTC) + ) diff --git a/backend/capellacollab/health/routes.py b/backend/capellacollab/health/routes.py index 84bbc1c1d..070852254 100644 --- a/backend/capellacollab/health/routes.py +++ b/backend/capellacollab/health/routes.py @@ -107,7 +107,7 @@ def project_status(db: orm.Session = fastapi.Depends(database.get_db)): def _create_tool_model_status_tasks( db: orm.Session, logger: logging.LoggerAdapter, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, ) -> models.ToolModelStatusTasks: return models.ToolModelStatusTasks( primary_git_repository_status=asyncio.create_task( diff --git a/backend/capellacollab/notices/models.py b/backend/capellacollab/notices/models.py index ef3800298..e454a21f6 100644 --- a/backend/capellacollab/notices/models.py +++ b/backend/capellacollab/notices/models.py @@ -34,7 +34,9 @@ class NoticeResponse(CreateNoticeRequest): class DatabaseNotice(database.Base): __tablename__ = "notices" - id: orm.Mapped[int] = orm.mapped_column(primary_key=True, index=True) + id: orm.Mapped[int] = orm.mapped_column( + init=False, primary_key=True, index=True + ) title: orm.Mapped[str] message: orm.Mapped[str] level: orm.Mapped[NoticeLevel] diff --git a/backend/capellacollab/projects/crud.py b/backend/capellacollab/projects/crud.py index af1b66d73..526ab95e5 100644 --- a/backend/capellacollab/projects/crud.py +++ b/backend/capellacollab/projects/crud.py @@ -8,6 +8,8 @@ from sqlalchemy import orm from capellacollab.core import database +from capellacollab.projects.users import models as project_users_models +from capellacollab.users import models as users_models from . import models @@ -40,6 +42,34 @@ def get_project_by_slug( ).scalar_one_or_none() +def get_common_projects_for_users( + db: orm.Session, + user1: users_models.DatabaseUser, + user2: users_models.DatabaseUser, +) -> abc.Sequence[models.DatabaseProject]: + user1_table = orm.aliased(project_users_models.ProjectUserAssociation) + user2_table = orm.aliased(project_users_models.ProjectUserAssociation) + + return ( + db.execute( + sa.select(models.DatabaseProject) + .join( + user1_table, + models.DatabaseProject.id == user1_table.project_id, + ) + .join( + user2_table, + models.DatabaseProject.id == user2_table.project_id, + ) + .where(user1_table.user_id == user1.id) + .where(user2_table.user_id == user2.id) + .distinct() + ) + .scalars() + .all() + ) + + def update_project( db: orm.Session, project: models.DatabaseProject, diff --git a/backend/capellacollab/projects/models.py b/backend/capellacollab/projects/models.py index be808655c..26ab9825e 100644 --- a/backend/capellacollab/projects/models.py +++ b/backend/capellacollab/projects/models.py @@ -14,7 +14,7 @@ from capellacollab.projects.users import models as project_users_models if t.TYPE_CHECKING: - from capellacollab.projects.toolmodels.models import DatabaseCapellaModel + from capellacollab.projects.toolmodels.models import DatabaseToolModel from capellacollab.projects.users.models import ProjectUserAssociation @@ -105,20 +105,25 @@ class DatabaseProject(database.Base): __tablename__ = "projects" id: orm.Mapped[int] = orm.mapped_column( - unique=True, primary_key=True, index=True + init=False, unique=True, primary_key=True, index=True ) name: orm.Mapped[str] = orm.mapped_column(unique=True, index=True) slug: orm.Mapped[str] = orm.mapped_column(unique=True, index=True) - description: orm.Mapped[str | None] - visibility: orm.Mapped[Visibility] - type: orm.Mapped[ProjectType] + + description: orm.Mapped[str | None] = orm.mapped_column(default=None) + visibility: orm.Mapped[Visibility] = orm.mapped_column( + default=Visibility.PRIVATE + ) + type: orm.Mapped[ProjectType] = orm.mapped_column( + default=ProjectType.GENERAL + ) users: orm.Mapped[list[ProjectUserAssociation]] = orm.relationship( - back_populates="project" + default_factory=list, back_populates="project" ) - models: orm.Mapped[list[DatabaseCapellaModel]] = orm.relationship( - back_populates="project" + models: orm.Mapped[list[DatabaseToolModel]] = orm.relationship( + default_factory=list, back_populates="project" ) is_archived: orm.Mapped[bool] = orm.mapped_column(default=False) diff --git a/backend/capellacollab/projects/routes.py b/backend/capellacollab/projects/routes.py index 15ed304bf..1dc522273 100644 --- a/backend/capellacollab/projects/routes.py +++ b/backend/capellacollab/projects/routes.py @@ -73,7 +73,6 @@ def get_projects( )(association.project.slug, username, db) ] - log.debug("Fetching the following projects: %s", projects) return projects diff --git a/backend/capellacollab/projects/toolmodels/backups/crud.py b/backend/capellacollab/projects/toolmodels/backups/crud.py index df5673a24..ff08ce3d2 100644 --- a/backend/capellacollab/projects/toolmodels/backups/crud.py +++ b/backend/capellacollab/projects/toolmodels/backups/crud.py @@ -28,7 +28,7 @@ def get_pipeline_by_id( def get_pipelines_for_tool_model( - db: orm.Session, model: toolmodels_models.DatabaseCapellaModel + db: orm.Session, model: toolmodels_models.DatabaseToolModel ) -> abc.Sequence[models.DatabaseBackup]: return ( db.execute( @@ -42,7 +42,7 @@ def get_pipelines_for_tool_model( def get_first_pipeline_for_tool_model( - db: orm.Session, model: toolmodels_models.DatabaseCapellaModel + db: orm.Session, model: toolmodels_models.DatabaseToolModel ) -> models.DatabaseBackup | None: return ( db.execute( diff --git a/backend/capellacollab/projects/toolmodels/backups/injectables.py b/backend/capellacollab/projects/toolmodels/backups/injectables.py index bd0511d98..6fc9a6098 100644 --- a/backend/capellacollab/projects/toolmodels/backups/injectables.py +++ b/backend/capellacollab/projects/toolmodels/backups/injectables.py @@ -16,7 +16,7 @@ def get_existing_pipeline( pipeline_id: int, - model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), diff --git a/backend/capellacollab/projects/toolmodels/backups/models.py b/backend/capellacollab/projects/toolmodels/backups/models.py index 356010a27..4de74fbad 100644 --- a/backend/capellacollab/projects/toolmodels/backups/models.py +++ b/backend/capellacollab/projects/toolmodels/backups/models.py @@ -16,7 +16,7 @@ ) if t.TYPE_CHECKING: - from capellacollab.projects.toolmodels.models import DatabaseCapellaModel + from capellacollab.projects.toolmodels.models import DatabaseToolModel from capellacollab.projects.toolmodels.modelsources.git.models import ( DatabaseGitModel, ) @@ -51,7 +51,7 @@ class Backup(pydantic.BaseModel): class DatabaseBackup(database.Base): __tablename__ = "backups" id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, autoincrement=True + init=False, primary_key=True, index=True, autoincrement=True ) created_by: orm.Mapped[str] @@ -64,17 +64,19 @@ class DatabaseBackup(database.Base): run_nightly: orm.Mapped[bool] git_model_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("git_models.id") + sa.ForeignKey("git_models.id"), init=False ) git_model: orm.Mapped["DatabaseGitModel"] = orm.relationship() t4c_model_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("t4c_models.id") + sa.ForeignKey("t4c_models.id"), init=False ) t4c_model: orm.Mapped["DatabaseT4CModel"] = orm.relationship() - model_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("models.id")) - model: orm.Mapped["DatabaseCapellaModel"] = orm.relationship() + model_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("models.id"), init=False + ) + model: orm.Mapped["DatabaseToolModel"] = orm.relationship() runs: orm.Mapped[ list["runs_models.DatabasePipelineRun"] @@ -82,4 +84,5 @@ class DatabaseBackup(database.Base): "DatabasePipelineRun", back_populates="pipeline", cascade="all, delete-orphan", + default_factory=list, ) diff --git a/backend/capellacollab/projects/toolmodels/backups/routes.py b/backend/capellacollab/projects/toolmodels/backups/routes.py index 5a2e2daf3..08aff2218 100644 --- a/backend/capellacollab/projects/toolmodels/backups/routes.py +++ b/backend/capellacollab/projects/toolmodels/backups/routes.py @@ -44,7 +44,7 @@ @router.get("", response_model=list[models.Backup]) def get_pipelines( - model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), @@ -67,7 +67,7 @@ def get_pipeline( @router.post("", response_model=models.Backup) def create_backup( body: models.CreateBackup, - capella_model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + capella_model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), diff --git a/backend/capellacollab/projects/toolmodels/backups/runs/models.py b/backend/capellacollab/projects/toolmodels/backups/runs/models.py index 21204aa43..0c9aad091 100644 --- a/backend/capellacollab/projects/toolmodels/backups/runs/models.py +++ b/backend/capellacollab/projects/toolmodels/backups/runs/models.py @@ -28,32 +28,37 @@ class PipelineRunStatus(enum.Enum): class DatabasePipelineRun(Base): __tablename__ = "pipeline_run" id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, autoincrement=True + init=False, primary_key=True, index=True, autoincrement=True ) - reference_id: orm.Mapped[str | None] status: orm.Mapped[PipelineRunStatus] pipeline_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("backups.id") + sa.ForeignKey("backups.id"), init=False ) pipeline: orm.Mapped[pipeline_models.DatabaseBackup] = orm.relationship( pipeline_models.DatabaseBackup ) triggerer_id: orm.Mapped[str] = orm.mapped_column( - sa.ForeignKey("users.id") + sa.ForeignKey("users.id"), init=False ) triggerer: orm.Mapped[users_models.DatabaseUser] = orm.relationship( users_models.DatabaseUser ) trigger_time: orm.Mapped[datetime.datetime] - end_time: orm.Mapped[datetime.datetime | None] - logs_last_fetched_timestamp: orm.Mapped[datetime.datetime | None] environment: orm.Mapped[dict[str, str]] + reference_id: orm.Mapped[str | None] = orm.mapped_column(default=None) + end_time: orm.Mapped[datetime.datetime | None] = orm.mapped_column( + default=None + ) + logs_last_fetched_timestamp: orm.Mapped[ + datetime.datetime | None + ] = orm.mapped_column(default=None) + class PipelineRun(pydantic.BaseModel): model_config = pydantic.ConfigDict(from_attributes=True) diff --git a/backend/capellacollab/projects/toolmodels/backups/validation.py b/backend/capellacollab/projects/toolmodels/backups/validation.py index 63838302e..d537feabd 100644 --- a/backend/capellacollab/projects/toolmodels/backups/validation.py +++ b/backend/capellacollab/projects/toolmodels/backups/validation.py @@ -10,7 +10,7 @@ def check_last_pipeline_run_status( - db: orm.Session, model: toolmodel_models.DatabaseCapellaModel + db: orm.Session, model: toolmodel_models.DatabaseToolModel ) -> runs_models.PipelineRunStatus | None: if pipeline := crud.get_first_pipeline_for_tool_model(db, model): # Only consider first pipeline for monitoring, usually there is only one pipeline. diff --git a/backend/capellacollab/projects/toolmodels/crud.py b/backend/capellacollab/projects/toolmodels/crud.py index 4c64e8489..8128b645b 100644 --- a/backend/capellacollab/projects/toolmodels/crud.py +++ b/backend/capellacollab/projects/toolmodels/crud.py @@ -14,17 +14,17 @@ from .restrictions import models as restrictions_models -def get_models(db: orm.Session) -> abc.Sequence[models.DatabaseCapellaModel]: - return db.execute(sa.select(models.DatabaseCapellaModel)).scalars().all() +def get_models(db: orm.Session) -> abc.Sequence[models.DatabaseToolModel]: + return db.execute(sa.select(models.DatabaseToolModel)).scalars().all() def get_models_by_version( db: orm.Session, version_id: int -) -> abc.Sequence[models.DatabaseCapellaModel]: +) -> abc.Sequence[models.DatabaseToolModel]: return ( db.execute( - sa.select(models.DatabaseCapellaModel).where( - models.DatabaseCapellaModel.version_id == version_id + sa.select(models.DatabaseToolModel).where( + models.DatabaseToolModel.version_id == version_id ) ) .scalars() @@ -34,11 +34,11 @@ def get_models_by_version( def get_models_by_nature( db: orm.Session, nature_id: int -) -> abc.Sequence[models.DatabaseCapellaModel]: +) -> abc.Sequence[models.DatabaseToolModel]: return ( db.execute( - sa.select(models.DatabaseCapellaModel).where( - models.DatabaseCapellaModel.nature_id == nature_id + sa.select(models.DatabaseToolModel).where( + models.DatabaseToolModel.nature_id == nature_id ) ) .scalars() @@ -48,11 +48,11 @@ def get_models_by_nature( def get_models_by_tool( db: orm.Session, tool_id: int -) -> abc.Sequence[models.DatabaseCapellaModel]: +) -> abc.Sequence[models.DatabaseToolModel]: return ( db.execute( - sa.select(models.DatabaseCapellaModel).where( - models.DatabaseCapellaModel.tool_id == tool_id + sa.select(models.DatabaseToolModel).where( + models.DatabaseToolModel.tool_id == tool_id ) ) .scalars() @@ -62,16 +62,16 @@ def get_models_by_tool( def get_model_by_slugs( db: orm.Session, project_slug: str, model_slug: str -) -> models.DatabaseCapellaModel | None: +) -> models.DatabaseToolModel | None: return db.execute( - sa.select(models.DatabaseCapellaModel) - .options(orm.joinedload(models.DatabaseCapellaModel.project)) + sa.select(models.DatabaseToolModel) + .options(orm.joinedload(models.DatabaseToolModel.project)) .where( - models.DatabaseCapellaModel.project.has( + models.DatabaseToolModel.project.has( projects_model.DatabaseProject.slug == project_slug ) ) - .where(models.DatabaseCapellaModel.slug == model_slug) + .where(models.DatabaseToolModel.slug == model_slug) ).scalar_one_or_none() @@ -84,10 +84,8 @@ def create_model( nature: tools_models.DatabaseNature | None = None, configuration: dict[str, str] | None = None, display_order: int | None = None, -) -> models.DatabaseCapellaModel: - restrictions = restrictions_models.DatabaseToolModelRestrictions() - - model = models.DatabaseCapellaModel( +) -> models.DatabaseToolModel: + model = models.DatabaseToolModel( name=post_model.name, slug=slugify.slugify(post_model.name), description=post_model.description if post_model.description else "", @@ -95,10 +93,13 @@ def create_model( tool=tool, version=version, nature=nature, - restrictions=restrictions, configuration=configuration, display_order=display_order, ) + + restrictions = restrictions_models.DatabaseToolModelRestrictions( + model=model + ) db.add(restrictions) db.add(model) db.commit() @@ -107,9 +108,9 @@ def create_model( def set_tool_for_model( db: orm.Session, - model: models.DatabaseCapellaModel, + model: models.DatabaseToolModel, tool: tools_models.DatabaseTool, -) -> models.DatabaseCapellaModel: +) -> models.DatabaseToolModel: model.tool = tool db.commit() return model @@ -117,10 +118,10 @@ def set_tool_for_model( def set_tool_details_for_model( db: orm.Session, - model: models.DatabaseCapellaModel, + model: models.DatabaseToolModel, version: tools_models.DatabaseVersion, nature: tools_models.DatabaseNature, -) -> models.DatabaseCapellaModel: +) -> models.DatabaseToolModel: model.version = version model.nature = nature db.commit() @@ -129,14 +130,14 @@ def set_tool_details_for_model( def update_model( db: orm.Session, - model: models.DatabaseCapellaModel, + model: models.DatabaseToolModel, description: str | None, name: str | None, - version: tools_models.DatabaseVersion, - nature: tools_models.DatabaseNature, + version: tools_models.DatabaseVersion | None, + nature: tools_models.DatabaseNature | None, project: projects_model.DatabaseProject, display_order: int | None, -) -> models.DatabaseCapellaModel: +) -> models.DatabaseToolModel: model.version = version model.nature = nature model.project = project @@ -151,6 +152,6 @@ def update_model( return model -def delete_model(db: orm.Session, model: models.DatabaseCapellaModel): +def delete_model(db: orm.Session, model: models.DatabaseToolModel): db.delete(model) db.commit() diff --git a/backend/capellacollab/projects/toolmodels/diagrams/validation.py b/backend/capellacollab/projects/toolmodels/diagrams/validation.py index 576fe75d5..e3bb6ca5a 100644 --- a/backend/capellacollab/projects/toolmodels/diagrams/validation.py +++ b/backend/capellacollab/projects/toolmodels/diagrams/validation.py @@ -12,7 +12,7 @@ async def check_diagram_cache_health( db: orm.Session, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, logger: logging.LoggerAdapter, ) -> git_models.ModelArtifactStatus: return await git_validation.check_pipeline_health( diff --git a/backend/capellacollab/projects/toolmodels/injectables.py b/backend/capellacollab/projects/toolmodels/injectables.py index bcfb49481..2ea3b1db2 100644 --- a/backend/capellacollab/projects/toolmodels/injectables.py +++ b/backend/capellacollab/projects/toolmodels/injectables.py @@ -18,7 +18,7 @@ def get_existing_capella_model( projects_injectables.get_existing_project ), db: orm.Session = fastapi.Depends(database.get_db), -) -> models.DatabaseCapellaModel: +) -> models.DatabaseToolModel: model = crud.get_model_by_slugs(db, project.slug, model_slug) if not model: raise fastapi.HTTPException( diff --git a/backend/capellacollab/projects/toolmodels/modelbadge/validation.py b/backend/capellacollab/projects/toolmodels/modelbadge/validation.py index 776a2bdc8..1ab469366 100644 --- a/backend/capellacollab/projects/toolmodels/modelbadge/validation.py +++ b/backend/capellacollab/projects/toolmodels/modelbadge/validation.py @@ -12,7 +12,7 @@ async def check_model_badge_health( db: orm.Session, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, logger: logging.LoggerAdapter, ) -> git_models.ModelArtifactStatus: return await git_validation.check_pipeline_health( diff --git a/backend/capellacollab/projects/toolmodels/models.py b/backend/capellacollab/projects/toolmodels/models.py index 0ab55fd17..247734839 100644 --- a/backend/capellacollab/projects/toolmodels/models.py +++ b/backend/capellacollab/projects/toolmodels/models.py @@ -62,12 +62,12 @@ class ToolDetails(pydantic.BaseModel): nature_id: int -class DatabaseCapellaModel(database.Base): +class DatabaseToolModel(database.Base): __tablename__ = "models" __table_args__ = (sa.UniqueConstraint("project_id", "slug"),) id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, unique=True + init=False, primary_key=True, index=True, unique=True ) name: orm.Mapped[str] = orm.mapped_column(index=True) @@ -78,36 +78,47 @@ class DatabaseCapellaModel(database.Base): configuration: orm.Mapped[dict[str, str] | None] project_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("projects.id") + sa.ForeignKey("projects.id"), init=False ) project: orm.Mapped[DatabaseProject] = orm.relationship( back_populates="models" ) - tool_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("tools.id")) + tool_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("tools.id"), init=False + ) tool: orm.Mapped[DatabaseTool] = orm.relationship() version_id: orm.Mapped[int | None] = orm.mapped_column( - sa.ForeignKey("versions.id") + sa.ForeignKey("versions.id"), init=False + ) + version: orm.Mapped[DatabaseVersion | None] = orm.relationship( + default=None ) - version: orm.Mapped[DatabaseVersion] = orm.relationship() nature_id: orm.Mapped[int | None] = orm.mapped_column( - sa.ForeignKey("types.id") + sa.ForeignKey("types.id"), init=False ) - nature: orm.Mapped[DatabaseNature] = orm.relationship() + nature: orm.Mapped[DatabaseNature | None] = orm.relationship(default=None) - editing_mode: orm.Mapped[EditingMode | None] + editing_mode: orm.Mapped[EditingMode | None] = orm.mapped_column( + default=None + ) t4c_models: orm.Mapped[list[DatabaseT4CModel]] = orm.relationship( - back_populates="model" + default_factory=list, back_populates="model" ) git_models: orm.Mapped[list[DatabaseGitModel]] = orm.relationship( - back_populates="model" + default_factory=list, back_populates="model" ) - restrictions: orm.Mapped[DatabaseToolModelRestrictions] = orm.relationship( - back_populates="model", uselist=False, cascade="delete" + restrictions: orm.Mapped[ + DatabaseToolModelRestrictions | None + ] = orm.relationship( + back_populates="model", + uselist=False, + cascade="delete", + default=None, ) diff --git a/backend/capellacollab/projects/toolmodels/modelsources/git/crud.py b/backend/capellacollab/projects/toolmodels/modelsources/git/crud.py index ca5bae0da..369641387 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/git/crud.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/git/crud.py @@ -30,13 +30,13 @@ def get_primary_git_model_of_capellamodel( def add_git_model_to_capellamodel( db: orm.Session, - capella_model: toolsmodels_models.DatabaseCapellaModel, + capella_model: toolsmodels_models.DatabaseToolModel, post_git_model: models.PostGitModel, ) -> models.DatabaseGitModel: primary = not get_primary_git_model_of_capellamodel(db, capella_model.id) git_model = models.DatabaseGitModel.from_post_git_model( - capella_model.id, primary, post_git_model + capella_model, primary, post_git_model ) db.add(git_model) diff --git a/backend/capellacollab/projects/toolmodels/modelsources/git/injectables.py b/backend/capellacollab/projects/toolmodels/modelsources/git/injectables.py index cda2be3d7..aa2608be8 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/git/injectables.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/git/injectables.py @@ -20,7 +20,7 @@ def get_existing_git_model( git_model_id: int, - capella_model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + capella_model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), @@ -39,7 +39,7 @@ def get_existing_git_model( def get_existing_primary_git_model( - capella_model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + capella_model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), diff --git a/backend/capellacollab/projects/toolmodels/modelsources/git/models.py b/backend/capellacollab/projects/toolmodels/modelsources/git/models.py index c8bc69488..f72eb7165 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/git/models.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/git/models.py @@ -12,7 +12,7 @@ from capellacollab.core import database if t.TYPE_CHECKING: - from capellacollab.projects.toolmodels.models import DatabaseCapellaModel + from capellacollab.projects.toolmodels.models import DatabaseToolModel class PostGitModel(pydantic.BaseModel): @@ -43,7 +43,7 @@ class DatabaseGitModel(database.Base): __tablename__ = "git_models" id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, autoincrement=True + init=False, primary_key=True, index=True, autoincrement=True ) name: orm.Mapped[str] path: orm.Mapped[str] @@ -51,8 +51,10 @@ class DatabaseGitModel(database.Base): revision: orm.Mapped[str] primary: orm.Mapped[bool] - model_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("models.id")) - model: orm.Mapped["DatabaseCapellaModel"] = orm.relationship( + model_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("models.id"), init=False + ) + model: orm.Mapped["DatabaseToolModel"] = orm.relationship( back_populates="git_models" ) @@ -61,12 +63,12 @@ class DatabaseGitModel(database.Base): @classmethod def from_post_git_model( - cls, model_id: int, primary: bool, new_model: PostGitModel + cls, model: "DatabaseToolModel", primary: bool, new_model: PostGitModel ): return cls( name="", primary=primary, - model_id=model_id, + model=model, **new_model.model_dump(), ) diff --git a/backend/capellacollab/projects/toolmodels/modelsources/git/routes.py b/backend/capellacollab/projects/toolmodels/modelsources/git/routes.py index c0f2965c6..987ff17df 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/git/routes.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/git/routes.py @@ -66,7 +66,7 @@ def validate_path( @router.get("", response_model=list[models.GitModel]) def get_git_models( - capella_model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + capella_model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), ) -> list[models.DatabaseGitModel]: @@ -151,7 +151,7 @@ async def get_revisions_with_model_credentials( ) def create_git_model( post_git_model: models.PostGitModel, - capella_model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + capella_model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), diff --git a/backend/capellacollab/projects/toolmodels/modelsources/git/validation.py b/backend/capellacollab/projects/toolmodels/modelsources/git/validation.py index fe98bd767..8ce2b57bd 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/git/validation.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/git/validation.py @@ -15,7 +15,7 @@ async def check_primary_git_repository( db: orm.Session, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, log: logging.LoggerAdapter, ) -> models.GitModelStatus: primary_repo = crud.get_primary_git_model_of_capellamodel(db, model.id) @@ -43,7 +43,7 @@ async def check_primary_git_repository( async def check_pipeline_health( db: orm.Session, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, job_name: str, logger: logging.LoggerAdapter, ) -> models.ModelArtifactStatus: diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/crud.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/crud.py index ee48d9b41..56a310771 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/t4c/crud.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/crud.py @@ -29,7 +29,7 @@ def get_t4c_models(db: orm.Session) -> abc.Sequence[models.DatabaseT4CModel]: def get_t4c_models_for_tool_model( - db: orm.Session, model: toolmodels_models.DatabaseCapellaModel + db: orm.Session, model: toolmodels_models.DatabaseToolModel ) -> abc.Sequence[models.DatabaseT4CModel]: return ( db.execute( @@ -44,7 +44,7 @@ def get_t4c_models_for_tool_model( def create_t4c_model( db: orm.Session, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, repository: repositories_models.DatabaseT4CRepository, name: str, ) -> models.DatabaseT4CModel: diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/injectables.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/injectables.py index 81ff0fe38..3f9946657 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/t4c/injectables.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/injectables.py @@ -16,7 +16,7 @@ def get_existing_t4c_model( t4c_model_id: int, - capella_model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + capella_model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/models.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/models.py index 7fc1f740a..9796609f0 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/t4c/models.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/models.py @@ -15,7 +15,7 @@ ) if t.TYPE_CHECKING: - from capellacollab.projects.toolmodels.models import DatabaseCapellaModel + from capellacollab.projects.toolmodels.models import DatabaseToolModel from capellacollab.settings.modelsources.t4c.repositories.models import ( DatabaseT4CRepository, ) @@ -28,19 +28,21 @@ class DatabaseT4CModel(database.Base): ) id: orm.Mapped[int] = orm.mapped_column( - unique=True, primary_key=True, index=True + init=False, unique=True, primary_key=True, index=True ) name: orm.Mapped[str] = orm.mapped_column(index=True) repository_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("t4c_repositories.id") + sa.ForeignKey("t4c_repositories.id"), init=False ) repository: orm.Mapped[DatabaseT4CRepository] = orm.relationship( back_populates="models" ) - model_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("models.id")) - model: orm.Mapped[DatabaseCapellaModel] = orm.relationship( + model_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("models.id"), init=False + ) + model: orm.Mapped[DatabaseToolModel] = orm.relationship( back_populates="t4c_models" ) diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/routes.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/routes.py index dd22a81cc..739ea75d4 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/t4c/routes.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/routes.py @@ -41,7 +41,7 @@ response_model=list[models.T4CModel], ) def list_t4c_models( - model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), @@ -74,7 +74,7 @@ def get_t4c_model( ) def create_t4c_model( body: models.SubmitT4CModel, - model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), diff --git a/backend/capellacollab/projects/toolmodels/restrictions/injectables.py b/backend/capellacollab/projects/toolmodels/restrictions/injectables.py index 4df8d1e2c..742a339ca 100644 --- a/backend/capellacollab/projects/toolmodels/restrictions/injectables.py +++ b/backend/capellacollab/projects/toolmodels/restrictions/injectables.py @@ -10,8 +10,10 @@ def get_model_restrictions( - model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), -) -> models.DatabaseToolModelRestrictions: - return model.restrictions +) -> models.DatabaseToolModelRestrictions | None: + restrictions = model.restrictions + assert restrictions # restrictions are only None for a short time during creation + return restrictions diff --git a/backend/capellacollab/projects/toolmodels/restrictions/models.py b/backend/capellacollab/projects/toolmodels/restrictions/models.py index e2d93d2a0..ee646f58b 100644 --- a/backend/capellacollab/projects/toolmodels/restrictions/models.py +++ b/backend/capellacollab/projects/toolmodels/restrictions/models.py @@ -12,7 +12,7 @@ from capellacollab.core import database if t.TYPE_CHECKING: - from capellacollab.projects.toolmodels.models import DatabaseCapellaModel + from capellacollab.projects.toolmodels.models import DatabaseToolModel class ToolModelRestrictions(pydantic.BaseModel): @@ -28,11 +28,13 @@ class DatabaseToolModelRestrictions(database.Base): __tablename__ = "model_restrictions" id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, unique=True + init=False, primary_key=True, index=True, unique=True ) - model_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("models.id")) - model: orm.Mapped[DatabaseCapellaModel] = orm.relationship( + model_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("models.id"), init=False + ) + model: orm.Mapped[DatabaseToolModel] = orm.relationship( back_populates="restrictions" ) diff --git a/backend/capellacollab/projects/toolmodels/restrictions/routes.py b/backend/capellacollab/projects/toolmodels/restrictions/routes.py index 9819b9327..4114d8455 100644 --- a/backend/capellacollab/projects/toolmodels/restrictions/routes.py +++ b/backend/capellacollab/projects/toolmodels/restrictions/routes.py @@ -41,12 +41,16 @@ def update_restrictions( restrictions: models.DatabaseToolModelRestrictions = fastapi.Depends( injectables.get_model_restrictions ), - model: toolmodels_models.DatabaseCapellaModel = fastapi.Depends( + model: toolmodels_models.DatabaseToolModel = fastapi.Depends( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseToolModelRestrictions: - if body.allow_pure_variants and not model.tool.integrations.pure_variants: + if ( + body.allow_pure_variants + and model.tool.integrations + and not model.tool.integrations.pure_variants + ): raise fastapi.HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={ diff --git a/backend/capellacollab/projects/toolmodels/routes.py b/backend/capellacollab/projects/toolmodels/routes.py index 32befc9ff..63f4ce900 100644 --- a/backend/capellacollab/projects/toolmodels/routes.py +++ b/backend/capellacollab/projects/toolmodels/routes.py @@ -45,7 +45,7 @@ def get_models( project: projects_models.DatabaseProject = fastapi.Depends( projects_injectables.get_existing_project ), -) -> list[models.DatabaseCapellaModel]: +) -> list[models.DatabaseToolModel]: return project.models @@ -55,10 +55,10 @@ def get_models( tags=["Projects - Models"], ) def get_model_by_slug( - model: models.DatabaseCapellaModel = fastapi.Depends( + model: models.DatabaseToolModel = fastapi.Depends( injectables.get_existing_capella_model ), -) -> models.DatabaseCapellaModel: +) -> models.DatabaseToolModel: return model @@ -80,13 +80,13 @@ def create_new_tool_model( projects_injectables.get_existing_project ), db: orm.Session = fastapi.Depends(database.get_db), -) -> models.DatabaseCapellaModel: +) -> models.DatabaseToolModel: tool = tools_injectables.get_existing_tool( tool_id=new_model.tool_id, db=db ) configuration = {} - if tool.integrations.jupyter: + if tool.integrations and tool.integrations.jupyter: configuration["workspace"] = str(uuid.uuid4()) try: @@ -102,7 +102,7 @@ def create_new_tool_model( }, ) - if tool.integrations.jupyter: + if tool.integrations and tool.integrations.jupyter: workspace.create_shared_workspace( configuration["workspace"], project, model, "2Gi" ) @@ -127,14 +127,14 @@ def patch_tool_model( project: projects_models.DatabaseProject = fastapi.Depends( projects_injectables.get_existing_project ), - model: models.DatabaseCapellaModel = fastapi.Depends( + model: models.DatabaseToolModel = fastapi.Depends( injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), user: users_models.DatabaseUser = fastapi.Depends( users_injectables.get_own_user ), -) -> models.DatabaseCapellaModel: +) -> models.DatabaseToolModel: if body.name: new_slug = slugify.slugify(body.name) @@ -154,7 +154,7 @@ def patch_tool_model( if body.version_id else model.version ) - if body.version_id and version.tool != model.tool: + if version and body.version_id and version.tool != model.tool: raise fastapi.HTTPException( status_code=status.HTTP_409_CONFLICT, detail={ @@ -167,7 +167,7 @@ def patch_tool_model( if body.nature_id else model.nature ) - if body.nature_id is not None and nature.tool != model.tool: + if nature and body.nature_id and nature.tool != model.tool: raise fastapi.HTTPException( status_code=status.HTTP_409_CONFLICT, detail={ @@ -208,7 +208,7 @@ def patch_tool_model( tags=["Projects - Models"], ) def delete_tool_model( - model: models.DatabaseCapellaModel = fastapi.Depends( + model: models.DatabaseToolModel = fastapi.Depends( injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), @@ -231,7 +231,8 @@ def delete_tool_model( ) if ( - model.tool.integrations.jupyter + model.tool.integrations + and model.tool.integrations.jupyter and model.configuration and "workspace" in model.configuration ): @@ -290,7 +291,7 @@ def determine_new_project_to_move_model( def raise_if_model_exists_in_project( - model: models.DatabaseCapellaModel, + model: models.DatabaseToolModel, project: projects_models.DatabaseProject, ): if model.slug in [model.slug for model in project.models]: diff --git a/backend/capellacollab/projects/toolmodels/validation.py b/backend/capellacollab/projects/toolmodels/validation.py index c7c8453f8..72650d9e3 100644 --- a/backend/capellacollab/projects/toolmodels/validation.py +++ b/backend/capellacollab/projects/toolmodels/validation.py @@ -5,7 +5,7 @@ def calculate_model_warnings( - model: models.DatabaseCapellaModel, + model: models.DatabaseToolModel, ) -> list[str]: warnings = [] if not model.nature: diff --git a/backend/capellacollab/projects/toolmodels/workspace.py b/backend/capellacollab/projects/toolmodels/workspace.py index 8cda5908f..2d1c0f3c2 100644 --- a/backend/capellacollab/projects/toolmodels/workspace.py +++ b/backend/capellacollab/projects/toolmodels/workspace.py @@ -10,7 +10,7 @@ def create_shared_workspace( name: str, project: projects_models.DatabaseProject, - model: models.DatabaseCapellaModel, + model: models.DatabaseToolModel, size: str, ): operators.get_operator().create_persistent_volume( diff --git a/backend/capellacollab/projects/users/models.py b/backend/capellacollab/projects/users/models.py index 86c5cafa5..4ed1fa272 100644 --- a/backend/capellacollab/projects/users/models.py +++ b/backend/capellacollab/projects/users/models.py @@ -53,14 +53,14 @@ class ProjectUserAssociation(database.Base): __tablename__ = "project_user_association" user_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("users.id"), primary_key=True + sa.ForeignKey("users.id"), primary_key=True, init=False ) user: orm.Mapped["DatabaseUser"] = orm.relationship( back_populates="projects" ) project_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("projects.id"), primary_key=True + sa.ForeignKey("projects.id"), primary_key=True, init=False ) project: orm.Mapped["DatabaseProject"] = orm.relationship( back_populates="users" diff --git a/backend/capellacollab/sessions/hooks/jupyter.py b/backend/capellacollab/sessions/hooks/jupyter.py index 1d941c009..e74ffd364 100644 --- a/backend/capellacollab/sessions/hooks/jupyter.py +++ b/backend/capellacollab/sessions/hooks/jupyter.py @@ -136,7 +136,7 @@ def _get_project_share_volume_mounts( def _is_project_member( self, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, username: str, db: orm.Session, ) -> bool: @@ -147,7 +147,7 @@ def _is_project_member( def _has_project_write_access( self, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, username: str, db: orm.Session, ) -> bool: diff --git a/backend/capellacollab/sessions/hooks/pure_variants.py b/backend/capellacollab/sessions/hooks/pure_variants.py index 642a793c5..cabd59299 100644 --- a/backend/capellacollab/sessions/hooks/pure_variants.py +++ b/backend/capellacollab/sessions/hooks/pure_variants.py @@ -82,7 +82,7 @@ def configuration_hook( # type: ignore def _model_allows_pure_variants( self, - model: toolmodels_models.DatabaseCapellaModel, + model: toolmodels_models.DatabaseToolModel, ): return model.restrictions and model.restrictions.allow_pure_variants diff --git a/backend/capellacollab/sessions/models.py b/backend/capellacollab/sessions/models.py index b489d3a16..8d934a3a9 100644 --- a/backend/capellacollab/sessions/models.py +++ b/backend/capellacollab/sessions/models.py @@ -94,35 +94,46 @@ class GuacamoleAuthentication(pydantic.BaseModel): class DatabaseSession(database.Base): __tablename__ = "sessions" + # Since the sessions are not persistent, we use a UUID as the primary key id: orm.Mapped[str] = orm.mapped_column(primary_key=True, index=True) ports: orm.Mapped[list[int]] = orm.mapped_column(sa.ARRAY(sa.Integer)) created_at: orm.Mapped[datetime.datetime] - rdp_password: orm.Mapped[str | None] - guacamole_username: orm.Mapped[str | None] - guacamole_password: orm.Mapped[str | None] - guacamole_connection_id: orm.Mapped[str | None] - host: orm.Mapped[str] type: orm.Mapped[WorkspaceType] owner_name: orm.Mapped[str] = orm.mapped_column( - sa.ForeignKey("users.name") + sa.ForeignKey("users.name"), init=False ) owner: orm.Mapped[DatabaseUser] = orm.relationship() - tool_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("tools.id")) + tool_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("tools.id"), init=False + ) tool: orm.Mapped[DatabaseTool] = orm.relationship() version_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("versions.id") + sa.ForeignKey("versions.id"), init=False ) version: orm.Mapped[DatabaseVersion] = orm.relationship() project_id: orm.Mapped[str | None] = orm.mapped_column( - sa.ForeignKey("projects.id") + sa.ForeignKey("projects.id"), init=False ) - project: orm.Mapped[projects_models.DatabaseProject] = orm.relationship() + project: orm.Mapped[ + projects_models.DatabaseProject | None + ] = orm.relationship() environment: orm.Mapped[dict[str, str] | None] + + rdp_password: orm.Mapped[str | None] = orm.mapped_column(default=None) + guacamole_username: orm.Mapped[str | None] = orm.mapped_column( + default=None + ) + guacamole_password: orm.Mapped[str | None] = orm.mapped_column( + default=None + ) + guacamole_connection_id: orm.Mapped[str | None] = orm.mapped_column( + default=None + ) diff --git a/backend/capellacollab/sessions/operators/k8s.py b/backend/capellacollab/sessions/operators/k8s.py index bbacb6267..1a21e6b9f 100644 --- a/backend/capellacollab/sessions/operators/k8s.py +++ b/backend/capellacollab/sessions/operators/k8s.py @@ -5,6 +5,7 @@ import base64 import binascii +import datetime import http import json import logging @@ -88,6 +89,13 @@ def is_openshift_cluster(api_client): return False +class Session(t.NamedTuple): + id: str + ports: set[int] + created_at: datetime.datetime + host: str + + class KubernetesOperator: def __init__(self) -> None: self.load_config() @@ -178,7 +186,7 @@ def start_session( prometheus_path="/metrics", prometheus_port=9118, limits="high", - ) -> dict[str, t.Any]: + ) -> Session: log.info("Launching a %s session for user %s", session_type, username) _id = self._generate_id() @@ -413,7 +421,7 @@ def _export_attrs( deployment: client.V1Deployment, service: client.V1Service, ports: dict[str, int], - ) -> dict[str, t.Any]: + ) -> Session: if "rdp" in ports: port = {ports["rdp"]} elif "http" in ports: @@ -423,14 +431,12 @@ def _export_attrs( "No rdp or http port defined on the deployed session" ) - return { - "id": deployment.to_dict()["metadata"]["name"], - "ports": port, - "created_at": deployment.to_dict()["metadata"][ - "creation_timestamp" - ], - "host": service.to_dict()["metadata"]["name"] + "." + namespace, - } + return Session( + id=deployment.to_dict()["metadata"]["name"], + ports=port, + created_at=deployment.to_dict()["metadata"]["creation_timestamp"], + host=service.to_dict()["metadata"]["name"] + "." + namespace, + ) def _map_volumes_to_k8s_volumes( self, diff --git a/backend/capellacollab/sessions/routes.py b/backend/capellacollab/sessions/routes.py index 68863b297..bf70a3322 100644 --- a/backend/capellacollab/sessions/routes.py +++ b/backend/capellacollab/sessions/routes.py @@ -231,7 +231,7 @@ def request_readonly_session( db=db, type=models.WorkspaceType.READONLY, session=session, - owner=db_user.name, + owner=db_user, rdp_password=rdp_password, tool=model.tool, version=model.version, @@ -244,7 +244,7 @@ def models_as_json( session_model_list: list[ tuple[ models.PostReadonlySessionEntry, - toolmodels_models.DatabaseCapellaModel, + toolmodels_models.DatabaseToolModel, ] ] ): @@ -267,7 +267,9 @@ def git_model_as_json( "revision": revision, "depth": 0 if deep_clone else 1, "entrypoint": git_model.entrypoint, - "nature": git_model.model.nature.name, + "nature": ( + git_model.model.nature.name if git_model.model.nature else "" + ), } if git_model.username: d["username"] = git_model.username @@ -317,7 +319,7 @@ def request_persistent_session( response = start_persistent_jupyter_session( db=db, operator=operator, - owner=user.name, + owner=user, tool=tool, version=version, volumes=volumes, @@ -329,7 +331,6 @@ def request_persistent_session( db=db, operator=operator, user=user, - owner=user.name, tool=tool, version=version, volumes=volumes, @@ -357,11 +358,12 @@ def raise_if_conflicting_persistent_sessions( # Currently, all tools share one workspace. Eclipse based tools lock the workspace. # We can only run one Eclipse-based tool at a time. # Status tracked in https://github.com/DSD-DBS/capella-collab-manager/issues/847 - if tool.integrations.jupyter: + if tool.integrations and tool.integrations.jupyter: # Check if there is already an Jupyter session running. if True in [ session.tool.integrations.jupyter for session in existing_user_sessions + if session.tool.integrations ]: raise fastapi.HTTPException( status_code=status.HTTP_409_CONFLICT, @@ -391,7 +393,7 @@ def raise_if_conflicting_persistent_sessions( def start_persistent_jupyter_session( db: orm.Session, operator: k8s.KubernetesOperator, - owner: str, + owner: users_models.DatabaseUser, tool: tools_models.DatabaseTool, environment: dict[str, str], version: tools_models.DatabaseVersion, @@ -400,7 +402,7 @@ def start_persistent_jupyter_session( ): session = operator.start_session( image=docker_image, - username=owner, + username=owner.name, session_type="persistent", tool_name=tool.name, version_name=version.name, @@ -428,7 +430,6 @@ def start_persistent_guacamole_session( db: orm.Session, operator: k8s.KubernetesOperator, user: users_models.DatabaseUser, - owner: str, tool: tools_models.DatabaseTool, version: tools_models.DatabaseVersion, volumes: list[operators_models.Volume], @@ -454,7 +455,7 @@ def start_persistent_guacamole_session( db=db, type=models.WorkspaceType.PERSISTENT, session=session, - owner=owner, + owner=user, rdp_password=rdp_password, tool=tool, version=version, @@ -464,26 +465,39 @@ def start_persistent_guacamole_session( return response +class RDPConnectionInformation(t.NamedTuple): + rdp_password: str + guacamole_username: str + guacamole_password: str + guacamole_connection_id: str + + def create_database_session( db: orm.Session, type: models.WorkspaceType, - session: dict[str, t.Any], - owner: str, + session: k8s.Session, + owner: users_models.DatabaseUser, tool: tools_models.DatabaseTool, version: tools_models.DatabaseVersion, project: projects_models.DatabaseProject | None, - **kwargs, + environment: dict[str, str], + rdp_connection_information: RDPConnectionInformation | None = None, ) -> models.GetSessionsResponse: db_session = crud.create_session( db, models.DatabaseSession( tool=tool, version=version, - owner_name=owner, + owner=owner, project=project, type=type, - **session, - **kwargs, + environment=environment, + **session._asdict(), + **( + rdp_connection_information._asdict() + if rdp_connection_information + else {} + ), ), ) @@ -499,8 +513,8 @@ def create_database_session( def create_database_and_guacamole_session( db: orm.Session, type: models.WorkspaceType, - session: dict[str, t.Any], - owner: str, + session: k8s.Session, + owner: users_models.DatabaseUser, rdp_password: str, tool: tools_models.DatabaseTool, version: tools_models.DatabaseVersion, @@ -518,8 +532,8 @@ def create_database_and_guacamole_session( guacamole_identifier = guacamole.create_connection( guacamole_token, rdp_password, - session["host"], - list(session["ports"])[0], + session.host, + list(session.ports)[0], )["identifier"] guacamole.assign_user_to_connection( @@ -535,10 +549,12 @@ def create_database_and_guacamole_session( version, project, environment=environment, - rdp_password=rdp_password, - guacamole_username=guacamole_username, - guacamole_password=guacamole_password, - guacamole_connection_id=guacamole_identifier, + rdp_connection_information=RDPConnectionInformation( + guacamole_username=guacamole_username, + guacamole_password=guacamole_password, + guacamole_connection_id=guacamole_identifier, + rdp_password=rdp_password, + ), ) diff --git a/backend/capellacollab/settings/configuration/models.py b/backend/capellacollab/settings/configuration/models.py index 829395a05..ea554bfcf 100644 --- a/backend/capellacollab/settings/configuration/models.py +++ b/backend/capellacollab/settings/configuration/models.py @@ -14,7 +14,7 @@ class DatabaseConfiguration(database.Base): __tablename__ = "configuration" id: orm.Mapped[int] = orm.mapped_column( - unique=True, primary_key=True, index=True + init=False, unique=True, primary_key=True, index=True ) name: orm.Mapped[str] = orm.mapped_column(unique=True, index=True) diff --git a/backend/capellacollab/settings/integrations/purevariants/models.py b/backend/capellacollab/settings/integrations/purevariants/models.py index bae5c8423..da77d1ad2 100644 --- a/backend/capellacollab/settings/integrations/purevariants/models.py +++ b/backend/capellacollab/settings/integrations/purevariants/models.py @@ -28,7 +28,9 @@ def validate_license_url(value: str | None): class DatabasePureVariantsLicenses(database.Base): __tablename__ = "pure_variants" - id: orm.Mapped[int] = orm.mapped_column(primary_key=True, index=True) + id: orm.Mapped[int] = orm.mapped_column( + init=False, primary_key=True, index=True + ) license_server_url: orm.Mapped[str | None] license_key_filename: orm.Mapped[str | None] diff --git a/backend/capellacollab/settings/modelsources/git/models.py b/backend/capellacollab/settings/modelsources/git/models.py index fa4708146..f408c6067 100644 --- a/backend/capellacollab/settings/modelsources/git/models.py +++ b/backend/capellacollab/settings/modelsources/git/models.py @@ -34,7 +34,7 @@ class DatabaseGitInstance(database.Base): __tablename__ = "git_instances" id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, autoincrement=True + init=False, primary_key=True, index=True, autoincrement=True ) name: orm.Mapped[str] diff --git a/backend/capellacollab/settings/modelsources/t4c/models.py b/backend/capellacollab/settings/modelsources/t4c/models.py index 91ad5d658..065a28716 100644 --- a/backend/capellacollab/settings/modelsources/t4c/models.py +++ b/backend/capellacollab/settings/modelsources/t4c/models.py @@ -46,39 +46,42 @@ class DatabaseT4CInstance(database.Base): __tablename__ = "t4c_instances" id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, autoincrement=True + init=False, primary_key=True, index=True, autoincrement=True ) name: orm.Mapped[str] = orm.mapped_column(unique=True) license: orm.Mapped[str] host: orm.Mapped[str] - port: orm.Mapped[int] = orm.mapped_column( - sa.CheckConstraint("port >= 0 AND port <= 65535"), default=2036 - ) - cdo_port: orm.Mapped[int] = orm.mapped_column( - sa.CheckConstraint("cdo_port >= 0 AND cdo_port <= 65535"), - default=12036, - ) - http_port: orm.Mapped[int | None] = orm.mapped_column( - sa.CheckConstraint("http_port >= 0 AND http_port <= 65535"), - ) + usage_api: orm.Mapped[str] rest_api: orm.Mapped[str] username: orm.Mapped[str] password: orm.Mapped[str] - protocol: orm.Mapped[Protocol] = orm.mapped_column(default=Protocol.tcp) - version_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("versions.id") + sa.ForeignKey("versions.id"), init=False ) version: orm.Mapped[DatabaseVersion] = orm.relationship() repositories: orm.Mapped[list[DatabaseT4CRepository]] = orm.relationship( - back_populates="instance", cascade="all, delete" + default_factory=list, back_populates="instance", cascade="all, delete" ) + port: orm.Mapped[int] = orm.mapped_column( + sa.CheckConstraint("port >= 0 AND port <= 65535"), default=2036 + ) + + http_port: orm.Mapped[int | None] = orm.mapped_column( + sa.CheckConstraint("http_port >= 0 AND http_port <= 65535"), + default=None, + ) + cdo_port: orm.Mapped[int] = orm.mapped_column( + sa.CheckConstraint("cdo_port >= 0 AND cdo_port <= 65535"), + default=12036, + ) + protocol: orm.Mapped[Protocol] = orm.mapped_column(default=Protocol.tcp) + is_archived: orm.Mapped[bool] = orm.mapped_column(default=False) diff --git a/backend/capellacollab/settings/modelsources/t4c/repositories/crud.py b/backend/capellacollab/settings/modelsources/t4c/repositories/crud.py index 885b1397b..f3654666f 100644 --- a/backend/capellacollab/settings/modelsources/t4c/repositories/crud.py +++ b/backend/capellacollab/settings/modelsources/t4c/repositories/crud.py @@ -82,9 +82,9 @@ def _get_user_write_t4c_repositories( sa.select(models.DatabaseT4CRepository) .join(models.DatabaseT4CRepository.models) .join(t4c_models.DatabaseT4CModel.model) - .join(toolmodels_models.DatabaseCapellaModel.version) + .join(toolmodels_models.DatabaseToolModel.version) .where(tools_models.DatabaseVersion.name == version_name) - .join(toolmodels_models.DatabaseCapellaModel.project) + .join(toolmodels_models.DatabaseToolModel.project) .where(projects_models.DatabaseProject.is_archived.is_(False)) .join(projects_models.DatabaseProject.users) .where( @@ -104,9 +104,9 @@ def _get_admin_t4c_repositories( sa.select(models.DatabaseT4CRepository) .join(models.DatabaseT4CRepository.models) .join(t4c_models.DatabaseT4CModel.model) - .join(toolmodels_models.DatabaseCapellaModel.version) + .join(toolmodels_models.DatabaseToolModel.version) .where(tools_models.DatabaseVersion.name == version_name) - .join(toolmodels_models.DatabaseCapellaModel.project) + .join(toolmodels_models.DatabaseToolModel.project) .where(projects_models.DatabaseProject.is_archived.is_(False)) ) diff --git a/backend/capellacollab/settings/modelsources/t4c/repositories/models.py b/backend/capellacollab/settings/modelsources/t4c/repositories/models.py index 2786d71f1..d661486ab 100644 --- a/backend/capellacollab/settings/modelsources/t4c/repositories/models.py +++ b/backend/capellacollab/settings/modelsources/t4c/repositories/models.py @@ -27,19 +27,25 @@ class DatabaseT4CRepository(database.Base): __table_args__ = (sa.UniqueConstraint("instance_id", "name"),) id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, autoincrement=True, unique=True + init=False, + primary_key=True, + index=True, + autoincrement=True, + unique=True, ) name: orm.Mapped[str] instance_id: orm.Mapped[int] = orm.mapped_column( - sa.ForeignKey("t4c_instances.id") + sa.ForeignKey("t4c_instances.id"), init=False ) instance: orm.Mapped[DatabaseT4CInstance] = orm.relationship( back_populates="repositories" ) models: orm.Mapped[list[DatabaseT4CModel]] = orm.relationship( - back_populates="repository", cascade="all, delete" + back_populates="repository", + cascade="all, delete", + default_factory=list, ) diff --git a/backend/capellacollab/settings/modelsources/t4c/routes.py b/backend/capellacollab/settings/modelsources/t4c/routes.py index cdf7eadfd..b3e2f79ef 100644 --- a/backend/capellacollab/settings/modelsources/t4c/routes.py +++ b/backend/capellacollab/settings/modelsources/t4c/routes.py @@ -59,7 +59,10 @@ def create_t4c_instance( raise exceptions.T4CInstanceWithNameAlreadyExistsError() version = toolmodels_routes.get_version_by_id_or_raise(db, body.version_id) - instance = models.DatabaseT4CInstance(**body.model_dump()) + body_dump = body.model_dump() + del body_dump["version_id"] + + instance = models.DatabaseT4CInstance(version=version, **body_dump) instance.version = version return crud.create_t4c_instance(db, instance) diff --git a/backend/capellacollab/tools/crud.py b/backend/capellacollab/tools/crud.py index 9692e9b8e..044820ead 100644 --- a/backend/capellacollab/tools/crud.py +++ b/backend/capellacollab/tools/crud.py @@ -38,7 +38,7 @@ def create_tool( db: orm.Session, tool: models.DatabaseTool ) -> models.DatabaseTool: tool.integrations = integrations_models.DatabaseToolIntegrations( - pure_variants=False, t4c=False, jupyter=False + tool=tool, pure_variants=False, t4c=False, jupyter=False ) db.add(tool) db.commit() @@ -151,7 +151,7 @@ def update_version( def create_version( db: orm.Session, - tool_id: int, + tool: models.DatabaseTool, name: str, is_recommended: bool = False, is_deprecated: bool = False, @@ -160,7 +160,7 @@ def create_version( name=name, is_recommended=is_recommended, is_deprecated=is_deprecated, - tool_id=tool_id, + tool=tool, ) db.add(version) db.commit() @@ -223,9 +223,9 @@ def get_natures_by_tool_id( def create_nature( - db: orm.Session, tool_id: int, name: str + db: orm.Session, tool: models.DatabaseTool, name: str ) -> models.DatabaseNature: - nature = models.DatabaseNature(name=name, tool_id=tool_id) + nature = models.DatabaseNature(name=name, tool=tool) db.add(nature) db.commit() return nature diff --git a/backend/capellacollab/tools/integrations/models.py b/backend/capellacollab/tools/integrations/models.py index 9615d5663..937885194 100644 --- a/backend/capellacollab/tools/integrations/models.py +++ b/backend/capellacollab/tools/integrations/models.py @@ -32,9 +32,11 @@ class PatchToolIntegrations(pydantic.BaseModel): class DatabaseToolIntegrations(database.Base): __tablename__ = "tool_integrations" - id: orm.Mapped[int] = orm.mapped_column(primary_key=True) + id: orm.Mapped[int] = orm.mapped_column(init=False, primary_key=True) - tool_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("tools.id")) + tool_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("tools.id"), init=False + ) tool: orm.Mapped[DatabaseTool] = orm.relationship( back_populates="integrations" ) diff --git a/backend/capellacollab/tools/integrations/routes.py b/backend/capellacollab/tools/integrations/routes.py index a5953e333..349144d19 100644 --- a/backend/capellacollab/tools/integrations/routes.py +++ b/backend/capellacollab/tools/integrations/routes.py @@ -31,4 +31,5 @@ def update_integrations( ), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseToolIntegrations: + assert tool.integrations return crud.update_integrations(db, tool.integrations, body) diff --git a/backend/capellacollab/tools/models.py b/backend/capellacollab/tools/models.py index ec682ad09..65e436211 100644 --- a/backend/capellacollab/tools/models.py +++ b/backend/capellacollab/tools/models.py @@ -20,22 +20,30 @@ class DatabaseTool(database.Base): __tablename__ = "tools" - id: orm.Mapped[int] = orm.mapped_column(primary_key=True) + id: orm.Mapped[int] = orm.mapped_column(init=False, primary_key=True) name: orm.Mapped[str] docker_image_template: orm.Mapped[str] - docker_image_backup_template: orm.Mapped[str | None] - readonly_docker_image_template: orm.Mapped[str | None] + docker_image_backup_template: orm.Mapped[str | None] = orm.mapped_column( + default=None + ) + readonly_docker_image_template: orm.Mapped[str | None] = orm.mapped_column( + default=None + ) + + integrations: orm.Mapped[ + DatabaseToolIntegrations | None + ] = orm.relationship( + default=None, + back_populates="tool", + uselist=False, + ) versions: orm.Mapped[list[DatabaseVersion]] = orm.relationship( - back_populates="tool" + default_factory=list, back_populates="tool" ) natures: orm.Mapped[list[DatabaseNature]] = orm.relationship( - back_populates="tool" - ) - - integrations: orm.Mapped[DatabaseToolIntegrations] = orm.relationship( - back_populates="tool", uselist=False + default_factory=list, back_populates="tool" ) @@ -43,14 +51,15 @@ class DatabaseVersion(database.Base): __tablename__ = "versions" __table_args__ = (sa.UniqueConstraint("tool_id", "name"),) - id: orm.Mapped[int] = orm.mapped_column(primary_key=True) + id: orm.Mapped[int] = orm.mapped_column(init=False, primary_key=True) name: orm.Mapped[str] is_recommended: orm.Mapped[bool] is_deprecated: orm.Mapped[bool] tool_id: orm.Mapped[int | None] = orm.mapped_column( - sa.ForeignKey("tools.id") + sa.ForeignKey("tools.id"), + init=False, ) tool: orm.Mapped[DatabaseTool] = orm.relationship( back_populates="versions" @@ -61,11 +70,11 @@ class DatabaseNature(database.Base): __tablename__ = "types" __table_args__ = (sa.UniqueConstraint("tool_id", "name"),) - id: orm.Mapped[int] = orm.mapped_column(primary_key=True) + id: orm.Mapped[int] = orm.mapped_column(init=False, primary_key=True) name: orm.Mapped[str] tool_id: orm.Mapped[int | None] = orm.mapped_column( - sa.ForeignKey("tools.id") + sa.ForeignKey("tools.id"), init=False ) tool: orm.Mapped[DatabaseTool] = orm.relationship(back_populates="natures") diff --git a/backend/capellacollab/tools/routes.py b/backend/capellacollab/tools/routes.py index 0b89b3207..668310a58 100644 --- a/backend/capellacollab/tools/routes.py +++ b/backend/capellacollab/tools/routes.py @@ -130,7 +130,7 @@ def create_tool_version( tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseVersion: - return crud.create_version(db, tool.id, body.name) + return crud.create_version(db, tool, body.name) @router.patch( @@ -194,11 +194,11 @@ def get_tool_natures( ], ) def create_tool_nature( - tool_id: int, body: models.CreateToolNature, + tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseNature: - return crud.create_nature(db, tool_id, body.name) + return crud.create_nature(db, tool, body.name) @router.delete( diff --git a/backend/capellacollab/users/models.py b/backend/capellacollab/users/models.py index d1ffef652..2abcb1c66 100644 --- a/backend/capellacollab/users/models.py +++ b/backend/capellacollab/users/models.py @@ -59,23 +59,34 @@ class PostUser(pydantic.BaseModel): class DatabaseUser(database.Base): __tablename__ = "users" - id: orm.Mapped[int] = orm.mapped_column(primary_key=True, index=True) + id: orm.Mapped[int] = orm.mapped_column( + init=False, primary_key=True, index=True + ) name: orm.Mapped[str] = orm.mapped_column(unique=True, index=True) role: orm.Mapped[Role] - created: orm.Mapped[datetime.datetime | None] - last_login: orm.Mapped[datetime.datetime | None] + created: orm.Mapped[datetime.datetime | None] = orm.mapped_column( + default=datetime.datetime.now(datetime.UTC) + ) projects: orm.Mapped[list[ProjectUserAssociation]] = orm.relationship( - back_populates="user" + default_factory=list, back_populates="user" ) sessions: orm.Mapped[list[DatabaseSession]] = orm.relationship( - back_populates="owner" + default_factory=list, back_populates="owner" ) events: orm.Mapped[list[DatabaseUserHistoryEvent]] = orm.relationship( - back_populates="user", foreign_keys="DatabaseUserHistoryEvent.user_id" + default_factory=list, + back_populates="user", + foreign_keys="DatabaseUserHistoryEvent.user_id", ) tokens: orm.Mapped[list[DatabaseUserToken]] = orm.relationship( - back_populates="user", cascade="all, delete-orphan" + default_factory=list, + back_populates="user", + cascade="all, delete-orphan", + ) + + last_login: orm.Mapped[datetime.datetime | None] = orm.mapped_column( + default=None ) diff --git a/backend/capellacollab/users/routes.py b/backend/capellacollab/users/routes.py index 16e9fbf4b..07306b509 100644 --- a/backend/capellacollab/users/routes.py +++ b/backend/capellacollab/users/routes.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 -import logging from collections import abc import fastapi @@ -9,7 +8,6 @@ from sqlalchemy import orm from capellacollab.core import database -from capellacollab.core import logging as core_logging from capellacollab.core.authentication import injectables as auth_injectables from capellacollab.events import crud as events_crud from capellacollab.events import models as events_models @@ -47,7 +45,8 @@ def get_user( ) -> models.DatabaseUser: if ( user.id == own_user.id - or len(get_common_projects_for_users(own_user, user, db)) > 0 + or len(projects_crud.get_common_projects_for_users(db, own_user, user)) + > 0 or auth_injectables.RoleVerification( required_role=models.Role.ADMIN, verify=False )(own_user.name, db) @@ -110,15 +109,16 @@ def get_common_projects( ), user: models.DatabaseUser = fastapi.Depends(injectables.get_own_user), db: orm.Session = fastapi.Depends(database.get_db), - log: logging.LoggerAdapter = fastapi.Depends( - core_logging.get_request_logger - ), -) -> set[projects_models.DatabaseProject]: - projects = get_common_projects_for_users( - user, user_for_common_projects, db +) -> list[projects_models.DatabaseProject]: + if user.role == models.Role.ADMIN: + return [ + association.project + for association in user_for_common_projects.projects + ] + projects = projects_crud.get_common_projects_for_users( + db, user, user_for_common_projects ) - log.info("Fetching the following projects: %s", projects) - return projects + return list(projects) @router.patch( @@ -190,18 +190,6 @@ def get_user_history( return user.events -def get_common_projects_for_users( - user_one: models.DatabaseUser, - user_two: models.DatabaseUser, - db: orm.Session, -) -> set[projects_models.DatabaseProject]: - first_user_projects = get_projects_for_user(user_one, db) - second_user_projects = get_projects_for_user(user_two, db) - - projects = set(first_user_projects).intersection(set(second_user_projects)) - return projects - - def get_projects_for_user( user: models.DatabaseUser, db: orm.Session ) -> list[projects_models.DatabaseProject]: diff --git a/backend/capellacollab/users/tokens/crud.py b/backend/capellacollab/users/tokens/crud.py index cadf80e54..233c2e7d4 100644 --- a/backend/capellacollab/users/tokens/crud.py +++ b/backend/capellacollab/users/tokens/crud.py @@ -9,23 +9,24 @@ from sqlalchemy import orm from capellacollab.core import credentials +from capellacollab.users import models as users_models from . import models def create_token( db: orm.Session, - user_id: int, + user: users_models.DatabaseUser, description: str, expiration_date: datetime.date | None, - source: str | None, + source: str, ) -> tuple[models.DatabaseUserToken, str]: password = "collabmanager_" + credentials.generate_password(32) ph = argon2.PasswordHasher() if not expiration_date: expiration_date = datetime.date.today() + datetime.timedelta(days=30) db_token = models.DatabaseUserToken( - user_id=user_id, + user=user, hash=ph.hash(password), expiration_date=expiration_date, description=description, diff --git a/backend/capellacollab/users/tokens/models.py b/backend/capellacollab/users/tokens/models.py index 38345a840..23acc32be 100644 --- a/backend/capellacollab/users/tokens/models.py +++ b/backend/capellacollab/users/tokens/models.py @@ -38,9 +38,11 @@ class DatabaseUserToken(database.Base): __tablename__ = "basic_auth_token" id: orm.Mapped[int] = orm.mapped_column( - primary_key=True, index=True, autoincrement=True + init=False, primary_key=True, index=True, autoincrement=True + ) + user_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("users.id"), init=False ) - user_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("users.id")) user: orm.Mapped["DatabaseUser"] = orm.relationship( back_populates="tokens", foreign_keys=[user_id] ) diff --git a/backend/capellacollab/users/tokens/routes.py b/backend/capellacollab/users/tokens/routes.py index 33ee0b844..fe08816e9 100644 --- a/backend/capellacollab/users/tokens/routes.py +++ b/backend/capellacollab/users/tokens/routes.py @@ -34,7 +34,7 @@ def create_token_for_user( ) -> models.UserTokenWithPassword: token, password = crud.create_token( db, - user.id, + user, post_token.description, post_token.expiration_date, post_token.source, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9269dc89d..386abc17a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -132,7 +132,8 @@ module = [ "alembic.*", "jwt.*", "argon2.*", - "websocket.*" + "websocket.*", + "testcontainers.*", ] ignore_missing_imports = true diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 90fb2da8e..cba082daf 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -15,25 +15,24 @@ from capellacollab.core import database # isort: split +import typing as t + import capellacollab.projects.crud as projects_crud import capellacollab.projects.models as projects_models import capellacollab.projects.users.crud as projects_users_crud import capellacollab.projects.users.models as projects_users_models -import capellacollab.users.crud as users_crud import capellacollab.users.models as users_models from capellacollab.__main__ import app -from capellacollab.core import database from capellacollab.core.authentication.jwt_bearer import JWTBearer from capellacollab.core.database import migration from capellacollab.users import crud as users_crud from capellacollab.users import injectables as users_injectables -from capellacollab.users import models as users_models os.environ["DEVELOPMENT_MODE"] = "1" @pytest.fixture(name="postgresql", scope="session") -def fixture_postgresql() -> engine.Engine: +def fixture_postgresql() -> t.Generator[engine.Engine, None, None]: with postgres.PostgresContainer(image="postgres:14.1") as _postgres: database_url = _postgres.get_connection_url() @@ -45,7 +44,7 @@ def fixture_postgresql() -> engine.Engine: @pytest.fixture(name="db") def fixture_db( postgresql: engine.Engine, monkeypatch: pytest.MonkeyPatch -) -> orm.Session: +) -> t.Generator[orm.Session, None, None]: session_local = orm.sessionmaker( autocommit=False, autoflush=False, bind=postgresql ) @@ -72,6 +71,7 @@ def mock_get_db() -> orm.Session: def fixture_executor_name(monkeypatch: pytest.MonkeyPatch) -> str: name = str(uuid1()) + # pylint: disable=unused-argument async def bearer_passthrough(self, request: fastapi.Request): return name @@ -88,7 +88,7 @@ def fixture_unique_username() -> str: @pytest.fixture(name="admin") def fixture_admin( db: orm.Session, executor_name: str -) -> users_models.DatabaseUser: +) -> t.Generator[users_models.DatabaseUser, None, None]: admin = users_crud.create_user(db, executor_name, users_models.Role.ADMIN) def get_mock_own_user(): @@ -104,7 +104,7 @@ def get_mock_own_user(): @pytest.fixture(name="user") def fixture_user( db: orm.Session, executor_name: str -) -> users_models.DatabaseUser: +) -> t.Generator[users_models.DatabaseUser, None, None]: user = users_crud.create_user(db, executor_name, users_models.Role.USER) def get_mock_own_user(): diff --git a/backend/tests/projects/toolmodels/conftest.py b/backend/tests/projects/toolmodels/conftest.py index 401d9ca21..a34ef4545 100644 --- a/backend/tests/projects/toolmodels/conftest.py +++ b/backend/tests/projects/toolmodels/conftest.py @@ -45,7 +45,7 @@ def fixture_capella_model( db: orm.Session, project: project_models.DatabaseProject, capella_tool_version: tools_models.DatabaseVersion, -) -> toolmodels_models.DatabaseCapellaModel: +) -> toolmodels_models.DatabaseToolModel: model = toolmodels_models.PostCapellaModel( name="test", description="test", tool_id=capella_tool_version.tool.id ) @@ -91,7 +91,7 @@ def fixture_git_instance( @pytest.fixture(name="git_model") def fixture_git_models( - db: orm.Session, capella_model: toolmodels_models.DatabaseCapellaModel + db: orm.Session, capella_model: toolmodels_models.DatabaseToolModel ) -> project_git_models.DatabaseGitModel: git_model = project_git_models.PostGitModel( path="https://example.com/test/project", @@ -277,7 +277,7 @@ def fixture_t4c_repository( @pytest.fixture(name="t4c_model") def fixture_t4c_model( db: orm.Session, - capella_model: toolmodels_models.DatabaseCapellaModel, + capella_model: toolmodels_models.DatabaseToolModel, t4c_repository: settings_t4c_repositories_models.DatabaseT4CRepository, ) -> models_t4c_models.DatabaseT4CModel: return models_t4c_crud.create_t4c_model( diff --git a/backend/tests/projects/toolmodels/test_toolmodel_routes.py b/backend/tests/projects/toolmodels/test_toolmodel_routes.py index 2b84e72ed..56fc196f0 100644 --- a/backend/tests/projects/toolmodels/test_toolmodel_routes.py +++ b/backend/tests/projects/toolmodels/test_toolmodel_routes.py @@ -43,7 +43,7 @@ def fixture_override_dependency(): def test_rename_toolmodel_successful( - capella_model: toolmodels_models.DatabaseCapellaModel, + capella_model: toolmodels_models.DatabaseToolModel, project: projects_models.DatabaseProject, client: testclient.TestClient, executor_name: str, @@ -89,7 +89,7 @@ def test_rename_toolmodel_where_name_already_exists( def test_update_toolmodel_order_successful( - capella_model: toolmodels_models.DatabaseCapellaModel, + capella_model: toolmodels_models.DatabaseToolModel, project: projects_models.DatabaseProject, client: testclient.TestClient, executor_name: str, diff --git a/backend/tests/sessions/k8s_operator/test_session_k8s_operator.py b/backend/tests/sessions/k8s_operator/test_session_k8s_operator.py index 80eb15839..a5dda1488 100644 --- a/backend/tests/sessions/k8s_operator/test_session_k8s_operator.py +++ b/backend/tests/sessions/k8s_operator/test_session_k8s_operator.py @@ -73,7 +73,7 @@ def create_namespaced_pod_disruption_budget(namespace, budget): assert service_counter == 1 assert disruption_budget_counter == 1 - assert session["id"] == "testname" + assert session.id == "testname" def test_kill_session(monkeypatch: pytest.MonkeyPatch): diff --git a/backend/tests/sessions/test_session_hooks.py b/backend/tests/sessions/test_session_hooks.py index 999f1d2ca..5cdae6872 100644 --- a/backend/tests/sessions/test_session_hooks.py +++ b/backend/tests/sessions/test_session_hooks.py @@ -1,19 +1,24 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 +import datetime import typing as t +import uuid import pytest from sqlalchemy import orm from capellacollab import __main__ from capellacollab.core import models as core_models +from capellacollab.projects import models as projects_models from capellacollab.sessions import crud as sessions_crud from capellacollab.sessions import hooks as sessions_hooks from capellacollab.sessions import models as sessions_models from capellacollab.sessions import operators from capellacollab.sessions import routes as sessions_routes from capellacollab.sessions.hooks import interface as hooks_interface +from capellacollab.sessions.operators import k8s +from capellacollab.sessions.operators import models as operators_models from capellacollab.tools import injectables as tools_injectables from capellacollab.tools import models as tools_models from capellacollab.tools.integrations import models as tools_integration_models @@ -21,12 +26,14 @@ class MockOperator: - def start_session(self, *args, **kwargs) -> dict[str, t.Any]: - pass + # pylint: disable=unused-argument + def start_session(self, *args, **kwargs) -> k8s.Session: + return k8s.Session("test", {1}, datetime.datetime.now(), "test") def kill_session(self, *args, **kwargs) -> None: pass + # pylint: disable=unused-argument def create_persistent_volume(self, *args, **kwargs): return @@ -39,12 +46,16 @@ class TestSessionHook(hooks_interface.HookRegistration): def configuration_hook( self, db: orm.Session, + operator: operators.KubernetesOperator, user: users_models.DatabaseUser, tool_version: tools_models.DatabaseVersion, tool: tools_models.DatabaseTool, - username: str, **kwargs, - ) -> tuple[dict[str, str], list[core_models.Message]]: + ) -> tuple[ + dict[str, str], + list[operators_models.Volume], + list[core_models.Message], + ]: self.configuration_hook_counter += 1 return {}, [], [] @@ -71,16 +82,18 @@ def pre_session_termination_hook( def fixture_session_hook(monkeypatch: pytest.MonkeyPatch) -> TestSessionHook: hook = TestSessionHook() - REGISTERED_HOOKS: dict[str, hooks_interface.HookRegistration] = { + REGISTER_HOOKS_AUTO_USE: dict[str, hooks_interface.HookRegistration] = { "test": hook, } - monkeypatch.setattr(sessions_hooks, "REGISTERED_HOOKS", REGISTERED_HOOKS) + monkeypatch.setattr( + sessions_hooks, "REGISTER_HOOKS_AUTO_USE", REGISTER_HOOKS_AUTO_USE + ) return hook @pytest.fixture(autouse=True, name="mockoperator") -def fixture_mockoperator() -> MockOperator: +def fixture_mockoperator() -> t.Generator[MockOperator, None, None]: mock = MockOperator() def get_mock_operator(): @@ -93,19 +106,21 @@ def get_mock_operator(): del __main__.app.dependency_overrides[operators.get_operator] -@pytest.fixture(autouse=True, name="tool_with_test_integration") -def fixture_tool_with_test_integration( +@pytest.fixture(autouse=True, name="tool") +def fixture_tool( monkeypatch: pytest.MonkeyPatch, ) -> tools_models.DatabaseTool: - mock_integration = tools_integration_models.DatabaseToolIntegrations() - mock_integration.test = True tool = tools_models.DatabaseTool( - id=0, name="test", docker_image_template="test", - integrations=mock_integration, ) + mock_integration = tools_integration_models.DatabaseToolIntegrations( + tool=tool + ) + tool.integrations = mock_integration + + # pylint: disable=unused-argument def mock_get_existing_tool(*args, **kwargs) -> tools_models.DatabaseTool: return tool @@ -117,16 +132,16 @@ def mock_get_existing_tool(*args, **kwargs) -> tools_models.DatabaseTool: @pytest.fixture(autouse=True, name="tool_version") def fixture_tool_version( - monkeypatch: pytest.MonkeyPatch, -) -> tools_models.DatabaseTool: + monkeypatch: pytest.MonkeyPatch, tool: tools_models.DatabaseTool +) -> tools_models.DatabaseVersion: version = tools_models.DatabaseVersion( - id=0, - name="test", + name="test", is_recommended=False, is_deprecated=False, tool=tool ) + # pylint: disable=unused-argument def get_exisiting_tool_version( *args, **kwargs - ) -> tools_models.DatabaseTool: + ) -> tools_models.DatabaseVersion: return version monkeypatch.setattr( @@ -137,18 +152,42 @@ def get_exisiting_tool_version( return version -@pytest.mark.usefixtures("tool_with_test_integration") +@pytest.fixture(name="session") +def fixture_session( + user: users_models.DatabaseUser, + tool: tools_models.DatabaseTool, + tool_version: tools_models.DatabaseVersion, + project: projects_models.DatabaseProject, +) -> sessions_models.DatabaseSession: + return sessions_models.DatabaseSession( + str(uuid.uuid1()), + ports=[1], + created_at=datetime.datetime.now(), + host="", + type=sessions_models.WorkspaceType.PERSISTENT, + environment={}, + rdp_password="", + guacamole_username="", + owner=user, + tool=tool, + version=tool_version, + project=project, + ) + + +@pytest.mark.usefixtures("tool") def test_session_creation_hook_is_called( monkeypatch: pytest.MonkeyPatch, db: orm.Session, user: users_models.DatabaseUser, mockoperator: MockOperator, session_hook: TestSessionHook, + session: sessions_models.DatabaseSession, ): monkeypatch.setattr( sessions_routes, "start_persistent_guacamole_session", - lambda *args, **kwargs: sessions_models.DatabaseSession(id="test"), + lambda *args, **kwargs: session, ) monkeypatch.setattr( @@ -161,7 +200,7 @@ def test_session_creation_hook_is_called( sessions_models.PostPersistentSessionRequest(tool_id=0, version_id=0), user, db, - mockoperator, + mockoperator, # type: ignore "testuser", ) @@ -172,8 +211,8 @@ def test_session_creation_hook_is_called( def test_pre_termination_hook_is_called( monkeypatch: pytest.MonkeyPatch, mockoperator: MockOperator, - session_hook: TestSessionHook, - tool_with_test_integration: tools_models.DatabaseTool, + db: orm.Session, + session: sessions_models.DatabaseSession, ): monkeypatch.setattr( sessions_crud, @@ -182,9 +221,7 @@ def test_pre_termination_hook_is_called( ) sessions_routes.end_session( - sessions_models.PostPersistentSessionRequest(tool_id=0, version_id=0), - sessions_models.DatabaseSession(tool=tool_with_test_integration), - mockoperator, + db, + session, + mockoperator, # type: ignore ) - - session_hook.post_termination_hook_counter == 1 diff --git a/backend/tests/sessions/test_sessions_routes.py b/backend/tests/sessions/test_sessions_routes.py index 695c6b906..2ed069f6e 100644 --- a/backend/tests/sessions/test_sessions_routes.py +++ b/backend/tests/sessions/test_sessions_routes.py @@ -2,51 +2,39 @@ # SPDX-License-Identifier: Apache-2.0 +import datetime import json import typing as t -from datetime import datetime from uuid import uuid1 import pytest from fastapi import testclient +from sqlalchemy import orm import capellacollab.sessions.guacamole from capellacollab.__main__ import app -from capellacollab.projects.crud import create_project -from capellacollab.projects.toolmodels.crud import create_model -from capellacollab.projects.toolmodels.models import PostCapellaModel -from capellacollab.projects.toolmodels.modelsources.git.crud import ( - add_git_model_to_capellamodel, -) -from capellacollab.projects.toolmodels.modelsources.git.models import ( - PostGitModel, -) -from capellacollab.projects.users.crud import add_user_to_project -from capellacollab.projects.users.models import ( - ProjectUserPermission, - ProjectUserRole, +from capellacollab.projects import crud as projects_crud +from capellacollab.projects import models as projects_models +from capellacollab.projects.toolmodels import crud as toolmodels_crud +from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.projects.toolmodels.modelsources.git import crud as git_crud +from capellacollab.projects.toolmodels.modelsources.git import ( + models as git_models, ) +from capellacollab.projects.users import crud as project_users_crud +from capellacollab.projects.users import models as project_users_models +from capellacollab.sessions import crud as sessions_crud from capellacollab.sessions import models as sessions_models -from capellacollab.sessions.crud import ( - create_session, - get_session_by_id, - get_sessions_for_user, -) -from capellacollab.sessions.operators import get_operator +from capellacollab.sessions import operators +from capellacollab.sessions.operators import k8s from capellacollab.sessions.operators import models as operators_models +from capellacollab.tools import crud as tools_crud from capellacollab.tools import models as tools_models -from capellacollab.tools.crud import ( - create_tool, - create_tool_with_name, - create_version, - get_natures, - get_versions, -) -from capellacollab.tools.integrations.crud import update_integrations -from capellacollab.tools.integrations.models import PatchToolIntegrations -from capellacollab.users.crud import create_user -from capellacollab.users.injectables import get_own_user -from capellacollab.users.models import Role +from capellacollab.tools.integrations import crud as integrations_crud +from capellacollab.tools.integrations import models as integrations_models +from capellacollab.users import crud as users_crud +from capellacollab.users import injectables as users_injectables +from capellacollab.users import models as users_models @pytest.fixture(autouse=True) @@ -54,6 +42,7 @@ def guacamole(monkeypatch): def get_admin_token() -> str: return "test" + # pylint: disable=unused-argument def create_user( token: str, username: str = "", @@ -61,6 +50,7 @@ def create_user( ) -> None: return + # pylint: disable=unused-argument def create_connection( token: str, rdp_password: str, @@ -69,6 +59,7 @@ def create_connection( ): return {"identifier": "test"} + # pylint: disable=unused-argument def assign_user_to_connection( token: str, username: str, connection_id: str ): @@ -93,8 +84,9 @@ def assign_user_to_connection( class MockOperator: - sessions = [] + sessions: list[dict[str, t.Any]] = [] + # pylint: disable=unused-argument def start_session( self, image: str, @@ -102,24 +94,23 @@ def start_session( session_type: str, tool_name: str, version_name: str, - volumes: list[operators_models.Volume], - environment: dict[str, str | None], + environment: dict[str, str], ports: dict[str, int], - persistent_workspace_claim_name: str | None = None, + volumes: list[operators_models.Volume], prometheus_path="/metrics", prometheus_port=9118, limits="high", - ) -> dict[str, t.Any]: + ) -> k8s.Session: assert image self.sessions.append( {"docker_image": image, "environment": environment} ) - return { - "id": str(uuid1()), - "host": "test", - "ports": [1], - "created_at": datetime.now(), - } + return k8s.Session( + id=str(uuid1()), + host="test", + ports={1}, + created_at=datetime.datetime.now(), + ) def create_public_route( self, @@ -131,55 +122,67 @@ def create_public_route( ): pass + # pylint: disable=unused-argument def get_session_state(self, id: str) -> str: return "" def kill_session(self, id: str) -> None: pass + # pylint: disable=unused-argument def create_persistent_volume( - self, name: str, size: str, labels: dict[str, str] = None + self, + name: str, + size: str, + labels: dict[str, str] | None = None, ): return @pytest.fixture(autouse=True, name="kubernetes") -def fixture_kubernetes(): +def fixture_kubernetes() -> t.Generator[MockOperator, None, None]: mock = MockOperator() mock.sessions.clear() def get_mock_operator(): return mock - app.dependency_overrides[get_operator] = get_mock_operator + app.dependency_overrides[operators.get_operator] = get_mock_operator yield mock - del app.dependency_overrides[get_operator] + del app.dependency_overrides[operators.get_operator] @pytest.fixture(name="user") -def fixture_user(db, executor_name): - user = create_user(db, executor_name, Role.USER) +def fixture_user( + db: orm.Session, executor_name: str +) -> t.Generator[users_models.DatabaseUser, None, None]: + user = users_crud.create_user(db, executor_name, users_models.Role.USER) def get_mock_own_user(): return user - app.dependency_overrides[get_own_user] = get_mock_own_user + app.dependency_overrides[ + users_injectables.get_own_user + ] = get_mock_own_user yield user - del app.dependency_overrides[get_own_user] + del app.dependency_overrides[users_injectables.get_own_user] -def test_get_sessions_not_authenticated(client): +def test_get_sessions_not_authenticated(client: testclient.TestClient): response = client.get("/api/v1/sessions") assert response.status_code == 403 assert response.json() == {"detail": "Not authenticated"} def test_create_readonly_session_as_user( - client: testclient.TestClient, db, user, kubernetes + client: testclient.TestClient, + db: orm.Session, + user: users_models.DatabaseUser, + kubernetes: MockOperator, ): _, version = next( (v.tool, v) - for v in get_versions(db) + for v in tools_crud.get_versions(db) if v.tool.name == "Capella" and v.name == "5.0.0" ) @@ -202,7 +205,7 @@ def test_create_readonly_session_as_user( assert response.status_code == 200 out = response.json() - session = get_session_by_id(db, out["id"]) + session = sessions_crud.get_session_by_id(db, out["id"]) assert session assert session.owner_name == user.name @@ -216,9 +219,14 @@ def test_create_readonly_session_as_user( ) -def test_no_readonly_session_as_user(client, db, user, kubernetes): - tool = create_tool_with_name(db, "Test") - version = create_version(db, tool.id, "test") +def test_no_readonly_session_as_user( + client: testclient.TestClient, + db: orm.Session, + user: users_models.DatabaseUser, + kubernetes: MockOperator, +): + tool = tools_crud.create_tool_with_name(db, "Test") + version = tools_crud.create_version(db, tool, "test") model, git_model = setup_git_model_for_user(db, user, version) @@ -238,18 +246,21 @@ def test_no_readonly_session_as_user(client, db, user, kubernetes): assert response.status_code == 409 - sessions = get_sessions_for_user(db, user.name) + sessions = sessions_crud.get_sessions_for_user(db, user.name) assert not sessions assert not kubernetes.sessions def test_one_readonly_sessions_as_user_per_tool_version( - client, db, user, kubernetes + client: testclient.TestClient, + db: orm.Session, + user: users_models.DatabaseUser, + kubernetes: MockOperator, ): version = next( v - for v in get_versions(db) + for v in tools_crud.get_versions(db) if v.tool.name == "Capella" and v.name == "5.0.0" ) @@ -274,20 +285,24 @@ def test_one_readonly_sessions_as_user_per_tool_version( assert not kubernetes.sessions -def setup_git_model_for_user(db, user, version): - project = create_project(db, name=str(uuid1())) - nature = get_natures(db)[0] - add_user_to_project( +def setup_git_model_for_user( + db: orm.Session, + user: users_models.DatabaseUser, + version: tools_models.DatabaseVersion, +): + project = projects_crud.create_project(db, name=str(uuid1())) + nature = tools_crud.get_natures(db)[0] + project_users_crud.add_user_to_project( db, project, user, - ProjectUserRole.USER, - ProjectUserPermission.READ, + project_users_models.ProjectUserRole.USER, + project_users_models.ProjectUserPermission.READ, ) - model = create_model( + model = toolmodels_crud.create_model( db, project, - PostCapellaModel( + toolmodels_models.PostCapellaModel( name=str(uuid1()), description="", tool_id=version.tool.id ), tool=version.tool, @@ -295,17 +310,22 @@ def setup_git_model_for_user(db, user, version): nature=nature, ) git_path = str(uuid1()) - git_model = add_git_model_to_capellamodel( + git_model = git_crud.add_git_model_to_capellamodel( db, model, - PostGitModel( + git_models.PostGitModel( path=git_path, entrypoint="", revision="", username="", password="" ), ) return model, git_model -def setup_active_readonly_session(db, user, project, version): +def setup_active_readonly_session( + db: orm.Session, + user: users_models.DatabaseUser, + project: projects_models.DatabaseProject, + version: tools_models.DatabaseVersion, +): database_model = sessions_models.DatabaseSession( id=str(uuid1()), type=sessions_models.WorkspaceType.READONLY, @@ -315,19 +335,21 @@ def setup_active_readonly_session(db, user, project, version): version=version, host="test", ports=[1], + created_at=datetime.datetime.now(), + environment={}, ) - return create_session(db=db, session=database_model) + return sessions_crud.create_session(db=db, session=database_model) def test_create_persistent_session_as_user( client: testclient.TestClient, - db, - user, - kubernetes, + db: orm.Session, + user: users_models.DatabaseUser, + kubernetes: MockOperator, ): tool, version = next( (v.tool, v) - for v in get_versions(db) + for v in tools_crud.get_versions(db) if v.tool.name == "Capella" and v.name == "5.0.0" ) @@ -339,7 +361,7 @@ def test_create_persistent_session_as_user( }, ) out = response.json() - session = get_session_by_id(db, out["id"]) + session = sessions_crud.get_session_by_id(db, out["id"]) assert response.status_code == 200 assert session @@ -351,13 +373,13 @@ def test_create_persistent_session_as_user( def test_create_read_only_session_as_user( client: testclient.TestClient, - db, - user, - kubernetes, + db: orm.Session, + user: users_models.DatabaseUser, + kubernetes: MockOperator, ): version = next( v - for v in get_versions(db) + for v in tools_crud.get_versions(db) if v.tool.name == "Capella" and v.name == "6.0.0" ) @@ -377,8 +399,7 @@ def test_create_read_only_session_as_user( }, ) out = response.json() - print(out) - session = get_session_by_id(db, out["id"]) + session = sessions_crud.get_session_by_id(db, out["id"]) assert response.status_code == 200 assert session @@ -392,20 +413,28 @@ def test_create_read_only_session_as_user( ) -def test_create_persistent_jupyter_session(client, db, user, kubernetes): - jupyter = create_tool( +def test_create_persistent_jupyter_session( + client: testclient.TestClient, + db: orm.Session, + user: users_models.DatabaseUser, + kubernetes: MockOperator, +): + jupyter = tools_crud.create_tool( db, tools_models.DatabaseTool( name="jupyter", docker_image_template="jupyter/minimal-notebook:$version", ), ) - update_integrations( - db, jupyter.integrations, PatchToolIntegrations(jupyter=True) + assert jupyter.integrations + integrations_crud.update_integrations( + db, + jupyter.integrations, + integrations_models.PatchToolIntegrations(jupyter=True), ) - jupyter_version = create_version( - db, name="python-3.10.8", tool_id=jupyter.id + jupyter_version = tools_crud.create_version( + db, name="python-3.10.8", tool=jupyter ) response = client.post( @@ -416,7 +445,7 @@ def test_create_persistent_jupyter_session(client, db, user, kubernetes): }, ) out = response.json() - session = get_session_by_id(db, out["id"]) + session = sessions_crud.get_session_by_id(db, out["id"]) assert response.status_code == 200 assert session diff --git a/backend/tests/settings/conftest.py b/backend/tests/settings/conftest.py index d0aa936b1..e27b2bd9a 100644 --- a/backend/tests/settings/conftest.py +++ b/backend/tests/settings/conftest.py @@ -15,7 +15,7 @@ @pytest.fixture(name="test_tool_version") def fixture_test_tool_version(db: orm.Session) -> tools_models.DatabaseVersion: tool = tools_crud.create_tool_with_name(db, "Test") - return tools_crud.create_version(db, tool.id, "test") + return tools_crud.create_version(db, tool, "test") @pytest.fixture(name="admin_user")