from __future__ import annotations from functools import lru_cache from pathlib import Path from pydantic import Field, SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Runtime settings. Secrets are `SecretStr` so that `repr()`/logging never prints them.""" model_config = SettingsConfigDict(env_prefix="FINDATA_", env_file=".env", env_file_encoding="utf-8", extra="ignore") env: str = "development" root: Path = Field(default_factory=lambda: Path.cwd()) lake: Path = Field(default_factory=lambda: Path.cwd() / "data" / "lake") database_url: SecretStr = SecretStr("postgresql://findata:findata-local@127.0.0.1:5432/findata") bind_host: str = "127.0.0.1" bind_port: int = 8600 public_url: str = "http://127.0.0.1:8600" secret_key: SecretStr = SecretStr("dev-only-change-me-dev-only-change-me") lake_soft_limit_gb: float = 250.0 lake_hard_min_free_gb: float = 60.0 # second lake root on the external SSD (cold datasets + raw archives); must contain .findata-cold-root lake_cold: Path | None = None lake_cold_hard_min_free_gb: float = 100.0 hot_datasets: str = ("market_bars.stock.1m.raw,market_bars.etf.1m.raw,market_bars.stock.1d.raw,market_bars.etf.1d.raw," "market_bars.stock.5m.raw,market_bars.etf.5m.raw") bulk_workers: int = 4 backup_target: str = "" # vendor + optional providers (no FINDATA_ prefix: read explicitly) frd_userid: SecretStr = Field(default=SecretStr(""), validation_alias="FRD_USERID") sec_edgar_user_agent: str = Field(default="", validation_alias="SEC_EDGAR_USER_AGENT") anthropic_api_key: SecretStr = Field(default=SecretStr(""), validation_alias="ANTHROPIC_API_KEY") openai_api_key: SecretStr = Field(default=SecretStr(""), validation_alias="OPENAI_API_KEY") # web static bundle web_dist: Path | None = None # rate limiting defaults (per API key / session) rate_limit_per_minute: int = 600 query_timeout_seconds: int = 30 query_max_rows: int = 100_000 bars_max_rows_json: int = 50_000 export_ttl_hours: int = 72 @property def is_production(self) -> bool: return self.env == "production" @property def hot_dataset_ids(self) -> set[str]: return {x.strip() for x in self.hot_datasets.split(",") if x.strip()} @property def raw_dir(self) -> Path: return (self.lake_cold or self.lake) / "raw" @property def curated_dir(self) -> Path: return self.lake / "curated" @property def exports_dir(self) -> Path: return self.lake / "exports" @property def tmp_dir(self) -> Path: return self.lake / "_tmp" def secret_values(self) -> list[str]: """Every secret string that must never appear in logs or error messages.""" vals = [self.frd_userid.get_secret_value(), self.anthropic_api_key.get_secret_value(), self.openai_api_key.get_secret_value()] pw = self.database_url.get_secret_value() if "@" in pw and ":" in pw.split("@")[0]: vals.append(pw.split("@")[0].rsplit(":", 1)[-1]) return [v for v in vals if v and len(v) >= 6] @lru_cache def get_settings() -> Settings: return Settings()