"""Runtime configuration, all from the environment.""" from __future__ import annotations import os from dataclasses import dataclass, field def _int(name: str, default: int) -> int: try: return int(os.getenv(name, "") or default) except ValueError: return default def _opt(name: str) -> str | None: # Compose passes unset optionals through as empty strings rather than # omitting them, so "" has to mean "absent" here. return (os.getenv(name) or "").strip() or None @dataclass(frozen=True) class Settings: auth_token: str = field(default_factory=lambda: os.environ["OWUITOOLS_AUTH_TOKEN"]) egress_proxy: str = field(default_factory=lambda: os.getenv("OWUITOOLS_EGRESS_PROXY", "http://owuitools-egress:3128")) searxng_url: str = field(default_factory=lambda: os.getenv("OWUITOOLS_SEARXNG_URL", "http://owuitools-searxng:8080").rstrip("/")) valkey_url: str = field(default_factory=lambda: os.getenv("OWUITOOLS_VALKEY_URL", "redis://owuitools-valkey:6379/0")) contact: str = field(default_factory=lambda: os.getenv("OWUITOOLS_CONTACT", "owuitools@localhost")) # Optional. Goes in the User-Agent so a site that wants to know who is # fetching can look you up. Leave it unset if this is not reachable # from outside, which is the normal case. public_url: str | None = field(default_factory=lambda: _opt("OWUITOOLS_PUBLIC_URL")) max_response_chars: int = field(default_factory=lambda: _int("OWUITOOLS_MAX_RESPONSE_CHARS", 12000)) probe_rate_per_hour: int = field(default_factory=lambda: _int("OWUITOOLS_PROBE_RATE_PER_HOUR", 30)) log_level: str = field(default_factory=lambda: os.getenv("OWUITOOLS_LOG_LEVEL", "INFO").upper()) github_token: str | None = field(default_factory=lambda: _opt("OWUITOOLS_GITHUB_TOKEN")) stackexchange_key: str | None = field(default_factory=lambda: _opt("OWUITOOLS_STACKEXCHANGE_KEY")) nvd_key: str | None = field(default_factory=lambda: _opt("OWUITOOLS_NVD_KEY")) @property def user_agent(self) -> str: # Wikimedia hard-blocks requests with no User-Agent (verified: 403) # and its policy explicitly disallows generic ones like "curl/8.5.0", # which pass today and may not tomorrow. OpenAlex and Crossref read # the same string to decide whether you get the polite pool. # # The URL is included only if there is one to give. It used to be # one deployment's address, hardcoded - which every other # deployment would then have been announcing as its own to every # site it fetched from. site = f"+{self.public_url}; " if self.public_url else "" return f"owuitools/1.0 ({site}{self.contact})" settings = Settings()