devscontext 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
devscontext/config.py ADDED
@@ -0,0 +1,264 @@
1
+ """Configuration loader for DevsContext.
2
+
3
+ This module handles loading and parsing configuration from YAML files,
4
+ with support for environment variable expansion.
5
+
6
+ Example:
7
+ config = load_devscontext_config()
8
+ if config.sources.jira.enabled:
9
+ print(f"Jira URL: {config.sources.jira.base_url}")
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import re
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import yaml
20
+ from pydantic import BaseModel, Field
21
+
22
+ from devscontext.constants import (
23
+ CONFIG_FILE_NAME,
24
+ DEFAULT_CACHE_MAX_SIZE,
25
+ DEFAULT_CACHE_TTL_SECONDS,
26
+ )
27
+ from devscontext.models import DevsContextConfig
28
+
29
+
30
+ class JiraConfig(BaseModel):
31
+ """Jira adapter configuration.
32
+
33
+ Attributes:
34
+ base_url: The Jira instance URL (e.g., https://company.atlassian.net).
35
+ email: Email address for Jira authentication.
36
+ api_token: API token for Jira authentication.
37
+ enabled: Whether the Jira adapter is enabled.
38
+ """
39
+
40
+ base_url: str = Field(default="", description="Jira instance URL")
41
+ email: str = Field(default="", description="Jira authentication email")
42
+ api_token: str = Field(default="", description="Jira API token")
43
+ enabled: bool = Field(default=False, description="Whether adapter is enabled")
44
+
45
+
46
+ class FirefliesConfig(BaseModel):
47
+ """Fireflies.ai adapter configuration.
48
+
49
+ Attributes:
50
+ api_key: API key for Fireflies.ai authentication.
51
+ enabled: Whether the Fireflies adapter is enabled.
52
+ """
53
+
54
+ api_key: str = Field(default="", description="Fireflies.ai API key")
55
+ enabled: bool = Field(default=False, description="Whether adapter is enabled")
56
+
57
+
58
+ class LocalDocsConfig(BaseModel):
59
+ """Local documentation adapter configuration.
60
+
61
+ Attributes:
62
+ paths: List of directory paths to search for documentation.
63
+ enabled: Whether the local docs adapter is enabled.
64
+ """
65
+
66
+ paths: list[str] = Field(default_factory=list, description="Paths to doc directories")
67
+ enabled: bool = Field(default=True, description="Whether adapter is enabled")
68
+
69
+
70
+ class AdaptersConfig(BaseModel):
71
+ """Configuration for all adapters.
72
+
73
+ Attributes:
74
+ jira: Jira adapter configuration.
75
+ fireflies: Fireflies adapter configuration.
76
+ local_docs: Local docs adapter configuration.
77
+ """
78
+
79
+ jira: JiraConfig = Field(default_factory=JiraConfig)
80
+ fireflies: FirefliesConfig = Field(default_factory=FirefliesConfig)
81
+ local_docs: LocalDocsConfig = Field(default_factory=LocalDocsConfig)
82
+
83
+
84
+ class CacheConfig(BaseModel):
85
+ """Cache configuration.
86
+
87
+ Attributes:
88
+ ttl_seconds: Time-to-live in seconds for cache entries.
89
+ max_size: Maximum number of entries in the cache.
90
+ """
91
+
92
+ ttl_seconds: int = Field(
93
+ default=DEFAULT_CACHE_TTL_SECONDS,
94
+ description="Cache entry TTL in seconds",
95
+ )
96
+ max_size: int = Field(
97
+ default=DEFAULT_CACHE_MAX_SIZE,
98
+ description="Maximum cache entries",
99
+ )
100
+
101
+
102
+ class Config(BaseModel):
103
+ """Root configuration for DevsContext.
104
+
105
+ Attributes:
106
+ adapters: Configuration for all adapters.
107
+ cache: Cache configuration.
108
+ """
109
+
110
+ adapters: AdaptersConfig = Field(default_factory=AdaptersConfig)
111
+ cache: CacheConfig = Field(default_factory=CacheConfig)
112
+
113
+
114
+ # Pattern to match ${VAR_NAME} or $VAR_NAME
115
+ ENV_VAR_PATTERN = re.compile(r"\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)")
116
+
117
+
118
+ def expand_env_vars(value: Any) -> Any:
119
+ """Recursively expand environment variables in config values.
120
+
121
+ Supports both ${VAR_NAME} and $VAR_NAME syntax.
122
+ If the env var is not set, the placeholder is left unchanged.
123
+
124
+ Args:
125
+ value: The value to expand (can be str, dict, list, or primitive).
126
+
127
+ Returns:
128
+ The value with environment variables expanded.
129
+ """
130
+ if isinstance(value, str):
131
+
132
+ def replace_env_var(match: re.Match[str]) -> str:
133
+ # Group 1 is ${VAR}, group 2 is $VAR
134
+ var_name = match.group(1) or match.group(2)
135
+ return os.environ.get(var_name, match.group(0))
136
+
137
+ return ENV_VAR_PATTERN.sub(replace_env_var, value)
138
+
139
+ elif isinstance(value, dict):
140
+ return {k: expand_env_vars(v) for k, v in value.items()}
141
+
142
+ elif isinstance(value, list):
143
+ return [expand_env_vars(item) for item in value]
144
+
145
+ else:
146
+ return value
147
+
148
+
149
+ def load_config(config_path: Path | None = None) -> Config:
150
+ """Load configuration from YAML file.
151
+
152
+ Environment variables in the format ${VAR_NAME} or $VAR_NAME are expanded.
153
+
154
+ Args:
155
+ config_path: Path to config file. If None, searches for .devscontext.yaml
156
+ in current directory and parent directories.
157
+
158
+ Returns:
159
+ Loaded configuration with env vars expanded.
160
+ """
161
+ if config_path is None:
162
+ config_path = find_config_file()
163
+
164
+ if config_path is None or not config_path.exists():
165
+ return Config()
166
+
167
+ with open(config_path) as f:
168
+ data: dict[str, Any] = yaml.safe_load(f) or {}
169
+
170
+ # Expand environment variables
171
+ data = expand_env_vars(data)
172
+
173
+ return Config.model_validate(data)
174
+
175
+
176
+ def find_config_file() -> Path | None:
177
+ """Search for .devscontext.yaml in current and parent directories.
178
+
179
+ Walks up the directory tree from the current working directory,
180
+ looking for a configuration file.
181
+
182
+ Returns:
183
+ Path to the config file if found, None otherwise.
184
+ """
185
+ current = Path.cwd()
186
+
187
+ for directory in [current, *current.parents]:
188
+ config_file = directory / CONFIG_FILE_NAME
189
+ if config_file.exists():
190
+ return config_file
191
+
192
+ return None
193
+
194
+
195
+ def load_devscontext_config(config_path: Path | None = None) -> DevsContextConfig:
196
+ """Load DevsContextConfig from YAML file.
197
+
198
+ This is the new config loader that returns DevsContextConfig with
199
+ the sources/synthesis/cache structure.
200
+
201
+ Environment variables in the format ${VAR_NAME} or $VAR_NAME are expanded.
202
+
203
+ Args:
204
+ config_path: Path to config file. If None, searches for .devscontext.yaml
205
+ in current directory and parent directories.
206
+
207
+ Returns:
208
+ Loaded DevsContextConfig with env vars expanded.
209
+ """
210
+ if config_path is None:
211
+ config_path = find_config_file()
212
+
213
+ if config_path is None or not config_path.exists():
214
+ return DevsContextConfig()
215
+
216
+ with open(config_path) as f:
217
+ data: dict[str, Any] = yaml.safe_load(f) or {}
218
+
219
+ # Expand environment variables
220
+ data = expand_env_vars(data)
221
+
222
+ # Transform old config format to new format if needed
223
+ if "adapters" in data and "sources" not in data:
224
+ data = _transform_legacy_config(data)
225
+
226
+ return DevsContextConfig.model_validate(data)
227
+
228
+
229
+ def _transform_legacy_config(data: dict[str, Any]) -> dict[str, Any]:
230
+ """Transform legacy config format to new format.
231
+
232
+ Legacy format uses 'adapters' key with 'local_docs'.
233
+ New format uses 'sources' key with 'docs'.
234
+
235
+ Args:
236
+ data: Legacy config data.
237
+
238
+ Returns:
239
+ Transformed config data for DevsContextConfig.
240
+ """
241
+ adapters = data.pop("adapters", {})
242
+ cache = data.get("cache", {})
243
+
244
+ # Transform adapters to sources
245
+ sources: dict[str, Any] = {}
246
+
247
+ if "jira" in adapters:
248
+ sources["jira"] = adapters["jira"]
249
+
250
+ if "fireflies" in adapters:
251
+ sources["fireflies"] = adapters["fireflies"]
252
+
253
+ if "local_docs" in adapters:
254
+ sources["docs"] = adapters["local_docs"]
255
+
256
+ # Convert cache TTL from seconds to minutes if present
257
+ if "ttl_seconds" in cache and "ttl_minutes" not in cache:
258
+ cache["ttl_minutes"] = cache.pop("ttl_seconds") // 60
259
+
260
+ return {
261
+ "sources": sources,
262
+ "synthesis": data.get("synthesis", {}),
263
+ "cache": cache,
264
+ }
@@ -0,0 +1,107 @@
1
+ """Constants and configuration defaults for DevsContext.
2
+
3
+ This module contains all magic values, default configurations, and constants
4
+ used throughout the application. Import from here instead of hardcoding values.
5
+ """
6
+
7
+ from typing import Final
8
+
9
+ # =============================================================================
10
+ # VERSION
11
+ # =============================================================================
12
+ VERSION: Final[str] = "0.1.0"
13
+
14
+ # =============================================================================
15
+ # CACHE DEFAULTS
16
+ # =============================================================================
17
+ DEFAULT_CACHE_TTL_SECONDS: Final[int] = 900 # 15 minutes
18
+ DEFAULT_CACHE_MAX_SIZE: Final[int] = 100
19
+
20
+ # =============================================================================
21
+ # HTTP CLIENT DEFAULTS
22
+ # =============================================================================
23
+ DEFAULT_HTTP_TIMEOUT_SECONDS: Final[float] = 30.0
24
+ DEFAULT_HTTP_MAX_RETRIES: Final[int] = 3
25
+
26
+ # =============================================================================
27
+ # JIRA API
28
+ # =============================================================================
29
+ JIRA_API_VERSION: Final[str] = "3"
30
+ JIRA_API_BASE_PATH: Final[str] = f"/rest/api/{JIRA_API_VERSION}"
31
+ JIRA_MAX_COMMENTS: Final[int] = 50
32
+ JIRA_TICKET_FIELDS: Final[str] = (
33
+ "summary,description,status,priority,assignee,reporter,labels,issuetype,created,updated"
34
+ )
35
+
36
+ # =============================================================================
37
+ # FIREFLIES API
38
+ # =============================================================================
39
+ FIREFLIES_API_URL: Final[str] = "https://api.fireflies.ai/graphql"
40
+ FIREFLIES_MAX_TRANSCRIPTS: Final[int] = 10
41
+ FIREFLIES_SEARCH_LIMIT: Final[int] = 5
42
+ FIREFLIES_CONTEXT_WINDOW: Final[int] = 3 # Sentences before/after a match to include
43
+
44
+ # =============================================================================
45
+ # LOCAL DOCS
46
+ # =============================================================================
47
+ SUPPORTED_DOC_EXTENSIONS: Final[tuple[str, ...]] = (".md", ".markdown", ".txt", ".rst")
48
+ MAX_DOC_FILE_SIZE_BYTES: Final[int] = 1_000_000 # 1MB
49
+ MAX_DOCS_TO_SEARCH: Final[int] = 100
50
+
51
+ # =============================================================================
52
+ # SYNTHESIS / LLM
53
+ # =============================================================================
54
+ DEFAULT_LLM_MODEL: Final[str] = "claude-3-haiku-20240307"
55
+ MAX_CONTEXT_LENGTH_CHARS: Final[int] = 100_000
56
+ MAX_SYNTHESIS_INPUT_CHARS: Final[int] = 50_000
57
+
58
+ # =============================================================================
59
+ # MCP SERVER
60
+ # =============================================================================
61
+ MCP_SERVER_NAME: Final[str] = "devscontext"
62
+
63
+ # =============================================================================
64
+ # CONFIG FILE
65
+ # =============================================================================
66
+ CONFIG_FILE_NAME: Final[str] = ".devscontext.yaml"
67
+ CONFIG_EXAMPLE_FILE_NAME: Final[str] = ".devscontext.yaml.example"
68
+
69
+ # =============================================================================
70
+ # LOGGING
71
+ # =============================================================================
72
+ LOG_FORMAT: Final[str] = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
73
+ LOG_DATE_FORMAT: Final[str] = "%Y-%m-%d %H:%M:%S"
74
+
75
+ # =============================================================================
76
+ # ADAPTER NAMES (for consistent referencing)
77
+ # =============================================================================
78
+ ADAPTER_JIRA: Final[str] = "jira"
79
+ ADAPTER_FIREFLIES: Final[str] = "fireflies"
80
+ ADAPTER_LOCAL_DOCS: Final[str] = "local_docs"
81
+ ADAPTER_SLACK: Final[str] = "slack"
82
+ ADAPTER_GMAIL: Final[str] = "gmail"
83
+
84
+ # =============================================================================
85
+ # SOURCE TYPES
86
+ # =============================================================================
87
+ SOURCE_TYPE_ISSUE_TRACKER: Final[str] = "issue_tracker"
88
+ SOURCE_TYPE_MEETING: Final[str] = "meeting"
89
+ SOURCE_TYPE_DOCUMENTATION: Final[str] = "documentation"
90
+ SOURCE_TYPE_COMMUNICATION: Final[str] = "communication"
91
+ SOURCE_TYPE_EMAIL: Final[str] = "email"
92
+
93
+ # =============================================================================
94
+ # SLACK API
95
+ # =============================================================================
96
+ SLACK_API_BASE_URL: Final[str] = "https://slack.com/api"
97
+ SLACK_RATE_LIMIT_REQUESTS_PER_MINUTE: Final[int] = 50
98
+ SLACK_CHANNEL_HISTORY_CACHE_TTL: Final[int] = 300 # 5 minutes
99
+ SLACK_MAX_MESSAGES_PER_CHANNEL: Final[int] = 100
100
+ SLACK_THREAD_REPLY_LIMIT: Final[int] = 50
101
+
102
+ # =============================================================================
103
+ # GMAIL API
104
+ # =============================================================================
105
+ GMAIL_API_SCOPES: Final[tuple[str, ...]] = ("https://www.googleapis.com/auth/gmail.readonly",)
106
+ GMAIL_BODY_MAX_CHARS: Final[int] = 5000
107
+ GMAIL_MAX_RESULTS_PER_QUERY: Final[int] = 25