from __future__ import annotations import pathlib import string from typing import Any, Self from fansifter_common.constants import PROD_ENVIRONMENT, QA_ENVIRONMENT from fansifter_common.utils.functional import lazy_proxy from pydantic import Field, ValidationError, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): # Main environment: str debug: bool = Field(validation_alias="APP_DEBUG", default=False) run_from_cli: bool = Field(validation_alias="APP_RUN_FROM_CLI", default=False) # Application params short_path_allowed_chars: str = string.ascii_letters + string.digits allowed_domains: list[str] = ["qa-ows-url-shortener.theorchard.com"] # DynamoDB dynamodb_write_threads_count: int = 8 dynamodb_max_write_retry_count: int = 10 # Kafka kafka_bootstrap_servers: str = "localhost" kafka_use_ssl: bool = True kafka_analytics_topic: str = "event.owsUrlShortener.click" # S3 failed_events_s3_bucket: str = "qa-ows-url-shortener-dead-letter-events" # Directories base_dir: pathlib.Path = pathlib.Path(__file__).resolve().parent root_dir: pathlib.Path = base_dir.parent # Service service_name: str = "ows-url-shortener" service_version: str = "1.0.0" # AWS aws_region_name: str = "us-east-1" dynamodb_tablename: str = "qa_ows_url_shortener_paths" # JWT Auth jwt_auth_enabled: bool = True jwt_auth_exclude_paths: list[str] = [ "/hello/", "/redoc", "/docs*", "/openapi.json", "/_debug_toolbar", "/redirect/*", ] # OpenAPI / Swagger qa_server_url: str = "https://qa-url-shortener.theorchard.com" openapi_url: str | None = "/openapi.json" @model_validator(mode="after") def _setup_openapi_url(self) -> Self: if self.environment == PROD_ENVIRONMENT: self.openapi_url = None return self openapi_filepath: pathlib.Path = root_dir / "spec" / "ows_url_shortener-1.0.0.yaml" openapi_servers: list[dict[str, Any]] | None = None @model_validator(mode="after") def _setup_openapi_servers(self) -> Self: if self.environment == "dev" and self.run_from_cli: self.openapi_servers = [{"url": self.qa_server_url}] return self # Logging logging_debug: bool = Field(False, validation_alias="LOGGING_DEBUG") @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", }, "null": { "level": "INFO", "class": "logging.NullHandler", }, }, "loggers": { "url_shortener": { "handlers": ["stream", "rich"], "level": "INFO", }, "httpx": { "handlers": ["stream", "rich"], "level": "WARNING", }, "aiokafka": { "handlers": ["stream", "rich"], "level": "WARNING", }, "uvicorn": { "handlers": ["stream", "rich"], "level": "INFO", }, "uvicorn.access": { "handlers": ["null"], "level": "INFO", }, }, } # 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 setup_settings(**defaults: Any) -> Settings: """Setup the application settings.""" try: return Settings(**defaults) except ValidationError as exc: errors = "\n".join( [ f" * {'.'.join(map(str, error['loc']))} - {error['msg']}" for error in exc.errors() ] ) raise RuntimeError(f"Failed to initialize settings:\n {errors}") from exc # Lazy proxy for settings settings = lazy_proxy(setup_settings)