mcp-ticketer 0.1.20__py3-none-any.whl → 0.1.22__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.

Files changed (42) hide show
  1. mcp_ticketer/__init__.py +7 -7
  2. mcp_ticketer/__version__.py +4 -2
  3. mcp_ticketer/adapters/__init__.py +4 -4
  4. mcp_ticketer/adapters/aitrackdown.py +54 -38
  5. mcp_ticketer/adapters/github.py +175 -109
  6. mcp_ticketer/adapters/hybrid.py +90 -45
  7. mcp_ticketer/adapters/jira.py +139 -130
  8. mcp_ticketer/adapters/linear.py +374 -225
  9. mcp_ticketer/cache/__init__.py +1 -1
  10. mcp_ticketer/cache/memory.py +14 -15
  11. mcp_ticketer/cli/__init__.py +1 -1
  12. mcp_ticketer/cli/configure.py +69 -93
  13. mcp_ticketer/cli/discover.py +43 -35
  14. mcp_ticketer/cli/main.py +250 -293
  15. mcp_ticketer/cli/mcp_configure.py +39 -15
  16. mcp_ticketer/cli/migrate_config.py +10 -12
  17. mcp_ticketer/cli/queue_commands.py +21 -58
  18. mcp_ticketer/cli/utils.py +115 -60
  19. mcp_ticketer/core/__init__.py +2 -2
  20. mcp_ticketer/core/adapter.py +36 -30
  21. mcp_ticketer/core/config.py +113 -77
  22. mcp_ticketer/core/env_discovery.py +51 -19
  23. mcp_ticketer/core/http_client.py +46 -29
  24. mcp_ticketer/core/mappers.py +79 -35
  25. mcp_ticketer/core/models.py +29 -15
  26. mcp_ticketer/core/project_config.py +131 -66
  27. mcp_ticketer/core/registry.py +12 -12
  28. mcp_ticketer/mcp/__init__.py +1 -1
  29. mcp_ticketer/mcp/server.py +183 -129
  30. mcp_ticketer/queue/__init__.py +2 -2
  31. mcp_ticketer/queue/__main__.py +1 -1
  32. mcp_ticketer/queue/manager.py +29 -25
  33. mcp_ticketer/queue/queue.py +144 -82
  34. mcp_ticketer/queue/run_worker.py +2 -3
  35. mcp_ticketer/queue/worker.py +48 -33
  36. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/METADATA +1 -1
  37. mcp_ticketer-0.1.22.dist-info/RECORD +42 -0
  38. mcp_ticketer-0.1.20.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/top_level.txt +0 -0
@@ -2,19 +2,19 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from typing import Dict, Any, Optional, List, Union, Callable
6
- from datetime import datetime, timedelta
7
- from enum import Enum
8
5
  import time
6
+ from enum import Enum
7
+ from typing import Any, Dict, List, Optional, Union
9
8
 
10
9
  import httpx
11
- from httpx import AsyncClient, HTTPStatusError, TimeoutException
10
+ from httpx import AsyncClient, TimeoutException
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
15
14
 
16
15
  class HTTPMethod(str, Enum):
17
16
  """HTTP methods."""
17
+
18
18
  GET = "GET"
19
19
  POST = "POST"
20
20
  PUT = "PUT"
@@ -33,7 +33,7 @@ class RetryConfig:
33
33
  exponential_base: float = 2.0,
34
34
  jitter: bool = True,
35
35
  retry_on_status: Optional[List[int]] = None,
36
- retry_on_exceptions: Optional[List[type]] = 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
@@ -41,7 +41,11 @@ class RetryConfig:
41
41
  self.exponential_base = exponential_base
42
42
  self.jitter = jitter
43
43
  self.retry_on_status = retry_on_status or [429, 502, 503, 504]
