mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__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 +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +394 -9
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.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
|
@@ -156,7 +156,7 @@ class CommonPatterns:
|
|
|
156
156
|
@staticmethod
|
|
157
157
|
def get_adapter(
|
|
158
158
|
override_adapter: str | None = None, override_config: dict | None = None
|
|
159
|
-
):
|
|
159
|
+
) -> Any:
|
|
160
160
|
"""Get configured adapter instance with environment variable support."""
|
|
161
161
|
config = CommonPatterns.load_config()
|
|
162
162
|
|
|
@@ -321,33 +321,35 @@ class CommonPatterns:
|
|
|
321
321
|
)
|
|
322
322
|
|
|
323
323
|
|
|
324
|
-
def async_command(f: Callable[...,
|
|
325
|
-
"""
|
|
324
|
+
def async_command(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
325
|
+
"""Handle async CLI commands via decorator."""
|
|
326
326
|
|
|
327
327
|
@wraps(f)
|
|
328
|
-
def wrapper(*args, **kwargs):
|
|
328
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
329
329
|
return asyncio.run(f(*args, **kwargs))
|
|
330
330
|
|
|
331
331
|
return wrapper
|
|
332
332
|
|
|
333
333
|
|
|
334
|
-
def with_adapter(f: Callable) -> Callable:
|
|
335
|
-
"""
|
|
334
|
+
def with_adapter(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
335
|
+
"""Inject adapter instance into CLI commands via decorator."""
|
|
336
336
|
|
|
337
337
|
@wraps(f)
|
|
338
|
-
def wrapper(adapter: str | None = None, *args, **kwargs):
|
|
338
|
+
def wrapper(adapter: str | None = None, *args: Any, **kwargs: Any) -> Any:
|
|
339
339
|
adapter_instance = CommonPatterns.get_adapter(override_adapter=adapter)
|
|
340
340
|
return f(adapter_instance, *args, **kwargs)
|
|
341
341
|
|
|
342
342
|
return wrapper
|
|
343
343
|
|
|
344
344
|
|
|
345
|
-
def with_progress(
|
|
346
|
-
|
|
345
|
+
def with_progress(
|
|
346
|
+
message: str = "Processing...",
|
|
347
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
348
|
+
"""Show progress spinner for long-running operations via decorator."""
|
|
347
349
|
|
|
348
|
-
def decorator(f: Callable) -> Callable:
|
|
350
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
349
351
|
@wraps(f)
|
|
350
|
-
def wrapper(*args, **kwargs):
|
|
352
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
351
353
|
with Progress(
|
|
352
354
|
SpinnerColumn(),
|
|
353
355
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -361,12 +363,14 @@ def with_progress(message: str = "Processing..."):
|
|
|
361
363
|
return decorator
|
|
362
364
|
|
|
363
365
|
|
|
364
|
-
def validate_required_fields(
|
|
365
|
-
|
|
366
|
+
def validate_required_fields(
|
|
367
|
+
**field_map: str,
|
|
368
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
369
|
+
"""Validate required fields are provided via decorator."""
|
|
366
370
|
|
|
367
|
-
def decorator(f: Callable) -> Callable:
|
|
371
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
368
372
|
@wraps(f)
|
|
369
|
-
def wrapper(*args, **kwargs):
|
|
373
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
370
374
|
missing_fields = []
|
|
371
375
|
for field_name, display_name in field_map.items():
|
|
372
376
|
if field_name in kwargs and kwargs[field_name] is None:
|
|
@@ -376,7 +380,7 @@ def validate_required_fields(**field_map):
|
|
|
376
380
|
console.print(
|
|
377
381
|
f"[red]Error:[/red] Missing required fields: {', '.join(missing_fields)}"
|
|
378
382
|
)
|
|
379
|
-
raise typer.Exit(1)
|
|
383
|
+
raise typer.Exit(1) from None
|
|
380
384
|
|
|
381
385
|
return f(*args, **kwargs)
|
|
382
386
|
|
|
@@ -385,24 +389,24 @@ def validate_required_fields(**field_map):
|
|
|
385
389
|
return decorator
|
|
386
390
|
|
|
387
391
|
|
|
388
|
-
def handle_adapter_errors(f: Callable) -> Callable:
|
|
389
|
-
"""
|
|
392
|
+
def handle_adapter_errors(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
393
|
+
"""Handle common adapter errors gracefully via decorator."""
|
|
390
394
|
|
|
391
395
|
@wraps(f)
|
|
392
|
-
def wrapper(*args, **kwargs):
|
|
396
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
393
397
|
try:
|
|
394
398
|
return f(*args, **kwargs)
|
|
395
399
|
except ConnectionError as e:
|
|
396
400
|
console.print(f"[red]Connection Error:[/red] {e}")
|
|
397
401
|
console.print("Check your network connection and adapter configuration")
|
|
398
|
-
raise typer.Exit(1)
|
|
402
|
+
raise typer.Exit(1) from None
|
|
399
403
|
except ValueError as e:
|
|
400
404
|
console.print(f"[red]Configuration Error:[/red] {e}")
|
|
401
405
|
console.print("Run 'mcp-ticketer init' to configure your adapter")
|
|
402
|
-
raise typer.Exit(1)
|
|
406
|
+
raise typer.Exit(1) from None
|
|
403
407
|
except Exception as e:
|
|
404
408
|
console.print(f"[red]Unexpected Error:[/red] {e}")
|
|
405
|
-
raise typer.Exit(1)
|
|
409
|
+
raise typer.Exit(1) from None
|
|
406
410
|
|
|
407
411
|
return wrapper
|
|
408
412
|
|
|
@@ -468,39 +472,39 @@ class ConfigValidator:
|
|
|
468
472
|
class CommandBuilder:
|
|
469
473
|
"""Builder for common CLI command patterns."""
|
|
470
474
|
|
|
471
|
-
def __init__(self):
|
|
472
|
-
self._validators = []
|
|
473
|
-
self._handlers = []
|
|
474
|
-
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]] = []
|
|
475
479
|
|
|
476
|
-
def with_adapter_validation(self):
|
|
480
|
+
def with_adapter_validation(self) -> "CommandBuilder":
|
|
477
481
|
"""Add adapter configuration validation."""
|
|
478
482
|
self._validators.append(self._validate_adapter)
|
|
479
483
|
return self
|
|
480
484
|
|
|
481
|
-
def with_async_support(self):
|
|
485
|
+
def with_async_support(self) -> "CommandBuilder":
|
|
482
486
|
"""Add async support to command."""
|
|
483
487
|
self._decorators.append(async_command)
|
|
484
488
|
return self
|
|
485
489
|
|
|
486
|
-
def with_error_handling(self):
|
|
490
|
+
def with_error_handling(self) -> "CommandBuilder":
|
|
487
491
|
"""Add error handling decorator."""
|
|
488
492
|
self._decorators.append(handle_adapter_errors)
|
|
489
493
|
return self
|
|
490
494
|
|
|
491
|
-
def with_progress(self, message: str = "Processing..."):
|
|
495
|
+
def with_progress(self, message: str = "Processing...") -> "CommandBuilder":
|
|
492
496
|
"""Add progress spinner."""
|
|
493
497
|
self._decorators.append(with_progress(message))
|
|
494
498
|
return self
|
|
495
499
|
|
|
496
|
-
def build(self, func: Callable) -> Callable:
|
|
500
|
+
def build(self, func: Callable[..., Any]) -> Callable[..., Any]:
|
|
497
501
|
"""Build the decorated function."""
|
|
498
502
|
decorated_func = func
|
|
499
503
|
for decorator in reversed(self._decorators):
|
|
500
504
|
decorated_func = decorator(decorated_func)
|
|
501
505
|
return decorated_func
|
|
502
506
|
|
|
503
|
-
def _validate_adapter(self, *args, **kwargs):
|
|
507
|
+
def _validate_adapter(self, *args: Any, **kwargs: Any) -> None:
|
|
504
508
|
"""Validate adapter configuration."""
|
|
505
509
|
config = CommonPatterns.load_config()
|
|
506
510
|
default_adapter = config.get("default_adapter", "aitrackdown")
|
|
@@ -514,10 +518,10 @@ class CommandBuilder:
|
|
|
514
518
|
for issue in issues:
|
|
515
519
|
console.print(f" • {issue}")
|
|
516
520
|
console.print("Run 'mcp-ticketer init' to fix configuration")
|
|
517
|
-
raise typer.Exit(1)
|
|
521
|
+
raise typer.Exit(1) from None
|
|
518
522
|
|
|
519
523
|
|
|
520
|
-
def create_standard_ticket_command(operation: str):
|
|
524
|
+
def create_standard_ticket_command(operation: str) -> Callable[..., str]:
|
|
521
525
|
"""Create a standard ticket operation command."""
|
|
522
526
|
|
|
523
527
|
def command_template(
|
|
@@ -529,7 +533,7 @@ def create_standard_ticket_command(operation: str):
|
|
|
529
533
|
assignee: str | None = None,
|
|
530
534
|
tags: list[str] | None = None,
|
|
531
535
|
adapter: str | None = None,
|
|
532
|
-
):
|
|
536
|
+
) -> str:
|
|
533
537
|
"""Template for ticket commands."""
|
|
534
538
|
# Build ticket data
|
|
535
539
|
ticket_data = {}
|
|
@@ -568,11 +572,11 @@ class TicketCommands:
|
|
|
568
572
|
@async_command
|
|
569
573
|
@handle_adapter_errors
|
|
570
574
|
async def list_tickets(
|
|
571
|
-
adapter_instance,
|
|
575
|
+
adapter_instance: Any,
|
|
572
576
|
state: TicketState | None = None,
|
|
573
577
|
priority: Priority | None = None,
|
|
574
578
|
limit: int = 10,
|
|
575
|
-
):
|
|
579
|
+
) -> None:
|
|
576
580
|
"""List tickets with filters."""
|
|
577
581
|
filters = {}
|
|
578
582
|
if state:
|
|
@@ -587,13 +591,13 @@ class TicketCommands:
|
|
|
587
591
|
@async_command
|
|
588
592
|
@handle_adapter_errors
|
|
589
593
|
async def show_ticket(
|
|
590
|
-
adapter_instance, ticket_id: str, show_comments: bool = False
|
|
591
|
-
):
|
|
594
|
+
adapter_instance: Any, ticket_id: str, show_comments: bool = False
|
|
595
|
+
) -> None:
|
|
592
596
|
"""Show ticket details."""
|
|
593
597
|
ticket = await adapter_instance.read(ticket_id)
|
|
594
598
|
if not ticket:
|
|
595
599
|
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
596
|
-
raise typer.Exit(1)
|
|
600
|
+
raise typer.Exit(1) from None
|
|
597
601
|
|
|
598
602
|
comments = None
|
|
599
603
|
if show_comments:
|
|
@@ -628,7 +632,7 @@ class TicketCommands:
|
|
|
628
632
|
"""Update a ticket."""
|
|
629
633
|
if not updates:
|
|
630
634
|
console.print("[yellow]No updates specified[/yellow]")
|
|
631
|
-
raise typer.Exit(1)
|
|
635
|
+
raise typer.Exit(1) from None
|
|
632
636
|
|
|
633
637
|
updates["ticket_id"] = ticket_id
|
|
634
638
|
return CommonPatterns.queue_operation(updates, "update", adapter)
|
mcp_ticketer/core/__init__.py
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
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 (
|
|
12
|
+
Attachment,
|
|
13
|
+
Comment,
|
|
14
|
+
Epic,
|
|
15
|
+
Priority,
|
|
16
|
+
ProjectUpdate,
|
|
17
|
+
ProjectUpdateHealth,
|
|
18
|
+
Task,
|
|
19
|
+
TicketState,
|
|
20
|
+
TicketType,
|
|
21
|
+
)
|
|
5
22
|
from .registry import AdapterRegistry
|
|
23
|
+
from .state_matcher import (
|
|
24
|
+
SemanticStateMatcher,
|
|
25
|
+
StateMatchResult,
|
|
26
|
+
ValidationResult,
|
|
27
|
+
get_state_matcher,
|
|
28
|
+
)
|
|
6
29
|
|
|
7
30
|
__all__ = [
|
|
8
31
|
"Epic",
|
|
9
32
|
"Task",
|
|
10
33
|
"Comment",
|
|
11
34
|
"Attachment",
|
|
35
|
+
"ProjectUpdate",
|
|
36
|
+
"ProjectUpdateHealth",
|
|
12
37
|
"TicketState",
|
|
13
38
|
"Priority",
|
|
14
39
|
"TicketType",
|
|
15
40
|
"BaseAdapter",
|
|
16
41
|
"AdapterRegistry",
|
|
42
|
+
"TicketInstructionsManager",
|
|
43
|
+
"InstructionsError",
|
|
44
|
+
"InstructionsNotFoundError",
|
|
45
|
+
"InstructionsValidationError",
|
|
46
|
+
"get_instructions",
|
|
47
|
+
"SemanticStateMatcher",
|
|
48
|
+
"StateMatchResult",
|
|
49
|
+
"ValidationResult",
|
|
50
|
+
"get_state_matcher",
|
|
17
51
|
]
|