#!/usr/bin/env python3 """Shared runtime paths and environment loading for Manual Invoice.""" from __future__ import annotations import json import os from pathlib import Path from typing import Dict, Optional PLUGIN_ROOT = Path(__file__).resolve().parents[3] PROJECT_CONFIG_NAME = ".manual-invoice.env" def user_config_path() -> Path: base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) return base.expanduser() / "manual-invoice" / "config.env" def runtime_cache_root() -> Path: override = os.environ.get("MANUAL_INVOICE_CACHE_DIR") if override: return Path(override).expanduser().resolve() base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) return base.expanduser() / "manual-invoice" def venv_python(cache_root: Optional[Path] = None) -> Path: root = cache_root or runtime_cache_root() relative = Path("Scripts/python.exe") if os.name == "nt" else Path("bin/python") return root / "venv" / relative def find_project_config(start: Optional[Path] = None) -> Optional[Path]: current = (start or Path.cwd()).expanduser().resolve() if current.is_file(): current = current.parent for directory in (current, *current.parents): candidate = directory / PROJECT_CONFIG_NAME if candidate.is_file(): return candidate return None def discover_config(start: Optional[Path] = None) -> Optional[Path]: explicit = os.environ.get("MANUAL_INVOICE_CONFIG") if explicit: return Path(explicit).expanduser().resolve() project_config = find_project_config(start) if project_config: return project_config user_config = user_config_path() if user_config.is_file(): return user_config return None def _decode_value(value: str) -> str: value = value.strip() if not value: return "" if value.startswith('"') and value.endswith('"'): try: decoded = json.loads(value) return decoded if isinstance(decoded, str) else value except json.JSONDecodeError: return value[1:-1] if value.startswith("'") and value.endswith("'"): return value[1:-1] return value def read_env_file(path: Path) -> Dict[str, str]: values: Dict[str, str] = {} if not path.is_file(): return values for raw_line in path.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) key = key.strip() if key: values[key] = _decode_value(value) return values def load_env_file(path: Path) -> None: values = read_env_file(path) for key, value in values.items(): if key not in os.environ: if key == "GOOGLE_APPLICATION_CREDENTIALS" and not value: continue os.environ[key] = value credentials_path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") if credentials_path: resolved = Path(credentials_path).expanduser() if not resolved.is_absolute(): resolved = (path.parent / resolved).resolve() os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = str(resolved) github_pat = os.environ.get("GITHUB_PAT") if github_pat: os.environ.setdefault("GITHUB_TOKEN", github_pat) os.environ.setdefault("GH_TOKEN", github_pat) os.environ.setdefault("ENV_FILE", str(path.resolve())) def load_runtime_config(start: Optional[Path] = None) -> Optional[Path]: path = discover_config(start) if path: load_env_file(path) return path