44
- self.retry_on_exceptions = retry_on_exceptions or [TimeoutException, httpx.ConnectTimeout, httpx.ReadTimeout]
44
+ self.retry_on_exceptions = retry_on_exceptions or [
45
+ TimeoutException,
46
+ httpx.ConnectTimeout,
47
+ httpx.ReadTimeout,
48
+ ]
45
49
 
46
50
 
47
51
  class RateLimiter:
@@ -53,6 +57,7 @@ class RateLimiter:
53
57
  Args:
54
58
  max_requests: Maximum number of requests allowed
55
59
  time_window: Time window in seconds
60
+
56
61
  """
57
62
  self.max_requests = max_requests
58
63
  self.time_window = time_window
@@ -69,7 +74,7 @@ class RateLimiter:
69
74
  time_passed = now - self.last_update
70
75
  self.tokens = min(
71
76
  self.max_requests,
72
- self.tokens + (time_passed / self.time_window) * self.max_requests
77
+ self.tokens + (time_passed / self.time_window) * self.max_requests,
73
78
  )
74
79
  self.last_update = now
75
80
 
@@ -95,7 +100,7 @@ class BaseHTTPClient:
95
100
  retry_config: Optional[RetryConfig] = None,
96
101
  rate_limiter: Optional[RateLimiter] = None,
97
102
  verify_ssl: bool = True,
98
- follow_redirects: bool = True
103
+ follow_redirects: bool = True,
99
104
  ):
100
105
  """Initialize HTTP client.
101
106
 
@@ -108,6 +113,7 @@ class BaseHTTPClient:
108
113
  rate_limiter: Rate limiter instance
109
114
  verify_ssl: Whether to verify SSL certificates
110
115
  follow_redirects: Whether to follow redirects
116
+
111
117
  """
112
118
  self.base_url = base_url.rstrip("/")
113
119
  self.default_headers = headers or {}
@@ -137,11 +143,13 @@ class BaseHTTPClient:
137
143
  auth=self.auth,
138
144
  timeout=self.timeout,
139
145
  verify=self.verify_ssl,
140
- follow_redirects=self.follow_redirects
146
+ follow_redirects=self.follow_redirects,
141
147
  )
142
148
  return self._client
143
149
 
144
- async def _calculate_delay(self, attempt: int, response: Optional[httpx.Response] = None) -> float:
150
+ async def _calculate_delay(
151
+ self, attempt: int, response: Optional[httpx.Response] = None
152
+ ) -> float:
145
153
  """Calculate delay for retry attempt."""
146
154
  if response and response.status_code == 429:
147
155
  # Use Retry-After header if available
@@ -162,7 +170,8 @@ class BaseHTTPClient:
162
170
  # Add jitter to prevent thundering herd
163
171
  if self.retry_config.jitter:
164
172
  import random
165
- delay *= (0.5 + random.random() * 0.5)
173
+
174
+ delay *= 0.5 + random.random() * 0.5
166
175
 
167
176
  return delay
168
177
 
@@ -170,7 +179,7 @@ class BaseHTTPClient:
170
179
  self,
171
180
  exception: Exception,
172
181
  response: Optional[httpx.Response] = None,
173
- attempt: int = 1
182
+ attempt: int = 1,
174
183
  ) -> bool:
175
184
  """Determine if request should be retried."""
176
185
  if attempt >= self.retry_config.max_retries:
@@ -197,7 +206,7 @@ class BaseHTTPClient:
197
206
  headers: Optional[Dict[str, str]] = None,
198
207
  timeout: Optional[float] = None,
199
208
  retry_count: int = 0,
200
- **kwargs
209
+ **kwargs,
201
210
  ) -> httpx.Response:
202
211
  """Make HTTP request with retry and rate limiting.
203
212
 
@@ -218,6 +227,7 @@ class BaseHTTPClient:
218
227
  Raises:
219
228
  HTTPStatusError: On HTTP errors
220
229
  TimeoutException: On timeout
