mcp-ticketer 0.1.21__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.21.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.21.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/utils.py CHANGED
@@ -2,71 +2,97 @@
2
2
 
3
3
  import asyncio
4
4
  import json
5
+ import logging
5
6
  import os
6
- from typing import Optional, Dict, Any, Callable, TypeVar, List
7
- from pathlib import Path
8
7
  from functools import wraps
8
+ from pathlib import Path
9
+ from typing import Any, Callable, Dict, List, Optional, TypeVar
9
10
 
10
11
  import typer
11
12
  from rich.console import Console
12
- from rich.table import Table
13
- from rich.panel import Panel
14
13
  from rich.progress import Progress, SpinnerColumn, TextColumn
14
+ from rich.table import Table
15
15
 
16
- from ..core import Task, TicketState, Priority, AdapterRegistry
17
- from ..core.config import get_config, get_adapter_config
18
- from ..queue import Queue, WorkerManager, QueueStatus
16
+ from ..core import AdapterRegistry, Priority, Task, TicketState
17
+ from ..queue import Queue, QueueStatus, WorkerManager
19
18
 
20
19
  # Type variable for async functions
21
- T = TypeVar('T')
20
+ T = TypeVar("T")
22
21
 
23
22
  console = Console()
23
+ logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
26
  class CommonPatterns:
27
27
  """Common CLI patterns and utilities."""
28
28
 
29
- # Configuration file management
30
- CONFIG_FILE = Path.home() / ".mcp-ticketer" / "config.json"
29
+ # Configuration file management - PROJECT-LOCAL ONLY
30
+ CONFIG_FILE = Path.cwd() / ".mcp-ticketer" / "config.json"
31
31
 
32
32
  @staticmethod
33
33
  def load_config() -> dict:
34
- """Load configuration from file.
34
+ """Load configuration from project-local config file ONLY.
35
+
36
+ SECURITY: This method ONLY reads from the current project directory
37
+ to prevent configuration leakage across projects. It will NEVER read
38
+ from user home directory or system-wide locations.
35
39
 
36
40
  Resolution order:
37
41
  1. Project-specific config (.mcp-ticketer/config.json in cwd)
38
- 2. Global config (~/.mcp-ticketer/config.json)
42
+ 2. Default to aitrackdown adapter
39
43
 
40
44
  Returns:
