"""Environment-backed application configuration.""" from decimal import Decimal from functools import lru_cache from pathlib import Path from typing import Literal from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Validated process configuration at the application boundary.""" app_name: str = "Zhixing Server" app_env: Literal["development", "test", "production"] = "development" log_level: str = "INFO" database_url: str = "postgresql://zhixing:zhixing@localhost:5432/zhixing" tushare_token: str = "" market_data_csv_root: Path = Path("./data/market-data") market_data_coverage_threshold: Decimal = Decimal("0.99") market_data_max_workers: int = Field(default=8, ge=1) market_data_request_interval_seconds: float = 0.2 market_data_max_retries: int = 3 market_data_retry_backoff_seconds: float = 1.0 market_data_advisory_lock_key: int = 7_380_521 sector_radar_coverage_threshold: Decimal = Decimal("0.99") sector_radar_request_interval_seconds: float = 0.2 sector_radar_max_retries: int = 3 sector_radar_retry_backoff_seconds: float = 1.0 sector_radar_advisory_lock_key: int = 7_380_522 selection_max_workers: int = Field(default=4, ge=1) selection_batch_size: int = Field(default=200, ge=1) selection_pattern_scoring_enabled: bool = True model_config = SettingsConfigDict( env_file=".env", env_prefix="ZHIXING_", extra="ignore", ) def sqlalchemy_database_url(database_url: str) -> str: """Add SQLAlchemy's Psycopg 3 driver to a PostgreSQL URL. ``Settings.database_url`` remains a plain ``postgresql://`` URL because Psycopg 3 accepts that form directly. Alembic and SQLAlchemy need the explicit ``postgresql+psycopg://`` dialect, while the rest of the URL (including query parameters such as ``sslmode``) must remain unchanged. Args: database_url: PostgreSQL connection URL from the process environment. Returns: The URL with SQLAlchemy's Psycopg 3 dialect prefix. Raises: ValueError: If the URL does not use a supported PostgreSQL scheme. """ if database_url.startswith("postgresql+psycopg://"): return database_url if database_url.startswith("postgresql://"): return "postgresql+psycopg://" + database_url.removeprefix("postgresql://") raise ValueError("database_url must use postgresql:// or postgresql+psycopg://") @lru_cache def get_settings() -> Settings: """Return one immutable-by-convention configuration object per process. Returns: Settings populated from defaults, `.env`, and process environment variables. """ return Settings()