230
+
221
231
  """
222
232
  # Rate limiting
223
233
  if self.rate_limiter:
@@ -243,7 +253,7 @@ class BaseHTTPClient:
243
253
  params=params,
244
254
  headers=request_headers,
245
255
  timeout=timeout or self.timeout,
246
- **kwargs
256
+ **kwargs,
247
257
  )
248
258
 
249
259
  # Update stats
@@ -258,7 +268,7 @@ class BaseHTTPClient:
258
268
  self.stats["errors"] += 1
259
269
 
260
270
  # Check if we should retry
261
- response = getattr(e, 'response', None)
271
+ response = getattr(e, "response", None)
262
272
  if self._should_retry(e, response, retry_count + 1):
263
273
  delay = await self._calculate_delay(retry_count + 1, response)
264
274
 
@@ -269,7 +279,15 @@ class BaseHTTPClient:
269
279
 
270
280
  await asyncio.sleep(delay)
271
281
  return await self.request(
272
- method, endpoint, data, json, params, headers, timeout, retry_count + 1, **kwargs
282
+ method,
283
+ endpoint,
284
+ data,
285
+ json,
286
+ params,
287
+ headers,
288
+ timeout,
289
+ retry_count + 1,
290
+ **kwargs,
273
291
  )
274
292
 
275
293
  # No more retries, re-raise the exception
@@ -364,6 +382,7 @@ class GitHubHTTPClient(BaseHTTPClient):
364
382
  Args:
365
383
  token: GitHub API token
366
384
  api_url: GitHub API URL
385
+
367
386
  """
368
387
  headers = {
369
388
  "Authorization": f"Bearer {token}",
@@ -379,9 +398,8 @@ class GitHubHTTPClient(BaseHTTPClient):
379
398
  headers=headers,
380
399
  rate_limiter=rate_limiter,
381
400
  retry_config=RetryConfig(
382
- max_retries=3,
383
- retry_on_status=[429, 502, 503, 504, 522, 524]
384
- )
401
+ max_retries=3, retry_on_status=[429, 502, 503, 504, 522, 524]
402
+ ),
385
403
  )
386
404
 
387
405
 
@@ -394,7 +412,7 @@ class JiraHTTPClient(BaseHTTPClient):
394
412
  api_token: str,
395
413
  server_url: str,
396
414
  is_cloud: bool = True,
397
- verify_ssl: bool = True
415
+ verify_ssl: bool = True,
398
416
  ):
399
417
  """Initialize JIRA HTTP client.
400
418
 
@@ -404,13 +422,13 @@ class JiraHTTPClient(BaseHTTPClient):
404
422
  server_url: JIRA server URL
405
423
  is_cloud: Whether this is JIRA Cloud
406
424
  verify_ssl: Whether to verify SSL certificates
425
+
407
426
  """
408
- api_base = f"{server_url}/rest/api/3" if is_cloud else f"{server_url}/rest/api/2"
427
+ api_base = (
428
+ f"{server_url}/rest/api/3" if is_cloud else f"{server_url}/rest/api/2"
429
+ )
409
430
 
410
- headers = {
411
- "Accept": "application/json",
412
- "Content-Type": "application/json"
413
- }
431
+ headers = {"Accept": "application/json", "Content-Type": "application/json"}
414
432
 
415
433
  auth = httpx.BasicAuth(email, api_token)
416
434
 
@@ -424,7 +442,6 @@ class JiraHTTPClient(BaseHTTPClient):
424
442
  rate_limiter=rate_limiter,
425
443
  verify_ssl=verify_ssl,
426
444
  retry_config=RetryConfig(
427
- max_retries=3,
428
- retry_on_status=[429, 502, 503, 504]
429
- )
430
- )
445
+ max_retries=3, retry_on_status=[429, 502, 503, 504]
446
+ ),
447
+ )
@@ -1,17 +1,16 @@
1
1
  """Centralized mapping utilities for state and priority conversions."""
2
2
 
3
3
  import logging
4
- from typing import Dict, List, Optional, Any, TypeVar, Generic, Callable
5
- from functools import lru_cache
6
4
  from abc import ABC, abstractmethod
