1from __future__ import annotations23from functools import lru_cache4from pathlib import Path56from pydantic import Field, SecretStr7from pydantic_settings import BaseSettings, SettingsConfigDict8910class Settings(BaseSettings):11 """Runtime settings. Secrets are `SecretStr` so that `repr()`/logging never prints them."""1213 model_config = SettingsConfigDict(env_prefix="FINDATA_", env_file=".env", env_file_encoding="utf-8", extra="ignore")1415 env: str = "development"16 root: Path = Field(default_factory=lambda: Path.cwd())17 lake: Path = Field(default_factory=lambda: Path.cwd() / "data" / "lake")18 database_url: SecretStr = SecretStr("postgresql://findata:findata-local@127.0.0.1:5432/findata")19 bind_host: str = "127.0.0.1"20 bind_port: int = 860021 public_url: str = "http://127.0.0.1:8600"22 secret_key: SecretStr = SecretStr("dev-only-change-me-dev-only-change-me")23 lake_soft_limit_gb: float = 250.024 lake_hard_min_free_gb: float = 60.025 backup_target: str = ""26 # vendor + optional providers (no FINDATA_ prefix: read explicitly)27 frd_userid: SecretStr = Field(default=SecretStr(""), validation_alias="FRD_USERID")28 sec_edgar_user_agent: str = Field(default="", validation_alias="SEC_EDGAR_USER_AGENT")29 anthropic_api_key: SecretStr = Field(default=SecretStr(""), validation_alias="ANTHROPIC_API_KEY")30 openai_api_key: SecretStr = Field(default=SecretStr(""), validation_alias="OPENAI_API_KEY")31 # web static bundle32 web_dist: Path | None = None33 # rate limiting defaults (per API key / session)34 rate_limit_per_minute: int = 60035 query_timeout_seconds: int = 3036 query_max_rows: int = 100_00037 bars_max_rows_json: int = 50_00038 export_ttl_hours: int = 723940 @property41 def is_production(self) -> bool:42 return self.env == "production"4344 @property45 def raw_dir(self) -> Path:46 return self.lake / "raw"4748 @property49 def curated_dir(self) -> Path:50 return self.lake / "curated"5152 @property53 def exports_dir(self) -> Path:54 return self.lake / "exports"5556 @property57 def tmp_dir(self) -> Path:58 return self.lake / "_tmp"5960 def secret_values(self) -> list[str]:61 """Every secret string that must never appear in logs or error messages."""62 vals = [self.frd_userid.get_secret_value(), self.anthropic_api_key.get_secret_value(),63 self.openai_api_key.get_secret_value()]64 pw = self.database_url.get_secret_value()65 if "@" in pw and ":" in pw.split("@")[0]:66 vals.append(pw.split("@")[0].rsplit(":", 1)[-1])67 return [v for v in vals if v and len(v) >= 6]686970@lru_cache71def get_settings() -> Settings:72 return Settings()73