diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b3949c0935815dd06dabb31ef4828be15033c4d..c9c2bec414a7395906f242a2f1b1cf6104fc39e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,19 +15,19 @@ 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/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 files: app args: [--config=.flake8] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.2.0 + rev: v1.4.1 hooks: - id: mypy files: app diff --git a/app/api/dependencies.py b/app/api/dependencies.py index c29d19ef85334842d72e86e586d06db65a3066ad..34d91981ae0cec7943a7f915fc2c50055cf87a59 100644 --- a/app/api/dependencies.py +++ b/app/api/dependencies.py @@ -51,7 +51,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 @@ -189,7 +189,7 @@ CurrentUser = Annotated[User, Depends(get_current_user)] async def get_user_by_path_uid( uid: str = Path( - default=..., description="UID of a user", example="28c5353b8bb34984a8bd4169ba94c606", max_length=64 + default=..., description="UID of a user", examples=["28c5353b8bb34984a8bd4169ba94c606"], max_length=64 ), db: AsyncSession = Depends(get_db), ) -> User: @@ -219,7 +219,7 @@ async def get_user_by_path_uid( async def get_current_bucket( - bucket_name: str = Path(..., description="Name of bucket", example="test-bucket", max_length=63, min_length=3), + bucket_name: str = Path(..., description="Name of bucket", examples=["test-bucket"], max_length=63, min_length=3), db: AsyncSession = Depends(get_db), ) -> Bucket: """ diff --git a/app/api/endpoints/buckets.py b/app/api/endpoints/buckets.py index 949cb530fb6cb9bac77ca1e5a6140ec9c2e8ca1b..83d385dd701f48c14534c447eea7626c0ae12b7d 100644 --- a/app/api/endpoints/buckets.py +++ b/app/api/endpoints/buckets.py @@ -329,22 +329,7 @@ async def get_bucket_object( db: DBSession, current_user: CurrentUser, authorization: Authorization, - object_path: str = Path( - ..., - decsription="Name of the object", - examples={ - "normal": { - "summary": "Normal file", - "description": "A normal file in a bucket", - "value": "test.txt", - }, - "pseudo-folder": { - "summary": "Pseudo-folder file", - "description": "A file in a pseudo folder", - "value": "pseudo/sub/folder/test.txt", - }, - }, - ), + object_path: str = Path(..., decsription="Name of the object", examples=["test.txt", "pseudo/sub/folder/test.txt"]), ) -> S3ObjectMetaInformation: """ Get the metadata of a specific object in a bucket.\n diff --git a/app/api/endpoints/s3key.py b/app/api/endpoints/s3key.py index df0b47616f8321a13aff9571124cb9857f91dfc6..562b05d20a5fdb045c20338ab050935793a0137b 100644 --- a/app/api/endpoints/s3key.py +++ b/app/api/endpoints/s3key.py @@ -16,7 +16,7 @@ AccessID = Annotated[ Path( ..., description="ID of the S3 access key", - example="CRJ6B037V2ZT4U3W17VC", + examples=["CRJ6B037V2ZT4U3W17VC"], ), ] diff --git a/app/ceph/rgw.py b/app/ceph/rgw.py index d77df5b1b94f78916d81be4cef0396a63ea5f1c1..256b42018307bb789fb6f99bb7be5f7c38d42fea 100644 --- a/app/ceph/rgw.py +++ b/app/ceph/rgw.py @@ -12,14 +12,14 @@ else: s3_resource: S3ServiceResource = resource( service_name="s3", - endpoint_url=settings.OBJECT_GATEWAY_URI, + 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=settings.OBJECT_GATEWAY_URI.startswith("https"), + verify=str(settings.OBJECT_GATEWAY_URI).startswith("https"), ) rgw = RGWAdmin( access_key=settings.USER_CEPH_ACCESS_KEY, secret_key=settings.USER_CEPH_SECRET_KEY, - secure=settings.OBJECT_GATEWAY_URI.startswith("https"), - server=settings.OBJECT_GATEWAY_URI.split("://")[-1], + secure=str(settings.OBJECT_GATEWAY_URI).startswith("https"), + server=str(settings.OBJECT_GATEWAY_URI).split("://")[-1][:-1], ) diff --git a/app/check_ceph_connection.py b/app/check_ceph_connection.py index 5106b509fe4a871f79261f9266973bd615638d4d..71a7cb7d2d2dda71d13efc8deb6560e83814e363 100644 --- a/app/check_ceph_connection.py +++ b/app/check_ceph_connection.py @@ -20,7 +20,7 @@ wait_seconds = 2 ) def init() -> None: try: - httpx.get(settings.OBJECT_GATEWAY_URI, timeout=5.0) + httpx.get(str(settings.OBJECT_GATEWAY_URI), timeout=5.0) except Exception as e: logger.error(e) raise 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 15c1fffb51c8c20a9f1b4c6c26727c0a6f6b7a9e..87d4d4575c3e360e820ce533c597b37f5016626b 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,47 +1,53 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Union -from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, Field, validator +from pydantic import AnyHttpUrl, AnyUrl, Field, computed_field, field_validator +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", 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) + @field_validator("BACKEND_CORS_ORIGINS", mode="before") + @classmethod 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(",")] @@ -55,21 +61,34 @@ class Settings(BaseSettings): 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.") USER_CEPH_ACCESS_KEY: str = Field( @@ -89,11 +108,7 @@ class Settings(BaseSettings): ) 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") - - 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") 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/crud/crud_bucket.py b/app/crud/crud_bucket.py index 7117a557d76213ce357c204792582ae577914931..d069cb9905a90da3ee5f2eb10608d46ff925a4bd 100644 --- a/app/crud/crud_bucket.py +++ b/app/crud/crud_bucket.py @@ -161,7 +161,7 @@ class CRUDBucket: bucket : clowmdb.models.Bucket | None Returns the created bucket. If None then there was a problem, e.g. the name of the bucket is already taken. """ - bucket = Bucket(**bucket_in.dict(), owner_id=uid) + bucket = Bucket(**bucket_in.model_dump(), owner_id=uid) if await CRUDBucket.get(db, bucket.name) is None: db.add(bucket) await db.commit() diff --git a/app/schemas/bucket.py b/app/schemas/bucket.py index 545a9aa81f66620fbf2ba2b4fa2d801d105103a9..646b6a6b6b50d7c345e7a0b5b23d246ee3f43a73 100644 --- a/app/schemas/bucket.py +++ b/app/schemas/bucket.py @@ -1,14 +1,17 @@ +import re from datetime import datetime from typing import TYPE_CHECKING from clowmdb.models import Bucket -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator if TYPE_CHECKING: from mypy_boto3_s3.service_resource import ObjectSummary else: ObjectSummary = object +ip_regex = re.compile(r"^((2(5[0-5]|[0-4]\d)|[01]?\d{1,2})\.){3}(2(5[0-5]|[0-4]\d)|[01]?\d{1,2})$") + class _BaseBucket(BaseModel): """ @@ -17,23 +20,32 @@ class _BaseBucket(BaseModel): name: str = Field( ..., - example="test-bucket", + examples=["test-bucket"], description="Name of the bucket", min_length=3, max_length=63, - regex=r"(?!(^((2(5[0-5]|[0-4]\d)|[01]?\d{1,2})\.){3}(2(5[0-5]|[0-4]\d)|[01]?\d{1,2})$))^[a-z\d][a-z\d.-]{1,61}[a-z\d]$", # noqa:E501 + pattern=r"^[a-z\d][a-z\d.-]{1,61}[a-z\d]$", # noqa:E501 ) description: str = Field( ..., - example="""\ + examples=[ + """\ This is a very long sample description of a bucket and its purpose which has to be more \ than 126 characters long and is mandatory. - """.strip(), + """.strip() + ], description="Description of the bucket", min_length=126, max_length=2**16, ) + @field_validator("name") + @classmethod + def name_is_not_an_ip_address(cls, name: str) -> str: + if ip_regex.search(name): + raise ValueError("no IP address as bucket name") + return name + class BucketIn(_BaseBucket): """ @@ -48,20 +60,18 @@ class BucketOut(_BaseBucket): created_at: datetime = Field( ..., - example=datetime(2022, 1, 1, 0, 0), + examples=[datetime(2022, 1, 1, 0, 0)], description="Time when the bucket was created", ) - owner: str = Field(..., description="UID of the owner", example="28c5353b8bb34984a8bd4169ba94c606") - num_objects: int = Field(..., description="Number of Objects in this bucket", example=6) - size: int = Field(..., description="Total size of objects in this bucket in bytes", example=3256216) + owner: str = Field(..., description="UID of the owner", examples=["28c5353b8bb34984a8bd4169ba94c606"]) + num_objects: int = Field(..., description="Number of Objects in this bucket", examples=[6]) + size: int = Field(..., description="Total size of objects in this bucket in bytes", examples=[3256216]) owner_constraint: Bucket.Constraint | None = Field(None, description="Constraint for the owner of the bucket") description: str = Field( ..., description="Description of the bucket", ) - - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class S3ObjectMetaInformation(BaseModel): @@ -72,21 +82,21 @@ class S3ObjectMetaInformation(BaseModel): key: str = Field( ..., description="Key of the Object in the S3 store", - example="test.txt", + examples=["test.txt"], max_length=512, ) bucket: str = Field( ..., description="Name of the Bucket in which the object is", - example="test-bucket", + examples=["test-bucket"], max_length=256, ) - content_type: str = Field(..., description="MIME type of the object", example="text/plain") - size: int = Field(..., description="Size of the object in Bytes", example=123456) + content_type: str = Field(..., description="MIME type of the object", examples=["text/plain"]) + size: int = Field(..., description="Size of the object in Bytes", examples=[123456]) last_modified: datetime = Field( ..., description="Last time the object was modified", - example=datetime(2022, 1, 1, 0, 0), + examples=[datetime(2022, 1, 1, 0, 0)], ) @staticmethod diff --git a/app/schemas/bucket_permission.py b/app/schemas/bucket_permission.py index 0b738390cf24d696f7e63d8e67a03e89160596b7..b9fcd5fc6ac18be891acf338dff5866a9c06bba6 100644 --- a/app/schemas/bucket_permission.py +++ b/app/schemas/bucket_permission.py @@ -12,20 +12,20 @@ class BucketPermissionParameters(BaseModel): """ from_timestamp: datetime | None = Field( - None, description="Start date of permission", example=datetime(2022, 1, 1, 0, 0) + None, description="Start date of permission", examples=[datetime(2022, 1, 1, 0, 0)] ) to_timestamp: datetime | None = Field( - None, description="End date of permission", example=datetime(2023, 1, 1, 0, 0) + None, description="End date of permission", examples=[datetime(2023, 1, 1, 0, 0)] ) - file_prefix: str | None = Field(None, description="Prefix of subfolder", example="pseudo/sub/folder/") + file_prefix: str | None = Field(None, description="Prefix of subfolder", examples=["pseudo/sub/folder/"]) permission: BucketPermissionDB.Permission | str = Field( - BucketPermissionDB.Permission.READ, description="Permission", example=BucketPermissionDB.Permission.READ + BucketPermissionDB.Permission.READ, description="Permission", examples=[BucketPermissionDB.Permission.READ] ) class BucketPermissionIn(BucketPermissionParameters): - uid: str = Field(..., description="UID of the grantee", example="28c5353b8bb34984a8bd4169ba94c606") - bucket_name: str = Field(..., description="Name of Bucket", example="test-bucket") + uid: str = Field(..., description="UID of the grantee", examples=["28c5353b8bb34984a8bd4169ba94c606"]) + bucket_name: str = Field(..., description="Name of Bucket", examples=["test-bucket"]) def to_hash(self, user_id: str) -> str: """ @@ -111,7 +111,7 @@ class BucketPermissionOut(BucketPermissionIn): Schema for the bucket permissions. """ - grantee_display_name: str = Field(..., description="Display Name of the grantee", example="Bilbo Baggins") + grantee_display_name: str = Field(..., description="Display Name of the grantee", examples=["Bilbo Baggins"]) @staticmethod def from_db_model( 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/user.py b/app/schemas/user.py index dac6d13416b9f3bae38c8facb2478b762b6a900e..c9ca4704b89afe36a3ac3ece46224fc908927d78 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -6,10 +6,12 @@ class S3Key(BaseModel): Schema for a S3 key associated with a user. """ - user: str = Field(..., description="UID of the user of that access key", example="28c5353b8bb34984a8bd4169ba94c606") - access_key: str = Field(..., description="ID of the S3 access key", example="CRJ6B037V2ZT4U3W17VC") + user: str = Field( + ..., description="UID of the user of that access key", examples=["28c5353b8bb34984a8bd4169ba94c606"] + ) + access_key: str = Field(..., description="ID of the S3 access key", examples=["CRJ6B037V2ZT4U3W17VC"]) secret_key: str = Field( ..., description="Secret of the S3 access key", - example="2F5uNTI1qvt4oAroXV0wWct8rWclL2QvFXKqSqjS", + examples=["2F5uNTI1qvt4oAroXV0wWct8rWclL2QvFXKqSqjS"], ) diff --git a/app/tests/api/test_bucket_permissions.py b/app/tests/api/test_bucket_permissions.py index 567ad256b2b24e8b924576968ef86d4bd06ed14b..65e3605643aa3058cfb5c2ce69153e29930f6861 100644 --- a/app/tests/api/test_bucket_permissions.py +++ b/app/tests/api/test_bucket_permissions.py @@ -269,7 +269,7 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes): Random bucket for testing. pytest fixture. """ permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid="ImpossibleUser") - response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump()) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio @@ -291,7 +291,7 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes): Random bucket for testing. pytest fixture. """ permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_user.user.uid) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump()) assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.asyncio @@ -316,7 +316,7 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes): permission = BucketPermissionSchema( bucket_name=random_bucket_permission_schema.bucket_name, uid=random_bucket_permission_schema.uid ) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump()) assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.asyncio @@ -340,7 +340,9 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes): """ permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_second_user.user.uid) - response = await client.post(self.base_path, headers=random_second_user.auth_headers, json=permission.dict()) + response = await client.post( + self.base_path, headers=random_second_user.auth_headers, json=permission.model_dump() + ) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.asyncio @@ -367,7 +369,7 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes): """ permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_second_user.user.uid) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump()) assert response.status_code == status.HTTP_201_CREATED created_permission = response.json() assert created_permission["uid"] == random_second_user.user.uid @@ -406,7 +408,7 @@ class TestBucketPermissionRoutesCreate(_TestBucketPermissionRoutes): await db.commit() permission = BucketPermissionSchema(bucket_name=random_bucket.name, uid=random_second_user.user.uid) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=permission.model_dump()) assert response.status_code == status.HTTP_403_FORBIDDEN @@ -568,7 +570,7 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes): response = await client.put( f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{random_bucket_permission_schema.uid}", # noqa:E501 headers=random_user.auth_headers, - content=json.dumps(new_params.dict(), default=json_datetime_converter), + content=json.dumps(new_params.model_dump(), default=json_datetime_converter), ) assert response.status_code == status.HTTP_200_OK updated_permission = response.json() @@ -604,7 +606,7 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes): response = await client.put( f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/impossibleUser", headers=random_user.auth_headers, - json=new_params.dict(), + json=new_params.model_dump(), ) assert response.status_code == status.HTTP_404_NOT_FOUND @@ -635,7 +637,7 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes): response = await client.put( f"{self.base_path}/bucket/{random_bucket.name}/user/{random_second_user.user.uid}", headers=random_user.auth_headers, - json=new_params.dict(), + json=new_params.model_dump(), ) assert response.status_code == status.HTTP_404_NOT_FOUND @@ -665,7 +667,7 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes): response = await client.put( f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{random_second_user.user.uid}", headers=random_second_user.auth_headers, - json=new_params.dict(), + json=new_params.model_dump(), ) assert response.status_code == status.HTTP_403_FORBIDDEN @@ -695,6 +697,6 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes): response = await client.put( f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{random_bucket_permission_schema.uid}", # noqa:E501 headers=random_third_user.auth_headers, - json=new_params.dict(), + json=new_params.model_dump(), ) assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/app/tests/api/test_buckets.py b/app/tests/api/test_buckets.py index b848fa988a5aa7a489dee3706c497d4ca2011986..b6bba3f870aa9d624cba127f401c91dd65c34e75 100644 --- a/app/tests/api/test_buckets.py +++ b/app/tests/api/test_buckets.py @@ -159,7 +159,7 @@ class TestBucketRoutesCreate(_TestBucketRoutes): Random user for testing. pytest fixture. """ bucket_info = BucketIn(name=random_lower_string(), description=random_lower_string(127)) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=bucket_info.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=bucket_info.model_dump()) assert response.status_code == status.HTTP_201_CREATED bucket = response.json() @@ -194,7 +194,7 @@ class TestBucketRoutesCreate(_TestBucketRoutes): Random user for testing. pytest fixture. """ bucket_info = BucketIn(name=random_bucket.name, description=random_lower_string(127)) - response = await client.post(self.base_path, headers=random_user.auth_headers, json=bucket_info.dict()) + response = await client.post(self.base_path, headers=random_user.auth_headers, json=bucket_info.model_dump()) assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/app/tests/conftest.py b/app/tests/conftest.py index dfd4bc0f80e6a31549dd798307a0082568d695db..8f1e4883fa9cbe9dbe8d3177897f2d4ce755bee7 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -73,10 +73,10 @@ async def client(mock_rgw_admin: MockRGWAdmin, mock_s3_service: MockS3ServiceRes async def get_mock_httpx_client() -> AsyncGenerator[httpx.AsyncClient, None]: def mock_request_handler(request: httpx.Request) -> httpx.Response: response_body = {} - if str(request.url).startswith(settings.OPA_URI): + if str(request.url).startswith(str(settings.OPA_URI)): response_body = AuthzResponse( result=not request_admin_permission(request), decision_id=str(uuid4()) - ).dict() + ).model_dump() return httpx.Response(200, json=response_body) async with httpx.AsyncClient(transport=httpx.MockTransport(mock_request_handler)) as http_client: @@ -97,7 +97,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/requirements-dev.txt b/requirements-dev.txt index 74dd93e9bc34a77ef1f4afcd2d507dafe2663dc1..e750f64bada01ec1e85e00175fa4efe2de0b9771 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,18 +1,18 @@ # 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 -flake8>=6.0.0,<6.1.0 -autoflake>=2.1.0,<2.2.0 -black>=23.03.0,<23.04.0 +flake8>=6.1.0,<6.2.0 +autoflake>=2.2.0,<2.3.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 python-dotenv diff --git a/requirements.txt b/requirements.txt index c22a45c761a4c6d22e2f1e4a637328e8fb75ec79..0eefe6d9249b16065ed6088068841621d319de90 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>=2.0.0 +uvicorn>=0.23.0,<0.24.0 # Database packages PyMySQL>=1.0.2,<1.1.0 SQLAlchemy>=1.4.0,<2.0.0 @@ -13,7 +14,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 rgwadmin>=2.4.0,<2.5.0 # Miscellaneous tenacity>=8.1.0,<8.2.0