7
- from enum import Enum
5
+ from functools import lru_cache
6
+ from typing import Any, Dict, Generic, List, Optional, TypeVar
8
7
 
9
- from .models import TicketState, Priority
8
+ from .models import Priority, TicketState
10
9
 
11
10
  logger = logging.getLogger(__name__)
12
11
 
13
- T = TypeVar('T')
14
- U = TypeVar('U')
12
+ T = TypeVar("T")
13
+ U = TypeVar("U")
15
14
 
16
15
 
17
16
  class BiDirectionalDict(Generic[T, U]):
@@ -22,6 +21,7 @@ class BiDirectionalDict(Generic[T, U]):
22
21
 
23
22
  Args:
24
23
  mapping: Forward mapping dictionary
24
+
25
25
  """
26
26
  self._forward: Dict[T, U] = mapping.copy()
27
27
  self._reverse: Dict[U, T] = {v: k for k, v in mapping.items()}
@@ -64,6 +64,7 @@ class BaseMapper(ABC):
64
64
 
65
65
  Args:
66
66
  cache_size: Size of LRU cache for mapping results
67
+
67
68
  """
68
69
  self.cache_size = cache_size
69
70
  self._cache: Dict[str, Any] = {}
@@ -81,12 +82,15 @@ class BaseMapper(ABC):
81
82
  class StateMapper(BaseMapper):
82
83
  """Universal state mapping utility."""
83
84
 
84
- def __init__(self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None):
85
+ def __init__(
86
+ self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None
87
+ ):
85
88
  """Initialize state mapper.
86
89
 
87
90
  Args:
88
91
  adapter_type: Type of adapter (github, jira, linear, etc.)
89
92
  custom_mappings: Custom state mappings to override defaults
93
+
90
94
  """
91
95
  super().__init__()
92
96
  self.adapter_type = adapter_type
@@ -104,11 +108,11 @@ class StateMapper(BaseMapper):
104
108
  "github": {
105
109
  TicketState.OPEN: "open",
106
110
  TicketState.IN_PROGRESS: "open", # Uses labels
107
- TicketState.READY: "open", # Uses labels
108
- TicketState.TESTED: "open", # Uses labels
111
+ TicketState.READY: "open", # Uses labels
112
+ TicketState.TESTED: "open", # Uses labels
109
113
  TicketState.DONE: "closed",
110
- TicketState.WAITING: "open", # Uses labels
111
- TicketState.BLOCKED: "open", # Uses labels
114
+ TicketState.WAITING: "open", # Uses labels
115
+ TicketState.BLOCKED: "open", # Uses labels
112
116
  TicketState.CLOSED: "closed",
113
117
  },
114
118
  "jira": {
@@ -124,8 +128,8 @@ class StateMapper(BaseMapper):
124
128
  "linear": {
125
129
  TicketState.OPEN: "backlog",
126
130
  TicketState.IN_PROGRESS: "started",
127
- TicketState.READY: "started", # Uses labels
128
- TicketState.TESTED: "started", # Uses labels
131
+ TicketState.READY: "started", # Uses labels
132
+ TicketState.TESTED: "started", # Uses labels
129
133
  TicketState.DONE: "completed",
130
134
  TicketState.WAITING: "unstarted", # Uses labels
131
135
  TicketState.BLOCKED: "unstarted", # Uses labels
@@ -140,7 +144,7 @@ class StateMapper(BaseMapper):
140
144
  TicketState.WAITING: "waiting",
141
145
  TicketState.BLOCKED: "blocked",
142
146
  TicketState.CLOSED: "closed",
143
- }
147
+ },
144
148
  }
145
149
 
146
150
  mapping = default_mappings.get(self.adapter_type, {})
@@ -160,6 +164,7 @@ class StateMapper(BaseMapper):
160
164
 
161
165
  Returns:
162
166
  Universal ticket state
