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 # second lake root on the external SSD (cold datasets + raw archives); must contain .findata-cold-root26 lake_cold: Path | None = None27 lake_cold_hard_min_free_gb: float = 100.028 hot_datasets: str = ("market_bars.stock.1m.raw,market_bars.etf.1m.raw,market_bars.stock.1d.raw,market_bars.etf.1d.raw,"29 "market_bars.stock.5m.raw,market_bars.etf.5m.raw")30 bulk_workers: int = 431 backup_target: str = ""32 # vendor + optional providers (no FINDATA_ prefix: read explicitly)33 frd_userid: SecretStr = Field(default=SecretStr(""), validation_alias="FRD_USERID")34 sec_edgar_user_agent: str = Field(default="", validation_alias="SEC_EDGAR_USER_AGENT")35 anthropic_api_key: SecretStr = Field(default=SecretStr(""), validation_alias="ANTHROPIC_API_KEY")36 openai_api_key: SecretStr = Field(default=SecretStr(""), validation_alias="OPENAI_API_KEY")37 # web static bundle38 web_dist: Path | None = None39 # rate limiting defaults (per API key / session)40 rate_limit_per_minute: int = 60041 query_timeout_seconds: int = 3042 query_max_rows: int = 100_00043 bars_max_rows_json: int = 50_00044 export_ttl_hours: int = 724546 @property47 def is_production(self) -> bool:48 return self.env == "production"4950 @property51 def hot_dataset_ids(self) -> set[str]:52 return {x.strip() for x in self.hot_datasets.split(",") if x.strip()}5354 @property55 def raw_dir(self) -> Path:56 return (self.lake_cold or self.lake) / "raw"5758 @property59 def curated_dir(self) -> Path:60 return self.lake / "curated"6162 @property63 def exports_dir(self) -> Path:64 return self.lake / "exports"6566 @property67 def tmp_dir(self) -> Path:68 return self.lake / "_tmp"6970 def secret_values(self) -> list[str]:71 """Every secret string that must never appear in logs or error messages."""72 vals = [self.frd_userid.get_secret_value(), self.anthropic_api_key.get_secret_value(),73 self.openai_api_key.get_secret_value()]74 pw = self.database_url.get_secret_value()75 if "@" in pw and ":" in pw.split("@")[0]:76 vals.append(pw.split("@")[0].rsplit(":", 1)[-1])77 return [v for v in vals if v and len(v) >= 6]787980@lru_cache81def get_settings() -> Settings:82 return Settings()83