"""Application settings, loaded from ``backend/.env`` and the environment. The database URL is assembled with :func:`sqlalchemy.engine.URL.create` rather than string formatting. That matters here: the TiDB password contains ``@``, and interpolating it into a DSN by hand silently truncates the password at the first ``@`` (it gets parsed as the user/host separator). ``URL.create`` percent-encodes each component correctly. """ from functools import lru_cache from pathlib import Path from typing import Union from pydantic_settings import BaseSettings, SettingsConfigDict from sqlalchemy import URL # backend/app/core/config.py -> backend/ BASE_DIR = Path(__file__).resolve().parents[2] class Settings(BaseSettings): """Runtime configuration for the API service.""" model_config = SettingsConfigDict( env_file=BASE_DIR / ".env", env_file_encoding="utf-8", extra="ignore", ) app_name: str = "paper-doc API" api_prefix: str = "/api" debug: bool = True # The Vite dev server proxies /api to this service, so CORS is normally not # exercised during development; it is configured for direct-origin setups. cors_origins: list[str] = [ "http://127.0.0.1:5173", "http://localhost:5173", ] # --- Database (TiDB) ------------------------------------------------- db_host: str = "127.0.0.1" db_port: int = 4000 db_user: str = "root" db_password: str = "" db_name: str = "paper_doc" # When set, this wins over the DB_* parts above and is used verbatim. database_url: str | None = None # --- Connection pool ------------------------------------------------- db_echo: bool = False db_pool_pre_ping: bool = True db_pool_recycle: int = 3600 @property def sqlalchemy_url(self) -> Union[URL, str]: """The SQLAlchemy connection URL. Returns a :class:`~sqlalchemy.engine.URL` unless ``DATABASE_URL`` was supplied explicitly, in which case that string is passed through. """ if self.database_url: return self.database_url return URL.create( drivername="mysql+pymysql", username=self.db_user, password=self.db_password, host=self.db_host, port=self.db_port, database=self.db_name, ) @property def safe_database_url(self) -> str: """The URL rendered with the password masked, for logs and diagnostics.""" url = self.sqlalchemy_url if isinstance(url, str): return URL.make_url(url).render_as_string(hide_password=True) return url.render_as_string(hide_password=True) @lru_cache def get_settings() -> Settings: """Return the process-wide settings singleton.""" return Settings()