167
+
163
168
  """
164
169
  cache_key = f"to_system_{adapter_state}"
165
170
  if cache_key in self._cache:
@@ -172,12 +177,17 @@ class StateMapper(BaseMapper):
172
177
  # Fallback: try case-insensitive matching
173
178
  adapter_state_lower = adapter_state.lower()
174
179
  for universal_state, system_state in mapping.items():
175
- if isinstance(system_state, str) and system_state.lower() == adapter_state_lower:
180
+ if (
181
+ isinstance(system_state, str)
182
+ and system_state.lower() == adapter_state_lower
183
+ ):
176
184
  result = universal_state
177
185
  break
178
186
 
179
187
  if result is None:
180
- logger.warning(f"Unknown {self.adapter_type} state: {adapter_state}, defaulting to OPEN")
188
+ logger.warning(
189
+ f"Unknown {self.adapter_type} state: {adapter_state}, defaulting to OPEN"
190
+ )
181
191
  result = TicketState.OPEN
182
192
 
183
193
  self._cache[cache_key] = result
@@ -191,6 +201,7 @@ class StateMapper(BaseMapper):
191
201
 
192
202
  Returns:
193
203
  State in adapter format
204
+
194
205
  """
195
206
  cache_key = f"from_system_{system_state.value}"
196
207
  if cache_key in self._cache:
@@ -200,7 +211,9 @@ class StateMapper(BaseMapper):
200
211
  result = mapping.get_forward(system_state)
201
212
 
202
213
  if result is None:
203
- logger.warning(f"No {self.adapter_type} mapping for state: {system_state}, using default")
214
+ logger.warning(
215
+ f"No {self.adapter_type} mapping for state: {system_state}, using default"
216
+ )
204
217
  # Fallback to first available state
205
218
  available_states = mapping.reverse_keys()
206
219
  result = available_states[0] if available_states else "open"
@@ -224,6 +237,7 @@ class StateMapper(BaseMapper):
224
237
 
225
238
  Returns:
226
239
  Label name if state requires a label, None otherwise
240
+
227
241
  """
228
242
  if not self.supports_state_labels():
229
243
  return None
@@ -243,12 +257,15 @@ class StateMapper(BaseMapper):
243
257
  class PriorityMapper(BaseMapper):
244
258
  """Universal priority mapping utility."""
245
259
 
246
- def __init__(self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None):
260
+ def __init__(
261
+ self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None
262
+ ):
247
263
  """Initialize priority mapper.
248
264
 
249
265
  Args:
250
266
  adapter_type: Type of adapter (github, jira, linear, etc.)
251
267
  custom_mappings: Custom priority mappings to override defaults
268
+
252
269
  """
253
270
  super().__init__()
254
271
  self.adapter_type = adapter_type
@@ -286,7 +303,7 @@ class PriorityMapper(BaseMapper):
286
303
  Priority.HIGH: "high",
287
304
  Priority.MEDIUM: "medium",
288
305
  Priority.LOW: "low",
289
- }
306
+ },
290
307
  }
291
308
 
292
309
  mapping = default_mappings.get(self.adapter_type, {})
@@ -306,6 +323,7 @@ class PriorityMapper(BaseMapper):
306
323
 
307
324
  Returns:
308
325
  Universal priority
326
+
309
327
  """
310
328
  cache_key = f"to_system_{adapter_priority}"
311
329
  if cache_key in self._cache:
@@ -319,18 +337,32 @@ class PriorityMapper(BaseMapper):
319
337
  if isinstance(adapter_priority, str):
320
338
  adapter_priority_lower = adapter_priority.lower()
321
339
  for universal_priority, system_priority in mapping.items():
322
- if isinstance(system_priority, str) and system_priority.lower() == adapter_priority_lower:
340
+ if (
341
+ isinstance(system_priority, str)
342
+ and system_priority.lower() == adapter_priority_lower
343
+ ):
323
344
  result = universal_priority
324
345
  break
325
346
  # Check for common priority patterns