41
- Configuration dictionary
45
+ Configuration dictionary with adapter and config keys.
46
+ Defaults to aitrackdown if no local config exists.
47
+
42
48
  """
43
- # Check project-specific config first
49
+ # ONLY check project-specific config in current working directory
44
50
  project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
45
51
  if project_config.exists():
52
+ # Validate that config file is actually in project directory
46
53
  try:
47
- with open(project_config, "r") as f:
48
- return json.load(f)
49
- except (json.JSONDecodeError, IOError) as e:
50
- console.print(f"[yellow]Warning: Could not load project config: {e}[/yellow]")
51
- # Fall through to global config
52
-
53
- # Fall back to global config
54
- if CommonPatterns.CONFIG_FILE.exists():
55
- try:
56
- with open(CommonPatterns.CONFIG_FILE, "r") as f:
57
- return json.load(f)
58
- except (json.JSONDecodeError, IOError) as e:
59
- console.print(f"[yellow]Warning: Could not load global config: {e}[/yellow]")
54
+ if not project_config.resolve().is_relative_to(Path.cwd().resolve()):
55
+ logger.error(
56
+ f"Security violation: Config file {project_config} "
57
+ "is not within project directory"
58
+ )
59
+ raise ValueError(
60
+ f"Security violation: Config file {project_config} "
61
+ "is not within project directory"
62
+ )
63
+ except (ValueError, RuntimeError):
64
+ # is_relative_to may raise ValueError in some cases
65
+ pass
60
66
 
61
- # Default fallback
67
+ try:
68
+ with open(project_config) as f:
69
+ config = json.load(f)
70
+ logger.info(
71
+ f"Loaded configuration from project-local: {project_config}"
72
+ )
73
+ return config
74
+ except (OSError, json.JSONDecodeError) as e:
75
+ logger.warning(f"Could not load project config: {e}, using defaults")
76
+ console.print(
77
+ f"[yellow]Warning: Could not load project config: {e}[/yellow]"
78
+ )
79
+
80
+ # Default to aitrackdown with local base path
81
+ logger.info("No project-local config found, defaulting to aitrackdown adapter")
62
82
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
63
83
 
64
84
  @staticmethod
65
85
  def save_config(config: dict) -> None:
66
- """Save configuration to file."""
67
- CommonPatterns.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
68
- with open(CommonPatterns.CONFIG_FILE, "w") as f:
86
+ """Save configuration to project-local config file ONLY.
87
+
88
+ SECURITY: This method ONLY saves to the current project directory
89
+ to prevent configuration leakage across projects.
90
+ """
91
+ project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
92
+ project_config.parent.mkdir(parents=True, exist_ok=True)
93
+ with open(project_config, "w") as f:
69
94
  json.dump(config, f, indent=2)
95
+ logger.info(f"Saved configuration to project-local: {project_config}")
70
96
 
71
97
  @staticmethod
72
98
  def merge_config(updates: dict) -> dict:
@@ -89,7 +115,9 @@ class CommonPatterns:
89
115
  return config
90
116
 
91
117
  @staticmethod
92
- def get_adapter(override_adapter: Optional[str] = None, override_config: Optional[dict] = None):
118
+ def get_adapter(
119
+ override_adapter: Optional[str] = None, override_config: Optional[dict] = None
120
+ ):
93
121
  """Get configured adapter instance with environment variable support."""
94
122
  config = CommonPatterns.load_config()
95
123
 
@@ -141,7 +169,7 @@ class CommonPatterns:
141
169
  ticket_data: Dict[str, Any],
142
170
  operation: str,
143
171
  adapter_name: Optional[str] = None,
144
- show_progress: bool = True
172
+ show_progress: bool = True,
145
173
  ) -> str:
146
174
  """Queue an operation and optionally start the worker."""
147
175
  if not adapter_name:
@@ -151,14 +179,14 @@ class CommonPatterns:
151
179
  # Add to queue
152
180
  queue = Queue()
153
181
  queue_id = queue.add(
154
- ticket_data=ticket_data,
155
- adapter=adapter_name,
156
- operation=operation
182
+ ticket_data=ticket_data, adapter=adapter_name, operation=operation
157
183
  )
158
184
 
159
185
  if show_progress:
160
186
  console.print(f"[green]✓[/green] Queued {operation}: {queue_id}")
161
- console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
187
+ console.print(
188
+ "[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]"
189
+ )
162
190
 
163
191
  # Start worker if needed
164
192
  manager = WorkerManager()
@@ -202,7 +230,7 @@ class CommonPatterns:
202
230
  console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
203
231
 
204
232
  if ticket.description:
205
- console.print(f"\n[dim]Description:[/dim]")
233
+ console.print("\n[dim]Description:[/dim]")
206
234
  console.print(ticket.description)
207
235
 
208
236
  if ticket.tags:
@@ -238,32 +266,41 @@ class CommonPatterns:
238
266
  # Show worker status
239
267
  worker_status = manager.get_status()
240
268
  if worker_status["running"]:
241
- console.print(f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})")
269
+ console.print(
270
+ f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
271
+ )
242
272
  else:
243
273
  console.print("\n[red]○ Worker is not running[/red]")
244
274
  if pending > 0:
245
- console.print("[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue start'[/yellow]")
275
+ console.print(
276
+ "[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue start'[/yellow]"
277
+ )
246
278
 
247
279
 
248
280
  def async_command(f: Callable[..., T]) -> Callable[..., T]:
249
281
  """Decorator to handle async CLI commands."""
282
+
250
283
  @wraps(f)
251
284
  def wrapper(*args, **kwargs):
252
285
  return asyncio.run(f(*args, **kwargs))
286
+
253
287
  return wrapper
254
288
 
255
289
 
256
290
  def with_adapter(f: Callable) -> Callable:
257
291
  """Decorator to inject adapter instance into CLI commands."""
292
+
258
293
  @wraps(f)
259
294
  def wrapper(adapter: Optional[str] = None, *args, **kwargs):
260
295
  adapter_instance = CommonPatterns.get_adapter(override_adapter=adapter)
261
296
  return f(adapter_instance, *args, **kwargs)
297
+
262
298
  return wrapper
263
299
 
264
300
 
265
301
  def with_progress(message: str = "Processing..."):
266
302
  """Decorator to show progress spinner for long-running operations."""
303
+
267
304
  def decorator(f: Callable) -> Callable:
268
305
  @wraps(f)
269
306
  def wrapper(*args, **kwargs):
@@ -274,12 +311,15 @@ def with_progress(message: str = "Processing..."):
274
311
  ) as progress:
275
312
  progress.add_task(description=message, total=None)
276
313
  return f(*args, **kwargs)
314
+
277
315
  return wrapper
316
+
278
317
  return decorator
279
318
 
280
319
 
281
320
  def validate_required_fields(**field_map):
282
321
  """Decorator to validate required fields are provided."""
322
+
283
323
  def decorator(f: Callable) -> Callable:
284
324
  @wraps(f)
285
325
  def wrapper(*args, **kwargs):
@@ -289,16 +329,21 @@ def validate_required_fields(**field_map):
289
329
  missing_fields.append(display_name)
290
330
 
291
331
  if missing_fields:
292
- console.print(f"[red]Error:[/red] Missing required fields: {', '.join(missing_fields)}")
332
+ console.print(
333
+ f"[red]Error:[/red] Missing required fields: {', '.join(missing_fields)}"
334
+ )
293
335
  raise typer.Exit(1)
294
336
 
295
337
  return f(*args, **kwargs)
338
+
296
339
  return wrapper
340
+
297
341
  return decorator
298
342
 
299
343
 
300
344
  def handle_adapter_errors(f: Callable) -> Callable:
301
345
  """Decorator to handle common adapter errors gracefully."""
346
+
302
347
  @wraps(f)
303
348
  def wrapper(*args, **kwargs):
304
349
  try:
@@ -314,6 +359,7 @@ def handle_adapter_errors(f: Callable) -> Callable:
314
359
  except Exception as e:
315
360
  console.print(f"[red]Unexpected Error:[/red] {e}")
316
361
  raise typer.Exit(1)
362
+
317
363
  return wrapper
318
364
 
319
365
 
@@ -350,7 +396,9 @@ class ConfigValidator:
350
396
  if not config.get(field):
351
397
  env_var = ConfigValidator._get_env_var(adapter_type, field)
352
398
  if env_var and not os.getenv(env_var):
353
- issues.append(f"Missing {display_name} (config.{field} or {env_var})")
399
+ issues.append(
400
+ f"Missing {display_name} (config.{field} or {env_var})"
401
+ )
354
402
 
355
403
  return issues
356
404
 
@@ -358,8 +406,16 @@ class ConfigValidator:
358
406
  def _get_env_var(adapter_type: str, field: str) -> Optional[str]:
359
407
  """Get corresponding environment variable name for a config field."""
360
408
  env_mapping = {
361
- "github": {"token": "GITHUB_TOKEN", "owner": "GITHUB_OWNER", "repo": "GITHUB_REPO"},
362
- "jira": {"api_token": "JIRA_API_TOKEN", "email": "JIRA_EMAIL", "server": "JIRA_SERVER"},
409
+ "github": {
410
+ "token": "GITHUB_TOKEN",
411
+ "owner": "GITHUB_OWNER",
412
+ "repo": "GITHUB_REPO",
413
+ },
414
+ "jira": {
415
+ "api_token": "JIRA_API_TOKEN",
416
+ "email": "JIRA_EMAIL",
417
+ "server": "JIRA_SERVER",
418
+ },
363
419
  "linear": {"api_key": "LINEAR_API_KEY"},
364
420
  }
365
421
  return env_mapping.get(adapter_type, {}).get(field)
@@ -406,7 +462,9 @@ class CommandBuilder:
406
462
  default_adapter = config.get("default_adapter", "aitrackdown")
407
463
  adapter_config = config.get("adapters", {}).get(default_adapter, {})
408
464
 
409
- issues = ConfigValidator.validate_adapter_config(default_adapter, adapter_config)
465
+ issues = ConfigValidator.validate_adapter_config(
466
+ default_adapter, adapter_config
467
+ )
410
468
  if issues:
411
469
  console.print("[red]Configuration Issues:[/red]")
412
470
  for issue in issues:
@@ -417,6 +475,7 @@ class CommandBuilder:
417
475
 
418
476
  def create_standard_ticket_command(operation: str):
419
477
  """Create a standard ticket operation command."""
478
+
420
479
  def command_template(
421
480
  ticket_id: Optional[str] = None,
422
481
  title: Optional[str] = None,
@@ -437,9 +496,11 @@ def create_standard_ticket_command(operation: str):
437
496
  if description:
438
497
  ticket_data["description"] = description
439
498
  if priority:
440
- ticket_data["priority"] = priority.value if hasattr(priority, 'value') else priority
499
+ ticket_data["priority"] = (
500
+ priority.value if hasattr(priority, "value") else priority
501
+ )
441
502
  if state:
442
- ticket_data["state"] = state.value if hasattr(state, 'value') else state
503
+ ticket_data["state"] = state.value if hasattr(state, "value") else state
443
504
  if assignee:
444
505
  ticket_data["assignee"] = assignee
445
506
  if tags:
@@ -466,7 +527,7 @@ class TicketCommands:
466
527
  adapter_instance,
467
528
  state: Optional[TicketState] = None,
468
529
  priority: Optional[Priority] = None,
469
- limit: int = 10
530
+ limit: int = 10,
470
531
  ):
471
532
  """List tickets with filters."""
472
533
  filters = {}
@@ -482,9 +543,7 @@ class TicketCommands:
482
543
  @async_command
483
544
  @handle_adapter_errors
484
545
  async def show_ticket(
485
- adapter_instance,
486
- ticket_id: str,
487
- show_comments: bool = False
546
+ adapter_instance, ticket_id: str, show_comments: bool = False
488
547
  ):
489
548
  """Show ticket details."""
490
549
  ticket = await adapter_instance.read(ticket_id)
@@ -505,7 +564,7 @@ class TicketCommands:
505
564
  priority: Priority = Priority.MEDIUM,
506
565
  tags: Optional[List[str]] = None,
507
566
  assignee: Optional[str] = None,
508
- adapter: Optional[str] = None
567
+ adapter: Optional[str] = None,
509
568
  ) -> str:
510
569
  """Create a new ticket."""
511
570
  ticket_data = {
@@ -520,9 +579,7 @@ class TicketCommands:
520
579
 
521
580
  @staticmethod
522
581
  def update_ticket(
523
- ticket_id: str,
524
- updates: Dict[str, Any],
525
- adapter: Optional[str] = None
582
+ ticket_id: str, updates: Dict[str, Any], adapter: Optional[str] = None
526
583
  ) -> str:
527
584
  """Update a ticket."""
528
585
  if not updates:
@@ -534,14 +591,12 @@ class TicketCommands:
534
591
 
535
592
  @staticmethod
536
593
  def transition_ticket(
537
- ticket_id: str,
538
- state: TicketState,
539
- adapter: Optional[str] = None
594
+ ticket_id: str, state: TicketState, adapter: Optional[str] = None
540
595
  ) -> str:
541
596
  """Transition ticket state."""
542
597
  ticket_data = {
543
598
  "ticket_id": ticket_id,
544
- "state": state.value if hasattr(state, 'value') else state
599
+ "state": state.value if hasattr(state, "value") else state,
545
600
  }
546
601
 
547
- return CommonPatterns.queue_operation(ticket_data, "transition", adapter)
602
+ return CommonPatterns.queue_operation(ticket_data, "transition", adapter)
@@ -1,7 +1,7 @@
1
1
  """Core models and abstractions for MCP Ticketer."""
2
2
 
3
- from .models import Epic, Task, Comment, TicketState, Priority, TicketType
4
3
  from .adapter import BaseAdapter
4
+ from .models import Comment, Epic, Priority, Task, TicketState, TicketType
5
5
  from .registry import AdapterRegistry
6
6
 
7
7
  __all__ = [
@@ -13,4 +13,4 @@ __all__ = [
13
13
  "TicketType",
14
14
  "BaseAdapter",
15
15
  "AdapterRegistry",
16
- ]
16
+ ]
@@ -1,8 +1,9 @@
1
1
  """Base adapter abstract class for ticket systems."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import List, Optional, Dict, Any, TypeVar, Generic, Union
