from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from typing import Dict, Optional @dataclass(frozen=True) class Settings: openai_api_key: Optional[str] qdrant_url: str qdrant_api_key: Optional[str] qdrant_collection: str redmine_url: str redmine_api_key: Optional[str] redmine_project_identifier: Optional[str] sample_limit: int bind_host: str bind_port: int service_api_key: Optional[str] refresh_state_path: Path def load_dotenv(path: str | Path = ".env") -> Dict[str, str]: values: Dict[str, str] = {} dotenv = Path(path) if not dotenv.exists(): return values for raw_line in dotenv.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) values[key.strip()] = value.strip().strip('"').strip("'") return values def resolve_dotenv_path(dotenv_path: str | Path = ".env") -> Path: primary = Path(dotenv_path) if primary.exists(): return primary package_env = primary.parent / "semantic_index" / ".env" if package_env.exists(): return package_env return primary def load_settings(dotenv_path: str | Path = ".env") -> Settings: env = {**load_dotenv(resolve_dotenv_path(dotenv_path)), **os.environ} return Settings( openai_api_key=env.get("OPENAI_API_KEY"), qdrant_url=env.get("QDRANT_URL", "http://localhost:6333"), qdrant_api_key=env.get("QDRANT_API_KEY"), qdrant_collection=env.get("QDRANT_COLLECTION", "redmine_semantic_sample"), redmine_url=env.get("REDMINE_URL", "http://localhost"), redmine_api_key=env.get("REDMINE_API_KEY"), redmine_project_identifier=env.get("REDMINE_PROJECT_IDENTIFIER"), sample_limit=int(env.get("REDMINE_SAMPLE_LIMIT", "500")), bind_host=env.get("SEMANTIC_INDEX_HOST", "127.0.0.1"), bind_port=int(env.get("SEMANTIC_INDEX_PORT", "8787")), service_api_key=env.get("SEMANTIC_INDEX_API_KEY"), refresh_state_path=Path(env.get("SEMANTIC_INDEX_REFRESH_STATE_PATH", ".cache/semantic_index/refresh_state.json")), )