import logging import os from asyncio import CancelledError from typing import Callable import marshmallow import sentry_sdk from aiohttp import ClientSession, TCPConnector, TraceConfig, web from aiohttp_apispec import validation_middleware, setup_aiohttp_apispec from aiohttp_apispec.aiohttp_apispec import resolver from apispec.ext.marshmallow import MarshmallowPlugin from ddtrace import tracer from ddtrace.contrib.aiohttp import trace_app from sentry_sdk import capture_exception from sentry_sdk.integrations.aiohttp import AioHttpIntegration from sentry_sdk.utils import BadDsn import config from log import init_logging, set_log_level from server.artist.routes import init_routes as init_artist_routes from server.track.routes import init_routes as init_track_routes from server.dna.routes import init_routes as init_dna_routes from server.core.exceptions import APIError, BadGateway from server.core.middlewares import auth_middleware from server.core.routes import init_routes as init_core_routes # from server.artist.client import Config as ArtistConfig, ArtistClient from server.atlas.client import Config as AtlasConfig, AtlasClient from server.user_service.client import Config as UserServiceConfig, UserServiceClient logger = logging.getLogger("app") def init_sentry(): """Init sentry for project""" if config.SENTRY_DSN: try: sentry_sdk.init(environment=config.ENVIRONMENT, dsn=config.SENTRY_DSN, integrations=[AioHttpIntegration()]) except BadDsn: pass @web.middleware async def error_middleware(request: web.Request, handler: Callable) -> web.Response: """Callback for handling exceptions""" try: return await handler(request) except APIError as ex: capture_exception(ex) return ex.respond() except CancelledError: return BadGateway(detail="Request was cancelled.").respond() @web.middleware async def logging_middleware(request: web.Request, handler: Callable) -> web.Response: """Callback for logging requests""" response = await handler(request) logger.info(f"{request.method} - {request.rel_url} - {response.status}") return response async def on_request_end(session, trace_config_ctx, params): req = params.response logger.info(f"Request {req.method} {req.url} {req.status}") async def context(app: web.Application) -> None: if config.DEBUG: trace_config = TraceConfig() trace_config.on_request_end.append(on_request_end) trace_configs = [trace_config] else: trace_configs = None session = ClientSession(connector=TCPConnector(ssl=False), trace_configs=trace_configs) # app["artist_api"] = ArtistClient(session=session, config=ArtistConfig(**config.ARTIST_API_CONFIG)) app["atlas_api"] = AtlasClient(session=session, config=AtlasConfig(**config.ATLAS_API_CONFIG)) app["user_service_api"] = UserServiceClient( session=session, config=UserServiceConfig(**config.USER_SERVICE_API_CONFIG) ) app["application"] = await app["user_service_api"]._get_application() yield await session.close() def create_app() -> web.Application: """Application factory""" init_logging() set_log_level(config.LOGLEVEL) init_sentry() app = web.Application( middlewares=[error_middleware, logging_middleware, auth_middleware, validation_middleware], ) app.cleanup_ctx.append(context) plugin = MarshmallowPlugin(schema_name_resolver=resolver) try: """ We extend default apispec DEFAULT_FIELD_MAPPING mapping with fields.Method and field.Fucntion to string representation so that our swagger displayed this fields as "string" as default (by default this fields have no representation and aren`t displayed in swagger) To use different data type representation you can add an example of the response to this fields right inside response schema by using ApispecTypes of apollo_utils.swagger.utils pkg """ plugin.Converter.field_mapping[marshmallow.fields.Method] = ("string", None) plugin.Converter.field_mapping[marshmallow.fields.Function] = ("string", None) # By default TimeDelta doesn't have any mapping, so it will be represented as string in swagger, # so we need to add new mapper where TimeDelta has int type in swagger. plugin.Converter.field_mapping[marshmallow.fields.TimeDelta] = ("integer", "int32") except NameError: pass setup_aiohttp_apispec( app=app, title="DNA API", version="1.0.0", swagger_path="/api/doc/", securityDefinitions={ "apiKey": { "type": "apiKey", "name": "Authorization", "in": "header", "description": "For example: Bearer test", }, "X-User-ID": { "type": "apiKey", "name": "X-User-Id", "in": "header", "description": "For example: 111111111111aaaaaaaa111111111111", }, }, schemes=["http", "https"], security=[{"apiKey": [], "X-User-ID": []}], ) setup_aiohttp_apispec.plugin = plugin init_artist_routes(app) init_track_routes(app) init_core_routes(app) init_dna_routes(app) if os.environ.get("DATADOG_SERVICE_NAME"): trace_app(app, tracer) return app async def main() -> web.Application: """Main entry point""" return create_app() if __name__ == "__main__": web.run_app(main())