"""Configuration loaded from environment variables. Real credentials must stay outside the repository. This module reads them from the process environment and only exposes redacted summaries for logs. """ from __future__ import annotations from dataclasses import dataclass import os class MissingConfigError(RuntimeError): pass @dataclass(frozen=True) class PostgresConfig: host: str user: str port: int database: str password: str @dataclass(frozen=True) class RedisConfig: host: str port: int password: str ssl: bool @dataclass(frozen=True) class BlobConfig: connection_string: str container: str @dataclass(frozen=True) class SwarmConfig: postgres: PostgresConfig redis: RedisConfig blob: BlobConfig @classmethod def from_env(cls) -> "SwarmConfig": missing = [ name for name in ( "PGHOST", "PGUSER", "PGDATABASE", "PGPASSWORD", "AZURE_STORAGE_CONNECTION_STRING", ) if not os.environ.get(name) ] redis_conn = os.environ.get("SWARM_REDIS_CONNECTION_STRING") redis_host = os.environ.get("REDIS_HOST") redis_password = os.environ.get("REDIS_PASSWORD") if not redis_conn and not (redis_host and redis_password): missing.append("SWARM_REDIS_CONNECTION_STRING or REDIS_HOST/REDIS_PASSWORD") if missing: raise MissingConfigError("missing required environment variables: " + ", ".join(missing)) return cls( postgres=PostgresConfig( host=os.environ["PGHOST"], user=os.environ["PGUSER"], port=int(os.environ.get("PGPORT", "5432")), database=os.environ["PGDATABASE"], password=os.environ["PGPASSWORD"], ), redis=parse_redis_config(redis_conn) if redis_conn else RedisConfig( host=os.environ["REDIS_HOST"], port=int(os.environ.get("REDIS_PORT", "6380")), password=os.environ["REDIS_PASSWORD"], ssl=os.environ.get("REDIS_SSL", "true").lower() in {"1", "true", "yes"}, ), blob=BlobConfig( connection_string=os.environ["AZURE_STORAGE_CONNECTION_STRING"], container=os.environ.get("SWARM_BLOB_CONTAINER", "swarm-artifacts"), ), ) def redacted_summary(self) -> dict[str, object]: account_name = _extract_connection_value(self.blob.connection_string, "AccountName") return { "postgres": { "host": self.postgres.host, "port": self.postgres.port, "database": self.postgres.database, "user": self.postgres.user, "password": "", }, "redis": { "host": self.redis.host, "port": self.redis.port, "ssl": self.redis.ssl, "password": "", }, "blob": { "account_name": account_name or "", "container": self.blob.container, "account_key": "", }, } def parse_redis_config(value: str) -> RedisConfig: parts = [part.strip() for part in value.split(",") if part.strip()] if not parts or ":" not in parts[0]: raise ValueError("redis connection string must start with host:port") host, port_text = parts[0].rsplit(":", 1) fields: dict[str, str] = {} for part in parts[1:]: if "=" in part: key, field_value = part.split("=", 1) fields[key.strip().lower()] = field_value.strip() password = fields.get("password") if not password: raise ValueError("redis connection string must include password") return RedisConfig( host=host, port=int(port_text), password=password, ssl=fields.get("ssl", "true").lower() in {"1", "true", "yes"}, ) def _extract_connection_value(connection_string: str, key: str) -> str | None: prefix = f"{key}=" for part in connection_string.split(";"): if part.startswith(prefix): return part[len(prefix) :] return None