326
- elif ("critical" in adapter_priority_lower or "urgent" in adapter_priority_lower or
327
- "highest" in adapter_priority_lower or adapter_priority_lower in ["p0", "0"]):
347
+ elif (
348
+ "critical" in adapter_priority_lower
349
+ or "urgent" in adapter_priority_lower
350
+ or "highest" in adapter_priority_lower
351
+ or adapter_priority_lower in ["p0", "0"]
352
+ ):
328
353
  result = Priority.CRITICAL
329
354
  break
330
- elif ("high" in adapter_priority_lower or adapter_priority_lower in ["p1", "1"]):
355
+ elif "high" in adapter_priority_lower or adapter_priority_lower in [
356
+ "p1",
357
+ "1",
358
+ ]:
331
359
  result = Priority.HIGH
332
360
  break
333
- elif ("low" in adapter_priority_lower or adapter_priority_lower in ["p3", "3", "lowest"]):
361
+ elif "low" in adapter_priority_lower or adapter_priority_lower in [
362
+ "p3",
363
+ "3",
364
+ "lowest",
365
+ ]:
334
366
  result = Priority.LOW
335
367
  break
336
368
  elif isinstance(adapter_priority, (int, float)):
@@ -345,7 +377,9 @@ class PriorityMapper(BaseMapper):
345
377
  result = Priority.MEDIUM
346
378
 
347
379
  if result is None:
348
- logger.warning(f"Unknown {self.adapter_type} priority: {adapter_priority}, defaulting to MEDIUM")
380
+ logger.warning(
381
+ f"Unknown {self.adapter_type} priority: {adapter_priority}, defaulting to MEDIUM"
382
+ )
349
383
  result = Priority.MEDIUM
350
384
 
351
385
  self._cache[cache_key] = result
@@ -359,6 +393,7 @@ class PriorityMapper(BaseMapper):
359
393
 
360
394
  Returns:
361
395
  Priority in adapter format
396
+
362
397
  """
363
398
  cache_key = f"from_system_{system_priority.value}"
364
399
  if cache_key in self._cache:
@@ -368,7 +403,9 @@ class PriorityMapper(BaseMapper):
368
403
  result = mapping.get_forward(system_priority)
369
404
 
370
405
  if result is None:
371
- logger.warning(f"No {self.adapter_type} mapping for priority: {system_priority}")
406
+ logger.warning(
407
+ f"No {self.adapter_type} mapping for priority: {system_priority}"
408
+ )
372
409
  # Fallback based on adapter type
373
410
  fallback_mappings = {
374
411
  "github": "P2",
@@ -393,6 +430,7 @@ class PriorityMapper(BaseMapper):
393
430
 
394
431
  Returns:
395
432
  List of possible label names
433
+
396
434
  """
397
435
  if self.adapter_type != "github":
398
436
  return []
@@ -415,6 +453,7 @@ class PriorityMapper(BaseMapper):
415
453
 
416
454
  Returns:
417
455
  Detected priority
456
+
418
457
  """
419
458
  if self.adapter_type != "github":
420
459
  return Priority.MEDIUM
@@ -422,7 +461,12 @@ class PriorityMapper(BaseMapper):
422
461
  labels_lower = [label.lower() for label in labels]
423
462
 
424
463
  # Check each priority level
425
- for priority in [Priority.CRITICAL, Priority.HIGH, Priority.LOW, Priority.MEDIUM]:
464
+ for priority in [
465
+ Priority.CRITICAL,
466
+ Priority.HIGH,
467
+ Priority.LOW,
468
+ Priority.MEDIUM,
469
+ ]:
426
470
  priority_labels = self.get_priority_labels(priority)
427
471
  for priority_label in priority_labels:
428
472
  if priority_label.lower() in labels_lower:
@@ -439,9 +483,7 @@ class MapperRegistry:
439
483
 
440
484
  @classmethod
441
485
  def get_state_mapper(
442
- self,
443
- adapter_type: str,
444
- custom_mappings: Optional[Dict[str, Any]] = None
486
+ self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None
445
487
  ) -> StateMapper:
446
488
  """Get or create state mapper for adapter type.
