Coverage for repo_ctx / config.py: 82%
121 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-25 17:42 +0100
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-25 17:42 +0100
1"""Configuration management."""
2import os
3import re
4from pathlib import Path
5from typing import Optional
6import yaml
7from pydantic import BaseModel
10class Config(BaseModel):
11 # GitLab (optional now - for multi-provider support)
12 gitlab_url: Optional[str] = None
13 gitlab_token: Optional[str] = None
15 # GitHub (optional)
16 github_url: Optional[str] = None
17 github_token: Optional[str] = None
19 # Storage
20 storage_path: str = "./data/context.db"
22 @classmethod
23 def from_env(cls) -> "Config":
24 """Load config from environment variables.
26 Supported environment variables:
27 GitLab:
28 - GITLAB_URL or GIT_CONTEXT_GITLAB_URL
29 - GITLAB_TOKEN or GIT_CONTEXT_GITLAB_TOKEN
31 GitHub:
32 - GITHUB_URL or GIT_CONTEXT_GITHUB_URL (default: https://api.github.com)
33 - GITHUB_TOKEN or GIT_CONTEXT_GITHUB_TOKEN (optional for public repos)
35 Storage:
36 - STORAGE_PATH or GIT_CONTEXT_STORAGE_PATH (default: ~/.repo-ctx/context.db)
38 At least one provider (GitLab or GitHub) must be configured.
39 """
40 # GitLab
41 gitlab_url = os.getenv("GIT_CONTEXT_GITLAB_URL") or os.getenv("GITLAB_URL")
42 gitlab_token = os.getenv("GIT_CONTEXT_GITLAB_TOKEN") or os.getenv("GITLAB_TOKEN")
44 # GitHub
45 github_url = os.getenv("GIT_CONTEXT_GITHUB_URL") or os.getenv("GITHUB_URL")
46 github_token = os.getenv("GIT_CONTEXT_GITHUB_TOKEN") or os.getenv("GITHUB_TOKEN")
48 # Default GitHub URL if token is provided but URL is not
49 if github_token and not github_url:
50 github_url = "https://api.github.com"
52 # Storage
53 storage_path = (
54 os.getenv("GIT_CONTEXT_STORAGE_PATH")
55 or os.getenv("STORAGE_PATH")
56 or os.path.expanduser("~/.repo-ctx/context.db")
57 )
59 # Validate at least one provider is configured
60 if not (gitlab_url and gitlab_token) and not github_url:
61 raise ValueError(
62 "At least one provider must be configured.\n"
63 "GitLab: Set GITLAB_URL and GITLAB_TOKEN\n"
64 "GitHub: Set GITHUB_URL (optional) and/or GITHUB_TOKEN\n"
65 "Or set GIT_CONTEXT_* prefixed versions"
66 )
68 return cls(
69 gitlab_url=gitlab_url,
70 gitlab_token=gitlab_token,
71 github_url=github_url,
72 github_token=github_token,
73 storage_path=storage_path
74 )
76 @classmethod
77 def from_yaml(cls, path: str = "config.yaml") -> "Config":
78 """Load config from YAML file with environment variable substitution.
80 Example config.yaml:
81 ```yaml
82 gitlab:
83 url: "https://gitlab.company.com"
84 token: "${GITLAB_TOKEN}"
86 github:
87 url: "https://api.github.com" # Optional, defaults to public GitHub
88 token: "${GITHUB_TOKEN}" # Optional for public repos
90 storage:
91 path: "~/.repo-ctx/context.db"
92 ```
93 """
94 config_path = Path(path)
95 if not config_path.exists():
96 raise FileNotFoundError(f"Config file not found: {path}")
98 with open(config_path) as f:
99 content = f.read()
101 # Substitute ${VAR} or $VAR with environment variables
102 def replace_env_var(match):
103 var_name = match.group(1) or match.group(2)
104 value = os.getenv(var_name)
105 if value is None:
106 raise ValueError(f"Environment variable {var_name} not set")
107 return value
109 # Replace ${VAR} and $VAR patterns
110 content = re.sub(r'\$\{([^}]+)\}|\$([A-Z_][A-Z0-9_]*)', replace_env_var, content)
112 data = yaml.safe_load(content)
114 # Extract GitLab config
115 gitlab_url = None
116 gitlab_token = None
117 if "gitlab" in data:
118 gitlab_url = data["gitlab"].get("url")
119 gitlab_token = data["gitlab"].get("token")
121 # Extract GitHub config
122 github_url = None
123 github_token = None
124 if "github" in data:
125 github_url = data["github"].get("url")
126 github_token = data["github"].get("token")
127 # Default GitHub URL
128 if github_token and not github_url:
129 github_url = "https://api.github.com"
131 # Extract storage path
132 storage_path = data.get("storage", {}).get("path", "./data/context.db")
134 # Validate at least one provider
135 if not (gitlab_url and gitlab_token) and not github_url:
136 raise ValueError(
137 "Config file must specify at least one provider (gitlab or github)"
138 )
140 return cls(
141 gitlab_url=gitlab_url,
142 gitlab_token=gitlab_token,
143 github_url=github_url,
144 github_token=github_token,
145 storage_path=storage_path
146 )
148 @classmethod
149 def find_config_file(cls) -> Optional[Path]:
150 """Find config file in standard locations.
152 Checks in order:
153 1. ./config.yaml (current directory)
154 2. ~/.config/repo-ctx/config.yaml
155 3. ~/.repo-ctx/config.yaml
157 Returns:
158 Path to config file if found, None otherwise
159 """
160 locations = [
161 Path("config.yaml"),
162 Path.home() / ".config" / "repo-ctx" / "config.yaml",
163 Path.home() / ".repo-ctx" / "config.yaml",
164 ]
166 for location in locations:
167 if location.exists():
168 return location
170 return None
172 @classmethod
173 def load(
174 cls,
175 config_path: Optional[str] = None,
176 gitlab_url: Optional[str] = None,
177 gitlab_token: Optional[str] = None,
178 github_url: Optional[str] = None,
179 github_token: Optional[str] = None,
180 storage_path: Optional[str] = None,
181 ) -> "Config":
182 """Load configuration from multiple sources with priority.
184 Priority (highest to lowest):
185 1. Explicit arguments
186 2. Specified config file (config_path)
187 3. Environment variables
188 4. Standard config file locations
190 Args:
191 config_path: Explicit path to config file
192 gitlab_url: GitLab URL (overrides all other sources)
193 gitlab_token: GitLab token (overrides all other sources)
194 github_url: GitHub URL (overrides all other sources)
195 github_token: GitHub token (overrides all other sources)
196 storage_path: Storage path (overrides all other sources)
198 Returns:
199 Config instance
201 Raises:
202 ValueError: If no valid configuration source is found
203 """
204 # If any provider is explicitly configured, use explicit params
205 has_gitlab = gitlab_url and gitlab_token
206 has_github = github_url or github_token
208 if has_gitlab or has_github:
209 # Default GitHub URL if not provided
210 if github_token and not github_url:
211 github_url = "https://api.github.com"
213 return cls(
214 gitlab_url=gitlab_url,
215 gitlab_token=gitlab_token,
216 github_url=github_url,
217 github_token=github_token,
218 storage_path=storage_path or os.path.expanduser("~/.repo-ctx/context.db")
219 )
221 # Try explicit config path
222 if config_path:
223 try:
224 config = cls.from_yaml(config_path)
225 # Override with explicit params if provided
226 if gitlab_url:
227 config.gitlab_url = gitlab_url
228 if gitlab_token:
229 config.gitlab_token = gitlab_token
230 if github_url:
231 config.github_url = github_url
232 if github_token:
233 config.github_token = github_token
234 if storage_path:
235 config.storage_path = storage_path
236 return config
237 except FileNotFoundError:
238 raise
239 except Exception as e:
240 raise ValueError(f"Error loading config from {config_path}: {e}")
242 # Try environment variables
243 try:
244 config = cls.from_env()
245 # Override with explicit params if provided
246 if gitlab_url:
247 config.gitlab_url = gitlab_url
248 if gitlab_token:
249 config.gitlab_token = gitlab_token
250 if github_url:
251 config.github_url = github_url
252 if github_token:
253 config.github_token = github_token
254 if storage_path:
255 config.storage_path = storage_path
256 return config
257 except ValueError:
258 pass # Environment variables not set, try config file
260 # Try standard config file locations
261 config_file = cls.find_config_file()
262 if config_file:
263 try:
264 config = cls.from_yaml(str(config_file))
265 # Override with explicit params if provided
266 if gitlab_url:
267 config.gitlab_url = gitlab_url
268 if gitlab_token:
269 config.gitlab_token = gitlab_token
270 if github_url:
271 config.github_url = github_url
272 if github_token:
273 config.github_token = github_token
274 if storage_path:
275 config.storage_path = storage_path
276 return config
277 except Exception as e:
278 raise ValueError(f"Error loading config from {config_file}: {e}")
280 # No valid config found
281 raise ValueError(
282 "No configuration found. Please configure at least one provider:\n\n"
283 "Option 1: Environment variables\n"
284 " GitLab: GITLAB_URL and GITLAB_TOKEN\n"
285 " GitHub: GITHUB_URL (optional) and GITHUB_TOKEN\n\n"
286 "Option 2: Config file in:\n"
287 " - Current directory (config.yaml)\n"
288 " - ~/.config/repo-ctx/config.yaml\n"
289 " - ~/.repo-ctx/config.yaml\n\n"
290 "Option 3: Command-line arguments\n"
291 " --gitlab-url --gitlab-token\n"
292 " --github-url --github-token\n\n"
293 "Option 4: Use --config to specify config file path"
294 )