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.
- mcp_ticketer/__init__.py +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +54 -38
- mcp_ticketer/adapters/github.py +175 -109
- mcp_ticketer/adapters/hybrid.py +90 -45
- mcp_ticketer/adapters/jira.py +139 -130
- mcp_ticketer/adapters/linear.py +374 -225
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +14 -15
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +250 -293
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +10 -12
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +115 -60
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +36 -30
- mcp_ticketer/core/config.py +113 -77
- mcp_ticketer/core/env_discovery.py +51 -19
- mcp_ticketer/core/http_client.py +46 -29
- mcp_ticketer/core/mappers.py +79 -35
- mcp_ticketer/core/models.py +29 -15
- mcp_ticketer/core/project_config.py +131 -66
- mcp_ticketer/core/registry.py +12 -12
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +183 -129
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +29 -25
- mcp_ticketer/queue/queue.py +144 -82
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +48 -33
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.22.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.20.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.20.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
|
|
17
|
-
from ..
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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": {
|
|
362
|
-
|
|
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(
|
|
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"] =
|
|
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,
|
|
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,
|
|
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)
|
mcp_ticketer/core/__init__.py
CHANGED
|
@@ -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
|
+
]
|
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -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
|
|
5
|
-
|
|
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
|