447
489
 
@@ -451,6 +493,7 @@ class MapperRegistry:
451
493
 
452
494
  Returns:
453
495
  State mapper instance
496
+
454
497
  """
455
498
  cache_key = f"{adapter_type}_{hash(str(custom_mappings))}"
456
499
  if cache_key not in self._state_mappers:
@@ -459,9 +502,7 @@ class MapperRegistry:
459
502
 
460
503
  @classmethod
461
504
  def get_priority_mapper(
462
- self,
463
- adapter_type: str,
464
- custom_mappings: Optional[Dict[str, Any]] = None
505
+ self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None
465
506
  ) -> PriorityMapper:
466
507
  """Get or create priority mapper for adapter type.
467
508
 
@@ -471,10 +512,13 @@ class MapperRegistry:
471
512
 
472
513
  Returns:
473
514
  Priority mapper instance
515
+
474
516
  """
475
517
  cache_key = f"{adapter_type}_{hash(str(custom_mappings))}"
476
518
  if cache_key not in self._priority_mappers:
477
- self._priority_mappers[cache_key] = PriorityMapper(adapter_type, custom_mappings)
519
+ self._priority_mappers[cache_key] = PriorityMapper(
520
+ adapter_type, custom_mappings
521
+ )
478
522
  return self._priority_mappers[cache_key]
479
523
 
480
524
  @classmethod
@@ -489,4 +533,4 @@ class MapperRegistry:
489
533
  def reset(cls) -> None:
490
534
  """Reset all mappers."""
491
535
  cls._state_mappers.clear()
492
- cls._priority_mappers.clear()
536
+ cls._priority_mappers.clear()
@@ -2,12 +2,14 @@
2
2
 
3
3
  from datetime import datetime
4
4
  from enum import Enum
5
- from typing import Optional, Dict, Any, List
6
- from pydantic import BaseModel, Field, ConfigDict
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
7
8
 
8
9
 
9
10
  class Priority(str, Enum):
10
11
  """Universal priority levels."""
12
+
11
13
  LOW = "low"
12
14
  MEDIUM = "medium"
13
15
  HIGH = "high"
@@ -16,14 +18,16 @@ class Priority(str, Enum):
16
18
 
17
19
  class TicketType(str, Enum):
18
20
  """Ticket type hierarchy."""
19
- EPIC = "epic" # Strategic level (Projects in Linear, Milestones in GitHub)
20
- ISSUE = "issue" # Work item level (standard issues/tasks)
21
- TASK = "task" # Sub-task level (sub-issues, checkboxes)
21
+
22
+ EPIC = "epic" # Strategic level (Projects in Linear, Milestones in GitHub)
23
+ ISSUE = "issue" # Work item level (standard issues/tasks)
24
+ TASK = "task" # Sub-task level (sub-issues, checkboxes)
22
25
  SUBTASK = "subtask" # Alias for task (for clarity)
23
26
 
24
27
 
25
28
  class TicketState(str, Enum):
26
29
  """Universal ticket states with state machine abstraction."""
30
+
27
31
  OPEN = "open"
28
32
  IN_PROGRESS = "in_progress"
29
33
  READY = "ready"
@@ -54,6 +58,7 @@ class TicketState(str, Enum):
54
58
 
55
59
  class BaseTicket(BaseModel):
56
60
  """Base model for all ticket types."""
61
+
57
62
  model_config = ConfigDict(use_enum_values=True)
58
63
 
59
64
  id: Optional[str] = Field(None, description="Unique identifier")
@@ -67,17 +72,18 @@ class BaseTicket(BaseModel):
67
72
 
68
73
  # Metadata for field mapping to different systems
69
74
  metadata: Dict[str, Any] = Field(
70
- default_factory=dict,
71
- description="System-specific metadata and field mappings"
75
+ default_factory=dict, description="System-specific metadata and field mappings"
72
76
  )
73
77
 
74
78
 
75
79
  class Epic(BaseTicket):
76
80
  """Epic - highest level container for work (Projects in Linear, Milestones in GitHub)."""
77
- ticket_type: TicketType = Field(default=TicketType.EPIC, frozen=True, description="Always EPIC type")
81
+
82
+ ticket_type: TicketType = Field(
83
+ default=TicketType.EPIC, frozen=True, description="Always EPIC type"
84
+ )
78
85
  child_issues: List[str] = Field(
79
- default_factory=list,
80
- description="IDs of child issues"
86
+ default_factory=list, description="IDs of child issues"
81
87
  )
82
88
 
83
89
  def validate_hierarchy(self) -> List[str]:
@@ -85,6 +91,7 @@ class Epic(BaseTicket):
85
91
 
86
92
  Returns:
87
93
  List of validation errors (empty if valid)
94
+
88
95
  """
