from collections.abc import AsyncIterator, Iterator import boto3 import jinja2 import sqlalchemy.pool from anydi import Container, Module, Provider, provider from cachelib import BaseCache as Cache, NullCache, RedisCache, SimpleCache from fansifter_common import context from fansifter_common.adapters.aws.secretsmanager import SecretsManager from fansifter_common.adapters.db import jinja2sql_filters from fansifter_common.adapters.graphql_router import GraphqlRouterClient from fansifter_common.adapters.ows_account import OwsAccountClient from fansifter_common.adapters.ows_pdp import OwsPdpClient from fansifter_common.artist.validators import ArtistValidator from fansifter_common.auth.account import Account, AccountAccess from fansifter_common.auth.authorization import ( AccessAuthorizationBackendStub, AuthorizationBackend, PdpAuthorizationBackend, ) from fansifter_common.auth.services import AuthService from fansifter_common.constants import PROD_ENVIRONMENT, QA_ENVIRONMENT from fansifter_common.m2m_token import M2MTokenManager from fansifter_common.utils import timezone from fansifter_common.utils.functional import lazy_proxy from jinja2sql import Jinja2SQL from jwtauth import JWTAuth from jwtauth.utils import ( get_default_audience, get_default_issuer, get_default_jwks_url, ) from owsclient import OwsClient from snowflake.snowpark import Session as SnowparkSession from dmp.adapters.aws.kms import KMS, BaseKMS, DummyKMS from dmp.adapters.aws.location import LocationClient from dmp.adapters.aws.s3 import S3Client from dmp.adapters.db import DefaultDB, ReportingDB from dmp.adapters.features import Features from dmp.adapters.fivetran import FivetranClient from dmp.adapters.fivetran.enums import AdsTimeframeMonths from dmp.adapters.google import GoogleClient from dmp.adapters.meta.client import FacebookClient from dmp.adapters.ows_notifications import OwsNotificationsClient from dmp.adapters.ows_socials import OwsSocialsClient from dmp.adapters.tiktok import TikTokClient from dmp.config import Settings, settings class AppModule(Module): @provider(scope="singleton") def jwt_auth(self, settings: Settings) -> JWTAuth: return JWTAuth( jwks_url=get_default_jwks_url(settings.environment), audience=get_default_audience(settings.environment), issuer=get_default_issuer(settings.environment), ) @provider(scope="singleton") def aws_session(self, settings: Settings) -> boto3.Session: return boto3.Session(region_name=settings.aws_region_name) @provider(scope="singleton") def secrets_manager( self, settings: Settings, session: boto3.Session ) -> SecretsManager: return SecretsManager(session=session, region_name=settings.aws_region_name) @provider(scope="singleton") def s3_client(self, settings: Settings, session: boto3.Session) -> S3Client: return S3Client(session=session, region_name=settings.aws_region_name) @provider(scope="singleton") def location_client(self, settings: Settings) -> Iterator[LocationClient]: with LocationClient( region_name=settings.aws_region_name, api_key=settings.aws_location_api_key, ) as client: yield client @provider(scope="singleton") def cache(self, settings: Settings) -> Cache: if settings.cache_backend in ["locmem", "memory"]: return SimpleCache(default_timeout=settings.cache_default_timeout) elif settings.cache_backend == "redis": return RedisCache( host=settings.redis_host, port=settings.redis_port, db=settings.cache_redis_db, ssl=settings.redis_ssl, default_timeout=settings.cache_default_timeout, key_prefix=settings.cache_key_prefix, ) return NullCache() @provider(scope="singleton") def m2m_token_manager( self, settings: Settings, secrets_manager: SecretsManager, cache: Cache ) -> M2MTokenManager: return M2MTokenManager( secrets_manager=secrets_manager, secret_name_key=settings.m2m_token_secret_key_name, secret_expire_name_key=settings.m2m_token_secret_expiry_key_name, cache=cache, ) @provider(scope="singleton") def ows_client( self, settings: Settings, m2m_token_manager: M2MTokenManager ) -> OwsClient: return OwsClient( environment=settings.environment, service_name=settings.service_name, m2m_token_manager=m2m_token_manager if settings.environment in [QA_ENVIRONMENT, PROD_ENVIRONMENT] else None, request_context_getter=context.get_request_context, correlation_id_getter=context.get_correlation_id, ) @provider(scope="singleton") def ows_pdp_client(self, ows_client: OwsClient) -> OwsPdpClient: return OwsPdpClient(ows_client=ows_client) @provider(scope="singleton") def ows_account_client(self, ows_client: OwsClient) -> OwsAccountClient: return OwsAccountClient(ows_client=ows_client) @provider(scope="singleton") def ows_notifications_client(self, ows_client: OwsClient) -> OwsNotificationsClient: return OwsNotificationsClient(ows_client=ows_client) @provider(scope="singleton") def ows_socials_client(self, ows_client: OwsClient) -> OwsSocialsClient: return OwsSocialsClient(ows_client=ows_client) @provider(scope="singleton") def graphql_router_client(self, ows_client: OwsClient) -> GraphqlRouterClient: return GraphqlRouterClient(ows_client=ows_client) @provider(scope="singleton") def features(self, settings: Settings) -> Iterator[Features]: features = Features( settings.splitio_api_key, block_until_ready_timeout=settings.splitio_block_until_ready_timeout, config=settings.splitio_config, ) features.start() yield features features.close() @provider(scope="singleton") def jinja2sql(self, settings: Settings, features: Features) -> Jinja2SQL: env = jinja2.Environment( loader=jinja2.FileSystemLoader(settings.jinja2sql_template_searchpath) ) jinja2sql = Jinja2SQL(env) jinja2sql.env.globals.update( # ty: ignore[no-matching-overload] { "settings": settings, "features": features, "current_timestamp": timezone.now, } ) # Register filters jinja2sql.register_filter("escape_like", jinja2sql_filters.escape_like) jinja2sql.register_filter( "orderby", jinja2sql_filters.orderby_filter, bind=True ) jinja2sql.register_filter( "enum_values", jinja2sql_filters.enum_values, bind=True ) jinja2sql.register_filter( "enum_choices", jinja2sql_filters.enum_choices, bind=True ) jinja2sql.register_filter( "array_values", jinja2sql_filters.array_values, bind=True ) return jinja2sql @provider(scope="singleton") def default_db( self, settings: Settings, jinja2sql: Jinja2SQL ) -> Iterator[DefaultDB]: with DefaultDB( url=settings.postgres_url, engine_args={ "echo": settings.postgres_echo, "poolclass": sqlalchemy.pool.QueuePool, "pool_size": settings.postgres_pool_size, "max_overflow": settings.postgres_pool_max_overflow, "pool_recycle": settings.postgres_pool_recycle, "pool_pre_ping": settings.postgres_pool_pre_ping, "pool_reset_on_return": settings.postgres_pool_reset_on_return, "connect_args": settings.postgres_connect_args, }, jinja2sql=jinja2sql, ) as db: yield db @provider(scope="request") async def default_db_session_factory(self, db: DefaultDB) -> AsyncIterator[None]: with db.session_factory(): yield @provider(scope="singleton") def reporting_db( self, settings: Settings, jinja2sql: Jinja2SQL ) -> Iterator[ReportingDB]: with ReportingDB( url=settings.snowflake_url, engine_args={ "echo": settings.snowflake_echo, "poolclass": sqlalchemy.pool.QueuePool, "pool_size": settings.snowflake_pool_size, "max_overflow": settings.snowflake_pool_max_overflow, "pool_recycle": settings.snowflake_pool_recycle, "pool_pre_ping": settings.snowflake_pool_pre_ping, "pool_reset_on_return": settings.snowflake_pool_reset_on_return, "connect_args": settings.snowflake_connect_args, }, jinja2sql=jinja2sql, ) as db: yield db @provider(scope="request") async def reporting_db_session_factory( self, db: ReportingDB ) -> AsyncIterator[None]: with db.session_factory(): yield @provider(scope="singleton") def fivetran_client(self, settings: Settings) -> Iterator[FivetranClient]: ads_timeframe_months = AdsTimeframeMonths.THREE reporting_sync_frequency = 1440 if settings.environment == PROD_ENVIRONMENT: ads_timeframe_months = AdsTimeframeMonths.ALL_TIME reporting_sync_frequency = 60 with FivetranClient( api_key=settings.fivetran_api_key, api_secret=settings.fivetran_api_secret, ads_timeframe_months=ads_timeframe_months, reporting_sync_frequency=reporting_sync_frequency, ) as client: yield client @provider(scope="singleton") def kms(self, settings: Settings, session: boto3.Session) -> BaseKMS: if settings.kms_enabled: return KMS( region_name=settings.aws_region_name, key_id=settings.kms_key_id, session=session, ) return DummyKMS() @provider(scope="singleton") def facebook_client(self, settings: Settings) -> Iterator[FacebookClient]: with FacebookClient( client_id=settings.facebook_app_id, client_secret=settings.facebook_app_secret, ) as facebook_client: yield facebook_client @provider(scope="singleton") def tiktok_client(self, settings: Settings) -> Iterator[TikTokClient]: with TikTokClient( client_id=settings.tiktok_app_id, client_secret=settings.tiktok_app_secret, ) as tiktok_client: yield tiktok_client @provider(scope="singleton") def google_client(self, settings: Settings) -> Iterator[GoogleClient]: with GoogleClient( client_id=settings.google_app_id, client_secret=settings.google_app_secret, developer_token=settings.google_developer_token, ) as google_client: yield google_client # Authorizer @provider(scope="singleton") def authorization_backend( self, ows_pdp_client: OwsPdpClient, ows_account_client: OwsAccountClient, cache: Cache, settings: Settings, ) -> AuthorizationBackend: # Local/dev environment only if settings.auth_allow_full_access: return AccessAuthorizationBackendStub( AccountAccess( accounts=[ Account(vendor_id=7123, subaccount_id=0), Account(vendor_id=34514, subaccount_id=0), ] ) ) return PdpAuthorizationBackend( ows_pdp_client=ows_pdp_client, ows_account_client=ows_account_client, cache=cache, cache_timeout=settings.account_access_cache_timeout, ) @provider(scope="singleton") def auth_service(self, authorization_backend: AuthorizationBackend) -> AuthService: return AuthService(authorization_backend=authorization_backend) @provider(scope="singleton") def artist_validator( self, graphql_router_client: GraphqlRouterClient ) -> ArtistValidator: return ArtistValidator(graphql_router_client=graphql_router_client) @provider(scope="singleton") def llm_session(self, db: ReportingDB) -> Iterator[SnowparkSession]: session = SnowparkSession.builder.configs( {"connection": db.engine.raw_connection().dbapi_connection} ).create() yield session session.close() def setup_container() -> Container: """Configure the application.""" return Container( providers=[ Provider(Settings, factory=lambda: settings, scope="singleton"), ], modules=[AppModule], ) # Lazy proxy for the container container = lazy_proxy(setup_container)