| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 | # (c) Nelen & Schuurmansfrom typing import Anyfrom typing import Callablefrom typing import Dictfrom typing import Listfrom typing import Optionalfrom typing import Setfrom fastapi import Dependsfrom fastapi import FastAPIfrom fastapi import Requestfrom fastapi.exceptions import RequestValidationErrorfrom starlette.types import ASGIAppfrom clean_python import BadRequestfrom clean_python import Conflictfrom clean_python import ctxfrom clean_python import DoesNotExistfrom clean_python import Gatewayfrom clean_python import PermissionDeniedfrom clean_python import Unauthorizedfrom clean_python.oauth2 import OAuth2SPAClientSettingsfrom clean_python.oauth2 import Tokenfrom clean_python.oauth2 import TokenVerifierSettingsfrom .error_responses import conflict_handlerfrom .error_responses import DefaultErrorResponsefrom .error_responses import not_found_handlerfrom .error_responses import not_implemented_handlerfrom .error_responses import permission_denied_handlerfrom .error_responses import unauthorized_handlerfrom .error_responses import validation_error_handlerfrom .error_responses import ValidationErrorResponsefrom .fastapi_access_logger import FastAPIAccessLoggerfrom .resource import APIVersionfrom .resource import clean_resourcesfrom .resource import Resourcefrom .security import get_tokenfrom .security import JWTBearerTokenSchemafrom .security import OAuth2SPAClientSchemafrom .security import set_verifier__all__ = ["Service"]def get_auth_kwargs(auth_client: Optional[OAuth2SPAClientSettings]) -> Dict[str, Any]:    if auth_client is None:        return {            "dependencies": [Depends(JWTBearerTokenSchema()), Depends(set_context)],        }    else:        return {            "dependencies": [                Depends(OAuth2SPAClientSchema(client=auth_client)),                Depends(set_context),            ],            "swagger_ui_init_oauth": {                "clientId": auth_client.client_id,                "usePkceWithAuthorizationCodeGrant": True,            },        }async def set_context(request: Request, token: Token = Depends(get_token)) -> None:    ctx.path = request.url    ctx.user = token.user    ctx.tenant = token.tenantasync def health_check():    """Simple health check route"""    return {"health": "OK"}class Service:    resources: List[Resource]    def __init__(self, *args: Resource):        self.resources = clean_resources(args)    @property    def versions(self) -> Set[APIVersion]:        return set([x.version for x in self.resources])    def _create_root_app(        self,        title: str,        description: str,        hostname: str,        on_startup: Optional[List[Callable[[], Any]]] = None,        access_logger_gateway: Optional[Gateway] = None,    ) -> FastAPI:        app = FastAPI(            title=title,            description=description,            on_startup=on_startup,            servers=[                {"url": f"{x.prefix}", "description": x.description}                for x in self.versions            ],            root_path_in_servers=False,        )        app.middleware("http")(            FastAPIAccessLogger(                hostname=hostname, gateway_override=access_logger_gateway            )        )        app.get("/health", include_in_schema=False)(health_check)        return app    def _create_versioned_app(self, version: APIVersion, **kwargs) -> FastAPI:        resources = [x for x in self.resources if x.version == version]        app = FastAPI(            version=version.prefix,            tags=sorted(                [x.get_openapi_tag().model_dump() for x in resources],                key=lambda x: x["name"],            ),            **kwargs,        )        for resource in resources:            app.include_router(                resource.get_router(                    version,                    responses={                        "400": {"model": ValidationErrorResponse},                        "default": {"model": DefaultErrorResponse},                    },                )            )        app.add_exception_handler(DoesNotExist, not_found_handler)        app.add_exception_handler(Conflict, conflict_handler)        app.add_exception_handler(RequestValidationError, validation_error_handler)        app.add_exception_handler(BadRequest, validation_error_handler)        app.add_exception_handler(NotImplementedError, not_implemented_handler)        app.add_exception_handler(PermissionDenied, permission_denied_handler)        app.add_exception_handler(Unauthorized, unauthorized_handler)        return app    def create_app(        self,        title: str,        description: str,        hostname: str,        auth: Optional[TokenVerifierSettings] = None,        auth_client: Optional[OAuth2SPAClientSettings] = None,        on_startup: Optional[List[Callable[[], Any]]] = None,        access_logger_gateway: Optional[Gateway] = None,    ) -> ASGIApp:        set_verifier(auth)        app = self._create_root_app(            title=title,            description=description,            hostname=hostname,            on_startup=on_startup,            access_logger_gateway=access_logger_gateway,        )        kwargs = {            "title": title,            "description": description,            **get_auth_kwargs(auth_client),        }        versioned_apps = {            v: self._create_versioned_app(v, **kwargs) for v in self.versions        }        for v, versioned_app in versioned_apps.items():            app.mount("/" + v.prefix, versioned_app)        return app
 |