mcp-ticketer 0.1.21__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.

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 +66 -49
  5. mcp_ticketer/adapters/github.py +192 -125
  6. mcp_ticketer/adapters/hybrid.py +99 -53
  7. mcp_ticketer/adapters/jira.py +161 -151
  8. mcp_ticketer/adapters/linear.py +396 -246
  9. mcp_ticketer/cache/__init__.py +1 -1
  10. mcp_ticketer/cache/memory.py +15 -16
  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 +283 -298
  15. mcp_ticketer/cli/mcp_configure.py +39 -15
  16. mcp_ticketer/cli/migrate_config.py +11 -13
  17. mcp_ticketer/cli/queue_commands.py +21 -58
  18. mcp_ticketer/cli/utils.py +121 -66
  19. mcp_ticketer/core/__init__.py +2 -2
  20. mcp_ticketer/core/adapter.py +46 -39
  21. mcp_ticketer/core/config.py +128 -92
  22. mcp_ticketer/core/env_discovery.py +69 -37
  23. mcp_ticketer/core/http_client.py +57 -40
  24. mcp_ticketer/core/mappers.py +98 -54
  25. mcp_ticketer/core/models.py +38 -24
  26. mcp_ticketer/core/project_config.py +145 -80
  27. mcp_ticketer/core/registry.py +16 -16
  28. mcp_ticketer/mcp/__init__.py +1 -1
  29. mcp_ticketer/mcp/server.py +199 -145
  30. mcp_ticketer/queue/__init__.py +2 -2
  31. mcp_ticketer/queue/__main__.py +1 -1
  32. mcp_ticketer/queue/manager.py +30 -26
  33. mcp_ticketer/queue/queue.py +147 -85
  34. mcp_ticketer/queue/run_worker.py +2 -3
  35. mcp_ticketer/queue/worker.py +55 -40
  36. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
  37. mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
  38. mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,31 @@
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, Generic, 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]):
18
17
  """Bidirectional dictionary for efficient lookups in both directions."""
19
18
 
20
- def __init__(self, mapping: Dict[T, U]):
19
+ def __init__(self, mapping: dict[T, U]):
21
20
  """Initialize with forward mapping.
22
21
 
23
22
  Args:
24
23
  mapping: Forward mapping dictionary
24
+
25
25
  """
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] = {}
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) -> List[T]:
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) -> List[U]:
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) -> List[tuple[T, U]]:
54
+ def items(self) -> list[tuple[T, U]]:
55
55
  """Get all key-value pairs."""
56
56
  return list(self._forward.items())
57
57
 
@@ -64,9 +64,10 @@ 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
- self._cache: Dict[str, Any] = {}
70
+ self._cache: dict[str, Any] = {}
70
71
 
71
72
  @abstractmethod
72
73
  def get_mapping(self) -> BiDirectionalDict:
@@ -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"
@@ -208,7 +221,7 @@ class StateMapper(BaseMapper):
208
221
  self._cache[cache_key] = result
209
222
  return result
210
223
 
211
- def get_available_states(self) -> List[str]:
224
+ def get_available_states(self) -> list[str]:
212
225
  """Get all available adapter states."""
213
226
  return self.get_mapping().reverse_keys()
214
227
 
@@ -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",
@@ -381,11 +418,11 @@ class PriorityMapper(BaseMapper):
381
418
  self._cache[cache_key] = result
382
419
  return result
383
420
 
384
- def get_available_priorities(self) -> List[Any]:
421
+ def get_available_priorities(self) -> list[Any]:
385
422
  """Get all available adapter priorities."""
386
423
  return self.get_mapping().reverse_keys()
387
424
 
388
- def get_priority_labels(self, priority: Priority) -> List[str]:
425
+ def get_priority_labels(self, priority: Priority) -> list[str]:
389
426
  """Get possible label names for a priority (GitHub-style).
390
427
 
391
428
  Args:
@@ -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 []
@@ -407,7 +445,7 @@ class PriorityMapper(BaseMapper):
407
445
 
408
446
  return priority_labels.get(priority, [])
409
447
 
410
- def detect_priority_from_labels(self, labels: List[str]) -> Priority:
448
+ def detect_priority_from_labels(self, labels: list[str]) -> Priority:
411
449
  """Detect priority from issue labels (GitHub-style).
412
450
 
413
451
  Args:
@@ -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:
@@ -434,14 +478,12 @@ class PriorityMapper(BaseMapper):
434
478
  class MapperRegistry:
435
479
  """Registry for managing mappers across different adapters."""
436
480
 
437
- _state_mappers: Dict[str, StateMapper] = {}
438
- _priority_mappers: Dict[str, PriorityMapper] = {}
481
+ _state_mappers: dict[str, StateMapper] = {}
482
+ _priority_mappers: dict[str, PriorityMapper] = {}
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
+ cls, 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,17 +493,16 @@ 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
- if cache_key not in self._state_mappers:
457
- self._state_mappers[cache_key] = StateMapper(adapter_type, custom_mappings)
458
- return self._state_mappers[cache_key]
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]
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
+ cls, 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,11 +512,14 @@ 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
- if cache_key not in self._priority_mappers:
477
- self._priority_mappers[cache_key] = PriorityMapper(adapter_type, custom_mappings)
478
- return self._priority_mappers[cache_key]
518
+ if cache_key not in cls._priority_mappers:
519
+ cls._priority_mappers[cache_key] = PriorityMapper(
520
+ adapter_type, custom_mappings
521
+ )
522
+ return cls._priority_mappers[cache_key]
479
523
 
480
524
  @classmethod
481
525
  def clear_cache(cls) -> None:
@@ -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, 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"
@@ -34,7 +38,7 @@ class TicketState(str, Enum):
34
38
  CLOSED = "closed"
35
39
 
36
40
  @classmethod
37
- def valid_transitions(cls) -> Dict[str, List[str]]:
41
+ def valid_transitions(cls) -> dict[str, list[str]]:
38
42
  """Define valid state transitions."""
39
43
  return {
40
44
  cls.OPEN: [cls.IN_PROGRESS, cls.WAITING, cls.BLOCKED, cls.CLOSED],
@@ -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")
@@ -61,30 +66,32 @@ class BaseTicket(BaseModel):
61
66
  description: Optional[str] = Field(None, description="Detailed description")
62
67
  state: TicketState = Field(TicketState.OPEN, description="Current state")
63
68
  priority: Priority = Field(Priority.MEDIUM, description="Priority level")
64
- tags: List[str] = Field(default_factory=list, description="Tags/labels")
69
+ tags: list[str] = Field(default_factory=list, description="Tags/labels")
65
70
  created_at: Optional[datetime] = Field(None, description="Creation timestamp")
66
71
  updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
67
72
 
68
73
  # Metadata for field mapping to different systems
69
- metadata: Dict[str, Any] = Field(
70
- default_factory=dict,
71
- description="System-specific metadata and field mappings"
74
+ metadata: dict[str, Any] = Field(
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")
78
- child_issues: List[str] = Field(
79
- default_factory=list,
80
- description="IDs of child issues"
81
+
82
+ ticket_type: TicketType = Field(
83
+ default=TicketType.EPIC, frozen=True, description="Always EPIC type"
84
+ )
85
+ child_issues: list[str] = Field(
86
+ default_factory=list, description="IDs of child issues"
81
87
  )
82
88
 
83
- def validate_hierarchy(self) -> List[str]:
89
+ def validate_hierarchy(self) -> list[str]:
84
90
  """Validate epic hierarchy rules.
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,11 +99,14 @@ 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")
99
- children: List[str] = Field(default_factory=list, description="Child task IDs")
109
+ children: list[str] = Field(default_factory=list, description="Child task IDs")
100
110
 
101
111
  # Additional fields common across systems
102
112
  estimated_hours: Optional[float] = Field(None, description="Time estimate")
@@ -114,11 +124,12 @@ class Task(BaseTicket):
114
124
  """Check if this is a sub-task."""
115
125
  return self.ticket_type in (TicketType.TASK, TicketType.SUBTASK)
116
126
 
117
- def validate_hierarchy(self) -> List[str]:
127
+ def validate_hierarchy(self) -> list[str]:
118
128
  """Validate ticket hierarchy rules.
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")
@@ -146,18 +160,18 @@ class Comment(BaseModel):
146
160
  author: Optional[str] = Field(None, description="Comment author")
147
161
  content: str = Field(..., min_length=1, description="Comment text")
148
162
  created_at: Optional[datetime] = Field(None, description="Creation timestamp")
149
- metadata: Dict[str, Any] = Field(
150
- default_factory=dict,
151
- description="System-specific metadata"
163
+ metadata: dict[str, Any] = Field(
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
- tags: Optional[List[str]] = Field(None, description="Filter by tags")
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")