mcp-ticketer 0.1.22__py3-none-any.whl → 0.1.23__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 +15 -14
- mcp_ticketer/adapters/github.py +21 -20
- mcp_ticketer/adapters/hybrid.py +13 -12
- mcp_ticketer/adapters/jira.py +28 -27
- mcp_ticketer/adapters/linear.py +26 -25
- mcp_ticketer/cache/memory.py +2 -2
- mcp_ticketer/cli/main.py +36 -8
- mcp_ticketer/cli/migrate_config.py +2 -2
- mcp_ticketer/cli/utils.py +8 -8
- mcp_ticketer/core/adapter.py +12 -11
- mcp_ticketer/core/config.py +17 -17
- mcp_ticketer/core/env_discovery.py +24 -24
- mcp_ticketer/core/http_client.py +13 -13
- mcp_ticketer/core/mappers.py +25 -25
- mcp_ticketer/core/models.py +10 -10
- mcp_ticketer/core/project_config.py +22 -22
- mcp_ticketer/core/registry.py +7 -7
- mcp_ticketer/mcp/server.py +18 -18
- mcp_ticketer/queue/manager.py +2 -2
- mcp_ticketer/queue/queue.py +7 -7
- mcp_ticketer/queue/worker.py +8 -8
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.22.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,7 @@ environment files, including:
|
|
|
11
11
|
import logging
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Any,
|
|
14
|
+
from typing import Any, Optional
|
|
15
15
|
|
|
16
16
|
from dotenv import dotenv_values
|
|
17
17
|
|
|
@@ -105,9 +105,9 @@ class DiscoveredAdapter:
|
|
|
105
105
|
"""Information about a discovered adapter configuration."""
|
|
106
106
|
|
|
107
107
|
adapter_type: str
|
|
108
|
-
config:
|
|
108
|
+
config: dict[str, Any]
|
|
109
109
|
confidence: float # 0.0-1.0 how complete the configuration is
|
|
110
|
-
missing_fields:
|
|
110
|
+
missing_fields: list[str] = field(default_factory=list)
|
|
111
111
|
found_in: str = ".env" # Which file it was found in
|
|
112
112
|
|
|
113
113
|
def is_complete(self) -> bool:
|
|
@@ -119,9 +119,9 @@ class DiscoveredAdapter:
|
|
|
119
119
|
class DiscoveryResult:
|
|
120
120
|
"""Result of environment file discovery."""
|
|
121
121
|
|
|
122
|
-
adapters:
|
|
123
|
-
warnings:
|
|
124
|
-
env_files_found:
|
|
122
|
+
adapters: list[DiscoveredAdapter] = field(default_factory=list)
|
|
123
|
+
warnings: list[str] = field(default_factory=list)
|
|
124
|
+
env_files_found: list[str] = field(default_factory=list)
|
|
125
125
|
|
|
126
126
|
def get_primary_adapter(self) -> Optional[DiscoveredAdapter]:
|
|
127
127
|
"""Get the adapter with highest confidence and completeness."""
|
|
@@ -209,7 +209,7 @@ class EnvDiscovery:
|
|
|
209
209
|
|
|
210
210
|
return result
|
|
211
211
|
|
|
212
|
-
def _load_env_files(self, result: DiscoveryResult) ->
|
|
212
|
+
def _load_env_files(self, result: DiscoveryResult) -> dict[str, str]:
|
|
213
213
|
"""Load environment variables from files.
|
|
214
214
|
|
|
215
215
|
Args:
|
|
@@ -219,7 +219,7 @@ class EnvDiscovery:
|
|
|
219
219
|
Merged dictionary of environment variables
|
|
220
220
|
|
|
221
221
|
"""
|
|
222
|
-
merged_env:
|
|
222
|
+
merged_env: dict[str, str] = {}
|
|
223
223
|
|
|
224
224
|
# Load files in reverse order (lowest priority first)
|
|
225
225
|
for env_file in reversed(self.ENV_FILE_ORDER):
|
|
@@ -239,7 +239,7 @@ class EnvDiscovery:
|
|
|
239
239
|
return merged_env
|
|
240
240
|
|
|
241
241
|
def _find_key_value(
|
|
242
|
-
self, env_vars:
|
|
242
|
+
self, env_vars: dict[str, str], patterns: list[str]
|
|
243
243
|
) -> Optional[str]:
|
|
244
244
|
"""Find first matching key value from patterns.
|
|
245
245
|
|
|
@@ -257,7 +257,7 @@ class EnvDiscovery:
|
|
|
257
257
|
return None
|
|
258
258
|
|
|
259
259
|
def _detect_linear(
|
|
260
|
-
self, env_vars:
|
|
260
|
+
self, env_vars: dict[str, str], found_in: str
|
|
261
261
|
) -> Optional[DiscoveredAdapter]:
|
|
262
262
|
"""Detect Linear adapter configuration.
|
|
263
263
|
|
|
@@ -274,12 +274,12 @@ class EnvDiscovery:
|
|
|
274
274
|
if not api_key:
|
|
275
275
|
return None
|
|
276
276
|
|
|
277
|
-
config:
|
|
277
|
+
config: dict[str, Any] = {
|
|
278
278
|
"api_key": api_key,
|
|
279
279
|
"adapter": AdapterType.LINEAR.value,
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
missing_fields:
|
|
282
|
+
missing_fields: list[str] = []
|
|
283
283
|
confidence = 0.6 # Has API key
|
|
284
284
|
|
|
285
285
|
# Extract team ID (recommended but not required)
|
|
@@ -305,7 +305,7 @@ class EnvDiscovery:
|
|
|
305
305
|
)
|
|
306
306
|
|
|
307
307
|
def _detect_github(
|
|
308
|
-
self, env_vars:
|
|
308
|
+
self, env_vars: dict[str, str], found_in: str
|
|
309
309
|
) -> Optional[DiscoveredAdapter]:
|
|
310
310
|
"""Detect GitHub adapter configuration.
|
|
311
311
|
|
|
@@ -322,12 +322,12 @@ class EnvDiscovery:
|
|
|
322
322
|
if not token:
|
|
323
323
|
return None
|
|
324
324
|
|
|
325
|
-
config:
|
|
325
|
+
config: dict[str, Any] = {
|
|
326
326
|
"token": token,
|
|
327
327
|
"adapter": AdapterType.GITHUB.value,
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
missing_fields:
|
|
330
|
+
missing_fields: list[str] = []
|
|
331
331
|
confidence = 0.4 # Has token
|
|
332
332
|
|
|
333
333
|
# Try to extract owner/repo from combined field
|
|
@@ -364,7 +364,7 @@ class EnvDiscovery:
|
|
|
364
364
|
)
|
|
365
365
|
|
|
366
366
|
def _detect_jira(
|
|
367
|
-
self, env_vars:
|
|
367
|
+
self, env_vars: dict[str, str], found_in: str
|
|
368
368
|
) -> Optional[DiscoveredAdapter]:
|
|
369
369
|
"""Detect JIRA adapter configuration.
|
|
370
370
|
|
|
@@ -381,12 +381,12 @@ class EnvDiscovery:
|
|
|
381
381
|
if not api_token:
|
|
382
382
|
return None
|
|
383
383
|
|
|
384
|
-
config:
|
|
384
|
+
config: dict[str, Any] = {
|
|
385
385
|
"api_token": api_token,
|
|
386
386
|
"adapter": AdapterType.JIRA.value,
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
missing_fields:
|
|
389
|
+
missing_fields: list[str] = []
|
|
390
390
|
confidence = 0.3 # Has token
|
|
391
391
|
|
|
392
392
|
# Extract server (required)
|
|
@@ -420,7 +420,7 @@ class EnvDiscovery:
|
|
|
420
420
|
)
|
|
421
421
|
|
|
422
422
|
def _detect_aitrackdown(
|
|
423
|
-
self, env_vars:
|
|
423
|
+
self, env_vars: dict[str, str], found_in: str
|
|
424
424
|
) -> Optional[DiscoveredAdapter]:
|
|
425
425
|
"""Detect AITrackdown adapter configuration.
|
|
426
426
|
|
|
@@ -439,7 +439,7 @@ class EnvDiscovery:
|
|
|
439
439
|
if not base_path and not aitrackdown_dir.exists():
|
|
440
440
|
return None
|
|
441
441
|
|
|
442
|
-
config:
|
|
442
|
+
config: dict[str, Any] = {
|
|
443
443
|
"adapter": AdapterType.AITRACKDOWN.value,
|
|
444
444
|
}
|
|
445
445
|
|
|
@@ -459,14 +459,14 @@ class EnvDiscovery:
|
|
|
459
459
|
found_in=found_in,
|
|
460
460
|
)
|
|
461
461
|
|
|
462
|
-
def _validate_security(self) ->
|
|
462
|
+
def _validate_security(self) -> list[str]:
|
|
463
463
|
"""Validate security of environment files.
|
|
464
464
|
|
|
465
465
|
Returns:
|
|
466
466
|
List of security warnings
|
|
467
467
|
|
|
468
468
|
"""
|
|
469
|
-
warnings:
|
|
469
|
+
warnings: list[str] = []
|
|
470
470
|
|
|
471
471
|
# Check if .env files are tracked in git
|
|
472
472
|
gitignore_path = self.project_path / ".gitignore"
|
|
@@ -524,7 +524,7 @@ class EnvDiscovery:
|
|
|
524
524
|
logger.debug(f"Git check failed: {e}")
|
|
525
525
|
return False
|
|
526
526
|
|
|
527
|
-
def validate_discovered_config(self, adapter: DiscoveredAdapter) ->
|
|
527
|
+
def validate_discovered_config(self, adapter: DiscoveredAdapter) -> list[str]:
|
|
528
528
|
"""Validate a discovered adapter configuration.
|
|
529
529
|
|
|
530
530
|
Args:
|
|
@@ -534,7 +534,7 @@ class EnvDiscovery:
|
|
|
534
534
|
List of validation warnings
|
|
535
535
|
|
|
536
536
|
"""
|
|
537
|
-
warnings:
|
|
537
|
+
warnings: list[str] = []
|
|
538
538
|
|
|
539
539
|
# Check API key/token length (basic sanity check)
|
|
540
540
|
if adapter.adapter_type == AdapterType.LINEAR.value:
|
mcp_ticketer/core/http_client.py
CHANGED
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import time
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Any,
|
|
7
|
+
from typing import Any, Optional, Union
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
from httpx import AsyncClient, TimeoutException
|
|
@@ -32,8 +32,8 @@ class RetryConfig:
|
|
|
32
32
|
max_delay: float = 60.0,
|
|
33
33
|
exponential_base: float = 2.0,
|
|
34
34
|
jitter: bool = True,
|
|
35
|
-
retry_on_status: Optional[
|
|
36
|
-
retry_on_exceptions: Optional[
|
|
35
|
+
retry_on_status: Optional[list[int]] = None,
|
|
36
|
+
retry_on_exceptions: Optional[list[type]] = None,
|
|
37
37
|
):
|
|
38
38
|
self.max_retries = max_retries
|
|
39
39
|
self.initial_delay = initial_delay
|
|
@@ -94,7 +94,7 @@ class BaseHTTPClient:
|
|
|
94
94
|
def __init__(
|
|
95
95
|
self,
|
|
96
96
|
base_url: str,
|
|
97
|
-
headers: Optional[
|
|
97
|
+
headers: Optional[dict[str, str]] = None,
|
|
98
98
|
auth: Optional[Union[httpx.Auth, tuple]] = None,
|
|
99
99
|
timeout: float = 30.0,
|
|
100
100
|
retry_config: Optional[RetryConfig] = None,
|
|
@@ -200,10 +200,10 @@ class BaseHTTPClient:
|
|
|
200
200
|
self,
|
|
201
201
|
method: Union[HTTPMethod, str],
|
|
202
202
|
endpoint: str,
|
|
203
|
-
data: Optional[
|
|
204
|
-
json: Optional[
|
|
205
|
-
params: Optional[
|
|
206
|
-
headers: Optional[
|
|
203
|
+
data: Optional[dict[str, Any]] = None,
|
|
204
|
+
json: Optional[dict[str, Any]] = None,
|
|
205
|
+
params: Optional[dict[str, Any]] = None,
|
|
206
|
+
headers: Optional[dict[str, str]] = None,
|
|
207
207
|
timeout: Optional[float] = None,
|
|
208
208
|
retry_count: int = 0,
|
|
209
209
|
**kwargs,
|
|
@@ -313,7 +313,7 @@ class BaseHTTPClient:
|
|
|
313
313
|
"""Make DELETE request."""
|
|
314
314
|
return await self.request(HTTPMethod.DELETE, endpoint, **kwargs)
|
|
315
315
|
|
|
316
|
-
async def get_json(self, endpoint: str, **kwargs) ->
|
|
316
|
+
async def get_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
|
|
317
317
|
"""Make GET request and return JSON response."""
|
|
318
318
|
response = await self.get(endpoint, **kwargs)
|
|
319
319
|
|
|
@@ -323,7 +323,7 @@ class BaseHTTPClient:
|
|
|
323
323
|
|
|
324
324
|
return response.json()
|
|
325
325
|
|
|
326
|
-
async def post_json(self, endpoint: str, **kwargs) ->
|
|
326
|
+
async def post_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
|
|
327
327
|
"""Make POST request and return JSON response."""
|
|
328
328
|
response = await self.post(endpoint, **kwargs)
|
|
329
329
|
|
|
@@ -333,7 +333,7 @@ class BaseHTTPClient:
|
|
|
333
333
|
|
|
334
334
|
return response.json()
|
|
335
335
|
|
|
336
|
-
async def put_json(self, endpoint: str, **kwargs) ->
|
|
336
|
+
async def put_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
|
|
337
337
|
"""Make PUT request and return JSON response."""
|
|
338
338
|
response = await self.put(endpoint, **kwargs)
|
|
339
339
|
|
|
@@ -343,7 +343,7 @@ class BaseHTTPClient:
|
|
|
343
343
|
|
|
344
344
|
return response.json()
|
|
345
345
|
|
|
346
|
-
async def patch_json(self, endpoint: str, **kwargs) ->
|
|
346
|
+
async def patch_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
|
|
347
347
|
"""Make PATCH request and return JSON response."""
|
|
348
348
|
response = await self.patch(endpoint, **kwargs)
|
|
349
349
|
|
|
@@ -353,7 +353,7 @@ class BaseHTTPClient:
|
|
|
353
353
|
|
|
354
354
|
return response.json()
|
|
355
355
|
|
|
356
|
-
def get_stats(self) ->
|
|
356
|
+
def get_stats(self) -> dict[str, Any]:
|
|
357
357
|
"""Get client statistics."""
|
|
358
358
|
return self.stats.copy()
|
|
359
359
|
|
mcp_ticketer/core/mappers.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from functools import lru_cache
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
7
7
|
|
|
8
8
|
from .models import Priority, TicketState
|
|
9
9
|
|
|
@@ -16,16 +16,16 @@ U = TypeVar("U")
|
|
|
16
16
|
class BiDirectionalDict(Generic[T, U]):
|
|
17
17
|
"""Bidirectional dictionary for efficient lookups in both directions."""
|
|
18
18
|
|
|
19
|
-
def __init__(self, mapping:
|
|
19
|
+
def __init__(self, mapping: dict[T, U]):
|
|
20
20
|
"""Initialize with forward mapping.
|
|
21
21
|
|
|
22
22
|
Args:
|
|
23
23
|
mapping: Forward mapping dictionary
|
|
24
24
|
|
|
25
25
|
"""
|
|
26
|
-
self._forward:
|
|
27
|
-
self._reverse:
|
|
28
|
-
self._cache:
|
|
26
|
+
self._forward: dict[T, U] = mapping.copy()
|
|
27
|
+
self._reverse: dict[U, T] = {v: k for k, v in mapping.items()}
|
|
28
|
+
self._cache: dict[str, Any] = {}
|
|
29
29
|
|
|
30
30
|
def get_forward(self, key: T, default: Optional[U] = None) -> Optional[U]:
|
|
31
31
|
"""Get value by forward key."""
|
|
@@ -43,15 +43,15 @@ class BiDirectionalDict(Generic[T, U]):
|
|
|
43
43
|
"""Check if reverse key exists."""
|
|
44
44
|
return key in self._reverse
|
|
45
45
|
|
|
46
|
-
def forward_keys(self) ->
|
|
46
|
+
def forward_keys(self) -> list[T]:
|
|
47
47
|
"""Get all forward keys."""
|
|
48
48
|
return list(self._forward.keys())
|
|
49
49
|
|
|
50
|
-
def reverse_keys(self) ->
|
|
50
|
+
def reverse_keys(self) -> list[U]:
|
|
51
51
|
"""Get all reverse keys."""
|
|
52
52
|
return list(self._reverse.keys())
|
|
53
53
|
|
|
54
|
-
def items(self) ->
|
|
54
|
+
def items(self) -> list[tuple[T, U]]:
|
|
55
55
|
"""Get all key-value pairs."""
|
|
56
56
|
return list(self._forward.items())
|
|
57
57
|
|
|
@@ -67,7 +67,7 @@ class BaseMapper(ABC):
|
|
|
67
67
|
|
|
68
68
|
"""
|
|
69
69
|
self.cache_size = cache_size
|
|
70
|
-
self._cache:
|
|
70
|
+
self._cache: dict[str, Any] = {}
|
|
71
71
|
|
|
72
72
|
@abstractmethod
|
|
73
73
|
def get_mapping(self) -> BiDirectionalDict:
|
|
@@ -83,7 +83,7 @@ class StateMapper(BaseMapper):
|
|
|
83
83
|
"""Universal state mapping utility."""
|
|
84
84
|
|
|
85
85
|
def __init__(
|
|
86
|
-
self, adapter_type: str, custom_mappings: Optional[
|
|
86
|
+
self, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
|
|
87
87
|
):
|
|
88
88
|
"""Initialize state mapper.
|
|
89
89
|
|
|
@@ -221,7 +221,7 @@ class StateMapper(BaseMapper):
|
|
|
221
221
|
self._cache[cache_key] = result
|
|
222
222
|
return result
|
|
223
223
|
|
|
224
|
-
def get_available_states(self) ->
|
|
224
|
+
def get_available_states(self) -> list[str]:
|
|
225
225
|
"""Get all available adapter states."""
|
|
226
226
|
return self.get_mapping().reverse_keys()
|
|
227
227
|
|
|
@@ -258,7 +258,7 @@ class PriorityMapper(BaseMapper):
|
|
|
258
258
|
"""Universal priority mapping utility."""
|
|
259
259
|
|
|
260
260
|
def __init__(
|
|
261
|
-
self, adapter_type: str, custom_mappings: Optional[
|
|
261
|
+
self, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
|
|
262
262
|
):
|
|
263
263
|
"""Initialize priority mapper.
|
|
264
264
|
|
|
@@ -418,11 +418,11 @@ class PriorityMapper(BaseMapper):
|
|
|
418
418
|
self._cache[cache_key] = result
|
|
419
419
|
return result
|
|
420
420
|
|
|
421
|
-
def get_available_priorities(self) ->
|
|
421
|
+
def get_available_priorities(self) -> list[Any]:
|
|
422
422
|
"""Get all available adapter priorities."""
|
|
423
423
|
return self.get_mapping().reverse_keys()
|
|
424
424
|
|
|
425
|
-
def get_priority_labels(self, priority: Priority) ->
|
|
425
|
+
def get_priority_labels(self, priority: Priority) -> list[str]:
|
|
426
426
|
"""Get possible label names for a priority (GitHub-style).
|
|
427
427
|
|
|
428
428
|
Args:
|
|
@@ -445,7 +445,7 @@ class PriorityMapper(BaseMapper):
|
|
|
445
445
|
|
|
446
446
|
return priority_labels.get(priority, [])
|
|
447
447
|
|
|
448
|
-
def detect_priority_from_labels(self, labels:
|
|
448
|
+
def detect_priority_from_labels(self, labels: list[str]) -> Priority:
|
|
449
449
|
"""Detect priority from issue labels (GitHub-style).
|
|
450
450
|
|
|
451
451
|
Args:
|
|
@@ -478,12 +478,12 @@ class PriorityMapper(BaseMapper):
|
|
|
478
478
|
class MapperRegistry:
|
|
479
479
|
"""Registry for managing mappers across different adapters."""
|
|
480
480
|
|
|
481
|
-
_state_mappers:
|
|
482
|
-
_priority_mappers:
|
|
481
|
+
_state_mappers: dict[str, StateMapper] = {}
|
|
482
|
+
_priority_mappers: dict[str, PriorityMapper] = {}
|
|
483
483
|
|
|
484
484
|
@classmethod
|
|
485
485
|
def get_state_mapper(
|
|
486
|
-
|
|
486
|
+
cls, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
|
|
487
487
|
) -> StateMapper:
|
|
488
488
|
"""Get or create state mapper for adapter type.
|
|
489
489
|
|
|
@@ -496,13 +496,13 @@ class MapperRegistry:
|
|
|
496
496
|
|
|
497
497
|
"""
|
|
498
498
|
cache_key = f"{adapter_type}_{hash(str(custom_mappings))}"
|
|
499
|
-
if cache_key not in
|
|
500
|
-
|
|
501
|
-
return
|
|
499
|
+
if cache_key not in cls._state_mappers:
|
|
500
|
+
cls._state_mappers[cache_key] = StateMapper(adapter_type, custom_mappings)
|
|
501
|
+
return cls._state_mappers[cache_key]
|
|
502
502
|
|
|
503
503
|
@classmethod
|
|
504
504
|
def get_priority_mapper(
|
|
505
|
-
|
|
505
|
+
cls, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
|
|
506
506
|
) -> PriorityMapper:
|
|
507
507
|
"""Get or create priority mapper for adapter type.
|
|
508
508
|
|
|
@@ -515,11 +515,11 @@ class MapperRegistry:
|
|
|
515
515
|
|
|
516
516
|
"""
|
|
517
517
|
cache_key = f"{adapter_type}_{hash(str(custom_mappings))}"
|
|
518
|
-
if cache_key not in
|
|
519
|
-
|
|
518
|
+
if cache_key not in cls._priority_mappers:
|
|
519
|
+
cls._priority_mappers[cache_key] = PriorityMapper(
|
|
520
520
|
adapter_type, custom_mappings
|
|
521
521
|
)
|
|
522
|
-
return
|
|
522
|
+
return cls._priority_mappers[cache_key]
|
|
523
523
|
|
|
524
524
|
@classmethod
|
|
525
525
|
def clear_cache(cls) -> None:
|
mcp_ticketer/core/models.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, Optional
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
@@ -38,7 +38,7 @@ class TicketState(str, Enum):
|
|
|
38
38
|
CLOSED = "closed"
|
|
39
39
|
|
|
40
40
|
@classmethod
|
|
41
|
-
def valid_transitions(cls) ->
|
|
41
|
+
def valid_transitions(cls) -> dict[str, list[str]]:
|
|
42
42
|
"""Define valid state transitions."""
|
|
43
43
|
return {
|
|
44
44
|
cls.OPEN: [cls.IN_PROGRESS, cls.WAITING, cls.BLOCKED, cls.CLOSED],
|
|
@@ -66,12 +66,12 @@ class BaseTicket(BaseModel):
|
|
|
66
66
|
description: Optional[str] = Field(None, description="Detailed description")
|
|
67
67
|
state: TicketState = Field(TicketState.OPEN, description="Current state")
|
|
68
68
|
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
|
69
|
-
tags:
|
|
69
|
+
tags: list[str] = Field(default_factory=list, description="Tags/labels")
|
|
70
70
|
created_at: Optional[datetime] = Field(None, description="Creation timestamp")
|
|
71
71
|
updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
|
|
72
72
|
|
|
73
73
|
# Metadata for field mapping to different systems
|
|
74
|
-
metadata:
|
|
74
|
+
metadata: dict[str, Any] = Field(
|
|
75
75
|
default_factory=dict, description="System-specific metadata and field mappings"
|
|
76
76
|
)
|
|
77
77
|
|
|
@@ -82,11 +82,11 @@ class Epic(BaseTicket):
|
|
|
82
82
|
ticket_type: TicketType = Field(
|
|
83
83
|
default=TicketType.EPIC, frozen=True, description="Always EPIC type"
|
|
84
84
|
)
|
|
85
|
-
child_issues:
|
|
85
|
+
child_issues: list[str] = Field(
|
|
86
86
|
default_factory=list, description="IDs of child issues"
|
|
87
87
|
)
|
|
88
88
|
|
|
89
|
-
def validate_hierarchy(self) ->
|
|
89
|
+
def validate_hierarchy(self) -> list[str]:
|
|
90
90
|
"""Validate epic hierarchy rules.
|
|
91
91
|
|
|
92
92
|
Returns:
|
|
@@ -106,7 +106,7 @@ class Task(BaseTicket):
|
|
|
106
106
|
parent_issue: Optional[str] = Field(None, description="Parent issue ID (for tasks)")
|
|
107
107
|
parent_epic: Optional[str] = Field(None, description="Parent epic ID (for issues)")
|
|
108
108
|
assignee: Optional[str] = Field(None, description="Assigned user")
|
|
109
|
-
children:
|
|
109
|
+
children: list[str] = Field(default_factory=list, description="Child task IDs")
|
|
110
110
|
|
|
111
111
|
# Additional fields common across systems
|
|
112
112
|
estimated_hours: Optional[float] = Field(None, description="Time estimate")
|
|
@@ -124,7 +124,7 @@ class Task(BaseTicket):
|
|
|
124
124
|
"""Check if this is a sub-task."""
|
|
125
125
|
return self.ticket_type in (TicketType.TASK, TicketType.SUBTASK)
|
|
126
126
|
|
|
127
|
-
def validate_hierarchy(self) ->
|
|
127
|
+
def validate_hierarchy(self) -> list[str]:
|
|
128
128
|
"""Validate ticket hierarchy rules.
|
|
129
129
|
|
|
130
130
|
Returns:
|
|
@@ -160,7 +160,7 @@ class Comment(BaseModel):
|
|
|
160
160
|
author: Optional[str] = Field(None, description="Comment author")
|
|
161
161
|
content: str = Field(..., min_length=1, description="Comment text")
|
|
162
162
|
created_at: Optional[datetime] = Field(None, description="Creation timestamp")
|
|
163
|
-
metadata:
|
|
163
|
+
metadata: dict[str, Any] = Field(
|
|
164
164
|
default_factory=dict, description="System-specific metadata"
|
|
165
165
|
)
|
|
166
166
|
|
|
@@ -171,7 +171,7 @@ class SearchQuery(BaseModel):
|
|
|
171
171
|
query: Optional[str] = Field(None, description="Text search query")
|
|
172
172
|
state: Optional[TicketState] = Field(None, description="Filter by state")
|
|
173
173
|
priority: Optional[Priority] = Field(None, description="Filter by priority")
|
|
174
|
-
tags: Optional[
|
|
174
|
+
tags: Optional[list[str]] = Field(None, description="Filter by tags")
|
|
175
175
|
assignee: Optional[str] = Field(None, description="Filter by assignee")
|
|
176
176
|
limit: int = Field(10, gt=0, le=100, description="Maximum results")
|
|
177
177
|
offset: int = Field(0, ge=0, description="Result offset for pagination")
|
|
@@ -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,
|
|
17
|
+
from typing import Any, Optional
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
@@ -69,9 +69,9 @@ class AdapterConfig:
|
|
|
69
69
|
project_id: Optional[str] = None
|
|
70
70
|
|
|
71
71
|
# Additional adapter-specific configuration
|
|
72
|
-
additional_config:
|
|
72
|
+
additional_config: dict[str, Any] = field(default_factory=dict)
|
|
73
73
|
|
|
74
|
-
def to_dict(self) ->
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
75
|
"""Convert to dictionary, filtering None values."""
|
|
76
76
|
result = {}
|
|
77
77
|
for key, value in asdict(self).items():
|
|
@@ -80,7 +80,7 @@ class AdapterConfig:
|
|
|
80
80
|
return result
|
|
81
81
|
|
|
82
82
|
@classmethod
|
|
83
|
-
def from_dict(cls, data:
|
|
83
|
+
def from_dict(cls, data: dict[str, Any]) -> "AdapterConfig":
|
|
84
84
|
"""Create from dictionary."""
|
|
85
85
|
# Extract known fields
|
|
86
86
|
known_fields = {
|
|
@@ -126,14 +126,14 @@ class ProjectConfig:
|
|
|
126
126
|
api_key: Optional[str] = None
|
|
127
127
|
project_id: Optional[str] = None
|
|
128
128
|
team_id: Optional[str] = None
|
|
129
|
-
additional_config:
|
|
129
|
+
additional_config: dict[str, Any] = field(default_factory=dict)
|
|
130
130
|
|
|
131
|
-
def to_dict(self) ->
|
|
131
|
+
def to_dict(self) -> dict[str, Any]:
|
|
132
132
|
"""Convert to dictionary."""
|
|
133
133
|
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
134
134
|
|
|
135
135
|
@classmethod
|
|
136
|
-
def from_dict(cls, data:
|
|
136
|
+
def from_dict(cls, data: dict[str, Any]) -> "ProjectConfig":
|
|
137
137
|
"""Create from dictionary."""
|
|
138
138
|
return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
|
|
139
139
|
|
|
@@ -143,18 +143,18 @@ class HybridConfig:
|
|
|
143
143
|
"""Configuration for hybrid mode (multi-adapter sync)."""
|
|
144
144
|
|
|
145
145
|
enabled: bool = False
|
|
146
|
-
adapters:
|
|
146
|
+
adapters: list[str] = field(default_factory=list)
|
|
147
147
|
primary_adapter: Optional[str] = None
|
|
148
148
|
sync_strategy: SyncStrategy = SyncStrategy.PRIMARY_SOURCE
|
|
149
149
|
|
|
150
|
-
def to_dict(self) ->
|
|
150
|
+
def to_dict(self) -> dict[str, Any]:
|
|
151
151
|
"""Convert to dictionary."""
|
|
152
152
|
result = asdict(self)
|
|
153
153
|
result["sync_strategy"] = self.sync_strategy.value
|
|
154
154
|
return result
|
|
155
155
|
|
|
156
156
|
@classmethod
|
|
157
|
-
def from_dict(cls, data:
|
|
157
|
+
def from_dict(cls, data: dict[str, Any]) -> "HybridConfig":
|
|
158
158
|
"""Create from dictionary."""
|
|
159
159
|
data = data.copy()
|
|
160
160
|
if "sync_strategy" in data:
|
|
@@ -167,11 +167,11 @@ class TicketerConfig:
|
|
|
167
167
|
"""Complete ticketer configuration with hierarchical resolution."""
|
|
168
168
|
|
|
169
169
|
default_adapter: str = "aitrackdown"
|
|
170
|
-
project_configs:
|
|
171
|
-
adapters:
|
|
170
|
+
project_configs: dict[str, ProjectConfig] = field(default_factory=dict)
|
|
171
|
+
adapters: dict[str, AdapterConfig] = field(default_factory=dict)
|
|
172
172
|
hybrid_mode: Optional[HybridConfig] = None
|
|
173
173
|
|
|
174
|
-
def to_dict(self) ->
|
|
174
|
+
def to_dict(self) -> dict[str, Any]:
|
|
175
175
|
"""Convert to dictionary for JSON serialization."""
|
|
176
176
|
return {
|
|
177
177
|
"default_adapter": self.default_adapter,
|
|
@@ -185,7 +185,7 @@ class TicketerConfig:
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
@classmethod
|
|
188
|
-
def from_dict(cls, data:
|
|
188
|
+
def from_dict(cls, data: dict[str, Any]) -> "TicketerConfig":
|
|
189
189
|
"""Create from dictionary."""
|
|
190
190
|
# Parse project configs
|
|
191
191
|
project_configs = {}
|
|
@@ -216,7 +216,7 @@ class ConfigValidator:
|
|
|
216
216
|
"""Validate adapter configurations."""
|
|
217
217
|
|
|
218
218
|
@staticmethod
|
|
219
|
-
def validate_linear_config(config:
|
|
219
|
+
def validate_linear_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
220
220
|
"""Validate Linear adapter configuration.
|
|
221
221
|
|
|
222
222
|
Returns:
|
|
@@ -238,7 +238,7 @@ class ConfigValidator:
|
|
|
238
238
|
return True, None
|
|
239
239
|
|
|
240
240
|
@staticmethod
|
|
241
|
-
def validate_github_config(config:
|
|
241
|
+
def validate_github_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
242
242
|
"""Validate GitHub adapter configuration.
|
|
243
243
|
|
|
244
244
|
Returns:
|
|
@@ -267,7 +267,7 @@ class ConfigValidator:
|
|
|
267
267
|
return True, None
|
|
268
268
|
|
|
269
269
|
@staticmethod
|
|
270
|
-
def validate_jira_config(config:
|
|
270
|
+
def validate_jira_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
271
271
|
"""Validate JIRA adapter configuration.
|
|
272
272
|
|
|
273
273
|
Returns:
|
|
@@ -288,7 +288,7 @@ class ConfigValidator:
|
|
|
288
288
|
|
|
289
289
|
@staticmethod
|
|
290
290
|
def validate_aitrackdown_config(
|
|
291
|
-
config:
|
|
291
|
+
config: dict[str, Any],
|
|
292
292
|
) -> tuple[bool, Optional[str]]:
|
|
293
293
|
"""Validate AITrackdown adapter configuration.
|
|
294
294
|
|
|
@@ -302,7 +302,7 @@ class ConfigValidator:
|
|
|
302
302
|
|
|
303
303
|
@classmethod
|
|
304
304
|
def validate(
|
|
305
|
-
cls, adapter_type: str, config:
|
|
305
|
+
cls, adapter_type: str, config: dict[str, Any]
|
|
306
306
|
) -> tuple[bool, Optional[str]]:
|
|
307
307
|
"""Validate configuration for any adapter type.
|
|
308
308
|
|
|
@@ -459,8 +459,8 @@ class ConfigResolver:
|
|
|
459
459
|
def resolve_adapter_config(
|
|
460
460
|
self,
|
|
461
461
|
adapter_name: Optional[str] = None,
|
|
462
|
-
cli_overrides: Optional[
|
|
463
|
-
) ->
|
|
462
|
+
cli_overrides: Optional[dict[str, Any]] = None,
|
|
463
|
+
) -> dict[str, Any]:
|
|
464
464
|
"""Resolve adapter configuration with hierarchical precedence.
|
|
465
465
|
|
|
466
466
|
Resolution order (highest to lowest priority):
|
|
@@ -551,7 +551,7 @@ class ConfigResolver:
|
|
|
551
551
|
|
|
552
552
|
return resolved_config
|
|
553
553
|
|
|
554
|
-
def _get_env_overrides(self, adapter_type: str) ->
|
|
554
|
+
def _get_env_overrides(self, adapter_type: str) -> dict[str, Any]:
|
|
555
555
|
"""Get configuration overrides from environment variables.
|
|
556
556
|
|
|
557
557
|
Args:
|