import pathlib from typing import Any, Self from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from fansifter_common.adapters.aws.secretsmanager import SecretsManager from fansifter_common.constants import PROD_ENVIRONMENT, QA_ENVIRONMENT from pydantic import ( Field, ValidationError, model_validator, ) from pydantic_settings import BaseSettings, SettingsConfigDict from snowflake.sqlalchemy import URL from app.exceptions import ImproperlyConfigured class Settings(BaseSettings): environment: str debug: bool = Field(validation_alias="APP_DEBUG", default=False) base_dir: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent service_name: str = "lambda-audience-ad-thumbnail-download" service_version: str = "0.1.4" # AWS region configuration aws_region_name: str = "us-east-1" aws_access_key_id: str = "" aws_secret_access_key: str = "" aws_session_token: str = "" assets_s3_bucket_name: str = "qa-audience-campaign-assets" assets_s3_raw_path: str = "ad-creative-thumbnails/raw" assets_s3_thumbnail_path: str = "ad-creative-thumbnails/64x64" # Database snowflake_account: str = "account" snowflake_database: str = "database" snowflake_schema: str = "schema" snowflake_warehouse: str = "warehouse" snowflake_role: str = "role" snowflake_user: str = "user" snowflake_host: str = "localhost" snowflake_port: int = 443 snowflake_private_key_secret_name: str = "private_key_secret_name" snowflake_key_passphrase_secret_name: str = "key_passphrase_secret_name" snowflake_connect_args: dict[str, Any] = {} # For local development only snowflake_private_key_location: str = "snowflake_private_key_location" snowflake_private_key_password: str = "snowflake_private_key_password" snowflake_url: str = "" @model_validator(mode="after") def _setup_snowflake_url(self) -> Self: self.snowflake_url = URL( account=self.snowflake_account, user=self.snowflake_user, database=self.snowflake_database, schema=self.snowflake_schema, warehouse=self.snowflake_warehouse, role=self.snowflake_role, client_session_keep_alive=True, ) return self @model_validator(mode="after") def _setup_snowflake_private_key(self) -> Self: if self.environment in [QA_ENVIRONMENT, PROD_ENVIRONMENT]: secrets_manager = SecretsManager(region_name=self.aws_region_name) private_key = secrets_manager.get_secret( self.snowflake_private_key_secret_name ) private_key_passphrase = secrets_manager.get_secret( self.snowflake_key_passphrase_secret_name ) else: # For local development only if not pathlib.Path(self.snowflake_private_key_location).exists(): return self with open(self.snowflake_private_key_location) as fo: private_key = fo.read() private_key_passphrase = self.snowflake_private_key_password pem_private_key = serialization.load_pem_private_key( private_key.encode(), password=private_key_passphrase.encode() if private_key_passphrase else None, backend=default_backend(), ) private_bytes = pem_private_key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ) self.snowflake_connect_args["private_key"] = private_bytes return self db_fetch_chunk_size: int = 50 threads_count: int = 8 max_fails_count: int = 50 @property def fansifter_pg_reporting_schema(self) -> str: if self.environment == PROD_ENVIRONMENT: return "prod_ows_dmp_public" return "qa_ows_dmp_public" @property def meta_connection_table(self) -> str: return f"fansifter_pg_reporting.{self.fansifter_pg_reporting_schema}.meta_ad_reporting_connection" @property def tiktok_connection_table(self) -> str: return f"fansifter_pg_reporting.{self.fansifter_pg_reporting_schema}.tiktok_ad_reporting_connection" @property def google_connection_table(self) -> str: return f"fansifter_pg_reporting.{self.fansifter_pg_reporting_schema}.google_ad_reporting_connection" jinja2sql_template_searchpath: list[pathlib.Path] = [ base_dir / "connectors/database/sql", ] # Logging logging_debug: bool = Field(validation_alias="LOGGING_DEBUG", default=False) @property def logging_config(self) -> dict[str, Any]: return { "version": 1, "disable_existing_loggers": True, "formatters": { "json": { "()": "owslogger.logger.DDJsonFormatter", "service_name": self.service_name, "service_version": self.service_version, "env": self.environment, }, "console": { "()": "fansifter_common.logging.ConsoleFormatter", }, }, "filters": { "require_debug_true": { "()": "fansifter_common.logging.RequireDebugTrueFilter", "value": self.logging_debug, }, "require_debug_false": { "()": "fansifter_common.logging.RequireDebugFalseFilter", "value": self.logging_debug, }, }, "handlers": { "stream": { "level": "INFO", "class": "logging.StreamHandler", "formatter": "json", "filters": ["require_debug_false"], }, "rich": { "level": "INFO", "class": "rich.logging.RichHandler", "filters": ["require_debug_true"], "formatter": "console", }, }, "loggers": { "app": { "handlers": ["stream", "rich"], "level": "INFO", "propagate": False, }, "httpx": { "handlers": ["stream", "rich"], "level": "WARNING", "propagate": False, }, "sqlalchemy.engine": { "handlers": ["stream", "rich"], "level": "WARNING", "propagate": False, }, }, } # Sentry sentry_dsn: str | None = None @model_validator(mode="after") def _setup_sentry_dsn(self) -> Self: if not self.sentry_dsn and self.environment in [ QA_ENVIRONMENT, PROD_ENVIRONMENT, ]: raise ValueError(f"Sentry is not configured in {self.environment}.") return self model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="allow", ) def initialize_settings(**defaults: Any) -> Settings: try: return Settings(**defaults) except ValidationError as exc: errors = "\n".join([str(e) for e in exc.errors()]) raise ImproperlyConfigured( f"Failed to initialize settings:\n {errors}" ) from exc