5
- from .models import Epic, Task, Comment, SearchQuery, TicketState, TicketType
4
+ from typing import Any, Dict, Generic, List, Optional, TypeVar
5
+
6
+ from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
6
7
 
7
8
  # Generic type for tickets
8
9
  T = TypeVar("T", Epic, Task)
@@ -16,6 +17,7 @@ class BaseAdapter(ABC, Generic[T]):
16
17
 
17
18
  Args:
18
19
  config: Adapter-specific configuration dictionary
20
+
19
21
  """
20
22
  self.config = config
21
23
  self._state_mapping = self._get_state_mapping()
@@ -26,6 +28,7 @@ class BaseAdapter(ABC, Generic[T]):
26
28
 
27
29
  Returns:
28
30
  Dictionary mapping TicketState to system-specific state strings
31
+
29
32
  """
30
33
  pass
31
34
 
@@ -35,6 +38,7 @@ class BaseAdapter(ABC, Generic[T]):
35
38
 
36
39
  Returns:
37
40
  (is_valid, error_message) - Tuple of validation result and error message
41
+
38
42
  """
39
43
  pass
40
44
 
@@ -47,6 +51,7 @@ class BaseAdapter(ABC, Generic[T]):
47
51
 
48
52
  Returns:
49
53
  Created ticket with ID populated
54
+
50
55
  """
