diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cd3a549c1217eabb2ff186be5d067735fdd51d1c..8aa44886588ae5feeacad5da6e55c8a8724a4d54 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,9 +4,8 @@ variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" PYTHONPATH: "$CI_PROJECT_DIR" OBJECT_GATEWAY_URI: "http://127.0.0.1:8001" - CEPH_ACCESS_KEY: "" - CEPH_SECRET_KEY: "" - CEPH_USERNAME: "" + BUCKET_CEPH_ACCESS_KEY: "" + BUCKET_CEPH_SECRET_KEY: "" FF_NETWORK_PER_BUILD: 1 PUBLIC_KEY_VALUE: "empty" OPA_URI: "http://127.0.0.1:8181" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0c5251a83107b080366ebfd6da4ce551e98283f..f547a4fe938f063d7c07547e799a4fb7f3491c07 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,17 +15,17 @@ repos: - id: check-merge-conflict - id: check-ast - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black files: app args: [--check] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.262' + rev: 'v0.0.281' hooks: - id: ruff - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.2.0 + rev: v1.4.1 hooks: - id: mypy files: app diff --git a/README.md b/README.md index 534f2693216da226fc1ce74c980a0215006b758e..6ce47f4a1ec8eb9b83809df3c14d45158c0c48f3 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ This is the Workflow service of the CloWM service. | Variable | Default | Value | Description | |----------------------------------------|--------------------|---------------------------------|----------------------------------------------------------------------------------------------| | `PUBLIC_KEY_VALUE` / `PUBLIC_KEY_FILE` | randomly generated | Public Key / Path to Public Key | Public part of RSA Key in PEM format to verify JWTs | -| `DB_HOST` | unset | <db hostname / IP> | IP or Hostname Adress of DB | +| `DB_HOST` | unset | <db hostname / IP> | IP or Hostname Address of DB | | `DB_PORT` | 3306 | Number | Port of the database | | `DB_USER` | unset | \<db username> | Username of the database user | | `DB_PASSWORD` | unset | \<db password> | Password of the database user | | `DB_DATABASE` | unset | \<db name> | Name of the database | | `OBJECT_GATEWAY_URI` | unset | HTTP URL | HTTP URL of the Ceph Object Gateway | -| `CEPH_ACCESS_KEY` | unset | \<access key> | Ceph access key with admin privileges | -| `CEPH_SECRET_KEY` | unset | \<secret key> | Ceph secret key with admin privileges | +| `BUCKET_CEPH_ACCESS_KEY` | unset | \<access key> | Ceph access key with admin privileges | +| `BUCKET_CEPH_SECRET_KEY` | unset | \<secret key> | Ceph secret key with admin privileges | | `OPA_URI` | unset | HTTP URL | HTTP URL of the OPA service | | `SLURM_ENDPOINT` | unset | HTTP URL | HTTP URL to communicate with the Slurm cluster | | `SLURM_TOKEN` | unset | \<JWT> | JWT for communication with the Slurm REST API. Should belong to the user of the `SLURM_USER` | diff --git a/app/api/dependencies.py b/app/api/dependencies.py index eeba6f3cdb672e54ff8b78f263d33ebf291ad431..3dad80d4ef50a6e15e6c096ab2cdad0e643f7832 100644 --- a/app/api/dependencies.py +++ b/app/api/dependencies.py @@ -41,7 +41,7 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]: Async session object with the database """ async with get_async_session( - settings.SQLALCHEMY_DATABASE_ASYNC_URI, verbose=settings.SQLALCHEMY_VERBOSE_LOGGER + str(settings.SQLALCHEMY_DATABASE_ASYNC_URI), verbose=settings.SQLALCHEMY_VERBOSE_LOGGER )() as db: yield db @@ -179,7 +179,7 @@ async def get_current_user(token: JWT = Depends(decode_bearer_token), db: AsyncS async def get_current_workflow( - wid: UUID = Path(..., description="ID of a workflow", example="0cc78936-381b-4bdd-999d-736c40591078"), + wid: UUID = Path(..., description="ID of a workflow", examples=["0cc78936-381b-4bdd-999d-736c40591078"]), db: AsyncSession = Depends(get_db), ) -> Workflow: """ @@ -207,7 +207,7 @@ async def get_current_workflow( async def get_current_workflow_execution( - eid: UUID = Path(..., description="ID of a workflow execution.", example="0cc78936-381b-4bdd-999d-736c40591078"), + eid: UUID = Path(..., description="ID of a workflow execution.", examples=["0cc78936-381b-4bdd-999d-736c40591078"]), db: AsyncSession = Depends(get_db), ) -> WorkflowExecution: """ diff --git a/app/api/endpoints/workflow.py b/app/api/endpoints/workflow.py index 966548006c8583d33c9c4f6e26eb86fd0446ec93..236bbc9c3ef7a4d8dc534caee506ac94294e7251 100644 --- a/app/api/endpoints/workflow.py +++ b/app/api/endpoints/workflow.py @@ -1,7 +1,8 @@ from typing import Annotated, Any, Awaitable, Callable from clowmdb.models import Workflow, WorkflowVersion -from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, Query, Response, UploadFile, status +from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Query, Response, UploadFile, status +from pydantic import AnyHttpUrl from app.api.dependencies import AuthorizationDependency, CurrentUser, CurrentWorkflow, DBSession, HTTPClient, S3Service from app.api.utils import check_repo, upload_icon @@ -38,7 +39,7 @@ async def list_workflows( | None = Query( None, description="Filter for workflow by developer. If current user is the same as developer ID, permission 'workflow:list' required, otherwise 'workflow:list_filter'.", # noqa: E501 - example="28c5353b8bb34984a8bd4169ba94c606", + examples=["28c5353b8bb34984a8bd4169ba94c606"], ), ) -> list[WorkflowOut]: """ @@ -91,7 +92,40 @@ async def create_workflow( authorization: Authorization, client: HTTPClient, s3: S3Service, - workflow: WorkflowIn = Depends(WorkflowIn.as_form), # type: ignore[attr-defined] + name: str = Form( + ..., + description="Short descriptive name of the workflow", + examples=["RNA ReadMapper"], + max_length=64, + min_length=3, + ), + short_description: str = Form( + ..., + description="Short description of the workflow", + examples=["This should be a very good example of a short and descriptive description"], + max_length=256, + min_length=64, + ), + repository_url: AnyHttpUrl = Form( + ..., + description="URL to the Git repository belonging to this workflow", + examples=["https://github.com/example-user/example"], + ), + git_commit_hash: str = Form( + ..., + description="Hash of the git commit", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], + pattern=r"^[0-9a-f]{40}$", + min_length=40, + max_length=40, + ), + initial_version: str = Form( + default="v1.0.0", + description="Initial version of the Workflow. Should follow semantic versioning", + examples=["v1.0.0"], + min_length=5, + max_length=10, + ), icon: UploadFile | None = File(None, description="Optional Icon for the Workflow."), ) -> WorkflowOut: """ @@ -102,10 +136,18 @@ async def create_workflow( ---------- background_tasks : fastapi.BackgroundTasks Entrypoint for new BackgroundTasks. Provided by FastAPI. - workflow : app.schemas.workflow.WorkflowIn - Parameters to create a new Workflow. HTML Form. icon : fastapi.UploadFile | None, default None Optional Icon for the first workflow version. HTML Form + name : fastapi.Form + Required Name for the workflow. HTML Form. + short_description : fastapi.Form + Required short description for the workflow. HTML Form. + repository_url : fastapi.Form + Required repository URL of the workflow. HTML Form. + git_commit_hash : fastapi.Form + Required Git commit hash of the workflow version. HTML Form. + initial_version : fastapi.Form + Initials version of the workflow. HTML Form. db : sqlalchemy.ext.asyncio.AsyncSession. Async database session to perform query on. Dependency Injection. current_user : clowmdb.models.User @@ -123,6 +165,13 @@ async def create_workflow( The newly created workflow """ await authorization("create") + workflow = WorkflowIn( + name=name, + short_description=short_description, + repository_url=repository_url, + git_commit_hash=git_commit_hash, + initial_version=initial_version, + ) # Check if name is workflow name is already taken if await CRUDWorkflow.get_by_name(db, workflow.name) is not None: raise HTTPException( @@ -156,8 +205,8 @@ async def create_workflow( icon_slug = upload_icon(s3=s3, background_tasks=background_tasks, icon=icon) workflow_db = await CRUDWorkflow.create(db, workflow, current_user.uid, icon_slug=icon_slug) - initial_version = await CRUDWorkflowVersion.get(db, workflow.git_commit_hash) - return WorkflowOut.from_db_workflow(workflow_db, [initial_version]) + initial_version_workflow = await CRUDWorkflowVersion.get(db, workflow.git_commit_hash) + return WorkflowOut.from_db_workflow(workflow_db, [initial_version_workflow]) @router.get("/{wid}", status_code=status.HTTP_200_OK, summary="Get a workflow") @@ -285,11 +334,25 @@ async def update_workflow( current_user: CurrentUser, s3: S3Service, authorization: Authorization, - version_update: WorkflowVersionUpdate = Depends(WorkflowVersionUpdate.as_form), # type: ignore[attr-defined] icon: UploadFile | None = File( None, description="Optional Icon for the workflow version. If None, then the previous one will be reused." ), + version: str = Form( + ..., + description="Version of the Workflow. Should follow semantic versioning", + examples=["v1.1.0"], + min_length=5, + max_length=10, + ), + git_commit_hash: str = Form( + ..., + description="Hash of the git commit", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], + pattern=r"^[0-9a-f]{40}$", + min_length=40, + max_length=40, + ), ) -> WorkflowVersionFull: """ Create a new workflow version.\n @@ -301,10 +364,12 @@ async def update_workflow( Entrypoint for new BackgroundTasks. Provided by FastAPI workflow : clowmdb.models.Workflow Workflow with given ID. Dependency Injection. - version_update : app.schemas.workflow_version.WorkflowVersionUpdate - Parameters to create a new Workflow version. HTML Form. icon : fastapi.UploadFile | None, default None Optional Icon for the workflow version. If None, then the previous one will be reused. HTML Form. + version : Fastapi.Form + Required version for the updated workflow. HTML Form. + git_commit_hash : fastapi.Form + Required git commit hash for the updated workflow. HTML Form db : sqlalchemy.ext.asyncio.AsyncSession. Async database session to perform query on. Dependency Injection. current_user : clowmdb.models.User @@ -322,6 +387,7 @@ async def update_workflow( The new workflow version """ await authorization("update") + version_update = WorkflowVersionUpdate(version=version, git_commit_hash=git_commit_hash) if current_user.uid != workflow.developer_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only the developer can update his workflow") diff --git a/app/api/endpoints/workflow_execution.py b/app/api/endpoints/workflow_execution.py index 677b08dd440d6b2360f2097faf8ed28c5fa831c2..c49b396ed12cce962919268a2018d5edd0a7f6d6 100644 --- a/app/api/endpoints/workflow_execution.py +++ b/app/api/endpoints/workflow_execution.py @@ -237,7 +237,7 @@ async def list_workflow_executions( | None = Query( None, description="Filter for workflow executions by a user. If none, Permission 'workflow_execution:read_any' required.", # noqa: E501 - example="28c5353b8bb34984a8bd4169ba94c606", + examples=["28c5353b8bb34984a8bd4169ba94c606"], ), execution_status: list[WorkflowExecution.WorkflowExecutionStatus] | None = Query(None, description="Filter for status of workflow execution"), @@ -245,7 +245,7 @@ async def list_workflow_executions( | None = Query( None, description="Filter for workflow version", - example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], regex=r"^[0-9a-f]{40}$", ), ) -> list[WorkflowExecutionOut]: diff --git a/app/api/endpoints/workflow_version.py b/app/api/endpoints/workflow_version.py index 50e4ad73875aa53971d7d01ec57606a794ccfec5..ae1c356caf3e358a6f8b5884c95504eafc5a907b 100644 --- a/app/api/endpoints/workflow_version.py +++ b/app/api/endpoints/workflow_version.py @@ -18,7 +18,7 @@ GitCommitHash = Annotated[ ..., description="Git commit git_commit_hash of specific version.", regex=r"^([0-9a-f]{40}|latest)$", - example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], ), ] @@ -89,18 +89,7 @@ async def get_workflow_version( ..., description="Git commit git_commit_hash of specific version or 'latest'.", regex=r"^([0-9a-f]{40}|latest)$", - examples={ - "normal": { - "summary": "Latest version", - "description": "Get the latest version of the workflow", - "value": "latest", - }, - "pseudo-folder": { - "summary": "Specific version", - "description": "Get a specific version of the workflow", - "value": "ba8bcd9294c2c96aedefa1763a84a18077c50c0f", - }, - }, + examples=["latest", "ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], ), ) -> WorkflowVersionFull: """ diff --git a/app/ceph/rgw.py b/app/ceph/rgw.py index 5c8cc331ff6c6585e161e8c9a10f21ae16a530a1..ca195d8afbae065f2a1fb20923ee45645afa43d3 100644 --- a/app/ceph/rgw.py +++ b/app/ceph/rgw.py @@ -11,8 +11,8 @@ else: s3_resource: S3ServiceResource = resource( service_name="s3", - endpoint_url=settings.OBJECT_GATEWAY_URI, - aws_access_key_id=settings.CEPH_ACCESS_KEY, - aws_secret_access_key=settings.CEPH_SECRET_KEY, - verify=settings.OBJECT_GATEWAY_URI.startswith("https"), + endpoint_url=str(settings.OBJECT_GATEWAY_URI)[:-1], + aws_access_key_id=settings.BUCKET_CEPH_ACCESS_KEY, + aws_secret_access_key=settings.BUCKET_CEPH_SECRET_KEY, + verify=str(settings.OBJECT_GATEWAY_URI).startswith("https"), ) diff --git a/app/check_ceph_connection.py b/app/check_ceph_connection.py index 23a95bd156effde36c1145291e0e7764403c9ea6..0e73fd3076b7877a306cbe83fe3b2fc2843bd7cc 100644 --- a/app/check_ceph_connection.py +++ b/app/check_ceph_connection.py @@ -21,7 +21,7 @@ wait_seconds = 2 ) def init() -> None: try: - response = httpx.get(settings.OBJECT_GATEWAY_URI, timeout=5.0, follow_redirects=False) + response = httpx.get(str(settings.OBJECT_GATEWAY_URI), timeout=5.0, follow_redirects=False) assert response.status_code == status.HTTP_200_OK except Exception as e: logger.error(e) diff --git a/app/check_database_connection.py b/app/check_database_connection.py index 62db2c5a1b46b37bfcd13abe267efeec942e75e2..ae54e93d69f91628dd1b90245652923206877406 100644 --- a/app/check_database_connection.py +++ b/app/check_database_connection.py @@ -22,7 +22,7 @@ wait_seconds = 2 ) def init() -> None: try: - with get_session(url=settings.SQLALCHEMY_DATABASE_NORMAL_URI)() as db: + with get_session(url=str(settings.SQLALCHEMY_DATABASE_NORMAL_URI))() as db: # Try to create session to check if DB is awake db_revision = db.execute(text("SELECT version_num FROM alembic_version LIMIT 1")).scalar_one_or_none() if db_revision != latest_revision: diff --git a/app/core/config.py b/app/core/config.py index ddd9d4fb1bed6fe9e49dd72d427fd4499327403a..b389531b465ec4674fc2028f243cfc595ff273c0 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,79 +1,93 @@ from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional -from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, Field, validator +from pydantic import AnyHttpUrl, AnyUrl, Field, computed_field +from pydantic_settings import BaseSettings, SettingsConfigDict def _assemble_db_uri(values: Dict[str, Any], async_flag: bool = True) -> Any: return AnyUrl.build( scheme=f"mysql+{'aiomysql' if async_flag else 'pymysql'}", password=values.get("DB_PASSWORD"), - user=values.get("DB_USER"), - port=str(values.get("DB_PORT")), - host=values.get("DB_HOST"), - path=f"/{values.get('DB_DATABASE') or ''}", + username=values.get("DB_USER"), + port=values.get("DB_PORT"), + host=values.get("DB_HOST"), # type: ignore[arg-type] + path=f"{values.get('DB_DATABASE') or ''}", ) +def _load_public_key(pub_key_val: Optional[str], pub_key_file: Optional[Path]) -> str: + pub_key = "" + if pub_key_val is not None: + pub_key = pub_key_val + if pub_key_file is not None: + with open(pub_key_file) as f: + pub_key = f.read() + if len(pub_key) == 0: + raise ValueError("PUBLIC_KEY_VALUE or PUBLIC_KEY_FILE must be set") + return pub_key + + class Settings(BaseSettings): API_PREFIX: str = Field("/api/workflow-service", description="Path Prefix for all API endpoints.") public_key_value: str | None = Field( - None, description="Public RSA Key in PEM format to sign the JWTs.", env="PUBLIC_KEY_VALUE" + None, description="Public RSA Key in PEM format to sign the JWTs.", validation_alias="PUBLIC_KEY_VALUE" ) public_key_file: Path | None = Field( - None, description="Path to Public RSA Key in PEM format to sign the JWTs.", env="PUBLIC_KEY_FILE" + None, description="Path to Public RSA Key in PEM format to sign the JWTs.", validation_alias="PUBLIC_KEY_FILE" ) - PUBLIC_KEY: str = "" - - @validator("PUBLIC_KEY", pre=True) - def load_public_key(cls, v: str, values: Dict[str, Any]) -> str: - pub_key = "" - if values["public_key_value"] is not None: - pub_key = values["public_key_value"] - if values["public_key_file"] is not None: - with open(values["public_key_file"]) as f: - pub_key = f.read() - if len(pub_key) == 0: - raise ValueError("PUBLIC_KEY_VALUE or PUBLIC_KEY_FILE must be set") - return pub_key + + @computed_field # type: ignore[misc] + @property + def PUBLIC_KEY(self) -> str: + return _load_public_key(self.public_key_value, self.public_key_file) # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = Field([], description="List of all valid CORS origins") - @validator("BACKEND_CORS_ORIGINS", pre=True) - def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: - if isinstance(v, str) and not v.startswith("["): - return [i.strip() for i in v.split(",")] - elif isinstance(v, (list, str)): - return v - raise ValueError(v) - DB_HOST: str = Field(..., description="Host of the database.") DB_USER: str = Field(..., description="Username in the database.") DB_PASSWORD: str = Field(..., description="Password for the database user.") DB_DATABASE: str = Field(..., description="Name of the database.") DB_PORT: int = Field(3306, description="Port of the database.") SQLALCHEMY_VERBOSE_LOGGER: bool = Field(False, description="Flag whether to print the SQL Queries in the logs.") - SQLALCHEMY_DATABASE_ASYNC_URI: AnyUrl | None = None - - @validator("SQLALCHEMY_DATABASE_ASYNC_URI", pre=True) - def assemble_async_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: - if isinstance(v, str): - return v - return _assemble_db_uri(values, async_flag=True) - SQLALCHEMY_DATABASE_NORMAL_URI: AnyUrl | None = None - - @validator("SQLALCHEMY_DATABASE_NORMAL_URI", pre=True) - def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: - if isinstance(v, str): - return v - return _assemble_db_uri(values, async_flag=False) + @computed_field # type: ignore[misc] + @property + def SQLALCHEMY_DATABASE_ASYNC_URI(self) -> AnyUrl: + return _assemble_db_uri( + { + "DB_HOST": self.DB_HOST, + "DB_USER": self.DB_USER, + "DB_PASSWORD": self.DB_PASSWORD, + "DB_DATABASE": self.DB_DATABASE, + "DB_PORT": self.DB_PORT, + }, + async_flag=True, + ) + + @computed_field # type: ignore[misc] + @property + def SQLALCHEMY_DATABASE_NORMAL_URI(self) -> AnyUrl: + return _assemble_db_uri( + { + "DB_HOST": self.DB_HOST, + "DB_USER": self.DB_USER, + "DB_PASSWORD": self.DB_PASSWORD, + "DB_DATABASE": self.DB_DATABASE, + "DB_PORT": self.DB_PORT, + }, + async_flag=False, + ) OBJECT_GATEWAY_URI: AnyHttpUrl = Field(..., description="URI of the Ceph Object Gateway.") - CEPH_ACCESS_KEY: str = Field(..., description="Access key for the Ceph Object Gateway with admin privileges.") - CEPH_SECRET_KEY: str = Field(..., description="Secret key for the Ceph Object Gateway with admin privileges.") + BUCKET_CEPH_ACCESS_KEY: str = Field( + ..., description="Access key for the Ceph Object Gateway with admin privileges." + ) + BUCKET_CEPH_SECRET_KEY: str = Field( + ..., description="Secret key for the Ceph Object Gateway with admin privileges." + ) OPA_URI: AnyHttpUrl = Field(..., description="URI of the OPA Service") OPA_POLICY_PATH: str = Field("/clowm/authz/allow", description="Path to the OPA Policy for Authorization") PARAMS_BUCKET: str = Field("nxf-params", description="Bucket where the nextflow configurations should be saved") @@ -92,11 +106,7 @@ class Settings(BaseSettings): ) ACTIVE_WORKFLOW_EXECUTION_LIMIT: int = Field(3, description="The limit of active workflow executions per user.") DEV_SYSTEM: bool = Field(False, description="Open a endpoint where to execute arbitrary workflows.") - - class Config: - case_sensitive = True - env_file = ".env" - secrets_dir = "/run/secrets" + model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", secrets_dir="/run/secrets", extra="ignore") settings = Settings() diff --git a/app/core/security.py b/app/core/security.py index f34d133aefcc09f58909bdd4204b001bdb7cc61a..60f9fa2f6074b78fdcca0a4c73910d58189f8016 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -54,7 +54,7 @@ async def request_authorization(request_params: AuthzRequest, client: AsyncClien Response by the Auth service about the authorization request """ response = await client.post( - f"{settings.OPA_URI}/v1/data{settings.OPA_POLICY_PATH}", json={"input": request_params.dict()} + f"{settings.OPA_URI}v1/data{settings.OPA_POLICY_PATH}", json={"input": request_params.model_dump()} ) parsed_response = AuthzResponse(**response.json()) diff --git a/app/git_repository/__init__.py b/app/git_repository/__init__.py index be15a9752b1c11297e7e1c913ba6b4530c619718..2474334881f98f2a85600c7b5d1737f9f068f6f0 100644 --- a/app/git_repository/__init__.py +++ b/app/git_repository/__init__.py @@ -1,11 +1,13 @@ from urllib.parse import urlparse +from pydantic import AnyHttpUrl + from app.git_repository.abstract_repository import GitRepository from app.git_repository.github import GithubRepository from app.git_repository.gitlab import GitlabRepository -def build_repository(url: str, git_commit_hash: str) -> GitRepository: +def build_repository(url: AnyHttpUrl, git_commit_hash: str) -> GitRepository: """ Build the right git repository object based on the url @@ -14,16 +16,16 @@ def build_repository(url: str, git_commit_hash: str) -> GitRepository: url : str URL of the git repository git_commit_hash : str - Pin dowm git commit hash + Pin down git commit hash Returns ------- repo : GitRepository Specialized Git repository object """ - domain = urlparse(url).netloc + domain = urlparse(str(url)).netloc if "github" in domain: - return GithubRepository(url=url, git_commit_hash=git_commit_hash) + return GithubRepository(url=str(url), git_commit_hash=git_commit_hash) elif "gitlab" in domain: - return GitlabRepository(url=url, git_commit_hash=git_commit_hash) + return GitlabRepository(url=str(url), git_commit_hash=git_commit_hash) raise NotImplementedError("Unknown Git repository Provider") diff --git a/app/git_repository/abstract_repository.py b/app/git_repository/abstract_repository.py index 054f5cd213c7cc5a8aee938f97078c389bcd4b7e..180b64518705e0e18ab4a63584278852c5f9e2f2 100644 --- a/app/git_repository/abstract_repository.py +++ b/app/git_repository/abstract_repository.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING from fastapi import HTTPException, status from httpx import AsyncClient +from pydantic import AnyHttpUrl if TYPE_CHECKING: from mypy_boto3_s3.service_resource import Object @@ -47,7 +48,7 @@ class GitRepository(ABC): ... @abstractmethod - def downloadFileURL(self, filepath: str) -> str: + def downloadFileURL(self, filepath: str) -> AnyHttpUrl: """ Construct an URL where to download a file from @@ -86,7 +87,7 @@ class GitRepository(ABC): exist : bool Flag if the file exists. """ - response = await client.head(self.downloadFileURL(filepath), follow_redirects=True) + response = await client.head(str(self.downloadFileURL(filepath)), follow_redirects=True) return response.status_code == status.HTTP_200_OK async def check_files_exist(self, files: list[str], client: AsyncClient, raise_error: bool = True) -> list[bool]: @@ -148,6 +149,6 @@ class GitRepository(ABC): file_handle : BytesIO Write the file into this stream in binary mode. """ - async with client.stream("GET", self.downloadFileURL(filepath)) as response: + async with client.stream("GET", str(self.downloadFileURL(filepath))) as response: async for chunk in response.aiter_bytes(): file_handle.write(chunk) diff --git a/app/git_repository/github.py b/app/git_repository/github.py index 73bbda133e2c131bb759f56dafa19488ee1a4804..c8a634bb54aa36a2a69705473a130f430513f8fb 100644 --- a/app/git_repository/github.py +++ b/app/git_repository/github.py @@ -30,7 +30,7 @@ class GithubRepository(GitRepository): self.repository = bla[1] self.commit = git_commit_hash - def downloadFileURL(self, filepath: str) -> str: + def downloadFileURL(self, filepath: str) -> AnyHttpUrl: return AnyHttpUrl.build( scheme="https", host="raw.githubusercontent.com/", diff --git a/app/git_repository/gitlab.py b/app/git_repository/gitlab.py index 548f9e1ccf5d8e250d482336062e1580567a1072..55a9016f6f627fbcc8393b51edfec5883e4d6c13 100644 --- a/app/git_repository/gitlab.py +++ b/app/git_repository/gitlab.py @@ -31,7 +31,7 @@ class GitlabRepository(GitRepository): self.repository = path_parts[-1] self.commit = git_commit_hash - def downloadFileURL(self, filepath: str) -> str: + def downloadFileURL(self, filepath: str) -> AnyHttpUrl: return AnyHttpUrl.build( scheme="https", host=self.domain, diff --git a/app/schemas/security.py b/app/schemas/security.py index 5bd3799182272053553920c247ddf47010b7e177..3c0f596a45e25b0eaa0ea6b86f961733c0d7e3c7 100644 --- a/app/schemas/security.py +++ b/app/schemas/security.py @@ -9,7 +9,7 @@ class AuthzResponse(BaseModel): decision_id: str = Field( ..., description="Decision ID for for the specific decision", - example="8851dce0-7546-4e81-a89d-111cbec376c1", + examples=["8851dce0-7546-4e81-a89d-111cbec376c1"], ) result: bool = Field(..., description="Result of the Authz request") @@ -17,9 +17,9 @@ class AuthzResponse(BaseModel): class AuthzRequest(BaseModel): """Schema for a Request to OPA""" - uid: str = Field(..., description="UID of user", example="28c5353b8bb34984a8bd4169ba94c606") - operation: str = Field(..., description="Operation the user wants to perform", example="read") - resource: str = Field(..., description="Resource the operation should be performed on", example="bucket") + uid: str = Field(..., description="UID of user", examples=["28c5353b8bb34984a8bd4169ba94c606"]) + operation: str = Field(..., description="Operation the user wants to perform", examples=["read"]) + resource: str = Field(..., description="Resource the operation should be performed on", examples=["bucket"]) class JWT(BaseModel): diff --git a/app/schemas/utils.py b/app/schemas/utils.py deleted file mode 100644 index b8f42b8152d3527ff14325674fea8f660c839b45..0000000000000000000000000000000000000000 --- a/app/schemas/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -import inspect -from typing import Any, Type - -from fastapi import Form -from pydantic import BaseModel -from pydantic.fields import ModelField - - -# https://stackoverflow.com/a/60670614 -def as_form(cls: Type[BaseModel]) -> Type[BaseModel]: - new_parameters = [] - - for field_name, model_field in cls.__fields__.items(): - model_field: ModelField # type: ignore - - new_parameters.append( - inspect.Parameter( - model_field.alias, - inspect.Parameter.POSITIONAL_ONLY, - default=Form(...) if model_field.required else Form(model_field.default), - annotation=model_field.outer_type_, - ) - ) - - async def as_form_func(**data: dict[str, Any]) -> BaseModel: - return cls(**data) - - sig = inspect.signature(as_form_func) - sig = sig.replace(parameters=new_parameters) - as_form_func.__signature__ = sig # type: ignore - setattr(cls, "as_form", as_form_func) - return cls diff --git a/app/schemas/workflow.py b/app/schemas/workflow.py index 9b5e6d6242b394144183b8c6c64e7f0e1bbc3958..00722197d3c88f6e03a60669136f6daae3e5fc2e 100644 --- a/app/schemas/workflow.py +++ b/app/schemas/workflow.py @@ -3,54 +3,60 @@ from uuid import UUID from clowmdb.models import Workflow as WorkflowDB from clowmdb.models import WorkflowVersion as WorkflowVersionDB -from pydantic import AnyHttpUrl, BaseModel, Field +from pydantic import AnyHttpUrl, BaseModel, Field, FieldSerializationInfo, field_serializer -from app.schemas.utils import as_form from app.schemas.workflow_version import WorkflowVersionReduced class _BaseWorkflow(BaseModel): name: str = Field( - ..., description="Short descriptive name of the workflow", example="RNA ReadMapper", max_length=64, min_length=3 + ..., + description="Short descriptive name of the workflow", + examples=["RNA ReadMapper"], + max_length=64, + min_length=3, ) short_description: str = Field( ..., description="Short description of the workflow", - example="This should be a very good example of a short and descriptive description", + examples=["This should be a very good example of a short and descriptive description"], max_length=256, min_length=64, ) repository_url: AnyHttpUrl = Field( ..., description="URL to the Git repository belonging to this workflow", - example="https://github.com/example-user/example", + examples=["https://github.com/example-user/example"], ) + @field_serializer("repository_url") + def serialize_dt(self, url: AnyHttpUrl, _info: FieldSerializationInfo) -> str: + return str(url) + -@as_form class WorkflowIn(_BaseWorkflow): git_commit_hash: str = Field( ..., description="Hash of the git commit", - example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", - regex=r"^[0-9a-f]{40}$", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], + pattern=r"^[0-9a-f]{40}$", min_length=40, max_length=40, ) initial_version: str = Field( "v1.0.0", description="Initial version of the Workflow. Should follow semantic versioning", - example="v1.0.0", - minlength=5, + examples=["v1.0.0"], + min_length=5, max_length=10, ) class WorkflowOut(_BaseWorkflow): - workflow_id: UUID = Field(..., description="Id of the workflow", example="0cc78936-381b-4bdd-999d-736c40591078") + workflow_id: UUID = Field(..., description="Id of the workflow", examples=["0cc78936-381b-4bdd-999d-736c40591078"]) versions: list[WorkflowVersionReduced] = Field(..., description="Versions of the workflow") developer_id: str = Field( - ..., description="Id of developer of the workflow", example="28c5353b8bb34984a8bd4169ba94c606" + ..., description="Id of developer of the workflow", examples=["28c5353b8bb34984a8bd4169ba94c606"] ) @staticmethod @@ -71,5 +77,5 @@ class WorkflowOut(_BaseWorkflow): class WorkflowStatistic(BaseModel): - day: date = Field(..., description="Day of the datapoint", example=date(day=1, month=1, year=2023)) - count: int = Field(..., description="Number of started workflows on that day", example=1) + day: date = Field(..., description="Day of the datapoint", examples=[date(day=1, month=1, year=2023)]) + count: int = Field(..., description="Number of started workflows on that day", examples=[1]) diff --git a/app/schemas/workflow_execution.py b/app/schemas/workflow_execution.py index d59ab60762204e689e7272863715a96ee9d8c35c..19df2e5ceb05a2d480cab72b44ec1d4b3bded920 100644 --- a/app/schemas/workflow_execution.py +++ b/app/schemas/workflow_execution.py @@ -3,15 +3,15 @@ from typing import Any from uuid import UUID from clowmdb.models import WorkflowExecution -from pydantic import AnyHttpUrl, BaseModel, Field +from pydantic import AnyHttpUrl, BaseModel, Field, FieldSerializationInfo, field_serializer class _BaseWorkflowExecution(BaseModel): workflow_version_id: str = Field( ..., description="Workflow version git commit hash", - example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", - regex=r"^[0-9a-f]{40}$", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], + pattern=r"^[0-9a-f]{40}$", min_length=40, max_length=40, ) @@ -19,7 +19,7 @@ class _BaseWorkflowExecution(BaseModel): None, description="Optional notes for this workflow execution", max_length=2**16, - example="Some workflow execution specific notes", + examples=["Some workflow execution specific notes"], ) @@ -30,33 +30,33 @@ class WorkflowExecutionIn(_BaseWorkflowExecution): description="Bucket where to save the Nextflow report. If None, no report will be generated. With our without prefix 's3://'", # noqa: E501 min_length=3, max_length=63, - example="some-bucket", + examples=["some-bucket"], ) class WorkflowExecutionOut(_BaseWorkflowExecution): execution_id: UUID = Field( - ..., description="ID of the workflow execution", example="0cc78936-381b-4bdd-999d-736c40591078" + ..., description="ID of the workflow execution", examples=["0cc78936-381b-4bdd-999d-736c40591078"] ) user_id: str = Field( - ..., description="UID of user who started the workflow", example="28c5353b8bb34984a8bd4169ba94c606" + ..., description="UID of user who started the workflow", examples=["28c5353b8bb34984a8bd4169ba94c606"] ) start_time: datetime = Field( - ..., description="Start time of the workflow execution", example=datetime(year=2023, month=1, day=1) + ..., description="Start time of the workflow execution", examples=[datetime(year=2023, month=1, day=1)] ) end_time: datetime | None = Field( - None, description="End time of the workflow execution", example=datetime(year=2023, month=1, day=1) + None, description="End time of the workflow execution", examples=[datetime(year=2023, month=1, day=1)] ) status: WorkflowExecution.WorkflowExecutionStatus = Field( ..., description="Status of the workflow execution", - example=WorkflowExecution.WorkflowExecutionStatus.RUNNING, + examples=[WorkflowExecution.WorkflowExecutionStatus.RUNNING], ) workflow_version_id: str | None = Field( # type: ignore[assignment] - ..., description="Workflow version git commit hash", example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f" + None, description="Workflow version git commit hash", examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"] ) workflow_id: UUID | None = Field( - ..., description="Id of the workflow", example="0cc78936-381b-4bdd-999d-736c40591078" + None, description="Id of the workflow", examples=["0cc78936-381b-4bdd-999d-736c40591078"] ) @staticmethod @@ -80,18 +80,22 @@ class DevWorkflowExecutionIn(BaseModel): description="Bucket where to save the Nextflow report. If None, no report will be generated", min_length=3, max_length=63, - example="some-bucket", + examples=["some-bucket"], ) git_commit_hash: str = Field( ..., description="Hash of the git commit", - example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", - regex=r"^[0-9a-f]{40}$", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], + pattern=r"^[0-9a-f]{40}$", min_length=40, max_length=40, ) repository_url: AnyHttpUrl = Field( ..., description="URL to the Git repository belonging to this workflow", - example="https://github.com/example-user/example", + examples=["https://github.com/example-user/example"], ) + + @field_serializer("repository_url") + def serialize_dt(self, url: AnyHttpUrl, _info: FieldSerializationInfo) -> str: + return str(url) diff --git a/app/schemas/workflow_version.py b/app/schemas/workflow_version.py index d1e4890a08bc9640d77a83f621bd5da99b98ab13..e8a75615c8c92d757801f6152f7cba5a7b22a90f 100644 --- a/app/schemas/workflow_version.py +++ b/app/schemas/workflow_version.py @@ -6,12 +6,11 @@ from pydantic import AnyHttpUrl, BaseModel, Field from app.core.config import settings from app.git_repository.abstract_repository import GitRepository -from app.schemas.utils import as_form class WorkflowVersionStatus(BaseModel): status: WorkflowVersionDB.Status = Field( - ..., description="Status of the workflow version", example=WorkflowVersionDB.Status.PUBLISHED + ..., description="Status of the workflow version", examples=[WorkflowVersionDB.Status.PUBLISHED] ) @@ -19,32 +18,34 @@ class WorkflowVersionReduced(WorkflowVersionStatus): version: str = Field( ..., description="Version of the Workflow. Should follow semantic versioning", - example="v1.0.0", + examples=["v1.0.0"], minlength=5, max_length=10, ) git_commit_hash: str = Field( ..., description="Hash of the git commit", - example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", - regex=r"^[0-9a-f]{40}$", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], + pattern=r"^[0-9a-f]{40}$", min_length=40, max_length=40, ) icon_url: AnyHttpUrl | None = Field( None, description="URL of the icon for this workflow version", - example=f"{settings.OBJECT_GATEWAY_URI}/{settings.ICON_BUCKET}/{uuid4().hex}.png", + examples=[f"{settings.OBJECT_GATEWAY_URI}{settings.ICON_BUCKET}/{uuid4().hex}.png"], ) created_at: datetime = Field( - ..., description="Timestamp when the version was created", example=datetime(year=2023, month=1, day=1) + ..., + description="Timestamp when the version was created", + examples=[datetime(year=2023, month=1, day=1)], ) @staticmethod def from_db_version(db_version: WorkflowVersionDB) -> "WorkflowVersionReduced": icon_url = None if db_version.icon_slug is not None: - icon_url = "/".join([settings.OBJECT_GATEWAY_URI, settings.ICON_BUCKET, db_version.icon_slug]) + icon_url = str(settings.OBJECT_GATEWAY_URI) + "/".join([settings.ICON_BUCKET, db_version.icon_slug]) return WorkflowVersionReduced( version=db_version.version, git_commit_hash=db_version.git_commit_hash, @@ -55,31 +56,31 @@ class WorkflowVersionReduced(WorkflowVersionStatus): class WorkflowVersionFull(WorkflowVersionReduced): - workflow_id: UUID = Field(..., description="ID of the corresponding workflow", example=uuid4()) + workflow_id: UUID = Field(..., description="ID of the corresponding workflow", examples=[uuid4()]) readme_url: AnyHttpUrl = Field( ..., description="URL to download README.md from", - example="https://raw.githubusercontent.com/example/example/README.md", + examples=["https://raw.githubusercontent.com/example/example/README.md"], ) changelog_url: AnyHttpUrl = Field( ..., description="URL to download CHANGELOG.md from", - example="https://raw.githubusercontent.com/example/example/CHANGELOG.md", + examples=["https://raw.githubusercontent.com/example/example/CHANGELOG.md"], ) usage_url: AnyHttpUrl = Field( ..., description="URL to download usage.md from", - example="https://raw.githubusercontent.com/example/example/docs/usage.md", + examples=["https://raw.githubusercontent.com/example/example/docs/usage.md"], ) output_url: AnyHttpUrl = Field( ..., description="URL to download output.md from", - example="https://raw.githubusercontent.com/example/example/docs/output.md", + examples=["https://raw.githubusercontent.com/example/example/docs/output.md"], ) parameter_schema_url: AnyHttpUrl = Field( ..., description="URL to download nextflow_schema.json from", - example="https://raw.githubusercontent.com/example/example/nextflow_schema.json", + examples=["https://raw.githubusercontent.com/example/example/nextflow_schema.json"], ) @staticmethod @@ -91,24 +92,23 @@ class WorkflowVersionFull(WorkflowVersionReduced): usage_url=repo.downloadFileURL("docs/usage.md"), output_url=repo.downloadFileURL("docs/output.md"), parameter_schema_url=repo.downloadFileURL("nextflow_schema.json"), - **WorkflowVersionReduced.from_db_version(db_version).dict(), + **WorkflowVersionReduced.from_db_version(db_version).model_dump(), ) -@as_form class WorkflowVersionUpdate(BaseModel): version: str = Field( ..., description="Version of the Workflow. Should follow semantic versioning", - example="v1.1.0", + examples=["v1.1.0"], minlength=5, max_length=10, ) git_commit_hash: str = Field( ..., description="Hash of the git commit", - example="ba8bcd9294c2c96aedefa1763a84a18077c50c0f", - regex=r"^[0-9a-f]{40}$", + examples=["ba8bcd9294c2c96aedefa1763a84a18077c50c0f"], + pattern=r"^[0-9a-f]{40}$", min_length=40, max_length=40, ) diff --git a/app/slurm/slurm_rest_client.py b/app/slurm/slurm_rest_client.py index 2ad1ef1d551b788a274ade94f588391a287929b1..fdbe486b208983754c96845659baba2bbb8e7e01 100644 --- a/app/slurm/slurm_rest_client.py +++ b/app/slurm/slurm_rest_client.py @@ -55,7 +55,7 @@ class SlurmClient: }, } response = await self._client.post( - f"{settings.SLURM_ENDPOINT}/slurm/{self.version}/job/submit", headers=self._headers, json=body + f"{settings.SLURM_ENDPOINT}slurm/{self.version}/job/submit", headers=self._headers, json=body ) return int(response.json()["job_id"]) diff --git a/app/tests/api/test_workflow.py b/app/tests/api/test_workflow.py index 549b2873a20394dae1256cab4b18052e3700ce48..a488e06fadfd6c5a443ba068b6d218a04b13249e 100644 --- a/app/tests/api/test_workflow.py +++ b/app/tests/api/test_workflow.py @@ -47,7 +47,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): name=random_lower_string(10), short_description=random_lower_string(65), repository_url="https://github.de/example-user/example", - ).dict() + ).model_dump() response = await client.post(self.base_path, data=workflow, headers=random_user.auth_headers) assert response.status_code == status.HTTP_201_CREATED created_workflow = response.json() @@ -89,7 +89,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): name=random_lower_string(10), short_description=random_lower_string(65), repository_url="https://gitlab.de/example-user/example", - ).dict() + ).model_dump() response = await client.post(self.base_path, data=workflow, headers=random_user.auth_headers) assert response.status_code == status.HTTP_201_CREATED created_workflow = response.json() @@ -126,7 +126,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): name=random_lower_string(10), short_description=random_lower_string(65), repository_url="https://github.de/example-user/example", - ).dict() + ).model_dump() response = await client.post( self.base_path, params={"raise_error": True}, data=workflow, headers=random_user.auth_headers ) @@ -162,7 +162,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): name=random_lower_string(10), short_description=random_lower_string(65), repository_url="https://github.de/example-user/example", - ).dict() + ).model_dump() files = {"icon": ("RickRoll.txt", BytesIO(b"Never gonna give you up"), "plain/text")} response = await client.post(self.base_path, data=workflow, headers=random_user.auth_headers, files=files) assert response.status_code == status.HTTP_201_CREATED @@ -197,7 +197,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): name=random_workflow.name, short_description=random_lower_string(65), repository_url="https://github.de/example-user/example", - ).dict() + ).model_dump() response = await client.post(self.base_path, data=workflow, headers=random_user.auth_headers) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -222,7 +222,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): name=random_lower_string(10), short_description=random_lower_string(65), repository_url="https://github.de/example-user/example", - ).dict() + ).model_dump() response = await client.post(self.base_path, data=workflow, headers=random_user.auth_headers) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -247,7 +247,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): name=random_lower_string(10), short_description=random_lower_string(65), repository_url="https://example.org", - ).dict() + ).model_dump() response = await client.post(self.base_path, data=workflow, headers=random_user.auth_headers) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -465,7 +465,7 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): version_update = WorkflowVersionUpdate( git_commit_hash=git_commit_hash, version=random_lower_string(8), - ).dict() + ).model_dump() response = await client.post( "/".join([self.base_path, str(random_workflow.workflow_id), "update"]), data=version_update, @@ -475,7 +475,7 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): created_version = response.json() assert created_version["git_commit_hash"] == git_commit_hash assert created_version["status"] == WorkflowVersion.Status.CREATED - assert created_version["icon_url"] == random_workflow.versions[0].icon_url + assert created_version["icon_url"] == str(random_workflow.versions[0].icon_url) stmt = select(WorkflowVersion).where(WorkflowVersion.git_commit_hash == git_commit_hash) db_version = (await db.execute(stmt)).scalar() @@ -501,7 +501,7 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): version_update = WorkflowVersionUpdate( git_commit_hash=random_hex_string(), version=random_lower_string(8), - ).dict() + ).model_dump() response = await client.post( "/".join([self.base_path, str(random_workflow.workflow_id), "update"]), params={"raise_error": True}, @@ -540,7 +540,7 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): version_update = WorkflowVersionUpdate( git_commit_hash=random_hex_string(), version=random_lower_string(8), - ).dict() + ).model_dump() files = {"icon": ("RickRoll.txt", BytesIO(b"Never gonna give you up"), "plain/text")} response = await client.post( @@ -575,7 +575,7 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): version_update = WorkflowVersionUpdate( git_commit_hash=random_workflow.versions[0].git_commit_hash, version=random_lower_string(8), - ).dict() + ).model_dump() response = await client.post( "/".join([self.base_path, str(random_workflow.workflow_id), "update"]), data=version_update, @@ -600,7 +600,7 @@ class TestWorkflowRoutesUpdate(_TestWorkflowRoutes): version_update = WorkflowVersionUpdate( git_commit_hash=random_hex_string(), version=random_lower_string(8), - ).dict() + ).model_dump() response = await client.post( "/".join([self.base_path, str(random_workflow.workflow_id), "update"]), data=version_update, diff --git a/app/tests/api/test_workflow_execution.py b/app/tests/api/test_workflow_execution.py index a186dec235c80b4df357f74d54f8a3a829bccaf5..a374d868d002e16785d96f2fcdb1f90f9f3dc4e3 100644 --- a/app/tests/api/test_workflow_execution.py +++ b/app/tests/api/test_workflow_execution.py @@ -43,7 +43,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): Mock S3 Service to manipulate objects. pytest fixture. """ execution_in = WorkflowExecutionIn(workflow_version_id=random_workflow_version.git_commit_hash, parameters={}) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_201_CREATED execution_response = response.json() assert execution_response["workflow_version_id"] == execution_in.workflow_version_id @@ -88,7 +88,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): await db.execute(stmt) await db.commit() execution_in = WorkflowExecutionIn(workflow_version_id=random_workflow_version.git_commit_hash, parameters={}) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_201_CREATED execution_response = response.json() assert execution_response["workflow_version_id"] == execution_in.workflow_version_id @@ -134,7 +134,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): active_execution_counter += 1 await db.commit() execution_in = WorkflowExecutionIn(workflow_version_id=random_workflow_version.git_commit_hash, parameters={}) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.asyncio @@ -155,7 +155,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): workflow_version_id=random_hex_string(), parameters={}, ) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio @@ -190,7 +190,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): workflow_version_id=random_workflow_version.git_commit_hash, parameters={}, ) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.asyncio @@ -216,7 +216,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): workflow_version_id=random_workflow_version.git_commit_hash, parameters={"dir": "s3://" + random_lower_string()}, ) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.asyncio @@ -245,7 +245,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): workflow_version_id=random_workflow_version.git_commit_hash, parameters={"dir": f"s3://{random_bucket.name}"}, ) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_201_CREATED @pytest.mark.asyncio @@ -278,7 +278,9 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): workflow_version_id=random_workflow_version.git_commit_hash, parameters={"dir": f"s3://{random_bucket.name}"}, ) - response = await client.post(self.base_path, headers=random_second_user.auth_headers, json=execution_in.dict()) + response = await client.post( + self.base_path, headers=random_second_user.auth_headers, json=execution_in.model_dump() + ) assert response.status_code == status.HTTP_201_CREATED @pytest.mark.asyncio @@ -307,7 +309,9 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): workflow_version_id=random_workflow_version.git_commit_hash, parameters={"dir": f"s3://{random_bucket.name}"}, ) - response = await client.post(self.base_path, headers=random_second_user.auth_headers, json=execution_in.dict()) + response = await client.post( + self.base_path, headers=random_second_user.auth_headers, json=execution_in.model_dump() + ) assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.asyncio @@ -337,7 +341,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): parameters={}, report_output_bucket=random_bucket.name, ) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_201_CREATED @pytest.mark.asyncio @@ -364,7 +368,7 @@ class TestWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): parameters={}, report_output_bucket=random_lower_string(), ) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=execution_in.model_dump()) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -392,7 +396,7 @@ class TestDevWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): git_commit_hash=random_hex_string(), repository_url="https://github.com/example/example", parameters={} ) response = await client.post( - f"{self.base_path}/arbitrary", headers=random_user.auth_headers, json=execution_in.dict() + f"{self.base_path}/arbitrary", headers=random_user.auth_headers, json=execution_in.model_dump() ) assert response.status_code == status.HTTP_201_CREATED execution_response = response.json() @@ -427,8 +431,10 @@ class TestDevWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): git_commit_hash=random_hex_string(), repository_url="https://bitbucket.com/example/example", parameters={} ) response = await client.post( - f"{self.base_path}/arbitrary", headers=random_user.auth_headers, json=execution_in.dict() + f"{self.base_path}/arbitrary", headers=random_user.auth_headers, json=execution_in.model_dump() ) + print(execution_in.model_dump()) + print(response.json()) assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/app/tests/api/test_workflow_version.py b/app/tests/api/test_workflow_version.py index e92257bc3585c8645da587941bb48ff7d1b02ac9..c87dc024d10420bdd0a41042c16105c3c12f2818 100644 --- a/app/tests/api/test_workflow_version.py +++ b/app/tests/api/test_workflow_version.py @@ -127,7 +127,7 @@ class TestWorkflowVersionRoutesUpdate(_TestWorkflowVersionRoutes): ] ), headers=random_user.auth_headers, - json=WorkflowVersionStatus(status=WorkflowVersion.Status.PUBLISHED).dict(), + json=WorkflowVersionStatus(status=WorkflowVersion.Status.PUBLISHED).model_dump(), ) assert response.status_code == status.HTTP_200_OK @@ -188,7 +188,7 @@ class TestWorkflowVersionRoutesUpdate(_TestWorkflowVersionRoutes): response = await client.patch( "/".join([self.base_path, str(random_workflow.workflow_id), "versions", random_hex_string(), "status"]), headers=random_user.auth_headers, - json=WorkflowVersionStatus(status=WorkflowVersion.Status.PUBLISHED).dict(), + json=WorkflowVersionStatus(status=WorkflowVersion.Status.PUBLISHED).model_dump(), ) assert response.status_code == status.HTTP_404_NOT_FOUND diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 55e72fc8e9a6dfa1bb7b3833d4e933800fbc262c..5224b747ebd7544b823edaf91939c41e726aa4ad 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -77,7 +77,7 @@ async def db() -> AsyncGenerator[AsyncSession, None]: Fixture for creating a database session to connect to. """ async with get_async_session( - url=settings.SQLALCHEMY_DATABASE_ASYNC_URI, verbose=settings.SQLALCHEMY_VERBOSE_LOGGER + url=str(settings.SQLALCHEMY_DATABASE_ASYNC_URI), verbose=settings.SQLALCHEMY_VERBOSE_LOGGER )() as dbSession: yield dbSession diff --git a/app/tests/mocks/authorization_service.py b/app/tests/mocks/authorization_service.py index 8680e3ac368320d8f279eecdb93fc7ae3eb37be2..f2a02e8e1a60a6f8bc10b81a13bb960a5dcc7789 100644 --- a/app/tests/mocks/authorization_service.py +++ b/app/tests/mocks/authorization_service.py @@ -20,7 +20,7 @@ def handle_request(body: dict[str, str]) -> Response: response : httpx.Response Mock response. """ - response_body = AuthzResponse(result=not request_admin_permission(body), decision_id=str(uuid4())).dict() + response_body = AuthzResponse(result=not request_admin_permission(body), decision_id=str(uuid4())).model_dump() return Response(status_code=status.HTTP_200_OK, json=response_body) diff --git a/app/tests/utils/utils.py b/app/tests/utils/utils.py index 584503d63799d69eaf5e9f4403e665a3a890d286..1fb5e6265861ff3676fbf3c2904bc9772973590d 100644 --- a/app/tests/utils/utils.py +++ b/app/tests/utils/utils.py @@ -94,10 +94,10 @@ def handle_http_request(request: httpx.Request, raise_error: bool = False) -> ht Generated mock request. """ url = str(request.url) - if url.startswith(settings.OPA_URI): + if url.startswith(str(settings.OPA_URI)): request_body: dict[str, str] = eval(request.content.decode("utf-8"))["input"] return auth_handle_request(body=request_body) - elif url.startswith(settings.SLURM_ENDPOINT): + elif url.startswith(str(settings.SLURM_ENDPOINT)): return slurm_handle_request(request.method) elif raise_error: return httpx.Response(status_code=status.HTTP_404_NOT_FOUND, json={}) diff --git a/requirements-dev.txt b/requirements-dev.txt index 822a0542b20fe265f8dbf49556080ef11d56e4e9..c9635291c502c7b89302fb150639b53d396eff23 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,16 +1,16 @@ # test packages -pytest>=7.3.0,<7.4.0 +pytest>=7.4.0,<7.5.0 pytest-asyncio>=0.21.0,<0.22.0 -pytest-cov>=4.0.0,<4.1.0 +pytest-cov>=4.1.0,<4.2.0 coverage[toml]>=7.2.0,<7.3.0 # Linters ruff -black>=23.03.0,<23.04.0 +black>=23.07.0,<23.08.0 isort>=5.12.0,<5.13.0 -mypy>=1.2.0,<1.3.0 +mypy>=1.4.0,<1.5.0 # stubs for mypy -boto3-stubs-lite[s3]>=1.26.0,<1.27.0 +boto3-stubs-lite[s3]>=1.28.0,<1.29.0 sqlalchemy2-stubs types-requests # Miscellaneous -pre-commit>=3.2.0,<3.3.0 +pre-commit>=3.3.0,<3.4.0 diff --git a/requirements.txt b/requirements.txt index 47d8fb8e74646b8af7b7a737451617daa5b30dd4..ccedf9f65648c017d52970f35ed6234713e27784 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,11 @@ clowmdb>=1.3.0,<1.4.0 # Webserver packages -anyio>=3.6.0,<3.7.0 -fastapi>=0.95.0,<0.96.0 -pydantic>=1.10.0,<2.0.0 -uvicorn>=0.21.0,<0.22.0 +anyio>=3.7.0,<3.8.0 +fastapi>=0.100.0,<0.101.0 +pydantic>=2.1.0,<2.2.0 +pydantic-settings +uvicorn>=0.23.0,<0.24.0 python-multipart # Database packages PyMySQL>=1.0.2,<1.1.0 @@ -14,7 +15,7 @@ aiomysql>=0.1.0,<0.2.0 # Security packages authlib>=1.2.0,<1.3.0 # Ceph and S3 packages -boto3>=1.26.0,<1.27.0 +boto3>=1.28.0,<1.29.0 # Miscellaneous tenacity>=8.1.0,<8.2.0 httpx>=0.24.0,<0.25.0