diff --git a/Dockerfile b/Dockerfile index c68406e15ee89081d9aa17a4465880e508901f38..0800cf06e23a7b50cb5f3c0a7b6b7e7130ab9d03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,13 +17,13 @@ WORKDIR /home/worker/code ENV PYTHONPATH=/home/worker/code ENV PATH="/home/worker/.local/bin:${PATH}" -COPY ./start_service_uvicorn.sh /home/worker/code/start.sh -COPY ./scripts/prestart.sh /home/worker/code/prestart.sh +COPY --chown=worker:worker ./start_service_uvicorn.sh ./start.sh +COPY --chown=worker:worker ./scripts/prestart.sh ./prestart.sh COPY --chown=worker:worker requirements.txt ./requirements.txt RUN pip install --user --no-cache-dir --upgrade -r requirements.txt -COPY --chown=worker:worker ./app /home/worker/code/app +COPY --chown=worker:worker ./app ./app CMD ["./start.sh"] diff --git a/app/api/api.py b/app/api/api.py index 3ba0060f85d9dc0c78a3ea8ff2d2afded3f9e462..0a2071438bd996ebf26256cfcbf3cd41f85c8286 100644 --- a/app/api/api.py +++ b/app/api/api.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, status from app.api.dependencies import decode_bearer_token from app.api.endpoints import bucket_permissions, buckets, s3key +from app.api.endpoints.miscellaneous_endpoints import router as miscellaneous_router from app.schemas.security import ErrorDetail alternative_responses: Dict[Union[int, str], Dict[str, Any]] = { @@ -40,3 +41,4 @@ api_router.include_router( dependencies=[Depends(decode_bearer_token)], responses=alternative_responses, ) +api_router.include_router(miscellaneous_router) diff --git a/app/api/dependencies.py b/app/api/dependencies.py index 69be9130118e79c9c33aaa97573f4a6e1e2d8fbc..c4182daa3f466dc3090da5020caf6288d0d9d136 100644 --- a/app/api/dependencies.py +++ b/app/api/dependencies.py @@ -44,7 +44,7 @@ def get_s3_resource() -> S3ServiceResource: S3Resource = Annotated[S3ServiceResource, Depends(get_s3_resource)] -async def get_db() -> AsyncGenerator[AsyncSession, None]: +async def get_db() -> AsyncGenerator[AsyncSession, None]: # pragma: no cover """ Get a Session with the database. @@ -142,7 +142,7 @@ async def get_current_user(token: Annotated[JWT, Depends(decode_bearer_token)], """ try: uid = UUID(token.sub) - except ValueError: + except ValueError: # pragma: no cover raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Malformed JWT") user = await CRUDUser.get(db, uid) if user: diff --git a/app/api/miscellaneous_endpoints.py b/app/api/endpoints/miscellaneous_endpoints.py similarity index 85% rename from app/api/miscellaneous_endpoints.py rename to app/api/endpoints/miscellaneous_endpoints.py index 0f4f786b06506ed0843fceb5f84a7a791441ab2e..1d188ffc6b6dc20bf7d360e6b4740f5539078a59 100644 --- a/app/api/miscellaneous_endpoints.py +++ b/app/api/endpoints/miscellaneous_endpoints.py @@ -2,10 +2,10 @@ from typing import Dict from fastapi import APIRouter, status -miscellaneous_router = APIRouter(include_in_schema=False) +router = APIRouter(include_in_schema=False) -@miscellaneous_router.get( +@router.get( "/health", tags=["Miscellaneous"], responses={ diff --git a/app/main.py b/app/main.py index fff58814b75024bda371b0a6ef0bdb8989f72152..0fad3197f0c886c5da38e9e5f986a1dac288bc44 100644 --- a/app/main.py +++ b/app/main.py @@ -2,10 +2,10 @@ from contextlib import asynccontextmanager from hashlib import md5 from typing import AsyncGenerator +from brotli_asgi import BrotliMiddleware from fastapi import FastAPI, Request, Response, status from fastapi.exception_handlers import http_exception_handler, request_validation_exception_handler from fastapi.exceptions import RequestValidationError, StarletteHTTPException -from fastapi.middleware.gzip import GZipMiddleware from fastapi.openapi.docs import get_swagger_ui_html from fastapi.responses import HTMLResponse, JSONResponse from fastapi.routing import APIRoute @@ -19,7 +19,6 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.trace import Status, StatusCode from app.api.api import api_router -from app.api.miscellaneous_endpoints import miscellaneous_router from app.core.config import settings description = """ @@ -49,7 +48,7 @@ app = FastAPI( "email": "dgoebel@techfak.uni-bielefeld.de", }, generate_unique_id_function=custom_generate_unique_id, - # license_info={"name": "MIT", "url": "https://mit-license.org/"}, + license_info={"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0"}, root_path=settings.API_PREFIX, openapi_url=None, # create it manually to enable caching on client side lifespan=lifespan, @@ -84,12 +83,11 @@ FastAPIInstrumentor.instrument_app( app, excluded_urls="health,docs,openapi.json", tracer_provider=trace.get_tracer_provider() ) -# Enable gzip compression for large responses -app.add_middleware(GZipMiddleware, minimum_size=500) +# Enable br compression for large responses, fallback gzip +app.add_middleware(BrotliMiddleware) # Include all routes app.include_router(api_router) -app.include_router(miscellaneous_router) # manually add Swagger UI route diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..926fe48166e02c6c576eba62e455c3a82b51fc63 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -0,0 +1,6 @@ +from typing import Annotated +from uuid import UUID as NativeUUID + +from pydantic.functional_serializers import PlainSerializer + +UUID = Annotated[NativeUUID, PlainSerializer(lambda uuid: str(uuid), return_type=str, when_used="unless-none")] diff --git a/app/schemas/bucket.py b/app/schemas/bucket.py index c30770b7c515f734eac3ace2356c74a78c944116..bb81a3834c2f5bd7115847737c459b2bc4066804 100644 --- a/app/schemas/bucket.py +++ b/app/schemas/bucket.py @@ -1,10 +1,11 @@ import re from typing import Optional -from uuid import UUID from clowmdb.models import Bucket from pydantic import BaseModel, ConfigDict, Field, field_validator +from app.schemas import UUID + ip_like_regex = re.compile(r"^(\d+\.){3}\d+$") diff --git a/app/schemas/bucket_permission.py b/app/schemas/bucket_permission.py index 99217183c251d9d139e0067eda9fc5767d35eaa2..6a1225e2dfb47773ad6259a77942cf65ac274da3 100644 --- a/app/schemas/bucket_permission.py +++ b/app/schemas/bucket_permission.py @@ -1,10 +1,11 @@ import hashlib from datetime import datetime from typing import Any, Dict, List, Optional, Union -from uuid import UUID from clowmdb.models import BucketPermission as BucketPermissionDB -from pydantic import BaseModel, Field, field_serializer +from pydantic import BaseModel, Field + +from app.schemas import UUID class BucketPermissionParameters(BaseModel): @@ -32,10 +33,6 @@ class BucketPermissionIn(BucketPermissionParameters): uid: UUID = Field(..., description="UID of the grantee", examples=["1d3387f3-95c0-4813-8767-2cad87faeebf"]) bucket_name: str = Field(..., description="Name of Bucket", examples=["test-bucket"]) - @field_serializer("uid") - def serialize_uid(self, uid: UUID, _info: Any) -> str: - return str(uid) - def to_hash(self, uid: UUID) -> str: """ Combine the bucket name and user id and produce the MD5 hash of it. diff --git a/app/schemas/s3key.py b/app/schemas/s3key.py index eae80d27a78ccd1eb46149f1a6eed815f2a6e860..233a0e914051c58f4d4284959892d33d3f18a803 100644 --- a/app/schemas/s3key.py +++ b/app/schemas/s3key.py @@ -1,7 +1,6 @@ -from typing import Any -from uuid import UUID +from pydantic import BaseModel, Field -from pydantic import BaseModel, Field, field_serializer +from app.schemas import UUID class S3Key(BaseModel): @@ -18,7 +17,3 @@ class S3Key(BaseModel): description="Secret of the S3 access key", examples=["2F5uNTI1qvt4oAroXV0wWct8rWclL2QvFXKqSqjS"], ) - - @field_serializer("uid") - def serialize_uid(self, uid: UUID, _info: Any) -> str: - return str(uid) diff --git a/app/schemas/security.py b/app/schemas/security.py index a6d7bd8293e64f047eeff0369271b577612a74e5..d065001db07cf1c76133a507398889f46f8a0bac 100644 --- a/app/schemas/security.py +++ b/app/schemas/security.py @@ -1,8 +1,8 @@ from datetime import datetime -from typing import Any -from uuid import UUID -from pydantic import BaseModel, Field, field_serializer +from pydantic import BaseModel, Field + +from app.schemas import UUID class AuthzResponse(BaseModel): @@ -15,10 +15,6 @@ class AuthzResponse(BaseModel): ) result: bool = Field(..., description="Result of the Authz request") - @field_serializer("decision_id") - def serialize_decision_id(self, decision_id: UUID, _info: Any) -> str: - return str(decision_id) - class AuthzRequest(BaseModel): """Schema for a Request to OPA""" diff --git a/app/tests/api/test_bucket_permissions.py b/app/tests/api/test_bucket_permissions.py index 6ea86c774cf1bc379fa3e00856cb54b4b4bd340e..153e0d8b0adee8eebb7ff1cbe2868464c609d5af 100644 --- a/app/tests/api/test_bucket_permissions.py +++ b/app/tests/api/test_bucket_permissions.py @@ -1,4 +1,3 @@ -import json from datetime import datetime from uuid import uuid4 @@ -14,7 +13,6 @@ from app.schemas.bucket_permission import BucketPermissionOut from app.schemas.bucket_permission import BucketPermissionParameters as BucketPermissionParametersSchema from app.tests.utils.bucket import add_permission_for_bucket from app.tests.utils.user import UserWithAuthHeader -from app.tests.utils.utils import json_datetime_converter class _TestBucketPermissionRoutes: @@ -604,7 +602,7 @@ class TestBucketPermissionRoutesUpdate(_TestBucketPermissionRoutes): response = await client.put( f"{self.base_path}/bucket/{random_bucket_permission_schema.bucket_name}/user/{str(random_bucket_permission_schema.uid)}", # noqa:E501 headers=random_user.auth_headers, - content=json.dumps(new_params.model_dump(), default=json_datetime_converter), + json=new_params.model_dump(), ) assert response.status_code == status.HTTP_200_OK updated_permission = BucketPermissionOut.model_validate(response.json()) diff --git a/app/tests/utils/utils.py b/app/tests/utils/utils.py index e20970d9ce67751af07a4fac165f3527fa906569..85cd4090ac8ea8bc2b2e5311f8a5dbb9908cdb42 100644 --- a/app/tests/utils/utils.py +++ b/app/tests/utils/utils.py @@ -1,7 +1,6 @@ import random import string -from datetime import datetime -from typing import Any, Dict, Optional +from typing import Dict import httpx @@ -35,26 +34,6 @@ def random_ipv4_string() -> str: return ".".join(str(random.randint(0, 255)) for _ in range(4)) -def json_datetime_converter(obj: Any) -> Optional[str]: - """ - Helper function for the json converter to covert the object into a string format if it is a datetime object.\n - Parse a datetime object into the format YYYY-MM-DDTHH:MM:SS, e.g. 2022-01-01T00:00:00 - - Parameters - ---------- - obj : Any - Object to try convert as a datetime object. - - Returns - ------- - time : str | None - The str representation of a datetime object, None otherwise - """ - if isinstance(obj, datetime): - return obj.strftime("%Y-%m-%dT%H:%M:%S") - return None - - def request_admin_permission(request: httpx.Request) -> bool: """ Rudimentary helper function to determine if the authorization request needs the 'administrator' role. diff --git a/pyproject.toml b/pyproject.toml index 7e3d3f75d952f63ed36f9363f22d4e03863e69d8..cc3c81d43a9679ee28ba9f36b46ea5befe903f8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ line-length = 120 [tool.ruff] line-length = 120 -target-version = "py311" +target-version = "py312" [tool.mypy] plugins = ["pydantic.mypy", "sqlalchemy.ext.mypy.plugin"] diff --git a/requirements.txt b/requirements.txt index ddcfc6cd3b3b34b3839c96a0147d8f3dca097669..e4fb6fd35530ed153a77d488e63dba3be88ae9e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,9 @@ rgwadmin>=2.4.0,<2.5.0 tenacity>=8.2.0,<8.3.0 httpx>=0.26.0,<0.27.0 itsdangerous +# Compression with br algorithm +brotli-asgi>=1.4.0,<1.5.0 + # Monitoring opentelemetry-instrumentation-fastapi opentelemetry-exporter-otlp-proto-grpc