51
56
  pass
52
57
 
@@ -59,6 +64,7 @@ class BaseAdapter(ABC, Generic[T]):
59
64
 
60
65
  Returns:
61
66
  Ticket if found, None otherwise
67
+
62
68
  """
63
69
  pass
64
70
 
@@ -72,6 +78,7 @@ class BaseAdapter(ABC, Generic[T]):
72
78
 
73
79
  Returns:
74
80
  Updated ticket if successful, None otherwise
81
+
75
82
  """
76
83
  pass
77
84
 
@@ -84,15 +91,13 @@ class BaseAdapter(ABC, Generic[T]):
84
91
 
85
92
  Returns:
86
93
  True if deleted, False otherwise
94
+
87
95
  """
88
96
  pass
89
97
 
90
98
  @abstractmethod
91
99
  async def list(
92
- self,
93
- limit: int = 10,
94
- offset: int = 0,
95
- filters: Optional[Dict[str, Any]] = None
100
+ self, limit: int = 10, offset: int = 0, filters: Optional[Dict[str, Any]] = None
96
101
  ) -> List[T]:
97
102
  """List tickets with pagination and filters.
98
103
 
@@ -103,6 +108,7 @@ class BaseAdapter(ABC, Generic[T]):
103
108
 
104
109
  Returns:
105
110
  List of tickets matching criteria
111
+
106
112
  """
107
113
  pass
108
114
 
@@ -115,14 +121,13 @@ class BaseAdapter(ABC, Generic[T]):
115
121
 
116
122
  Returns:
117
123
  List of tickets matching search criteria
124
+
118
125
  """
119
126
  pass
120
127
 
121
128
  @abstractmethod
122
129
  async def transition_state(
123
- self,
124
- ticket_id: str,
125
- target_state: TicketState
130
+ self, ticket_id: str, target_state: TicketState
126
131
  ) -> Optional[T]:
127
132
  """Transition ticket to a new state.
128
133
 
@@ -132,6 +137,7 @@ class BaseAdapter(ABC, Generic[T]):
132
137
 
133
138
  Returns:
134
139
  Updated ticket if transition successful, None otherwise
140
+
135
141
  """
136
142
  pass
137
143
 
@@ -144,15 +150,13 @@ class BaseAdapter(ABC, Generic[T]):
144
150
 
145
151
  Returns:
146
152
  Created comment with ID populated
153
+
147
154
  """
148
155
  pass
149
156
 
150
157
  @abstractmethod
151
158
  async def get_comments(
152
- self,
153
- ticket_id: str,
154
- limit: int = 10,
155
- offset: int = 0
159
+ self, ticket_id: str, limit: int = 10, offset: int = 0
156
160
  ) -> List[Comment]:
157
161
  """Get comments for a ticket.