89
96
  # Epics don't have parents in our hierarchy
90
97
  return []
@@ -92,7 +99,10 @@ class Epic(BaseTicket):
92
99
 
93
100
  class Task(BaseTicket):
94
101
  """Task - individual work item (can be ISSUE or TASK type)."""
95
- ticket_type: TicketType = Field(default=TicketType.ISSUE, description="Ticket type in hierarchy")
102
+
103
+ ticket_type: TicketType = Field(
104
+ default=TicketType.ISSUE, description="Ticket type in hierarchy"
105
+ )
96
106
  parent_issue: Optional[str] = Field(None, description="Parent issue ID (for tasks)")
97
107
  parent_epic: Optional[str] = Field(None, description="Parent epic ID (for issues)")
98
108
  assignee: Optional[str] = Field(None, description="Assigned user")
@@ -119,6 +129,7 @@ class Task(BaseTicket):
119
129
 
120
130
  Returns:
121
131
  List of validation errors (empty if valid)
132
+
122
133
  """
123
134
  errors = []
124
135
 
@@ -132,13 +143,16 @@ class Task(BaseTicket):
132
143
 
133
144
  # Tasks should not have both parent_issue and parent_epic
134
145
  if self.is_task() and self.parent_epic:
135
- errors.append("Tasks should only have parent_issue, not parent_epic (epic comes from parent issue)")
146
+ errors.append(
147
+ "Tasks should only have parent_issue, not parent_epic (epic comes from parent issue)"
148
+ )
136
149
 
137
150
  return errors
138
151
 
139
152
 
140
153
  class Comment(BaseModel):
141
154
  """Comment on a ticket."""
155
+
142
156
  model_config = ConfigDict(use_enum_values=True)
143
157
 
144
158
  id: Optional[str] = Field(None, description="Comment ID")
@@ -147,17 +161,17 @@ class Comment(BaseModel):
147
161
  content: str = Field(..., min_length=1, description="Comment text")
148
162
  created_at: Optional[datetime] = Field(None, description="Creation timestamp")
149
163
  metadata: Dict[str, Any] = Field(
150
- default_factory=dict,
151
- description="System-specific metadata"
164
+ default_factory=dict, description="System-specific metadata"
152
165
  )
153
166
 
154
167
 
155
168
  class SearchQuery(BaseModel):
156
169
  """Search query parameters."""
170
+
157
171
  query: Optional[str] = Field(None, description="Text search query")
158
172
  state: Optional[TicketState] = Field(None, description="Filter by state")
159
173
  priority: Optional[Priority] = Field(None, description="Filter by priority")
160
174
  tags: Optional[List[str]] = Field(None, description="Filter by tags")
161
175
  assignee: Optional[str] = Field(None, description="Filter by assignee")
162
176
  limit: int = Field(10, gt=0, le=100, description="Maximum results")
163
- offset: int = Field(0, ge=0, description="Result offset for pagination")
177
+ offset: int = Field(0, ge=0, description="Result offset for pagination")