mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.0__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 +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +263 -14
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +326 -109
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +271 -25
- mcp_ticketer/adapters/linear/adapter.py +693 -39
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/adapters/linear/queries.py +9 -7
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +104 -15
- mcp_ticketer/cli/codex_configure.py +188 -32
- mcp_ticketer/cli/configure.py +37 -48
- mcp_ticketer/cli/diagnostics.py +20 -18
- mcp_ticketer/cli/discover.py +292 -26
- mcp_ticketer/cli/gemini_configure.py +107 -26
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +105 -22
- mcp_ticketer/cli/main.py +1830 -435
- mcp_ticketer/cli/mcp_configure.py +296 -89
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +773 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +67 -62
- mcp_ticketer/core/__init__.py +14 -1
- mcp_ticketer/core/adapter.py +84 -15
- mcp_ticketer/core/config.py +44 -39
- mcp_ticketer/core/env_discovery.py +42 -12
- mcp_ticketer/core/env_loader.py +15 -14
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +11 -11
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +57 -35
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
- mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
- mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
- mcp_ticketer/mcp/server/server_sdk.py +93 -0
- mcp_ticketer/mcp/server/tools/__init__.py +47 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +5 -4
- mcp_ticketer/queue/manager.py +15 -51
- mcp_ticketer/queue/queue.py +19 -19
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +14 -14
- mcp_ticketer/queue/worker.py +16 -14
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
- /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Update checker for mcp-ticketer package.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to check PyPI for new versions and notify users.
|
|
4
|
+
Uses the existing HTTP client infrastructure to avoid code duplication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Try to import packaging, fall back to simple string comparison if unavailable
|
|
15
|
+
try:
|
|
16
|
+
from packaging.version import Version
|
|
17
|
+
|
|
18
|
+
HAS_PACKAGING = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
HAS_PACKAGING = False
|
|
21
|
+
|
|
22
|
+
class Version:
|
|
23
|
+
"""Fallback version comparison using simple string sorting.
|
|
24
|
+
|
|
25
|
+
This is a minimal fallback when packaging is not available.
|
|
26
|
+
Works correctly for most semantic versions (X.Y.Z format).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, version_string: str):
|
|
30
|
+
"""Initialize with version string."""
|
|
31
|
+
self.version_string = version_string
|
|
32
|
+
# Parse into tuple of integers for proper comparison
|
|
33
|
+
try:
|
|
34
|
+
parts = version_string.split(".")
|
|
35
|
+
# Handle pre-release versions by splitting on non-digit chars
|
|
36
|
+
self.parts = []
|
|
37
|
+
for part in parts:
|
|
38
|
+
# Extract leading digits
|
|
39
|
+
digits = ""
|
|
40
|
+
for char in part:
|
|
41
|
+
if char.isdigit():
|
|
42
|
+
digits += char
|
|
43
|
+
else:
|
|
44
|
+
break
|
|
45
|
+
if digits:
|
|
46
|
+
self.parts.append(int(digits))
|
|
47
|
+
except (ValueError, AttributeError):
|
|
48
|
+
# Fallback to string comparison if parsing fails
|
|
49
|
+
self.parts = None
|
|
50
|
+
|
|
51
|
+
def __gt__(self, other: "Version") -> bool:
|
|
52
|
+
"""Compare versions."""
|
|
53
|
+
if self.parts is not None and other.parts is not None:
|
|
54
|
+
# Proper numeric comparison
|
|
55
|
+
return self.parts > other.parts
|
|
56
|
+
# Fallback to string comparison
|
|
57
|
+
return self.version_string > other.version_string
|
|
58
|
+
|
|
59
|
+
def __eq__(self, other: object) -> bool:
|
|
60
|
+
"""Check equality."""
|
|
61
|
+
if not isinstance(other, Version):
|
|
62
|
+
return False
|
|
63
|
+
if self.parts is not None and other.parts is not None:
|
|
64
|
+
return self.parts == other.parts
|
|
65
|
+
return self.version_string == other.version_string
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
from ..__version__ import __version__
|
|
69
|
+
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
72
|
+
# Cache configuration
|
|
73
|
+
CACHE_DIR = Path.home() / ".mcp-ticketer"
|
|
74
|
+
CACHE_FILE = CACHE_DIR / "update_check_cache.json"
|
|
75
|
+
CACHE_DURATION_HOURS = 24
|
|
76
|
+
|
|
77
|
+
# PyPI API configuration
|
|
78
|
+
PYPI_API_URL = "https://pypi.org/pypi/mcp-ticketer/json"
|
|
79
|
+
PYPI_PROJECT_URL = "https://pypi.org/project/mcp-ticketer/"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class UpdateInfo:
|
|
83
|
+
"""Container for update information."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
current_version: str,
|
|
88
|
+
latest_version: str,
|
|
89
|
+
needs_update: bool,
|
|
90
|
+
pypi_url: str,
|
|
91
|
+
release_date: str | None = None,
|
|
92
|
+
checked_at: str | None = None,
|
|
93
|
+
):
|
|
94
|
+
"""Initialize update information.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
current_version: Currently installed version
|
|
98
|
+
latest_version: Latest version on PyPI
|
|
99
|
+
needs_update: Whether an update is available
|
|
100
|
+
pypi_url: URL to package on PyPI
|
|
101
|
+
release_date: Release date of latest version (ISO format)
|
|
102
|
+
checked_at: Timestamp of when check was performed (ISO format)
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
self.current_version = current_version
|
|
106
|
+
self.latest_version = latest_version
|
|
107
|
+
self.needs_update = needs_update
|
|
108
|
+
self.pypi_url = pypi_url
|
|
109
|
+
self.release_date = release_date
|
|
110
|
+
self.checked_at = checked_at or datetime.now().isoformat()
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
"""Convert to dictionary."""
|
|
114
|
+
return {
|
|
115
|
+
"current_version": self.current_version,
|
|
116
|
+
"latest_version": self.latest_version,
|
|
117
|
+
"needs_update": self.needs_update,
|
|
118
|
+
"pypi_url": self.pypi_url,
|
|
119
|
+
"release_date": self.release_date,
|
|
120
|
+
"checked_at": self.checked_at,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_dict(cls, data: dict[str, Any]) -> "UpdateInfo":
|
|
125
|
+
"""Create from dictionary."""
|
|
126
|
+
return cls(
|
|
127
|
+
current_version=data["current_version"],
|
|
128
|
+
latest_version=data["latest_version"],
|
|
129
|
+
needs_update=data["needs_update"],
|
|
130
|
+
pypi_url=data["pypi_url"],
|
|
131
|
+
release_date=data.get("release_date"),
|
|
132
|
+
checked_at=data.get("checked_at"),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _ensure_cache_dir() -> None:
|
|
137
|
+
"""Ensure cache directory exists."""
|
|
138
|
+
try:
|
|
139
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
except OSError as e:
|
|
141
|
+
logger.warning(f"Failed to create cache directory: {e}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _load_cache() -> dict[str, Any] | None:
|
|
145
|
+
"""Load cached update information.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Cached data or None if cache doesn't exist or is invalid
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
if not CACHE_FILE.exists():
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
with open(CACHE_FILE, encoding="utf-8") as f:
|
|
156
|
+
data = json.load(f)
|
|
157
|
+
|
|
158
|
+
# Validate cache structure
|
|
159
|
+
if not isinstance(data, dict) or "checked_at" not in data:
|
|
160
|
+
logger.debug("Invalid cache format, ignoring")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
return data
|
|
164
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
165
|
+
logger.debug(f"Failed to load cache: {e}")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _save_cache(update_info: UpdateInfo) -> None:
|
|
170
|
+
"""Save update information to cache.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
update_info: Update information to cache
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
_ensure_cache_dir()
|
|
178
|
+
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
|
179
|
+
json.dump(update_info.to_dict(), f, indent=2)
|
|
180
|
+
except OSError as e:
|
|
181
|
+
logger.warning(f"Failed to save cache: {e}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def should_check_updates(force: bool = False) -> bool:
|
|
185
|
+
"""Check if enough time has passed since last check.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
force: If True, always return True (force check)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if check should be performed
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
if force:
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
cache = _load_cache()
|
|
198
|
+
if not cache:
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
checked_at = datetime.fromisoformat(cache["checked_at"])
|
|
203
|
+
age = datetime.now() - checked_at
|
|
204
|
+
return age > timedelta(hours=CACHE_DURATION_HOURS)
|
|
205
|
+
except (ValueError, KeyError) as e:
|
|
206
|
+
logger.debug(f"Invalid cache timestamp: {e}")
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def check_for_updates(force: bool = False) -> UpdateInfo:
|
|
211
|
+
"""Check PyPI for latest version.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
force: If True, bypass cache and force check
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
UpdateInfo object with version information
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
Exception: If PyPI API request fails
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
# Suppress httpx INFO logging to keep output clean
|
|
224
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
225
|
+
|
|
226
|
+
current_version = __version__
|
|
227
|
+
|
|
228
|
+
# Check cache first (unless forced)
|
|
229
|
+
if not force:
|
|
230
|
+
cache = _load_cache()
|
|
231
|
+
if cache and cache.get("current_version") == current_version:
|
|
232
|
+
# Return cached info if it's for the current version
|
|
233
|
+
return UpdateInfo.from_dict(cache)
|
|
234
|
+
|
|
235
|
+
# Fetch from PyPI - use httpx directly for simplicity
|
|
236
|
+
import httpx
|
|
237
|
+
|
|
238
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
239
|
+
response = await client.get(PYPI_API_URL)
|
|
240
|
+
response.raise_for_status()
|
|
241
|
+
response_data = response.json()
|
|
242
|
+
|
|
243
|
+
# Extract version information
|
|
244
|
+
latest_version = response_data["info"]["version"]
|
|
245
|
+
|
|
246
|
+
# Get release date from releases data
|
|
247
|
+
releases = response_data.get("releases", {})
|
|
248
|
+
release_date = None
|
|
249
|
+
if latest_version in releases and releases[latest_version]:
|
|
250
|
+
# Get upload_time from first file in the release
|
|
251
|
+
upload_time = releases[latest_version][0].get("upload_time")
|
|
252
|
+
if upload_time:
|
|
253
|
+
# Convert to ISO format date only
|
|
254
|
+
release_date = upload_time.split("T")[0]
|
|
255
|
+
|
|
256
|
+
# Compare versions
|
|
257
|
+
needs_update = Version(latest_version) > Version(current_version)
|
|
258
|
+
|
|
259
|
+
# Create update info
|
|
260
|
+
update_info = UpdateInfo(
|
|
261
|
+
current_version=current_version,
|
|
262
|
+
latest_version=latest_version,
|
|
263
|
+
needs_update=needs_update,
|
|
264
|
+
pypi_url=PYPI_PROJECT_URL,
|
|
265
|
+
release_date=release_date,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Cache the result
|
|
269
|
+
_save_cache(update_info)
|
|
270
|
+
|
|
271
|
+
return update_info
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def detect_installation_method() -> str:
|
|
275
|
+
"""Detect how mcp-ticketer was installed.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Installation method: 'pipx', 'uv', or 'pip'
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
# Check for pipx
|
|
282
|
+
if "pipx" in sys.prefix or "pipx" in sys.executable:
|
|
283
|
+
return "pipx"
|
|
284
|
+
|
|
285
|
+
# Check for uv
|
|
286
|
+
if "uv" in sys.prefix or "uv" in sys.executable:
|
|
287
|
+
return "uv"
|
|
288
|
+
if ".venv" in sys.prefix and Path(sys.prefix).parent.name == ".venv":
|
|
289
|
+
# Common uv pattern
|
|
290
|
+
uv_bin = Path(sys.prefix).parent.parent / "uv"
|
|
291
|
+
if uv_bin.exists():
|
|
292
|
+
return "uv"
|
|
293
|
+
|
|
294
|
+
# Default to pip
|
|
295
|
+
return "pip"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_upgrade_command() -> str:
|
|
299
|
+
"""Get the appropriate upgrade command for the installation method.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Command string to upgrade mcp-ticketer
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
method = detect_installation_method()
|
|
306
|
+
|
|
307
|
+
commands = {
|
|
308
|
+
"pipx": "pipx upgrade mcp-ticketer",
|
|
309
|
+
"uv": "uv pip install --upgrade mcp-ticketer",
|
|
310
|
+
"pip": "pip install --upgrade mcp-ticketer",
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return commands.get(method, "pip install --upgrade mcp-ticketer")
|
mcp_ticketer/cli/utils.py
CHANGED
|
@@ -4,9 +4,10 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from functools import wraps
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, TypeVar
|
|
10
11
|
|
|
11
12
|
import typer
|
|
12
13
|
from rich.console import Console
|
|
@@ -154,8 +155,8 @@ class CommonPatterns:
|
|
|
154
155
|
|
|
155
156
|
@staticmethod
|
|
156
157
|
def get_adapter(
|
|
157
|
-
override_adapter:
|
|
158
|
-
):
|
|
158
|
+
override_adapter: str | None = None, override_config: dict | None = None
|
|
159
|
+
) -> Any:
|
|
159
160
|
"""Get configured adapter instance with environment variable support."""
|
|
160
161
|
config = CommonPatterns.load_config()
|
|
161
162
|
|
|
@@ -206,7 +207,7 @@ class CommonPatterns:
|
|
|
206
207
|
def queue_operation(
|
|
207
208
|
ticket_data: dict[str, Any],
|
|
208
209
|
operation: str,
|
|
209
|
-
adapter_name:
|
|
210
|
+
adapter_name: str | None = None,
|
|
210
211
|
show_progress: bool = True,
|
|
211
212
|
) -> str:
|
|
212
213
|
"""Queue an operation and optionally start the worker."""
|
|
@@ -265,7 +266,7 @@ class CommonPatterns:
|
|
|
265
266
|
console.print(table)
|
|
266
267
|
|
|
267
268
|
@staticmethod
|
|
268
|
-
def display_ticket_details(ticket: Task, comments:
|
|
269
|
+
def display_ticket_details(ticket: Task, comments: list | None = None) -> None:
|
|
269
270
|
"""Display detailed ticket information."""
|
|
270
271
|
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
271
272
|
console.print(f"Title: {ticket.title}")
|
|
@@ -320,33 +321,35 @@ class CommonPatterns:
|
|
|
320
321
|
)
|
|
321
322
|
|
|
322
323
|
|
|
323
|
-
def async_command(f: Callable[...,
|
|
324
|
-
"""
|
|
324
|
+
def async_command(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
325
|
+
"""Handle async CLI commands via decorator."""
|
|
325
326
|
|
|
326
327
|
@wraps(f)
|
|
327
|
-
def wrapper(*args, **kwargs):
|
|
328
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
328
329
|
return asyncio.run(f(*args, **kwargs))
|
|
329
330
|
|
|
330
331
|
return wrapper
|
|
331
332
|
|
|
332
333
|
|
|
333
|
-
def with_adapter(f: Callable) -> Callable:
|
|
334
|
-
"""
|
|
334
|
+
def with_adapter(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
335
|
+
"""Inject adapter instance into CLI commands via decorator."""
|
|
335
336
|
|
|
336
337
|
@wraps(f)
|
|
337
|
-
def wrapper(adapter:
|
|
338
|
+
def wrapper(adapter: str | None = None, *args: Any, **kwargs: Any) -> Any:
|
|
338
339
|
adapter_instance = CommonPatterns.get_adapter(override_adapter=adapter)
|
|
339
340
|
return f(adapter_instance, *args, **kwargs)
|
|
340
341
|
|
|
341
342
|
return wrapper
|
|
342
343
|
|
|
343
344
|
|
|
344
|
-
def with_progress(
|
|
345
|
-
|
|
345
|
+
def with_progress(
|
|
346
|
+
message: str = "Processing...",
|
|
347
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
348
|
+
"""Show progress spinner for long-running operations via decorator."""
|
|
346
349
|
|
|
347
|
-
def decorator(f: Callable) -> Callable:
|
|
350
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
348
351
|
@wraps(f)
|
|
349
|
-
def wrapper(*args, **kwargs):
|
|
352
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
350
353
|
with Progress(
|
|
351
354
|
SpinnerColumn(),
|
|
352
355
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -360,12 +363,14 @@ def with_progress(message: str = "Processing..."):
|
|
|
360
363
|
return decorator
|
|
361
364
|
|
|
362
365
|
|
|
363
|
-
def validate_required_fields(
|
|
364
|
-
|
|
366
|
+
def validate_required_fields(
|
|
367
|
+
**field_map: str,
|
|
368
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
369
|
+
"""Validate required fields are provided via decorator."""
|
|
365
370
|
|
|
366
|
-
def decorator(f: Callable) -> Callable:
|
|
371
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
367
372
|
@wraps(f)
|
|
368
|
-
def wrapper(*args, **kwargs):
|
|
373
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
369
374
|
missing_fields = []
|
|
370
375
|
for field_name, display_name in field_map.items():
|
|
371
376
|
if field_name in kwargs and kwargs[field_name] is None:
|
|
@@ -375,7 +380,7 @@ def validate_required_fields(**field_map):
|
|
|
375
380
|
console.print(
|
|
376
381
|
f"[red]Error:[/red] Missing required fields: {', '.join(missing_fields)}"
|
|
377
382
|
)
|
|
378
|
-
raise typer.Exit(1)
|
|
383
|
+
raise typer.Exit(1) from None
|
|
379
384
|
|
|
380
385
|
return f(*args, **kwargs)
|
|
381
386
|
|
|
@@ -384,24 +389,24 @@ def validate_required_fields(**field_map):
|
|
|
384
389
|
return decorator
|
|
385
390
|
|
|
386
391
|
|
|
387
|
-
def handle_adapter_errors(f: Callable) -> Callable:
|
|
388
|
-
"""
|
|
392
|
+
def handle_adapter_errors(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
393
|
+
"""Handle common adapter errors gracefully via decorator."""
|
|
389
394
|
|
|
390
395
|
@wraps(f)
|
|
391
|
-
def wrapper(*args, **kwargs):
|
|
396
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
392
397
|
try:
|
|
393
398
|
return f(*args, **kwargs)
|
|
394
399
|
except ConnectionError as e:
|
|
395
400
|
console.print(f"[red]Connection Error:[/red] {e}")
|
|
396
401
|
console.print("Check your network connection and adapter configuration")
|
|
397
|
-
raise typer.Exit(1)
|
|
402
|
+
raise typer.Exit(1) from None
|
|
398
403
|
except ValueError as e:
|
|
399
404
|
console.print(f"[red]Configuration Error:[/red] {e}")
|
|
400
405
|
console.print("Run 'mcp-ticketer init' to configure your adapter")
|
|
401
|
-
raise typer.Exit(1)
|
|
406
|
+
raise typer.Exit(1) from None
|
|
402
407
|
except Exception as e:
|
|
403
408
|
console.print(f"[red]Unexpected Error:[/red] {e}")
|
|
404
|
-
raise typer.Exit(1)
|
|
409
|
+
raise typer.Exit(1) from None
|
|
405
410
|
|
|
406
411
|
return wrapper
|
|
407
412
|
|
|
@@ -446,7 +451,7 @@ class ConfigValidator:
|
|
|
446
451
|
return issues
|
|
447
452
|
|
|
448
453
|
@staticmethod
|
|
449
|
-
def _get_env_var(adapter_type: str, field: str) ->
|
|
454
|
+
def _get_env_var(adapter_type: str, field: str) -> str | None:
|
|
450
455
|
"""Get corresponding environment variable name for a config field."""
|
|
451
456
|
env_mapping = {
|
|
452
457
|
"github": {
|
|
@@ -467,39 +472,39 @@ class ConfigValidator:
|
|
|
467
472
|
class CommandBuilder:
|
|
468
473
|
"""Builder for common CLI command patterns."""
|
|
469
474
|
|
|
470
|
-
def __init__(self):
|
|
471
|
-
self._validators = []
|
|
472
|
-
self._handlers = []
|
|
473
|
-
self._decorators = []
|
|
475
|
+
def __init__(self) -> None:
|
|
476
|
+
self._validators: list[Callable[..., Any]] = []
|
|
477
|
+
self._handlers: list[Callable[..., Any]] = []
|
|
478
|
+
self._decorators: list[Callable[..., Any]] = []
|
|
474
479
|
|
|
475
|
-
def with_adapter_validation(self):
|
|
480
|
+
def with_adapter_validation(self) -> "CommandBuilder":
|
|
476
481
|
"""Add adapter configuration validation."""
|
|
477
482
|
self._validators.append(self._validate_adapter)
|
|
478
483
|
return self
|
|
479
484
|
|
|
480
|
-
def with_async_support(self):
|
|
485
|
+
def with_async_support(self) -> "CommandBuilder":
|
|
481
486
|
"""Add async support to command."""
|
|
482
487
|
self._decorators.append(async_command)
|
|
483
488
|
return self
|
|
484
489
|
|
|
485
|
-
def with_error_handling(self):
|
|
490
|
+
def with_error_handling(self) -> "CommandBuilder":
|
|
486
491
|
"""Add error handling decorator."""
|
|
487
492
|
self._decorators.append(handle_adapter_errors)
|
|
488
493
|
return self
|
|
489
494
|
|
|
490
|
-
def with_progress(self, message: str = "Processing..."):
|
|
495
|
+
def with_progress(self, message: str = "Processing...") -> "CommandBuilder":
|
|
491
496
|
"""Add progress spinner."""
|
|
492
497
|
self._decorators.append(with_progress(message))
|
|
493
498
|
return self
|
|
494
499
|
|
|
495
|
-
def build(self, func: Callable) -> Callable:
|
|
500
|
+
def build(self, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
496
501
|
"""Build the decorated function."""
|
|
497
502
|
decorated_func = func
|
|
498
503
|
for decorator in reversed(self._decorators):
|
|
499
504
|
decorated_func = decorator(decorated_func)
|
|
500
505
|
return decorated_func
|
|
501
506
|
|
|
502
|
-
def _validate_adapter(self, *args, **kwargs):
|
|
507
|
+
def _validate_adapter(self, *args: Any, **kwargs: Any) -> None:
|
|
503
508
|
"""Validate adapter configuration."""
|
|
504
509
|
config = CommonPatterns.load_config()
|
|
505
510
|
default_adapter = config.get("default_adapter", "aitrackdown")
|
|
@@ -513,22 +518,22 @@ class CommandBuilder:
|
|
|
513
518
|
for issue in issues:
|
|
514
519
|
console.print(f" • {issue}")
|
|
515
520
|
console.print("Run 'mcp-ticketer init' to fix configuration")
|
|
516
|
-
raise typer.Exit(1)
|
|
521
|
+
raise typer.Exit(1) from None
|
|
517
522
|
|
|
518
523
|
|
|
519
|
-
def create_standard_ticket_command(operation: str):
|
|
524
|
+
def create_standard_ticket_command(operation: str) -> Callable[..., str]:
|
|
520
525
|
"""Create a standard ticket operation command."""
|
|
521
526
|
|
|
522
527
|
def command_template(
|
|
523
|
-
ticket_id:
|
|
524
|
-
title:
|
|
525
|
-
description:
|
|
526
|
-
priority:
|
|
527
|
-
state:
|
|
528
|
-
assignee:
|
|
529
|
-
tags:
|
|
530
|
-
adapter:
|
|
531
|
-
):
|
|
528
|
+
ticket_id: str | None = None,
|
|
529
|
+
title: str | None = None,
|
|
530
|
+
description: str | None = None,
|
|
531
|
+
priority: Priority | None = None,
|
|
532
|
+
state: TicketState | None = None,
|
|
533
|
+
assignee: str | None = None,
|
|
534
|
+
tags: list[str] | None = None,
|
|
535
|
+
adapter: str | None = None,
|
|
536
|
+
) -> str:
|
|
532
537
|
"""Template for ticket commands."""
|
|
533
538
|
# Build ticket data
|
|
534
539
|
ticket_data = {}
|
|
@@ -567,11 +572,11 @@ class TicketCommands:
|
|
|
567
572
|
@async_command
|
|
568
573
|
@handle_adapter_errors
|
|
569
574
|
async def list_tickets(
|
|
570
|
-
adapter_instance,
|
|
571
|
-
state:
|
|
572
|
-
priority:
|
|
575
|
+
adapter_instance: Any,
|
|
576
|
+
state: TicketState | None = None,
|
|
577
|
+
priority: Priority | None = None,
|
|
573
578
|
limit: int = 10,
|
|
574
|
-
):
|
|
579
|
+
) -> None:
|
|
575
580
|
"""List tickets with filters."""
|
|
576
581
|
filters = {}
|
|
577
582
|
if state:
|
|
@@ -586,13 +591,13 @@ class TicketCommands:
|
|
|
586
591
|
@async_command
|
|
587
592
|
@handle_adapter_errors
|
|
588
593
|
async def show_ticket(
|
|
589
|
-
adapter_instance, ticket_id: str, show_comments: bool = False
|
|
590
|
-
):
|
|
594
|
+
adapter_instance: Any, ticket_id: str, show_comments: bool = False
|
|
595
|
+
) -> None:
|
|
591
596
|
"""Show ticket details."""
|
|
592
597
|
ticket = await adapter_instance.read(ticket_id)
|
|
593
598
|
if not ticket:
|
|
594
599
|
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
595
|
-
raise typer.Exit(1)
|
|
600
|
+
raise typer.Exit(1) from None
|
|
596
601
|
|
|
597
602
|
comments = None
|
|
598
603
|
if show_comments:
|
|
@@ -603,11 +608,11 @@ class TicketCommands:
|
|
|
603
608
|
@staticmethod
|
|
604
609
|
def create_ticket(
|
|
605
610
|
title: str,
|
|
606
|
-
description:
|
|
611
|
+
description: str | None = None,
|
|
607
612
|
priority: Priority = Priority.MEDIUM,
|
|
608
|
-
tags:
|
|
609
|
-
assignee:
|
|
610
|
-
adapter:
|
|
613
|
+
tags: list[str] | None = None,
|
|
614
|
+
assignee: str | None = None,
|
|
615
|
+
adapter: str | None = None,
|
|
611
616
|
) -> str:
|
|
612
617
|
"""Create a new ticket."""
|
|
613
618
|
ticket_data = {
|
|
@@ -622,19 +627,19 @@ class TicketCommands:
|
|
|
622
627
|
|
|
623
628
|
@staticmethod
|
|
624
629
|
def update_ticket(
|
|
625
|
-
ticket_id: str, updates: dict[str, Any], adapter:
|
|
630
|
+
ticket_id: str, updates: dict[str, Any], adapter: str | None = None
|
|
626
631
|
) -> str:
|
|
627
632
|
"""Update a ticket."""
|
|
628
633
|
if not updates:
|
|
629
634
|
console.print("[yellow]No updates specified[/yellow]")
|
|
630
|
-
raise typer.Exit(1)
|
|
635
|
+
raise typer.Exit(1) from None
|
|
631
636
|
|
|
632
637
|
updates["ticket_id"] = ticket_id
|
|
633
638
|
return CommonPatterns.queue_operation(updates, "update", adapter)
|
|
634
639
|
|
|
635
640
|
@staticmethod
|
|
636
641
|
def transition_ticket(
|
|
637
|
-
ticket_id: str, state: TicketState, adapter:
|
|
642
|
+
ticket_id: str, state: TicketState, adapter: str | None = None
|
|
638
643
|
) -> str:
|
|
639
644
|
"""Transition ticket state."""
|
|
640
645
|
ticket_data = {
|
mcp_ticketer/core/__init__.py
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
"""Core models and abstractions for MCP Ticketer."""
|
|
2
2
|
|
|
3
3
|
from .adapter import BaseAdapter
|
|
4
|
-
from .
|
|
4
|
+
from .instructions import (
|
|
5
|
+
InstructionsError,
|
|
6
|
+
InstructionsNotFoundError,
|
|
7
|
+
InstructionsValidationError,
|
|
8
|
+
TicketInstructionsManager,
|
|
9
|
+
get_instructions,
|
|
10
|
+
)
|
|
11
|
+
from .models import Attachment, Comment, Epic, Priority, Task, TicketState, TicketType
|
|
5
12
|
from .registry import AdapterRegistry
|
|
6
13
|
|
|
7
14
|
__all__ = [
|
|
8
15
|
"Epic",
|
|
9
16
|
"Task",
|
|
10
17
|
"Comment",
|
|
18
|
+
"Attachment",
|
|
11
19
|
"TicketState",
|
|
12
20
|
"Priority",
|
|
13
21
|
"TicketType",
|
|
14
22
|
"BaseAdapter",
|
|
15
23
|
"AdapterRegistry",
|
|
24
|
+
"TicketInstructionsManager",
|
|
25
|
+
"InstructionsError",
|
|
26
|
+
"InstructionsNotFoundError",
|
|
27
|
+
"InstructionsValidationError",
|
|
28
|
+
"get_instructions",
|
|
16
29
|
]
|