from functools import lru_cache from pathlib import Path from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Application settings sourced from environment / .env file.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) yunke_base_url: str = "https://crm2.yunkecn.com" yunke_username: str = "" yunke_password: str = "" db_url: str = "mysql+asyncmy://user:pass@localhost:3306/yunke" redis_url: str = "redis://localhost:6379/0" media_dir: Path = Path("./media") # Fernet key for encrypting stored credentials. Required for real runs; # empty default lets tests construct Settings without a key. encryption_key: str = "" # Existing-session creds (optional; bypasses programmatic login — e.g. for # validation with a live browser session). Read from .env / environment. yk_userid: str = "" yk_company: str = "" yk_cookie: str = "" # Shared bearer token for the viewing API. Empty (default) -> auth DISABLED # (we trust the network boundary; this is a local/intranet tool). When set, # every API request must present this value via ``Authorization: Bearer # `` or ``?token=``. See app/api/auth.py. api_token: str = "" # --- Anti-risk-control / stealth tuning --------------------------------- # These are the knobs that pace automated scraping so it resembles the # normal web app. Defaults are deliberately CONSERVATIVE (slow + jittered): # roughly one request every ~1.0..1.8s. Loosen these only once we have # evidence the CRM tolerates a faster pace. See app.session.yunke_client. # Lower bound on seconds between successive upstream requests. request_min_interval: float = 1.0 # Random extra 0..jitter seconds added on top of ``request_min_interval`` # per request, so the cadence is not a perfectly periodic signature. request_jitter: float = 0.8 # Hard cap on the number of upstream requests a single client may send in # one run. 0 = unlimited; >0 = enforce, then raise RequestCapReached. max_requests_per_run: int = 0 # Exponential backoff parameters applied when the CRM signals risk control # (HTTP 429 or a 200 body carrying a 风控/限流 marker). Delay for retry N # (0-indexed) is ``risk_backoff_base * 2**N`` clamped to ``risk_backoff_max``. risk_backoff_base: float = 2.0 risk_backoff_max: float = 60.0 # Number of retries attempted after the initial request before giving up. risk_backoff_attempts: int = 4 @lru_cache def get_settings() -> Settings: """Return the process-wide Settings singleton.""" return Settings()