158
162
 
@@ -163,6 +167,7 @@ class BaseAdapter(ABC, Generic[T]):
163
167
 
164
168
  Returns:
165
169
  List of comments for the ticket
170
+
166
171
  """
167
172
  pass
168
173
 
@@ -174,6 +179,7 @@ class BaseAdapter(ABC, Generic[T]):
174
179
 
175
180
  Returns:
176
181
  System-specific state string
182
+
177
183
  """
178
184
  return self._state_mapping.get(state, state.value)
179
185
 
@@ -185,14 +191,13 @@ class BaseAdapter(ABC, Generic[T]):
185
191
 
186
192
  Returns:
187
193
  Universal ticket state
194
+
188
195
  """
189
196
  reverse_mapping = {v: k for k, v in self._state_mapping.items()}
190
197
  return reverse_mapping.get(system_state, TicketState.OPEN)
191
198
 
192
199
  async def validate_transition(
193
- self,
194
- ticket_id: str,
195
- target_state: TicketState
200
+ self, ticket_id: str, target_state: TicketState
196
201
  ) -> bool:
197
202
  """Validate if state transition is allowed.
198
203
 
@@ -202,6 +207,7 @@ class BaseAdapter(ABC, Generic[T]):
202
207
 
203
208
  Returns:
204
209
  True if transition is valid
210
+
205
211
  """
206
212
  ticket = await self.read(ticket_id)
207
213
  if not ticket:
@@ -218,10 +224,7 @@ class BaseAdapter(ABC, Generic[T]):
218
224
  # Epic/Issue/Task Hierarchy Methods
219
225
 
220
226
  async def create_epic(
221
- self,
222
- title: str,
223
- description: Optional[str] = None,
224
- **kwargs
227
+ self, title: str, description: Optional[str] = None, **kwargs
225
228
  ) -> Optional[Epic]:
226
229
  """Create epic (top-level grouping).
227
230
 
@@ -232,12 +235,13 @@ class BaseAdapter(ABC, Generic[T]):
232
235
 
233
236
  Returns:
234
237
  Created epic or None if failed
238
+
235
239
  """
236
240
  epic = Epic(
237
241
  title=title,
238
242
  description=description,
239
243
  ticket_type=TicketType.EPIC,
240
- **{k: v for k, v in kwargs.items() if k in Epic.__fields__}
244
+ **{k: v for k, v in kwargs.items() if k in Epic.__fields__},
241
245
  )
242
246
  result = await self.create(epic)
243
247
  if isinstance(result, Epic):
@@ -252,6 +256,7 @@ class BaseAdapter(ABC, Generic[T]):
252
256
 
253
257
  Returns:
254
258
  Epic if found, None otherwise
259
+
255
260
  """
