mcp-ticketer 0.1.12__py3-none-any.whl → 0.1.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/linear.py +427 -3
- mcp_ticketer/cli/discover.py +402 -0
- mcp_ticketer/cli/main.py +4 -0
- mcp_ticketer/core/__init__.py +2 -1
- mcp_ticketer/core/adapter.py +155 -2
- mcp_ticketer/core/env_discovery.py +555 -0
- mcp_ticketer/core/models.py +58 -6
- mcp_ticketer/core/project_config.py +62 -9
- {mcp_ticketer-0.1.12.dist-info → mcp_ticketer-0.1.13.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.12.dist-info → mcp_ticketer-0.1.13.dist-info}/RECORD +15 -13
- {mcp_ticketer-0.1.12.dist-info → mcp_ticketer-0.1.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.12.dist-info → mcp_ticketer-0.1.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.12.dist-info → mcp_ticketer-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.12.dist-info → mcp_ticketer-0.1.13.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""CLI command for auto-discovering configuration from .env files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich import print as rprint
|
|
12
|
+
|
|
13
|
+
from ..core.env_discovery import EnvDiscovery, DiscoveredAdapter
|
|
14
|
+
from ..core.project_config import (
|
|
15
|
+
ConfigResolver,
|
|
16
|
+
TicketerConfig,
|
|
17
|
+
AdapterConfig,
|
|
18
|
+
ConfigValidator,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
app = typer.Typer(help="Auto-discover configuration from .env files")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _mask_sensitive(value: str, key: str) -> str:
|
|
26
|
+
"""Mask sensitive values for display.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
value: Value to potentially mask
|
|
30
|
+
key: Key name to determine if masking needed
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Masked or original value
|
|
34
|
+
"""
|
|
35
|
+
sensitive_keys = ["token", "key", "password", "secret", "api_token"]
|
|
36
|
+
|
|
37
|
+
# Check if key contains any sensitive pattern
|
|
38
|
+
key_lower = key.lower()
|
|
39
|
+
is_sensitive = any(pattern in key_lower for pattern in sensitive_keys)
|
|
40
|
+
|
|
41
|
+
# Don't mask team_id, team_key, project_key, etc.
|
|
42
|
+
if "team" in key_lower or "project" in key_lower:
|
|
43
|
+
is_sensitive = False
|
|
44
|
+
|
|
45
|
+
if is_sensitive and value:
|
|
46
|
+
# Show first 4 and last 4 characters
|
|
47
|
+
if len(value) > 12:
|
|
48
|
+
return f"{value[:4]}...{value[-4:]}"
|
|
49
|
+
else:
|
|
50
|
+
return "***"
|
|
51
|
+
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _display_discovered_adapter(adapter: DiscoveredAdapter, discovery: EnvDiscovery) -> None:
|
|
56
|
+
"""Display information about a discovered adapter.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
adapter: Discovered adapter to display
|
|
60
|
+
discovery: EnvDiscovery instance for validation
|
|
61
|
+
"""
|
|
62
|
+
# Header
|
|
63
|
+
completeness = "✅ Complete" if adapter.is_complete() else "⚠️ Incomplete"
|
|
64
|
+
confidence_percent = int(adapter.confidence * 100)
|
|
65
|
+
|
|
66
|
+
console.print(
|
|
67
|
+
f"\n[bold cyan]{adapter.adapter_type.upper()}[/bold cyan] "
|
|
68
|
+
f"({completeness}, {confidence_percent}% confidence)"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Configuration details
|
|
72
|
+
console.print(f" [dim]Found in: {adapter.found_in}[/dim]")
|
|
73
|
+
|
|
74
|
+
for key, value in adapter.config.items():
|
|
75
|
+
if key == "adapter":
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
display_value = _mask_sensitive(str(value), key)
|
|
79
|
+
console.print(f" {key}: [green]{display_value}[/green]")
|
|
80
|
+
|
|
81
|
+
# Missing fields
|
|
82
|
+
if adapter.missing_fields:
|
|
83
|
+
console.print(f" [yellow]Missing:[/yellow] {', '.join(adapter.missing_fields)}")
|
|
84
|
+
|
|
85
|
+
# Validation warnings
|
|
86
|
+
warnings = discovery.validate_discovered_config(adapter)
|
|
87
|
+
if warnings:
|
|
88
|
+
for warning in warnings:
|
|
89
|
+
console.print(f" {warning}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.command()
|
|
93
|
+
def show(
|
|
94
|
+
project_path: Optional[Path] = typer.Option(
|
|
95
|
+
None,
|
|
96
|
+
"--path",
|
|
97
|
+
"-p",
|
|
98
|
+
help="Project path to scan (defaults to current directory)"
|
|
99
|
+
),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Show discovered configuration without saving."""
|
|
102
|
+
proj_path = project_path or Path.cwd()
|
|
103
|
+
|
|
104
|
+
console.print(f"\n[bold]🔍 Auto-discovering configuration in:[/bold] {proj_path}\n")
|
|
105
|
+
|
|
106
|
+
# Discover
|
|
107
|
+
discovery = EnvDiscovery(proj_path)
|
|
108
|
+
result = discovery.discover()
|
|
109
|
+
|
|
110
|
+
# Show env files found
|
|
111
|
+
if result.env_files_found:
|
|
112
|
+
console.print("[bold]Environment files found:[/bold]")
|
|
113
|
+
for env_file in result.env_files_found:
|
|
114
|
+
console.print(f" ✅ {env_file}")
|
|
115
|
+
else:
|
|
116
|
+
console.print("[yellow]No .env files found[/yellow]")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Show discovered adapters
|
|
120
|
+
if result.adapters:
|
|
121
|
+
console.print("\n[bold]Detected adapter configurations:[/bold]")
|
|
122
|
+
for adapter in sorted(result.adapters, key=lambda a: a.confidence, reverse=True):
|
|
123
|
+
_display_discovered_adapter(adapter, discovery)
|
|
124
|
+
|
|
125
|
+
# Show recommended adapter
|
|
126
|
+
primary = result.get_primary_adapter()
|
|
127
|
+
if primary:
|
|
128
|
+
console.print(
|
|
129
|
+
f"\n[bold green]Recommended adapter:[/bold green] {primary.adapter_type} "
|
|
130
|
+
f"(most complete configuration)"
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
console.print("\n[yellow]No adapter configurations detected[/yellow]")
|
|
134
|
+
console.print("[dim]Make sure your .env file contains adapter credentials[/dim]")
|
|
135
|
+
|
|
136
|
+
# Show warnings
|
|
137
|
+
if result.warnings:
|
|
138
|
+
console.print("\n[bold yellow]Warnings:[/bold yellow]")
|
|
139
|
+
for warning in result.warnings:
|
|
140
|
+
console.print(f" {warning}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
def save(
|
|
145
|
+
adapter: Optional[str] = typer.Option(
|
|
146
|
+
None,
|
|
147
|
+
"--adapter",
|
|
148
|
+
"-a",
|
|
149
|
+
help="Which adapter to save (defaults to recommended)"
|
|
150
|
+
),
|
|
151
|
+
global_config: bool = typer.Option(
|
|
152
|
+
False,
|
|
153
|
+
"--global",
|
|
154
|
+
"-g",
|
|
155
|
+
help="Save to global config instead of project config"
|
|
156
|
+
),
|
|
157
|
+
dry_run: bool = typer.Option(
|
|
158
|
+
False,
|
|
159
|
+
"--dry-run",
|
|
160
|
+
help="Show what would be saved without saving"
|
|
161
|
+
),
|
|
162
|
+
project_path: Optional[Path] = typer.Option(
|
|
163
|
+
None,
|
|
164
|
+
"--path",
|
|
165
|
+
"-p",
|
|
166
|
+
help="Project path to scan (defaults to current directory)"
|
|
167
|
+
),
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Discover configuration and save to config file.
|
|
170
|
+
|
|
171
|
+
By default, saves to project-specific config (.mcp-ticketer/config.json).
|
|
172
|
+
Use --global to save to global config (~/.mcp-ticketer/config.json).
|
|
173
|
+
"""
|
|
174
|
+
proj_path = project_path or Path.cwd()
|
|
175
|
+
|
|
176
|
+
console.print(f"\n[bold]🔍 Auto-discovering configuration in:[/bold] {proj_path}\n")
|
|
177
|
+
|
|
178
|
+
# Discover
|
|
179
|
+
discovery = EnvDiscovery(proj_path)
|
|
180
|
+
result = discovery.discover()
|
|
181
|
+
|
|
182
|
+
if not result.adapters:
|
|
183
|
+
console.print("[red]No adapter configurations detected[/red]")
|
|
184
|
+
console.print("[dim]Make sure your .env file contains adapter credentials[/dim]")
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
|
|
187
|
+
# Determine which adapter to save
|
|
188
|
+
if adapter:
|
|
189
|
+
discovered_adapter = result.get_adapter_by_type(adapter)
|
|
190
|
+
if not discovered_adapter:
|
|
191
|
+
console.print(f"[red]No configuration found for adapter: {adapter}[/red]")
|
|
192
|
+
console.print(f"[dim]Available: {', '.join(a.adapter_type for a in result.adapters)}[/dim]")
|
|
193
|
+
raise typer.Exit(1)
|
|
194
|
+
else:
|
|
195
|
+
# Use recommended adapter
|
|
196
|
+
discovered_adapter = result.get_primary_adapter()
|
|
197
|
+
if not discovered_adapter:
|
|
198
|
+
console.print("[red]Could not determine recommended adapter[/red]")
|
|
199
|
+
raise typer.Exit(1)
|
|
200
|
+
|
|
201
|
+
console.print(
|
|
202
|
+
f"[bold]Using recommended adapter:[/bold] {discovered_adapter.adapter_type}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Display what will be saved
|
|
206
|
+
_display_discovered_adapter(discovered_adapter, discovery)
|
|
207
|
+
|
|
208
|
+
# Validate configuration
|
|
209
|
+
is_valid, error_msg = ConfigValidator.validate(
|
|
210
|
+
discovered_adapter.adapter_type,
|
|
211
|
+
discovered_adapter.config
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if not is_valid:
|
|
215
|
+
console.print(f"\n[red]Configuration validation failed:[/red] {error_msg}")
|
|
216
|
+
console.print("[dim]Fix the configuration in your .env file and try again[/dim]")
|
|
217
|
+
raise typer.Exit(1)
|
|
218
|
+
|
|
219
|
+
if dry_run:
|
|
220
|
+
console.print("\n[yellow]Dry run - no changes made[/yellow]")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
# Load or create config
|
|
224
|
+
resolver = ConfigResolver(proj_path)
|
|
225
|
+
|
|
226
|
+
if global_config:
|
|
227
|
+
config = resolver.load_global_config()
|
|
228
|
+
else:
|
|
229
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
230
|
+
|
|
231
|
+
# Set default adapter
|
|
232
|
+
config.default_adapter = discovered_adapter.adapter_type
|
|
233
|
+
|
|
234
|
+
# Create adapter config
|
|
235
|
+
adapter_config = AdapterConfig.from_dict(discovered_adapter.config)
|
|
236
|
+
|
|
237
|
+
# Add to config
|
|
238
|
+
config.adapters[discovered_adapter.adapter_type] = adapter_config
|
|
239
|
+
|
|
240
|
+
# Save
|
|
241
|
+
try:
|
|
242
|
+
if global_config:
|
|
243
|
+
resolver.save_global_config(config)
|
|
244
|
+
config_location = resolver.GLOBAL_CONFIG_PATH
|
|
245
|
+
else:
|
|
246
|
+
resolver.save_project_config(config, proj_path)
|
|
247
|
+
config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
248
|
+
|
|
249
|
+
console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
|
|
250
|
+
console.print(f"[green]✅ Default adapter set to:[/green] {discovered_adapter.adapter_type}")
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
console.print(f"\n[red]Failed to save configuration:[/red] {e}")
|
|
254
|
+
raise typer.Exit(1)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@app.command()
|
|
258
|
+
def interactive(
|
|
259
|
+
project_path: Optional[Path] = typer.Option(
|
|
260
|
+
None,
|
|
261
|
+
"--path",
|
|
262
|
+
"-p",
|
|
263
|
+
help="Project path to scan (defaults to current directory)"
|
|
264
|
+
),
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Interactive mode for discovering and saving configuration."""
|
|
267
|
+
proj_path = project_path or Path.cwd()
|
|
268
|
+
|
|
269
|
+
console.print(f"\n[bold]🔍 Auto-discovering configuration in:[/bold] {proj_path}\n")
|
|
270
|
+
|
|
271
|
+
# Discover
|
|
272
|
+
discovery = EnvDiscovery(proj_path)
|
|
273
|
+
result = discovery.discover()
|
|
274
|
+
|
|
275
|
+
# Show env files
|
|
276
|
+
if result.env_files_found:
|
|
277
|
+
console.print("[bold]Environment files found:[/bold]")
|
|
278
|
+
for env_file in result.env_files_found:
|
|
279
|
+
console.print(f" ✅ {env_file}")
|
|
280
|
+
else:
|
|
281
|
+
console.print("[red]No .env files found[/red]")
|
|
282
|
+
raise typer.Exit(1)
|
|
283
|
+
|
|
284
|
+
# Show discovered adapters
|
|
285
|
+
if not result.adapters:
|
|
286
|
+
console.print("\n[red]No adapter configurations detected[/red]")
|
|
287
|
+
console.print("[dim]Make sure your .env file contains adapter credentials[/dim]")
|
|
288
|
+
raise typer.Exit(1)
|
|
289
|
+
|
|
290
|
+
console.print("\n[bold]Detected adapter configurations:[/bold]")
|
|
291
|
+
for i, adapter in enumerate(result.adapters, 1):
|
|
292
|
+
completeness = "✅" if adapter.is_complete() else "⚠️ "
|
|
293
|
+
console.print(
|
|
294
|
+
f" {i}. {completeness} [cyan]{adapter.adapter_type}[/cyan] "
|
|
295
|
+
f"({int(adapter.confidence * 100)}% confidence)"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Show warnings
|
|
299
|
+
if result.warnings:
|
|
300
|
+
console.print("\n[bold yellow]Warnings:[/bold yellow]")
|
|
301
|
+
for warning in result.warnings:
|
|
302
|
+
console.print(f" {warning}")
|
|
303
|
+
|
|
304
|
+
# Ask user which adapter to save
|
|
305
|
+
primary = result.get_primary_adapter()
|
|
306
|
+
console.print(
|
|
307
|
+
f"\n[bold]Recommended:[/bold] {primary.adapter_type if primary else 'None'}"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Prompt for selection
|
|
311
|
+
console.print("\n[bold]Select an option:[/bold]")
|
|
312
|
+
console.print(" 1. Save recommended adapter to project config")
|
|
313
|
+
console.print(" 2. Save recommended adapter to global config")
|
|
314
|
+
console.print(" 3. Choose different adapter")
|
|
315
|
+
console.print(" 4. Save all adapters")
|
|
316
|
+
console.print(" 5. Cancel")
|
|
317
|
+
|
|
318
|
+
choice = typer.prompt("Enter choice", type=int, default=1)
|
|
319
|
+
|
|
320
|
+
if choice == 5:
|
|
321
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# Determine adapters to save
|
|
325
|
+
if choice in [1, 2]:
|
|
326
|
+
if not primary:
|
|
327
|
+
console.print("[red]No recommended adapter found[/red]")
|
|
328
|
+
raise typer.Exit(1)
|
|
329
|
+
adapters_to_save = [primary]
|
|
330
|
+
default_adapter = primary.adapter_type
|
|
331
|
+
elif choice == 3:
|
|
332
|
+
# Let user choose
|
|
333
|
+
console.print("\n[bold]Available adapters:[/bold]")
|
|
334
|
+
for i, adapter in enumerate(result.adapters, 1):
|
|
335
|
+
console.print(f" {i}. {adapter.adapter_type}")
|
|
336
|
+
|
|
337
|
+
adapter_choice = typer.prompt("Select adapter", type=int, default=1)
|
|
338
|
+
if 1 <= adapter_choice <= len(result.adapters):
|
|
339
|
+
selected = result.adapters[adapter_choice - 1]
|
|
340
|
+
adapters_to_save = [selected]
|
|
341
|
+
default_adapter = selected.adapter_type
|
|
342
|
+
else:
|
|
343
|
+
console.print("[red]Invalid choice[/red]")
|
|
344
|
+
raise typer.Exit(1)
|
|
345
|
+
else: # choice == 4
|
|
346
|
+
adapters_to_save = result.adapters
|
|
347
|
+
default_adapter = primary.adapter_type if primary else result.adapters[0].adapter_type
|
|
348
|
+
|
|
349
|
+
# Determine save location
|
|
350
|
+
save_global = choice == 2
|
|
351
|
+
|
|
352
|
+
# Load or create config
|
|
353
|
+
resolver = ConfigResolver(proj_path)
|
|
354
|
+
|
|
355
|
+
if save_global:
|
|
356
|
+
config = resolver.load_global_config()
|
|
357
|
+
else:
|
|
358
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
359
|
+
|
|
360
|
+
# Set default adapter
|
|
361
|
+
config.default_adapter = default_adapter
|
|
362
|
+
|
|
363
|
+
# Add adapters
|
|
364
|
+
for discovered_adapter in adapters_to_save:
|
|
365
|
+
# Validate
|
|
366
|
+
is_valid, error_msg = ConfigValidator.validate(
|
|
367
|
+
discovered_adapter.adapter_type,
|
|
368
|
+
discovered_adapter.config
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if not is_valid:
|
|
372
|
+
console.print(
|
|
373
|
+
f"\n[yellow]Warning:[/yellow] {discovered_adapter.adapter_type} "
|
|
374
|
+
f"validation failed: {error_msg}"
|
|
375
|
+
)
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
# Create adapter config
|
|
379
|
+
adapter_config = AdapterConfig.from_dict(discovered_adapter.config)
|
|
380
|
+
config.adapters[discovered_adapter.adapter_type] = adapter_config
|
|
381
|
+
|
|
382
|
+
console.print(f" ✅ Added {discovered_adapter.adapter_type}")
|
|
383
|
+
|
|
384
|
+
# Save
|
|
385
|
+
try:
|
|
386
|
+
if save_global:
|
|
387
|
+
resolver.save_global_config(config)
|
|
388
|
+
config_location = resolver.GLOBAL_CONFIG_PATH
|
|
389
|
+
else:
|
|
390
|
+
resolver.save_project_config(config, proj_path)
|
|
391
|
+
config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
392
|
+
|
|
393
|
+
console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
|
|
394
|
+
console.print(f"[green]✅ Default adapter:[/green] {config.default_adapter}")
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
console.print(f"\n[red]Failed to save configuration:[/red] {e}")
|
|
398
|
+
raise typer.Exit(1)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == "__main__":
|
|
402
|
+
app()
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -21,6 +21,7 @@ from .queue_commands import app as queue_app
|
|
|
21
21
|
from ..__version__ import __version__
|
|
22
22
|
from .configure import configure_wizard, show_current_config, set_adapter_config
|
|
23
23
|
from .migrate_config import migrate_config_command
|
|
24
|
+
from .discover import app as discover_app
|
|
24
25
|
|
|
25
26
|
# Load environment variables
|
|
26
27
|
load_dotenv()
|
|
@@ -865,6 +866,9 @@ def search(
|
|
|
865
866
|
# Add queue command to main app
|
|
866
867
|
app.add_typer(queue_app, name="queue")
|
|
867
868
|
|
|
869
|
+
# Add discover command to main app
|
|
870
|
+
app.add_typer(discover_app, name="discover")
|
|
871
|
+
|
|
868
872
|
|
|
869
873
|
@app.command()
|
|
870
874
|
def check(
|
mcp_ticketer/core/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Core models and abstractions for MCP Ticketer."""
|
|
2
2
|
|
|
3
|
-
from .models import Epic, Task, Comment, TicketState, Priority
|
|
3
|
+
from .models import Epic, Task, Comment, TicketState, Priority, TicketType
|
|
4
4
|
from .adapter import BaseAdapter
|
|
5
5
|
from .registry import AdapterRegistry
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@ __all__ = [
|
|
|
10
10
|
"Comment",
|
|
11
11
|
"TicketState",
|
|
12
12
|
"Priority",
|
|
13
|
+
"TicketType",
|
|
13
14
|
"BaseAdapter",
|
|
14
15
|
"AdapterRegistry",
|
|
15
16
|
]
|
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
|
5
|
-
from .models import Epic, Task, Comment, SearchQuery, TicketState
|
|
4
|
+
from typing import List, Optional, Dict, Any, TypeVar, Generic, Union
|
|
5
|
+
from .models import Epic, Task, Comment, SearchQuery, TicketState, TicketType
|
|
6
6
|
|
|
7
7
|
# Generic type for tickets
|
|
8
8
|
T = TypeVar("T", Epic, Task)
|
|
@@ -206,6 +206,159 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
206
206
|
return False
|
|
207
207
|
return current_state.can_transition_to(target_state)
|
|
208
208
|
|
|
209
|
+
# Epic/Issue/Task Hierarchy Methods
|
|
210
|
+
|
|
211
|
+
async def create_epic(
|
|
212
|
+
self,
|
|
213
|
+
title: str,
|
|
214
|
+
description: Optional[str] = None,
|
|
215
|
+
**kwargs
|
|
216
|
+
) -> Optional[Epic]:
|
|
217
|
+
"""Create epic (top-level grouping).
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
title: Epic title
|
|
221
|
+
description: Epic description
|
|
222
|
+
**kwargs: Additional adapter-specific fields
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Created epic or None if failed
|
|
226
|
+
"""
|
|
227
|
+
epic = Epic(
|
|
228
|
+
title=title,
|
|
229
|
+
description=description,
|
|
230
|
+
ticket_type=TicketType.EPIC,
|
|
231
|
+
**{k: v for k, v in kwargs.items() if k in Epic.__fields__}
|
|
232
|
+
)
|
|
233
|
+
result = await self.create(epic)
|
|
234
|
+
if isinstance(result, Epic):
|
|
235
|
+
return result
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
async def get_epic(self, epic_id: str) -> Optional[Epic]:
|
|
239
|
+
"""Get epic by ID.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
epic_id: Epic identifier
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Epic if found, None otherwise
|
|
246
|
+
"""
|
|
247
|
+
# Default implementation - subclasses should override for platform-specific logic
|
|
248
|
+
result = await self.read(epic_id)
|
|
249
|
+
if isinstance(result, Epic):
|
|
250
|
+
return result
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
async def list_epics(self, **kwargs) -> List[Epic]:
|
|
254
|
+
"""List all epics.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
**kwargs: Adapter-specific filter parameters
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of epics
|
|
261
|
+
"""
|
|
262
|
+
# Default implementation - subclasses should override
|
|
263
|
+
filters = kwargs.copy()
|
|
264
|
+
filters["ticket_type"] = TicketType.EPIC
|
|
265
|
+
results = await self.list(filters=filters)
|
|
266
|
+
return [r for r in results if isinstance(r, Epic)]
|
|
267
|
+
|
|
268
|
+
async def create_issue(
|
|
269
|
+
self,
|
|
270
|
+
title: str,
|
|
271
|
+
description: Optional[str] = None,
|
|
272
|
+
epic_id: Optional[str] = None,
|
|
273
|
+
**kwargs
|
|
274
|
+
) -> Optional[Task]:
|
|
275
|
+
"""Create issue, optionally linked to epic.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
title: Issue title
|
|
279
|
+
description: Issue description
|
|
280
|
+
epic_id: Optional parent epic ID
|
|
281
|
+
**kwargs: Additional adapter-specific fields
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Created issue or None if failed
|
|
285
|
+
"""
|
|
286
|
+
task = Task(
|
|
287
|
+
title=title,
|
|
288
|
+
description=description,
|
|
289
|
+
ticket_type=TicketType.ISSUE,
|
|
290
|
+
parent_epic=epic_id,
|
|
291
|
+
**{k: v for k, v in kwargs.items() if k in Task.__fields__}
|
|
292
|
+
)
|
|
293
|
+
return await self.create(task)
|
|
294
|
+
|
|
295
|
+
async def list_issues_by_epic(self, epic_id: str) -> List[Task]:
|
|
296
|
+
"""List all issues in epic.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
epic_id: Epic identifier
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
List of issues belonging to epic
|
|
303
|
+
"""
|
|
304
|
+
# Default implementation - subclasses should override for efficiency
|
|
305
|
+
filters = {"parent_epic": epic_id, "ticket_type": TicketType.ISSUE}
|
|
306
|
+
results = await self.list(filters=filters)
|
|
307
|
+
return [r for r in results if isinstance(r, Task) and r.is_issue()]
|
|
308
|
+
|
|
309
|
+
async def create_task(
|
|
310
|
+
self,
|
|
311
|
+
title: str,
|
|
312
|
+
parent_id: str,
|
|
313
|
+
description: Optional[str] = None,
|
|
314
|
+
**kwargs
|
|
315
|
+
) -> Optional[Task]:
|
|
316
|
+
"""Create task as sub-ticket of parent issue.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
title: Task title
|
|
320
|
+
parent_id: Required parent issue ID
|
|
321
|
+
description: Task description
|
|
322
|
+
**kwargs: Additional adapter-specific fields
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Created task or None if failed
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
ValueError: If parent_id is not provided
|
|
329
|
+
"""
|
|
330
|
+
if not parent_id:
|
|
331
|
+
raise ValueError("Tasks must have a parent_id (issue)")
|
|
332
|
+
|
|
333
|
+
task = Task(
|
|
334
|
+
title=title,
|
|
335
|
+
description=description,
|
|
336
|
+
ticket_type=TicketType.TASK,
|
|
337
|
+
parent_issue=parent_id,
|
|
338
|
+
**{k: v for k, v in kwargs.items() if k in Task.__fields__}
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Validate hierarchy before creating
|
|
342
|
+
errors = task.validate_hierarchy()
|
|
343
|
+
if errors:
|
|
344
|
+
raise ValueError(f"Invalid task hierarchy: {'; '.join(errors)}")
|
|
345
|
+
|
|
346
|
+
return await self.create(task)
|
|
347
|
+
|
|
348
|
+
async def list_tasks_by_issue(self, issue_id: str) -> List[Task]:
|
|
349
|
+
"""List all tasks under an issue.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
issue_id: Issue identifier
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
List of tasks belonging to issue
|
|
356
|
+
"""
|
|
357
|
+
# Default implementation - subclasses should override for efficiency
|
|
358
|
+
filters = {"parent_issue": issue_id, "ticket_type": TicketType.TASK}
|
|
359
|
+
results = await self.list(filters=filters)
|
|
360
|
+
return [r for r in results if isinstance(r, Task) and r.is_task()]
|
|
361
|
+
|
|
209
362
|
async def close(self) -> None:
|
|
210
363
|
"""Close adapter and cleanup resources."""
|
|
211
364
|
pass
|