"""Application initialisation. Creates a FastAPI application. """ from types import SimpleNamespace from typing import Awaitable from typing import Callable from ddtrace import tracer from fastapi import FastAPI from fastapi import HTTPException from fastapi import Request from fastapi import Response from fastapi.middleware.gzip import GZipMiddleware from jwtauth.asgi.middleware import JWTAuthenticationMiddleware from jwtauth.utils import jwt_auth_enabled_for_env from jwtauth.utils import jwt_auth_from_environment from owsrequest.rules import EndpointRulesValidator import sentry_sdk from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from moneyhub.config import Config from moneyhub.connectors.database_connector import DBSessionMiddleware from moneyhub.connectors.mysql import db as mysql from moneyhub.connectors.snowflake import db as snowflake from moneyhub.constants.constants import CORRELATION_ID_HEADER from moneyhub.constants.constants import DefaultEndpoints from moneyhub.constants.constants import Header from moneyhub.handlers.account import account_router from moneyhub.handlers.account_statements import account_statements_router from moneyhub.handlers.base import base_router from moneyhub.handlers.ecs_task import ecs_task_router from moneyhub.handlers.expense import expenses_router from moneyhub.handlers.ledger_adjustment import ledger_adjustment_router from moneyhub.handlers.ledger_contract_advance_applied import ledger_contract_advance_applied_router from moneyhub.handlers.ledger_reserve_release_schedule import ledger_reserve_release_schedule_router from moneyhub.handlers.ledger_vat_summary import ledger_vat_router as ledger_vat_router from moneyhub.handlers.payment_entity import payment_entity_router from moneyhub.handlers.reference_signing_entity import reference_signing_entity_router from moneyhub.handlers.report_custom import report_custom_router from moneyhub.handlers.reserves import reserves_router from moneyhub.handlers.revenue_analysis import revenue_analysis_router from moneyhub.handlers.statement_attachment import statement_attachment_router from moneyhub.handlers.statement_attachment_invoice import invoice_router from moneyhub.handlers.statement_reserves import statement_reserves_router from moneyhub.utils.middleware import BaseMiddleware from moneyhub.utils.request import extract_token from moneyhub.utils.request import verify_rules_access from moneyhub.utils.request_context import g from moneyhub.utils.request_context import request_global # Typing utility MiddlewareNextFunc = Callable[[Request], Awaitable[Response]] # Request validator rules_validator = EndpointRulesValidator('moneyhub/access_rules.yml') # Global exception handling middleware async def exception_handler(request: Request, call_next: MiddlewareNextFunc) -> Response: """Handle error when uncaught exception is raised. Default exception handler. Note: Exception will also be sent to Sentry if Config.SENTRY is set. Args: request (Request): Request object call_next (Callable): Function to call next Returns: fastapi.Response: A 500 response with JSON 'code' & 'message' payload. """ try: return await call_next(request) except Exception as e: message = str(e) sentry_sdk.capture_exception(e) return Response(message, status_code=500) async def extract_request_context_middleware( request: Request, call_next: MiddlewareNextFunc ) -> Response: """Middleware to extract the request context to a global object. Args: request (Request): Request object call_next (Callable): Function to call next Returns: Response """ span = tracer.trace('extract context middleware function') # span is started once created g().authorization = request.headers.get(Header.AUTHORIZATION) g().correlation_id = request.headers.get(CORRELATION_ID_HEADER) g().token = extract_token(request) span.finish() return await call_next(request) # Access rules middleware async def log_request_middleware( request: Request, call_next: MiddlewareNextFunc ) -> Response: """Middleware to verify access from rules before requests. Args: request (Request): Request object call_next (Callable): Function to call next Returns: Response """ # get request info if not Config.ONLY_LOG_ACCESS_ERRORS: headers = request.headers.raw query_params = request.query_params # Get the active span current_span = tracer.current_span() if current_span: # set spans for request info current_span.set_tag('request.query_params', query_params) if query_params else None for header in headers: key, value = header current_span.set_tag(f'request.headers.{key.decode()}', value.decode()) return await call_next(request) async def request_global_middleware( request: Request, call_next: MiddlewareNextFunc ) -> Response: """Middleware to setup the request-context global object. Args: request (Request): Request object call_next (Callable): Function to call next Returns: Response """ initial_g = SimpleNamespace() request_global.set(initial_g) return await call_next(request) # Access rules middleware async def verify_rules_access_middleware( request: Request, call_next: MiddlewareNextFunc ) -> Response: """Middleware to verify access from rules before requests. Args: request (Request): Request object call_next (Callable): Function to call next Returns: Response """ try: verify_rules_access( request=request, validator=rules_validator, log_only=Config.ONLY_LOG_ACCESS_ERRORS, exclude_paths=DefaultEndpoints.to_list() ) return await call_next(request) except HTTPException as e: return Response(e.detail, status_code=e.status_code) # Setup FastAPI application def create_app(config: Config) -> FastAPI: """Create an app. Args: config (Config): configuration parameters Returns: FastAPI: instance of the class FastAPI """ jwt_middleware_config = { 'environment': config.ENVIRONMENT, 'enabled': jwt_auth_enabled_for_env(config.ENVIRONMENT), 'exclude_paths': DefaultEndpoints.to_list(), 'auth': jwt_auth_from_environment(config.ENVIRONMENT), } if config.ENVIRONMENT == config.PROD_ENVIRONMENT: app = FastAPI( title=config.SERVICE_NAME, version=Config.SERVICE_VERSION, redoc_url=None, docs_url=None, ) else: app = FastAPI(title=Config.SERVICE_NAME, version=Config.SERVICE_VERSION) app.add_middleware(BaseMiddleware, dispatch=verify_rules_access_middleware) app.add_middleware(BaseMiddleware, dispatch=log_request_middleware) app.add_middleware(BaseMiddleware, dispatch=exception_handler) app.add_middleware(DBSessionMiddleware, database=mysql) app.add_middleware(DBSessionMiddleware, database=snowflake) app.add_middleware(BaseMiddleware, dispatch=extract_request_context_middleware) app.add_middleware(JWTAuthenticationMiddleware, **jwt_middleware_config) app.add_middleware(BaseMiddleware, dispatch=request_global_middleware) app.add_middleware(GZipMiddleware, minimum_size=1000) if Config.SENTRY: sentry_sdk.init(dsn=Config.SENTRY) app.add_middleware(SentryAsgiMiddleware) app.include_router(account_router, prefix='') app.include_router(base_router, prefix='') app.include_router(ecs_task_router, prefix='') app.include_router(expenses_router, prefix='') app.include_router(ledger_adjustment_router, prefix='') app.include_router(ledger_contract_advance_applied_router, prefix='') app.include_router(ledger_reserve_release_schedule_router, prefix='') app.include_router(report_custom_router, prefix='') app.include_router(reference_signing_entity_router, prefix='') app.include_router(statement_attachment_router, prefix='') app.include_router(invoice_router, prefix='') app.include_router(statement_reserves_router, prefix='') app.include_router(revenue_analysis_router, prefix='') app.include_router(ledger_vat_router, prefix='') app.include_router(payment_entity_router, prefix='') app.include_router(reserves_router, prefix='') app.include_router(account_statements_router, prefix='') return app