import pathlib from typing import Any from audience_common.enums import Environment from audience_common.exceptions import ImproperlyConfigured from audience_common.logger import LogFormat, prepare_logging_config from audience_common.types import Base64ToStr from pydantic import BaseSettings, Field, ValidationError, validator from sqlalchemy.engine import URL from campaigns.core.types import PlainToken class AppSettings(BaseSettings): # Main environment: Environment debug: bool = Field(env="APP_DEBUG", default=False) run_from_cli: bool = Field(env="APP_RUN_FROM_CLI", default=False) # Directories base_dir: pathlib.Path = pathlib.Path(__file__).resolve().parent root_dir: pathlib.Path = base_dir.parent # Service service_name: str = "ows-campaigns" service_version: str = "1.0.0" # AWS region aws_region_name: str = "us-east-1" # KMS kms_enabled: bool = True @validator("kms_enabled") def _setup_kms_enabled(cls, v: bool, values: dict[str, Any]) -> bool: environment: Environment = values["environment"] if environment in [Environment.QA, Environment.PROD]: return True return v kms_key_id: str = Field(default="", env="DMP_KMS_KEY_ID") @validator("kms_key_id") def _setup_kms_key_id(cls, v: str, values: dict[str, Any]) -> str: kms_enabled: bool = values["kms_enabled"] if kms_enabled and not v: raise ValueError("`kms_key_id` must be set.") return v # Database postgres_user: str = "postgres" postgres_password: Base64ToStr = "postgres" postgres_host: str = "localhost" postgres_port: int = 5432 postgres_name: str = "fansifter" postgres_echo: bool = False postgres_pool_size: int = 10 postgres_pool_max_overflow: int = 20 postgres_pool_recycle: int = 4 * 60 * 60 # ~4 hours postgres_pool_pre_ping: bool = False postgres_pool_reset_on_return: bool | None = None postgres_connect_args: dict[str, Any] = {} @property def postgres_url(self) -> URL: return URL.create( drivername="postgresql+psycopg", username=self.postgres_user, password=self.postgres_password, host=self.postgres_host, port=self.postgres_port, database=self.postgres_name, ) jinjasql_template_searchpath: list[pathlib.Path] = [ base_dir / "campaigns" / "sql", ] # ORM orm_models: list[str] = [ "campaigns.assets.models", "campaigns.meta.models", ] # Redis redis_host: str = "localhost" redis_port: int = 6379 redis_ssl: bool = False @validator("redis_ssl") def _setup_redis_ssl(cls, v: bool, values: dict[str, Any]) -> bool: environment: Environment = values["environment"] if environment in [Environment.QA, Environment.PROD]: return True return v # Cache cache_backend: str = "locmem" cache_default_timeout: int = 300 cache_key_prefix: str = "ows-campaigns:" cache_redis_db: int = 0 @validator("cache_backend") def _setup_cache_backend(cls, v: str, values: dict[str, Any]) -> str: environment: Environment = values["environment"] if environment in [Environment.QA, Environment.PROD]: return "redis" return v # M2M Token @property def m2m_token_secret_key_name(self) -> str: return f"{self.environment}/lambda-jwt-refresh/jwt_token" @property def m2m_token_secret_expiry_key_name(self) -> str: return f"{self.environment}/lambda-jwt-refresh/jwt_token_expiration" # API jwt_auth_enabled: bool = False @validator("jwt_auth_enabled") def _setup_jwt_auth_enabled(cls, v: bool, values: dict[str, Any]) -> bool: environment: Environment = values["environment"] if environment in [Environment.PROD]: return True return v jwt_auth_exclude_paths: list[str] = ["/hello/", "/redoc", "/docs*", "/openapi.json"] jwt_auth_leeway: int = 0 @validator("jwt_auth_leeway") def _setup_jwt_auth_leeway(cls, v: int, values: dict[str, Any]) -> int: environment: Environment = values["environment"] if environment == Environment.PROD and v > 0: raise ValueError(f"leeway must be 0 in PROD. Found: {v}") return v auth_allow_full_access: bool = False @validator("auth_allow_full_access") def _setup_auth_allow_full_access(cls, v: bool, values: dict[str, Any]) -> bool: """Allow to set full access authorization for local/dev environment(s) only.""" environment: Environment = values["environment"] if environment in [Environment.QA, Environment.PROD]: return False return v # Testing @property def include_test_router(self) -> bool: return self.environment != Environment.PROD and not self.run_from_cli # OpenAPI / Swagger qa_server_url: str = "https://qa-ows-campaigns.theorchard.io" openapi_url: str | None = "/openapi.json" @validator("openapi_url") def _setup_openapi_url(cls, v: str | None, values: dict[str, Any]) -> str | None: environment: Environment = values["environment"] if environment == Environment.PROD: return None return v openapi_filepath: pathlib.Path = root_dir / "spec/ows_campaigns-1.0.0.yaml" openapi_servers: list[dict[str, Any]] | None = None @validator("openapi_servers") def _setup_openapi_servers( cls, v: list[dict[str, Any]] | None, values: dict[str, Any] ) -> list[dict[str, Any]] | None: environment: Environment = values["environment"] run_from_cli: bool = values["run_from_cli"] qa_server_url: str = values["qa_server_url"] if environment == Environment.DEV and run_from_cli: return [{"url": qa_server_url}] return v # Assets assets_bucket_name: str = Field(default="", env="CAMPAIGN_ASSETS_BUCKET_NAME") assets_upload_iam_role_arn: str = Field(default="", env="ASSET_UPLOAD_IAM_ROLE_ARN") assets_cdn_domain: str = Field(default="", env="ASSETS_CDN_DOMAIN") def get_asset_upload_policy(self, key: str) -> dict[str, Any]: return { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:CreateMultipartUpload", "s3:UploadPart", "s3:AbortMultipartUpload", "s3:CompleteMultipartUpload", ], "Resource": f"arn:aws:s3:::{self.assets_bucket_name}/{key}", } ], } # Facebook facebook_app_id: str = "" facebook_app_secret: str = "" facebook_app_system_user_access_token: PlainToken = PlainToken("") facebook_app_system_user_id: str = "" facebook_admin_system_user_access_token: PlainToken = PlainToken("") facebook_adaccount_id: str = "" facebook_business_id: str = "" facebook_assign_agency_access_to_pages_enabled: bool = True @validator("facebook_assign_agency_access_to_pages_enabled") def _setup_facebook_assign_agency_access_to_pages_enabled( cls, v: bool, values: dict[str, Any] ) -> bool: environment: Environment = values["environment"] facebook_admin_system_user_access_token: PlainToken = values[ "facebook_admin_system_user_access_token" ] if environment in [Environment.QA, Environment.PROD] or bool( facebook_admin_system_user_access_token ): return True return v # Account access account_access_cache_timeout: int = 60 * 10 # 10 minutes # Logging logger_name: str = "ows1" log_format: LogFormat = Field(default=LogFormat.JSON) logging_config: dict[str, Any] = { "version": 1, "disable_existing_loggers": False, "handlers": { "stream": { "level": "INFO", "class": "audience_common.logger.OwsStreamHandler", }, "null": { "level": "INFO", "class": "logging.NullHandler", }, }, "loggers": { "campaigns": { "handlers": ["stream"], "level": "INFO", }, "audience_common.ext.starlette.middleware.logger": { "handlers": ["stream"], "level": "INFO", }, "sqlalchemy.engine": { "handlers": ["stream"], "level": "WARNING", }, "httpx": { "handlers": ["stream"], "level": "WARNING", }, "uvicorn": { "handlers": ["stream"], "level": "INFO", }, "uvicorn.access": { "handlers": ["null"], "level": "INFO", }, }, } @validator("logging_config") def _setup_logging_config( cls, v: dict[str, Any], values: dict[str, Any] ) -> dict[str, Any]: environment: Environment = values["environment"] debug: bool = values["debug"] service_name: str = values["service_name"] service_version: str = values["service_version"] logger_name: str = values["logger_name"] log_format: LogFormat = values["log_format"] return prepare_logging_config( logging_config=v, environment=environment, service_name=service_name, service_version=service_version, logger_name=logger_name, log_format=log_format, debug=debug, ) # Sentry sentry_dsn: str | None @validator("sentry_dsn") def _setup_sentry_dsn(cls, v: str | None, values: dict[str, Any]) -> str | None: environment: Environment = values["environment"] if not v and environment in [Environment.QA, Environment.PROD]: raise ValueError(f"Sentry is not configured in {environment}.") return v class Config: env_file = ".env" env_file_encoding = "utf-8" case_sensitive = False try: settings = AppSettings() except ValidationError as exc: raise ImproperlyConfigured(f"Configuration error: {exc}") from exc