mcp-ticketer 0.3.1__py3-none-any.whl → 0.3.2__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.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +12 -15
- mcp_ticketer/adapters/github.py +7 -4
- mcp_ticketer/adapters/jira.py +23 -22
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +88 -89
- mcp_ticketer/adapters/linear/client.py +71 -52
- mcp_ticketer/adapters/linear/mappers.py +88 -68
- mcp_ticketer/adapters/linear/queries.py +28 -7
- mcp_ticketer/adapters/linear/types.py +57 -50
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/cli/adapter_diagnostics.py +86 -51
- mcp_ticketer/cli/diagnostics.py +165 -72
- mcp_ticketer/cli/linear_commands.py +156 -113
- mcp_ticketer/cli/main.py +153 -82
- mcp_ticketer/cli/simple_health.py +73 -45
- mcp_ticketer/cli/utils.py +15 -10
- mcp_ticketer/core/config.py +23 -19
- mcp_ticketer/core/env_discovery.py +5 -4
- mcp_ticketer/core/env_loader.py +109 -86
- mcp_ticketer/core/exceptions.py +20 -18
- mcp_ticketer/core/models.py +9 -0
- mcp_ticketer/core/project_config.py +1 -1
- mcp_ticketer/mcp/server.py +294 -139
- mcp_ticketer/queue/health_monitor.py +152 -121
- mcp_ticketer/queue/manager.py +11 -4
- mcp_ticketer/queue/queue.py +15 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +190 -132
- mcp_ticketer/queue/worker.py +54 -25
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/METADATA +1 -1
- mcp_ticketer-0.3.2.dist-info/RECORD +59 -0
- mcp_ticketer-0.3.1.dist-info/RECORD +0 -59
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/top_level.txt +0 -0
mcp_ticketer/core/env_loader.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Unified Environment Loading System for MCP Ticketer.
|
|
2
|
+
"""Unified Environment Loading System for MCP Ticketer.
|
|
4
3
|
|
|
5
4
|
This module provides a resilient environment loading system that:
|
|
6
5
|
1. Supports multiple naming conventions for each configuration key
|
|
@@ -9,11 +8,11 @@ This module provides a resilient environment loading system that:
|
|
|
9
8
|
4. Provides fallback mechanisms for different key aliases
|
|
10
9
|
"""
|
|
11
10
|
|
|
12
|
-
import os
|
|
13
11
|
import logging
|
|
14
|
-
|
|
15
|
-
from typing import Dict, Any, List, Optional, Union
|
|
12
|
+
import os
|
|
16
13
|
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
17
16
|
|
|
18
17
|
logger = logging.getLogger(__name__)
|
|
19
18
|
|
|
@@ -21,6 +20,7 @@ logger = logging.getLogger(__name__)
|
|
|
21
20
|
@dataclass
|
|
22
21
|
class EnvKeyConfig:
|
|
23
22
|
"""Configuration for environment variable key aliases."""
|
|
23
|
+
|
|
24
24
|
primary_key: str
|
|
25
25
|
aliases: List[str]
|
|
26
26
|
description: str
|
|
@@ -29,11 +29,10 @@ class EnvKeyConfig:
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class UnifiedEnvLoader:
|
|
32
|
-
"""
|
|
33
|
-
Unified environment loader that handles multiple naming conventions
|
|
32
|
+
"""Unified environment loader that handles multiple naming conventions
|
|
34
33
|
and provides consistent environment loading across all contexts.
|
|
35
34
|
"""
|
|
36
|
-
|
|
35
|
+
|
|
37
36
|
# Define key aliases for all adapters
|
|
38
37
|
KEY_MAPPINGS = {
|
|
39
38
|
# Linear adapter keys
|
|
@@ -41,93 +40,97 @@ class UnifiedEnvLoader:
|
|
|
41
40
|
primary_key="LINEAR_API_KEY",
|
|
42
41
|
aliases=["LINEAR_TOKEN", "LINEAR_ACCESS_TOKEN", "LINEAR_AUTH_TOKEN"],
|
|
43
42
|
description="Linear API key",
|
|
44
|
-
required=True
|
|
43
|
+
required=True,
|
|
45
44
|
),
|
|
46
45
|
"linear_team_id": EnvKeyConfig(
|
|
47
46
|
primary_key="LINEAR_TEAM_ID",
|
|
48
47
|
aliases=["LINEAR_TEAM_UUID", "LINEAR_TEAM_IDENTIFIER"],
|
|
49
48
|
description="Linear team ID (UUID)",
|
|
50
|
-
required=False
|
|
49
|
+
required=False,
|
|
51
50
|
),
|
|
52
51
|
"linear_team_key": EnvKeyConfig(
|
|
53
52
|
primary_key="LINEAR_TEAM_KEY",
|
|
54
53
|
aliases=["LINEAR_TEAM_IDENTIFIER", "LINEAR_TEAM_NAME"],
|
|
55
54
|
description="Linear team key (short name)",
|
|
56
|
-
required=False
|
|
55
|
+
required=False,
|
|
57
56
|
),
|
|
58
|
-
|
|
59
57
|
# JIRA adapter keys
|
|
60
58
|
"jira_server": EnvKeyConfig(
|
|
61
59
|
primary_key="JIRA_SERVER",
|
|
62
60
|
aliases=["JIRA_URL", "JIRA_HOST", "JIRA_BASE_URL"],
|
|
63
61
|
description="JIRA server URL",
|
|
64
|
-
required=True
|
|
62
|
+
required=True,
|
|
65
63
|
),
|
|
66
64
|
"jira_email": EnvKeyConfig(
|
|
67
65
|
primary_key="JIRA_EMAIL",
|
|
68
66
|
aliases=["JIRA_USER", "JIRA_USERNAME", "JIRA_ACCESS_USER"],
|
|
69
67
|
description="JIRA user email",
|
|
70
|
-
required=True
|
|
68
|
+
required=True,
|
|
71
69
|
),
|
|
72
70
|
"jira_api_token": EnvKeyConfig(
|
|
73
71
|
primary_key="JIRA_API_TOKEN",
|
|
74
|
-
aliases=[
|
|
72
|
+
aliases=[
|
|
73
|
+
"JIRA_TOKEN",
|
|
74
|
+
"JIRA_ACCESS_TOKEN",
|
|
75
|
+
"JIRA_AUTH_TOKEN",
|
|
76
|
+
"JIRA_PASSWORD",
|
|
77
|
+
],
|
|
75
78
|
description="JIRA API token",
|
|
76
|
-
required=True
|
|
79
|
+
required=True,
|
|
77
80
|
),
|
|
78
81
|
"jira_project_key": EnvKeyConfig(
|
|
79
82
|
primary_key="JIRA_PROJECT_KEY",
|
|
80
83
|
aliases=["JIRA_PROJECT", "JIRA_PROJECT_ID"],
|
|
81
84
|
description="JIRA project key",
|
|
82
|
-
required=False
|
|
85
|
+
required=False,
|
|
83
86
|
),
|
|
84
|
-
|
|
85
87
|
# GitHub adapter keys
|
|
86
88
|
"github_token": EnvKeyConfig(
|
|
87
89
|
primary_key="GITHUB_TOKEN",
|
|
88
90
|
aliases=["GITHUB_ACCESS_TOKEN", "GITHUB_API_TOKEN", "GITHUB_AUTH_TOKEN"],
|
|
89
91
|
description="GitHub access token",
|
|
90
|
-
required=True
|
|
92
|
+
required=True,
|
|
91
93
|
),
|
|
92
94
|
"github_owner": EnvKeyConfig(
|
|
93
95
|
primary_key="GITHUB_OWNER",
|
|
94
96
|
aliases=["GITHUB_USER", "GITHUB_USERNAME", "GITHUB_ORG"],
|
|
95
97
|
description="GitHub repository owner",
|
|
96
|
-
required=True
|
|
98
|
+
required=True,
|
|
97
99
|
),
|
|
98
100
|
"github_repo": EnvKeyConfig(
|
|
99
101
|
primary_key="GITHUB_REPO",
|
|
100
102
|
aliases=["GITHUB_REPOSITORY", "GITHUB_REPO_NAME"],
|
|
101
103
|
description="GitHub repository name",
|
|
102
|
-
required=True
|
|
104
|
+
required=True,
|
|
103
105
|
),
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
|
|
106
108
|
def __init__(self, project_root: Optional[Path] = None):
|
|
107
109
|
"""Initialize the environment loader.
|
|
108
|
-
|
|
110
|
+
|
|
109
111
|
Args:
|
|
110
112
|
project_root: Project root directory. If None, will auto-detect.
|
|
113
|
+
|
|
111
114
|
"""
|
|
112
115
|
self.project_root = project_root or self._find_project_root()
|
|
113
116
|
self._env_cache: Dict[str, str] = {}
|
|
114
117
|
self._load_env_files()
|
|
115
|
-
|
|
118
|
+
|
|
116
119
|
def _find_project_root(self) -> Path:
|
|
117
120
|
"""Find the project root directory."""
|
|
118
121
|
current = Path.cwd()
|
|
119
|
-
|
|
122
|
+
|
|
120
123
|
# Look for common project indicators
|
|
121
124
|
indicators = [".mcp-ticketer", ".git", "pyproject.toml", "setup.py"]
|
|
122
|
-
|
|
125
|
+
|
|
123
126
|
while current != current.parent:
|
|
124
127
|
if any((current / indicator).exists() for indicator in indicators):
|
|
125
128
|
return current
|
|
126
129
|
current = current.parent
|
|
127
|
-
|
|
130
|
+
|
|
128
131
|
# Fallback to current directory
|
|
129
132
|
return Path.cwd()
|
|
130
|
-
|
|
133
|
+
|
|
131
134
|
def _load_env_files(self):
|
|
132
135
|
"""Load environment variables from .env files."""
|
|
133
136
|
env_files = [
|
|
@@ -135,142 +138,160 @@ class UnifiedEnvLoader:
|
|
|
135
138
|
self.project_root / ".env",
|
|
136
139
|
Path.home() / ".mcp-ticketer" / ".env",
|
|
137
140
|
]
|
|
138
|
-
|
|
141
|
+
|
|
139
142
|
for env_file in env_files:
|
|
140
143
|
if env_file.exists():
|
|
141
144
|
logger.debug(f"Loading environment from: {env_file}")
|
|
142
145
|
self._load_env_file(env_file)
|
|
143
|
-
|
|
146
|
+
|
|
144
147
|
def _load_env_file(self, env_file: Path):
|
|
145
148
|
"""Load variables from a single .env file."""
|
|
146
149
|
try:
|
|
147
|
-
with open(env_file
|
|
150
|
+
with open(env_file) as f:
|
|
148
151
|
for line_num, line in enumerate(f, 1):
|
|
149
152
|
line = line.strip()
|
|
150
|
-
|
|
153
|
+
|
|
151
154
|
# Skip empty lines and comments
|
|
152
|
-
if not line or line.startswith(
|
|
155
|
+
if not line or line.startswith("#"):
|
|
153
156
|
continue
|
|
154
|
-
|
|
157
|
+
|
|
155
158
|
# Parse KEY=VALUE format
|
|
156
|
-
if
|
|
157
|
-
key, value = line.split(
|
|
159
|
+
if "=" in line:
|
|
160
|
+
key, value = line.split("=", 1)
|
|
158
161
|
key = key.strip()
|
|
159
162
|
value = value.strip()
|
|
160
|
-
|
|
163
|
+
|
|
161
164
|
# Remove quotes if present
|
|
162
165
|
if value.startswith('"') and value.endswith('"'):
|
|
163
166
|
value = value[1:-1]
|
|
164
167
|
elif value.startswith("'") and value.endswith("'"):
|
|
165
168
|
value = value[1:-1]
|
|
166
|
-
|
|
169
|
+
|
|
167
170
|
# Only set if not already in environment
|
|
168
171
|
if key not in os.environ:
|
|
169
172
|
os.environ[key] = value
|
|
170
173
|
self._env_cache[key] = value
|
|
171
174
|
logger.debug(f"Loaded {key} from {env_file}")
|
|
172
|
-
|
|
175
|
+
|
|
173
176
|
except Exception as e:
|
|
174
177
|
logger.warning(f"Failed to load {env_file}: {e}")
|
|
175
|
-
|
|
176
|
-
def get_value(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
|
|
179
|
+
def get_value(
|
|
180
|
+
self, config_key: str, config: Optional[Dict[str, Any]] = None
|
|
181
|
+
) -> Optional[str]:
|
|
182
|
+
"""Get a configuration value using the key alias system.
|
|
183
|
+
|
|
180
184
|
Args:
|
|
181
185
|
config_key: The configuration key (e.g., 'linear_api_key')
|
|
182
186
|
config: Optional configuration dictionary to check first
|
|
183
|
-
|
|
187
|
+
|
|
184
188
|
Returns:
|
|
185
189
|
The value if found, None otherwise
|
|
190
|
+
|
|
186
191
|
"""
|
|
187
192
|
if config_key not in self.KEY_MAPPINGS:
|
|
188
193
|
logger.warning(f"Unknown configuration key: {config_key}")
|
|
189
194
|
return None
|
|
190
|
-
|
|
195
|
+
|
|
191
196
|
key_config = self.KEY_MAPPINGS[config_key]
|
|
192
|
-
|
|
197
|
+
|
|
193
198
|
# 1. Check provided config dictionary first
|
|
194
199
|
if config:
|
|
195
200
|
# Check for the config key itself (without adapter prefix)
|
|
196
|
-
simple_key =
|
|
201
|
+
simple_key = (
|
|
202
|
+
config_key.split("_", 1)[1] if "_" in config_key else config_key
|
|
203
|
+
)
|
|
197
204
|
if simple_key in config:
|
|
198
205
|
value = config[simple_key]
|
|
199
206
|
if value:
|
|
200
207
|
logger.debug(f"Found {config_key} in config as {simple_key}")
|
|
201
208
|
return str(value)
|
|
202
|
-
|
|
209
|
+
|
|
203
210
|
# 2. Check environment variables (primary key first, then aliases)
|
|
204
211
|
all_keys = [key_config.primary_key] + key_config.aliases
|
|
205
|
-
|
|
212
|
+
|
|
206
213
|
for env_key in all_keys:
|
|
207
214
|
value = os.getenv(env_key)
|
|
208
215
|
if value:
|
|
209
216
|
logger.debug(f"Found {config_key} as {env_key}")
|
|
210
217
|
return value
|
|
211
|
-
|
|
218
|
+
|
|
212
219
|
# 3. Return default if available
|
|
213
220
|
if key_config.default:
|
|
214
221
|
logger.debug(f"Using default for {config_key}")
|
|
215
222
|
return key_config.default
|
|
216
|
-
|
|
223
|
+
|
|
217
224
|
# 4. Log if required key is missing
|
|
218
225
|
if key_config.required:
|
|
219
|
-
logger.warning(
|
|
220
|
-
|
|
226
|
+
logger.warning(
|
|
227
|
+
f"Required configuration key {config_key} not found. Tried: {all_keys}"
|
|
228
|
+
)
|
|
229
|
+
|
|
221
230
|
return None
|
|
222
|
-
|
|
223
|
-
def get_adapter_config(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
|
|
232
|
+
def get_adapter_config(
|
|
233
|
+
self, adapter_name: str, base_config: Optional[Dict[str, Any]] = None
|
|
234
|
+
) -> Dict[str, Any]:
|
|
235
|
+
"""Get complete configuration for an adapter with environment variable resolution.
|
|
236
|
+
|
|
227
237
|
Args:
|
|
228
238
|
adapter_name: Name of the adapter ('linear', 'jira', 'github')
|
|
229
239
|
base_config: Base configuration dictionary
|
|
230
|
-
|
|
240
|
+
|
|
231
241
|
Returns:
|
|
232
242
|
Complete configuration with environment variables resolved
|
|
243
|
+
|
|
233
244
|
"""
|
|
234
245
|
config = base_config.copy() if base_config else {}
|
|
235
|
-
|
|
246
|
+
|
|
236
247
|
# Get adapter-specific keys
|
|
237
|
-
adapter_keys = [
|
|
238
|
-
|
|
248
|
+
adapter_keys = [
|
|
249
|
+
key
|
|
250
|
+
for key in self.KEY_MAPPINGS.keys()
|
|
251
|
+
if key.startswith(f"{adapter_name}_")
|
|
252
|
+
]
|
|
253
|
+
|
|
239
254
|
for config_key in adapter_keys:
|
|
240
255
|
# Remove adapter prefix for the config key
|
|
241
|
-
simple_key = config_key.split(
|
|
242
|
-
|
|
256
|
+
simple_key = config_key.split("_", 1)[1]
|
|
257
|
+
|
|
243
258
|
# Only set if not already in config or if config value is empty
|
|
244
259
|
if simple_key not in config or not config[simple_key]:
|
|
245
260
|
value = self.get_value(config_key, config)
|
|
246
261
|
if value:
|
|
247
262
|
config[simple_key] = value
|
|
248
|
-
|
|
263
|
+
|
|
249
264
|
return config
|
|
250
|
-
|
|
251
|
-
def validate_adapter_config(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
|
|
266
|
+
def validate_adapter_config(
|
|
267
|
+
self, adapter_name: str, config: Dict[str, Any]
|
|
268
|
+
) -> List[str]:
|
|
269
|
+
"""Validate that all required configuration is present for an adapter.
|
|
270
|
+
|
|
255
271
|
Args:
|
|
256
272
|
adapter_name: Name of the adapter
|
|
257
273
|
config: Configuration dictionary
|
|
258
|
-
|
|
274
|
+
|
|
259
275
|
Returns:
|
|
260
276
|
List of missing required keys (empty if all required keys are present)
|
|
277
|
+
|
|
261
278
|
"""
|
|
262
279
|
missing_keys = []
|
|
263
|
-
adapter_keys = [
|
|
264
|
-
|
|
280
|
+
adapter_keys = [
|
|
281
|
+
key
|
|
282
|
+
for key in self.KEY_MAPPINGS.keys()
|
|
283
|
+
if key.startswith(f"{adapter_name}_")
|
|
284
|
+
]
|
|
285
|
+
|
|
265
286
|
for config_key in adapter_keys:
|
|
266
287
|
key_config = self.KEY_MAPPINGS[config_key]
|
|
267
288
|
if key_config.required:
|
|
268
|
-
simple_key = config_key.split(
|
|
289
|
+
simple_key = config_key.split("_", 1)[1]
|
|
269
290
|
if simple_key not in config or not config[simple_key]:
|
|
270
291
|
missing_keys.append(f"{simple_key} ({key_config.description})")
|
|
271
|
-
|
|
292
|
+
|
|
272
293
|
return missing_keys
|
|
273
|
-
|
|
294
|
+
|
|
274
295
|
def get_debug_info(self) -> Dict[str, Any]:
|
|
275
296
|
"""Get debug information about environment loading."""
|
|
276
297
|
return {
|
|
@@ -297,29 +318,31 @@ def get_env_loader() -> UnifiedEnvLoader:
|
|
|
297
318
|
return _env_loader
|
|
298
319
|
|
|
299
320
|
|
|
300
|
-
def load_adapter_config(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
321
|
+
def load_adapter_config(
|
|
322
|
+
adapter_name: str, base_config: Optional[Dict[str, Any]] = None
|
|
323
|
+
) -> Dict[str, Any]:
|
|
324
|
+
"""Convenience function to load adapter configuration with environment variables.
|
|
325
|
+
|
|
304
326
|
Args:
|
|
305
327
|
adapter_name: Name of the adapter ('linear', 'jira', 'github')
|
|
306
328
|
base_config: Base configuration dictionary
|
|
307
|
-
|
|
329
|
+
|
|
308
330
|
Returns:
|
|
309
331
|
Complete configuration with environment variables resolved
|
|
332
|
+
|
|
310
333
|
"""
|
|
311
334
|
return get_env_loader().get_adapter_config(adapter_name, base_config)
|
|
312
335
|
|
|
313
336
|
|
|
314
337
|
def validate_adapter_config(adapter_name: str, config: Dict[str, Any]) -> List[str]:
|
|
315
|
-
"""
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
"""Convenience function to validate adapter configuration.
|
|
339
|
+
|
|
318
340
|
Args:
|
|
319
341
|
adapter_name: Name of the adapter
|
|
320
342
|
config: Configuration dictionary
|
|
321
|
-
|
|
343
|
+
|
|
322
344
|
Returns:
|
|
323
345
|
List of missing required keys (empty if all required keys are present)
|
|
346
|
+
|
|
324
347
|
"""
|
|
325
348
|
return get_env_loader().validate_adapter_config(adapter_name, config)
|
mcp_ticketer/core/exceptions.py
CHANGED
|
@@ -9,6 +9,7 @@ from .models import TicketState
|
|
|
9
9
|
|
|
10
10
|
class MCPTicketerError(Exception):
|
|
11
11
|
"""Base exception for MCP Ticketer."""
|
|
12
|
+
|
|
12
13
|
pass
|
|
13
14
|
|
|
14
15
|
|
|
@@ -19,14 +20,15 @@ class AdapterError(MCPTicketerError):
|
|
|
19
20
|
self,
|
|
20
21
|
message: str,
|
|
21
22
|
adapter_name: str,
|
|
22
|
-
original_error: Optional[Exception] = None
|
|
23
|
+
original_error: Optional[Exception] = None,
|
|
23
24
|
):
|
|
24
25
|
"""Initialize adapter error.
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
Args:
|
|
27
28
|
message: Error message
|
|
28
29
|
adapter_name: Name of the adapter that raised the error
|
|
29
30
|
original_error: Original exception that caused this error
|
|
31
|
+
|
|
30
32
|
"""
|
|
31
33
|
super().__init__(message)
|
|
32
34
|
self.adapter_name = adapter_name
|
|
@@ -42,6 +44,7 @@ class AdapterError(MCPTicketerError):
|
|
|
42
44
|
|
|
43
45
|
class AuthenticationError(AdapterError):
|
|
44
46
|
"""Authentication failed with external service."""
|
|
47
|
+
|
|
45
48
|
pass
|
|
46
49
|
|
|
47
50
|
|
|
@@ -53,15 +56,16 @@ class RateLimitError(AdapterError):
|
|
|
53
56
|
message: str,
|
|
54
57
|
adapter_name: str,
|
|
55
58
|
retry_after: Optional[int] = None,
|
|
56
|
-
original_error: Optional[Exception] = None
|
|
59
|
+
original_error: Optional[Exception] = None,
|
|
57
60
|
):
|
|
58
61
|
"""Initialize rate limit error.
|
|
59
|
-
|
|
62
|
+
|
|
60
63
|
Args:
|
|
61
64
|
message: Error message
|
|
62
65
|
adapter_name: Name of the adapter
|
|
63
66
|
retry_after: Seconds to wait before retrying
|
|
64
67
|
original_error: Original exception
|
|
68
|
+
|
|
65
69
|
"""
|
|
66
70
|
super().__init__(message, adapter_name, original_error)
|
|
67
71
|
self.retry_after = retry_after
|
|
@@ -70,18 +74,14 @@ class RateLimitError(AdapterError):
|
|
|
70
74
|
class ValidationError(MCPTicketerError):
|
|
71
75
|
"""Data validation error."""
|
|
72
76
|
|
|
73
|
-
def __init__(
|
|
74
|
-
self,
|
|
75
|
-
message: str,
|
|
76
|
-
field: Optional[str] = None,
|
|
77
|
-
value: Any = None
|
|
78
|
-
):
|
|
77
|
+
def __init__(self, message: str, field: Optional[str] = None, value: Any = None):
|
|
79
78
|
"""Initialize validation error.
|
|
80
|
-
|
|
79
|
+
|
|
81
80
|
Args:
|
|
82
81
|
message: Error message
|
|
83
82
|
field: Field that failed validation
|
|
84
83
|
value: Value that failed validation
|
|
84
|
+
|
|
85
85
|
"""
|
|
86
86
|
super().__init__(message)
|
|
87
87
|
self.field = field
|
|
@@ -99,29 +99,27 @@ class ValidationError(MCPTicketerError):
|
|
|
99
99
|
|
|
100
100
|
class ConfigurationError(MCPTicketerError):
|
|
101
101
|
"""Configuration error."""
|
|
102
|
+
|
|
102
103
|
pass
|
|
103
104
|
|
|
104
105
|
|
|
105
106
|
class CacheError(MCPTicketerError):
|
|
106
107
|
"""Cache operation error."""
|
|
108
|
+
|
|
107
109
|
pass
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
class StateTransitionError(MCPTicketerError):
|
|
111
113
|
"""Invalid state transition."""
|
|
112
114
|
|
|
113
|
-
def __init__(
|
|
114
|
-
self,
|
|
115
|
-
message: str,
|
|
116
|
-
from_state: TicketState,
|
|
117
|
-
to_state: TicketState
|
|
118
|
-
):
|
|
115
|
+
def __init__(self, message: str, from_state: TicketState, to_state: TicketState):
|
|
119
116
|
"""Initialize state transition error.
|
|
120
|
-
|
|
117
|
+
|
|
121
118
|
Args:
|
|
122
119
|
message: Error message
|
|
123
120
|
from_state: Current state
|
|
124
121
|
to_state: Target state
|
|
122
|
+
|
|
125
123
|
"""
|
|
126
124
|
super().__init__(message)
|
|
127
125
|
self.from_state = from_state
|
|
@@ -134,19 +132,23 @@ class StateTransitionError(MCPTicketerError):
|
|
|
134
132
|
|
|
135
133
|
class NetworkError(AdapterError):
|
|
136
134
|
"""Network-related error."""
|
|
135
|
+
|
|
137
136
|
pass
|
|
138
137
|
|
|
139
138
|
|
|
140
139
|
class TimeoutError(AdapterError):
|
|
141
140
|
"""Request timeout error."""
|
|
141
|
+
|
|
142
142
|
pass
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
class NotFoundError(AdapterError):
|
|
146
146
|
"""Resource not found error."""
|
|
147
|
+
|
|
147
148
|
pass
|
|
148
149
|
|
|
149
150
|
|
|
150
151
|
class PermissionError(AdapterError):
|
|
151
152
|
"""Permission denied error."""
|
|
153
|
+
|
|
152
154
|
pass
|
mcp_ticketer/core/models.py
CHANGED
|
@@ -20,6 +20,7 @@ Example:
|
|
|
20
20
|
... state=TicketState.IN_PROGRESS
|
|
21
21
|
... )
|
|
22
22
|
>>> print(task.model_dump_json())
|
|
23
|
+
|
|
23
24
|
"""
|
|
24
25
|
|
|
25
26
|
from datetime import datetime
|
|
@@ -42,6 +43,7 @@ class Priority(str, Enum):
|
|
|
42
43
|
MEDIUM: Standard priority, default for most work
|
|
43
44
|
HIGH: High priority, should be addressed soon
|
|
44
45
|
CRITICAL: Critical priority, urgent work requiring immediate attention
|
|
46
|
+
|
|
45
47
|
"""
|
|
46
48
|
|
|
47
49
|
LOW = "low"
|
|
@@ -66,6 +68,7 @@ class TicketType(str, Enum):
|
|
|
66
68
|
ISSUE: Standard work items, the primary unit of work
|
|
67
69
|
TASK: Sub-work items, smaller pieces of an issue
|
|
68
70
|
SUBTASK: Alias for TASK for backward compatibility
|
|
71
|
+
|
|
69
72
|
"""
|
|
70
73
|
|
|
71
74
|
EPIC = "epic" # Strategic level (Projects in Linear, Milestones in GitHub)
|
|
@@ -101,6 +104,7 @@ class TicketState(str, Enum):
|
|
|
101
104
|
WAITING: Work is paused waiting for external dependency
|
|
102
105
|
BLOCKED: Work is blocked by an impediment
|
|
103
106
|
CLOSED: Final state, work is closed/archived
|
|
107
|
+
|
|
104
108
|
"""
|
|
105
109
|
|
|
106
110
|
OPEN = "open"
|
|
@@ -121,6 +125,7 @@ class TicketState(str, Enum):
|
|
|
121
125
|
|
|
122
126
|
Note:
|
|
123
127
|
CLOSED is a terminal state with no valid transitions
|
|
128
|
+
|
|
124
129
|
"""
|
|
125
130
|
return {
|
|
126
131
|
cls.OPEN: [cls.IN_PROGRESS, cls.WAITING, cls.BLOCKED, cls.CLOSED],
|
|
@@ -151,6 +156,7 @@ class TicketState(str, Enum):
|
|
|
151
156
|
True
|
|
152
157
|
>>> state.can_transition_to(TicketState.DONE)
|
|
153
158
|
False
|
|
159
|
+
|
|
154
160
|
"""
|
|
155
161
|
return target.value in self.valid_transitions().get(self, [])
|
|
156
162
|
|
|
@@ -183,6 +189,7 @@ class BaseTicket(BaseModel):
|
|
|
183
189
|
... tags=["bug", "authentication"]
|
|
184
190
|
... )
|
|
185
191
|
>>> ticket.state = TicketState.IN_PROGRESS
|
|
192
|
+
|
|
186
193
|
"""
|
|
187
194
|
|
|
188
195
|
model_config = ConfigDict(use_enum_values=True)
|
|
@@ -228,6 +235,7 @@ class Epic(BaseTicket):
|
|
|
228
235
|
... priority=Priority.HIGH
|
|
229
236
|
... )
|
|
230
237
|
>>> epic.child_issues = ["ISSUE-123", "ISSUE-124"]
|
|
238
|
+
|
|
231
239
|
"""
|
|
232
240
|
|
|
233
241
|
ticket_type: TicketType = Field(
|
|
@@ -245,6 +253,7 @@ class Epic(BaseTicket):
|
|
|
245
253
|
|
|
246
254
|
Returns:
|
|
247
255
|
Empty list (epics have no hierarchy constraints)
|
|
256
|
+
|
|
248
257
|
"""
|
|
249
258
|
# Epics don't have parents in our hierarchy
|
|
250
259
|
return []
|
|
@@ -14,7 +14,7 @@ import os
|
|
|
14
14
|
from dataclasses import asdict, dataclass, field
|
|
15
15
|
from enum import Enum
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Any, Optional
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
20
|
from .env_discovery import DiscoveryResult
|