256
261
  # Default implementation - subclasses should override for platform-specific logic
257
262
  result = await self.read(epic_id)
@@ -267,6 +272,7 @@ class BaseAdapter(ABC, Generic[T]):
267
272
 
268
273
  Returns:
269
274
  List of epics
275
+
270
276
  """
271
277
  # Default implementation - subclasses should override
272
278
  filters = kwargs.copy()
@@ -279,7 +285,7 @@ class BaseAdapter(ABC, Generic[T]):
279
285
  title: str,
280
286
  description: Optional[str] = None,
281
287
  epic_id: Optional[str] = None,
282
- **kwargs
288
+ **kwargs,
283
289
  ) -> Optional[Task]:
284
290
  """Create issue, optionally linked to epic.
285
291
 
@@ -291,13 +297,14 @@ class BaseAdapter(ABC, Generic[T]):
291
297
 
292
298
  Returns:
293
299
  Created issue or None if failed
300
+
294
301
  """
295
302
  task = Task(
296
303
  title=title,
297
304
  description=description,
298
305
  ticket_type=TicketType.ISSUE,
299
306
  parent_epic=epic_id,
300
- **{k: v for k, v in kwargs.items() if k in Task.__fields__}
307
+ **{k: v for k, v in kwargs.items() if k in Task.__fields__},
301
308
  )
302
309
  return await self.create(task)
303
310
 
@@ -309,6 +316,7 @@ class BaseAdapter(ABC, Generic[T]):
309
316
 
310
317
  Returns:
311
318
  List of issues belonging to epic
319
+
312
320
  """
313
321
  # Default implementation - subclasses should override for efficiency
314
322
  filters = {"parent_epic": epic_id, "ticket_type": TicketType.ISSUE}
@@ -316,11 +324,7 @@ class BaseAdapter(ABC, Generic[T]):
316
324
  return [r for r in results if isinstance(r, Task) and r.is_issue()]
317
325
 
318
326
  async def create_task(
319
- self,
320
- title: str,
321
- parent_id: str,
322
- description: Optional[str] = None,
323
- **kwargs
327
+ self, title: str, parent_id: str, description: Optional[str] = None, **kwargs
324
328
  ) -> Optional[Task]:
325
329
  """Create task as sub-ticket of parent issue.
326
330
 
@@ -335,6 +339,7 @@ class BaseAdapter(ABC, Generic[T]):
335
339
 
336
340
  Raises:
337
341
  ValueError: If parent_id is not provided
342
+
338
343
  """
339
344
  if not parent_id:
340
345
  raise ValueError("Tasks must have a parent_id (issue)")
@@ -344,7 +349,7 @@ class BaseAdapter(ABC, Generic[T]):
344
349
  description=description,
345
350
  ticket_type=TicketType.TASK,
346
351
  parent_issue=parent_id,
347
- **{k: v for k, v in kwargs.items() if k in Task.__fields__}
352
+ **{k: v for k, v in kwargs.items() if k in Task.__fields__},
348
353
  )
349
354
 
350
355
  # Validate hierarchy before creating
@@ -362,6 +367,7 @@ class BaseAdapter(ABC, Generic[T]):
362
367
 
363
368
  Returns:
364
369
  List of tasks belonging to issue
370
+
365
371
  """
366
372
  # Default implementation - subclasses should override for efficiency
367
373
  filters = {"parent_issue": issue_id, "ticket_type": TicketType.TASK}
@@ -370,4 +376,4 @@ class BaseAdapter(ABC, Generic[T]):
370
376
 
371
377
  async def close(self) -> None:
372
378
  """Close adapter and cleanup resources."""
373
- pass
379
+ pass