mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -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/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- 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 +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1284
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -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 +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/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- 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/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- 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 +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
- mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,34 +5,34 @@ import sqlite3
|
|
|
5
5
|
import threading
|
|
6
6
|
from datetime import datetime, timedelta
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from .queue import QueueItem, QueueStatus
|
|
8
|
+
from typing import Any
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
class TicketRegistry:
|
|
14
12
|
"""Persistent registry for tracking ticket IDs and their lifecycle."""
|
|
15
|
-
|
|
16
|
-
def __init__(self, db_path:
|
|
13
|
+
|
|
14
|
+
def __init__(self, db_path: Path | None = None):
|
|
17
15
|
"""Initialize ticket registry.
|
|
18
|
-
|
|
16
|
+
|
|
19
17
|
Args:
|
|
20
18
|
db_path: Path to SQLite database. Defaults to ~/.mcp-ticketer/tickets.db
|
|
19
|
+
|
|
21
20
|
"""
|
|
22
21
|
if db_path is None:
|
|
23
22
|
db_dir = Path.home() / ".mcp-ticketer"
|
|
24
23
|
db_dir.mkdir(parents=True, exist_ok=True)
|
|
25
24
|
db_path = db_dir / "tickets.db"
|
|
26
|
-
|
|
25
|
+
|
|
27
26
|
self.db_path = str(db_path)
|
|
28
27
|
self._lock = threading.Lock()
|
|
29
28
|
self._init_database()
|
|
30
|
-
|
|
31
|
-
def _init_database(self):
|
|
29
|
+
|
|
30
|
+
def _init_database(self) -> None:
|
|
32
31
|
"""Initialize database schema."""
|
|
33
32
|
with sqlite3.connect(self.db_path) as conn:
|
|
34
33
|
# Ticket registry table
|
|
35
|
-
conn.execute(
|
|
34
|
+
conn.execute(
|
|
35
|
+
"""
|
|
36
36
|
CREATE TABLE IF NOT EXISTS ticket_registry (
|
|
37
37
|
queue_id TEXT PRIMARY KEY,
|
|
38
38
|
ticket_id TEXT,
|
|
@@ -48,24 +48,32 @@ class TicketRegistry:
|
|
|
48
48
|
retry_count INTEGER DEFAULT 0,
|
|
49
49
|
CHECK (status IN ('queued', 'processing', 'completed', 'failed', 'recovered'))
|
|
50
50
|
)
|
|
51
|
-
"""
|
|
52
|
-
|
|
51
|
+
"""
|
|
52
|
+
)
|
|
53
|
+
|
|
53
54
|
# Create indices
|
|
54
|
-
conn.execute(
|
|
55
|
+
conn.execute(
|
|
56
|
+
"""
|
|
55
57
|
CREATE INDEX IF NOT EXISTS idx_ticket_registry_ticket_id
|
|
56
58
|
ON ticket_registry(ticket_id)
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
+
"""
|
|
60
|
+
)
|
|
61
|
+
conn.execute(
|
|
62
|
+
"""
|
|
59
63
|
CREATE INDEX IF NOT EXISTS idx_ticket_registry_status
|
|
60
64
|
ON ticket_registry(status)
|
|
61
|
-
"""
|
|
62
|
-
|
|
65
|
+
"""
|
|
66
|
+
)
|
|
67
|
+
conn.execute(
|
|
68
|
+
"""
|
|
63
69
|
CREATE INDEX IF NOT EXISTS idx_ticket_registry_adapter
|
|
64
70
|
ON ticket_registry(adapter)
|
|
65
|
-
"""
|
|
66
|
-
|
|
71
|
+
"""
|
|
72
|
+
)
|
|
73
|
+
|
|
67
74
|
# Ticket recovery log table
|
|
68
|
-
conn.execute(
|
|
75
|
+
conn.execute(
|
|
76
|
+
"""
|
|
69
77
|
CREATE TABLE IF NOT EXISTS recovery_log (
|
|
70
78
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
71
79
|
queue_id TEXT NOT NULL,
|
|
@@ -74,56 +82,61 @@ class TicketRegistry:
|
|
|
74
82
|
timestamp TEXT NOT NULL,
|
|
75
83
|
success BOOLEAN NOT NULL
|
|
76
84
|
)
|
|
77
|
-
"""
|
|
78
|
-
|
|
85
|
+
"""
|
|
86
|
+
)
|
|
87
|
+
|
|
79
88
|
def register_ticket_operation(
|
|
80
89
|
self,
|
|
81
90
|
queue_id: str,
|
|
82
91
|
adapter: str,
|
|
83
92
|
operation: str,
|
|
84
93
|
title: str,
|
|
85
|
-
ticket_data:
|
|
94
|
+
ticket_data: dict[str, Any],
|
|
86
95
|
) -> None:
|
|
87
96
|
"""Register a new ticket operation.
|
|
88
|
-
|
|
97
|
+
|
|
89
98
|
Args:
|
|
90
99
|
queue_id: Queue operation ID
|
|
91
100
|
adapter: Adapter name
|
|
92
101
|
operation: Operation type (create, update, etc.)
|
|
93
102
|
title: Ticket title
|
|
94
103
|
ticket_data: Original ticket data
|
|
104
|
+
|
|
95
105
|
"""
|
|
96
106
|
with self._lock:
|
|
97
107
|
with sqlite3.connect(self.db_path) as conn:
|
|
98
|
-
conn.execute(
|
|
108
|
+
conn.execute(
|
|
109
|
+
"""
|
|
99
110
|
INSERT OR REPLACE INTO ticket_registry (
|
|
100
111
|
queue_id, adapter, operation, title, status,
|
|
101
112
|
created_at, updated_at, ticket_data, retry_count
|
|
102
113
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
103
|
-
""",
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
""",
|
|
115
|
+
(
|
|
116
|
+
queue_id,
|
|
117
|
+
adapter,
|
|
118
|
+
operation,
|
|
119
|
+
title,
|
|
120
|
+
"queued",
|
|
121
|
+
datetime.now().isoformat(),
|
|
122
|
+
datetime.now().isoformat(),
|
|
123
|
+
json.dumps(ticket_data),
|
|
124
|
+
0,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
114
127
|
conn.commit()
|
|
115
|
-
|
|
128
|
+
|
|
116
129
|
def update_ticket_status(
|
|
117
130
|
self,
|
|
118
131
|
queue_id: str,
|
|
119
132
|
status: str,
|
|
120
|
-
ticket_id:
|
|
121
|
-
result_data:
|
|
122
|
-
error_message:
|
|
123
|
-
retry_count:
|
|
133
|
+
ticket_id: str | None = None,
|
|
134
|
+
result_data: dict[str, Any] | None = None,
|
|
135
|
+
error_message: str | None = None,
|
|
136
|
+
retry_count: int | None = None,
|
|
124
137
|
) -> None:
|
|
125
138
|
"""Update ticket operation status.
|
|
126
|
-
|
|
139
|
+
|
|
127
140
|
Args:
|
|
128
141
|
queue_id: Queue operation ID
|
|
129
142
|
status: New status
|
|
@@ -131,286 +144,331 @@ class TicketRegistry:
|
|
|
131
144
|
result_data: Operation result data
|
|
132
145
|
error_message: Error message if failed
|
|
133
146
|
retry_count: Current retry count
|
|
147
|
+
|
|
134
148
|
"""
|
|
135
149
|
with self._lock:
|
|
136
150
|
with sqlite3.connect(self.db_path) as conn:
|
|
137
151
|
update_fields = ["status = ?", "updated_at = ?"]
|
|
138
|
-
values = [status, datetime.now().isoformat()]
|
|
139
|
-
|
|
152
|
+
values: list[Any] = [status, datetime.now().isoformat()]
|
|
153
|
+
|
|
140
154
|
if ticket_id is not None:
|
|
141
155
|
update_fields.append("ticket_id = ?")
|
|
142
156
|
values.append(ticket_id)
|
|
143
|
-
|
|
157
|
+
|
|
144
158
|
if result_data is not None:
|
|
145
159
|
update_fields.append("result_data = ?")
|
|
146
160
|
values.append(json.dumps(result_data))
|
|
147
|
-
|
|
161
|
+
|
|
148
162
|
if error_message is not None:
|
|
149
163
|
update_fields.append("error_message = ?")
|
|
150
164
|
values.append(error_message)
|
|
151
|
-
|
|
165
|
+
|
|
152
166
|
if retry_count is not None:
|
|
153
167
|
update_fields.append("retry_count = ?")
|
|
154
168
|
values.append(retry_count)
|
|
155
|
-
|
|
169
|
+
|
|
156
170
|
values.append(queue_id)
|
|
157
|
-
|
|
158
|
-
conn.execute(
|
|
171
|
+
|
|
172
|
+
conn.execute(
|
|
173
|
+
f"""
|
|
159
174
|
UPDATE ticket_registry
|
|
160
175
|
SET {', '.join(update_fields)}
|
|
161
176
|
WHERE queue_id = ?
|
|
162
|
-
""",
|
|
177
|
+
""",
|
|
178
|
+
values,
|
|
179
|
+
)
|
|
163
180
|
conn.commit()
|
|
164
|
-
|
|
165
|
-
def get_ticket_info(self, queue_id: str) ->
|
|
181
|
+
|
|
182
|
+
def get_ticket_info(self, queue_id: str) -> dict[str, Any] | None:
|
|
166
183
|
"""Get ticket information by queue ID.
|
|
167
|
-
|
|
184
|
+
|
|
168
185
|
Args:
|
|
169
186
|
queue_id: Queue operation ID
|
|
170
|
-
|
|
187
|
+
|
|
171
188
|
Returns:
|
|
172
189
|
Ticket information or None if not found
|
|
190
|
+
|
|
173
191
|
"""
|
|
174
192
|
with sqlite3.connect(self.db_path) as conn:
|
|
175
|
-
cursor = conn.execute(
|
|
193
|
+
cursor = conn.execute(
|
|
194
|
+
"""
|
|
176
195
|
SELECT * FROM ticket_registry WHERE queue_id = ?
|
|
177
|
-
""",
|
|
178
|
-
|
|
196
|
+
""",
|
|
197
|
+
(queue_id,),
|
|
198
|
+
)
|
|
199
|
+
|
|
179
200
|
row = cursor.fetchone()
|
|
180
201
|
if not row:
|
|
181
202
|
return None
|
|
182
|
-
|
|
203
|
+
|
|
183
204
|
columns = [desc[0] for desc in cursor.description]
|
|
184
|
-
ticket_info = dict(zip(columns, row))
|
|
185
|
-
|
|
205
|
+
ticket_info = dict(zip(columns, row, strict=False))
|
|
206
|
+
|
|
186
207
|
# Parse JSON fields
|
|
187
208
|
if ticket_info.get("ticket_data"):
|
|
188
209
|
ticket_info["ticket_data"] = json.loads(ticket_info["ticket_data"])
|
|
189
210
|
if ticket_info.get("result_data"):
|
|
190
211
|
ticket_info["result_data"] = json.loads(ticket_info["result_data"])
|
|
191
|
-
|
|
212
|
+
|
|
192
213
|
return ticket_info
|
|
193
|
-
|
|
194
|
-
def find_tickets_by_id(self, ticket_id: str) ->
|
|
214
|
+
|
|
215
|
+
def find_tickets_by_id(self, ticket_id: str) -> list[dict[str, Any]]:
|
|
195
216
|
"""Find all operations for a specific ticket ID.
|
|
196
|
-
|
|
217
|
+
|
|
197
218
|
Args:
|
|
198
219
|
ticket_id: Ticket ID to search for
|
|
199
|
-
|
|
220
|
+
|
|
200
221
|
Returns:
|
|
201
222
|
List of ticket operations
|
|
223
|
+
|
|
202
224
|
"""
|
|
203
225
|
with sqlite3.connect(self.db_path) as conn:
|
|
204
|
-
cursor = conn.execute(
|
|
205
|
-
|
|
226
|
+
cursor = conn.execute(
|
|
227
|
+
"""
|
|
228
|
+
SELECT * FROM ticket_registry
|
|
206
229
|
WHERE ticket_id = ?
|
|
207
230
|
ORDER BY created_at DESC
|
|
208
|
-
""",
|
|
209
|
-
|
|
231
|
+
""",
|
|
232
|
+
(ticket_id,),
|
|
233
|
+
)
|
|
234
|
+
|
|
210
235
|
results = []
|
|
211
236
|
columns = [desc[0] for desc in cursor.description]
|
|
212
|
-
|
|
237
|
+
|
|
213
238
|
for row in cursor.fetchall():
|
|
214
|
-
ticket_info = dict(zip(columns, row))
|
|
215
|
-
|
|
239
|
+
ticket_info = dict(zip(columns, row, strict=False))
|
|
240
|
+
|
|
216
241
|
# Parse JSON fields
|
|
217
242
|
if ticket_info.get("ticket_data"):
|
|
218
243
|
ticket_info["ticket_data"] = json.loads(ticket_info["ticket_data"])
|
|
219
244
|
if ticket_info.get("result_data"):
|
|
220
245
|
ticket_info["result_data"] = json.loads(ticket_info["result_data"])
|
|
221
|
-
|
|
246
|
+
|
|
222
247
|
results.append(ticket_info)
|
|
223
|
-
|
|
248
|
+
|
|
224
249
|
return results
|
|
225
|
-
|
|
226
|
-
def get_failed_operations(self, limit: int = 50) ->
|
|
250
|
+
|
|
251
|
+
def get_failed_operations(self, limit: int = 50) -> list[dict[str, Any]]:
|
|
227
252
|
"""Get failed operations that might need recovery.
|
|
228
|
-
|
|
253
|
+
|
|
229
254
|
Args:
|
|
230
255
|
limit: Maximum number of operations to return
|
|
231
|
-
|
|
256
|
+
|
|
232
257
|
Returns:
|
|
233
258
|
List of failed operations
|
|
259
|
+
|
|
234
260
|
"""
|
|
235
261
|
with sqlite3.connect(self.db_path) as conn:
|
|
236
|
-
cursor = conn.execute(
|
|
237
|
-
|
|
262
|
+
cursor = conn.execute(
|
|
263
|
+
"""
|
|
264
|
+
SELECT * FROM ticket_registry
|
|
238
265
|
WHERE status = 'failed'
|
|
239
266
|
ORDER BY updated_at DESC
|
|
240
267
|
LIMIT ?
|
|
241
|
-
""",
|
|
242
|
-
|
|
268
|
+
""",
|
|
269
|
+
(limit,),
|
|
270
|
+
)
|
|
271
|
+
|
|
243
272
|
results = []
|
|
244
273
|
columns = [desc[0] for desc in cursor.description]
|
|
245
|
-
|
|
274
|
+
|
|
246
275
|
for row in cursor.fetchall():
|
|
247
|
-
ticket_info = dict(zip(columns, row))
|
|
248
|
-
|
|
276
|
+
ticket_info = dict(zip(columns, row, strict=False))
|
|
277
|
+
|
|
249
278
|
# Parse JSON fields
|
|
250
279
|
if ticket_info.get("ticket_data"):
|
|
251
280
|
ticket_info["ticket_data"] = json.loads(ticket_info["ticket_data"])
|
|
252
281
|
if ticket_info.get("result_data"):
|
|
253
282
|
ticket_info["result_data"] = json.loads(ticket_info["result_data"])
|
|
254
|
-
|
|
283
|
+
|
|
255
284
|
results.append(ticket_info)
|
|
256
|
-
|
|
285
|
+
|
|
257
286
|
return results
|
|
258
|
-
|
|
259
|
-
def get_orphaned_tickets(self) ->
|
|
287
|
+
|
|
288
|
+
def get_orphaned_tickets(self) -> list[dict[str, Any]]:
|
|
260
289
|
"""Get tickets that were created but queue operation failed.
|
|
261
|
-
|
|
290
|
+
|
|
262
291
|
Returns:
|
|
263
292
|
List of potentially orphaned tickets
|
|
293
|
+
|
|
264
294
|
"""
|
|
265
295
|
with sqlite3.connect(self.db_path) as conn:
|
|
266
|
-
cursor = conn.execute(
|
|
267
|
-
|
|
268
|
-
|
|
296
|
+
cursor = conn.execute(
|
|
297
|
+
"""
|
|
298
|
+
SELECT * FROM ticket_registry
|
|
299
|
+
WHERE ticket_id IS NOT NULL
|
|
269
300
|
AND status IN ('processing', 'failed')
|
|
270
301
|
ORDER BY updated_at DESC
|
|
271
|
-
"""
|
|
272
|
-
|
|
302
|
+
"""
|
|
303
|
+
)
|
|
304
|
+
|
|
273
305
|
results = []
|
|
274
306
|
columns = [desc[0] for desc in cursor.description]
|
|
275
|
-
|
|
307
|
+
|
|
276
308
|
for row in cursor.fetchall():
|
|
277
|
-
ticket_info = dict(zip(columns, row))
|
|
278
|
-
|
|
309
|
+
ticket_info = dict(zip(columns, row, strict=False))
|
|
310
|
+
|
|
279
311
|
# Parse JSON fields
|
|
280
312
|
if ticket_info.get("ticket_data"):
|
|
281
313
|
ticket_info["ticket_data"] = json.loads(ticket_info["ticket_data"])
|
|
282
314
|
if ticket_info.get("result_data"):
|
|
283
315
|
ticket_info["result_data"] = json.loads(ticket_info["result_data"])
|
|
284
|
-
|
|
316
|
+
|
|
285
317
|
results.append(ticket_info)
|
|
286
|
-
|
|
318
|
+
|
|
287
319
|
return results
|
|
288
|
-
|
|
289
|
-
def attempt_recovery(self, queue_id: str, recovery_type: str) ->
|
|
320
|
+
|
|
321
|
+
def attempt_recovery(self, queue_id: str, recovery_type: str) -> dict[str, Any]:
|
|
290
322
|
"""Attempt to recover a failed operation.
|
|
291
|
-
|
|
323
|
+
|
|
292
324
|
Args:
|
|
293
325
|
queue_id: Queue operation ID to recover
|
|
294
326
|
recovery_type: Type of recovery to attempt
|
|
295
|
-
|
|
327
|
+
|
|
296
328
|
Returns:
|
|
297
329
|
Recovery result
|
|
330
|
+
|
|
298
331
|
"""
|
|
299
332
|
ticket_info = self.get_ticket_info(queue_id)
|
|
300
333
|
if not ticket_info:
|
|
301
334
|
return {"success": False, "error": "Ticket operation not found"}
|
|
302
|
-
|
|
335
|
+
|
|
303
336
|
recovery_data = {
|
|
304
337
|
"original_status": ticket_info["status"],
|
|
305
338
|
"recovery_type": recovery_type,
|
|
306
|
-
"timestamp": datetime.now().isoformat()
|
|
339
|
+
"timestamp": datetime.now().isoformat(),
|
|
307
340
|
}
|
|
308
|
-
|
|
341
|
+
|
|
309
342
|
try:
|
|
310
343
|
if recovery_type == "mark_completed":
|
|
311
344
|
# Mark as completed if ticket ID exists
|
|
312
345
|
if ticket_info.get("ticket_id"):
|
|
313
|
-
self.update_ticket_status(
|
|
314
|
-
|
|
346
|
+
self.update_ticket_status(
|
|
347
|
+
queue_id,
|
|
348
|
+
"recovered",
|
|
349
|
+
result_data={"recovery": "marked_completed"},
|
|
350
|
+
)
|
|
315
351
|
recovery_data["success"] = True
|
|
316
|
-
recovery_data["action"] =
|
|
352
|
+
recovery_data["action"] = (
|
|
353
|
+
"Marked as completed based on existing ticket ID"
|
|
354
|
+
)
|
|
317
355
|
else:
|
|
318
356
|
recovery_data["success"] = False
|
|
319
|
-
recovery_data["error"] =
|
|
320
|
-
|
|
357
|
+
recovery_data["error"] = (
|
|
358
|
+
"No ticket ID available to mark as completed"
|
|
359
|
+
)
|
|
360
|
+
|
|
321
361
|
elif recovery_type == "retry_operation":
|
|
322
362
|
# Reset to queued status for retry
|
|
323
|
-
self.update_ticket_status(
|
|
324
|
-
|
|
325
|
-
|
|
363
|
+
self.update_ticket_status(
|
|
364
|
+
queue_id,
|
|
365
|
+
"queued",
|
|
366
|
+
error_message=None,
|
|
367
|
+
retry_count=ticket_info.get("retry_count", 0),
|
|
368
|
+
)
|
|
326
369
|
recovery_data["success"] = True
|
|
327
370
|
recovery_data["action"] = "Reset to queued for retry"
|
|
328
|
-
|
|
371
|
+
|
|
329
372
|
else:
|
|
330
373
|
recovery_data["success"] = False
|
|
331
374
|
recovery_data["error"] = f"Unknown recovery type: {recovery_type}"
|
|
332
|
-
|
|
375
|
+
|
|
333
376
|
# Log recovery attempt
|
|
334
|
-
self._log_recovery(
|
|
335
|
-
|
|
377
|
+
self._log_recovery(
|
|
378
|
+
queue_id, recovery_type, recovery_data, recovery_data["success"]
|
|
379
|
+
)
|
|
380
|
+
|
|
336
381
|
return recovery_data
|
|
337
|
-
|
|
382
|
+
|
|
338
383
|
except Exception as e:
|
|
339
384
|
recovery_data["success"] = False
|
|
340
385
|
recovery_data["error"] = str(e)
|
|
341
386
|
self._log_recovery(queue_id, recovery_type, recovery_data, False)
|
|
342
387
|
return recovery_data
|
|
343
|
-
|
|
388
|
+
|
|
344
389
|
def _log_recovery(
|
|
345
|
-
self,
|
|
346
|
-
queue_id: str,
|
|
347
|
-
recovery_type: str,
|
|
348
|
-
recovery_data:
|
|
349
|
-
success: bool
|
|
390
|
+
self,
|
|
391
|
+
queue_id: str,
|
|
392
|
+
recovery_type: str,
|
|
393
|
+
recovery_data: dict[str, Any],
|
|
394
|
+
success: bool,
|
|
350
395
|
) -> None:
|
|
351
396
|
"""Log recovery attempt."""
|
|
352
397
|
with self._lock:
|
|
353
398
|
with sqlite3.connect(self.db_path) as conn:
|
|
354
|
-
conn.execute(
|
|
399
|
+
conn.execute(
|
|
400
|
+
"""
|
|
355
401
|
INSERT INTO recovery_log (
|
|
356
402
|
queue_id, recovery_type, recovery_data, timestamp, success
|
|
357
403
|
) VALUES (?, ?, ?, ?, ?)
|
|
358
|
-
""",
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
404
|
+
""",
|
|
405
|
+
(
|
|
406
|
+
queue_id,
|
|
407
|
+
recovery_type,
|
|
408
|
+
json.dumps(recovery_data),
|
|
409
|
+
datetime.now().isoformat(),
|
|
410
|
+
success,
|
|
411
|
+
),
|
|
412
|
+
)
|
|
365
413
|
conn.commit()
|
|
366
|
-
|
|
367
|
-
def get_recovery_history(self, queue_id: str) ->
|
|
414
|
+
|
|
415
|
+
def get_recovery_history(self, queue_id: str) -> list[dict[str, Any]]:
|
|
368
416
|
"""Get recovery history for a queue operation.
|
|
369
|
-
|
|
417
|
+
|
|
370
418
|
Args:
|
|
371
419
|
queue_id: Queue operation ID
|
|
372
|
-
|
|
420
|
+
|
|
373
421
|
Returns:
|
|
374
422
|
List of recovery attempts
|
|
423
|
+
|
|
375
424
|
"""
|
|
376
425
|
with sqlite3.connect(self.db_path) as conn:
|
|
377
|
-
cursor = conn.execute(
|
|
378
|
-
|
|
426
|
+
cursor = conn.execute(
|
|
427
|
+
"""
|
|
428
|
+
SELECT * FROM recovery_log
|
|
379
429
|
WHERE queue_id = ?
|
|
380
430
|
ORDER BY timestamp DESC
|
|
381
|
-
""",
|
|
382
|
-
|
|
431
|
+
""",
|
|
432
|
+
(queue_id,),
|
|
433
|
+
)
|
|
434
|
+
|
|
383
435
|
results = []
|
|
384
436
|
columns = [desc[0] for desc in cursor.description]
|
|
385
|
-
|
|
437
|
+
|
|
386
438
|
for row in cursor.fetchall():
|
|
387
|
-
recovery_info = dict(zip(columns, row))
|
|
439
|
+
recovery_info = dict(zip(columns, row, strict=False))
|
|
388
440
|
if recovery_info.get("recovery_data"):
|
|
389
|
-
recovery_info["recovery_data"] = json.loads(
|
|
441
|
+
recovery_info["recovery_data"] = json.loads(
|
|
442
|
+
recovery_info["recovery_data"]
|
|
443
|
+
)
|
|
390
444
|
results.append(recovery_info)
|
|
391
|
-
|
|
445
|
+
|
|
392
446
|
return results
|
|
393
|
-
|
|
447
|
+
|
|
394
448
|
def cleanup_old_entries(self, days: int = 30) -> int:
|
|
395
449
|
"""Clean up old completed entries.
|
|
396
|
-
|
|
450
|
+
|
|
397
451
|
Args:
|
|
398
452
|
days: Remove entries older than this many days
|
|
399
|
-
|
|
453
|
+
|
|
400
454
|
Returns:
|
|
401
455
|
Number of entries removed
|
|
456
|
+
|
|
402
457
|
"""
|
|
403
458
|
cutoff_date = (datetime.now() - timedelta(days=days)).isoformat()
|
|
404
|
-
|
|
459
|
+
|
|
405
460
|
with self._lock:
|
|
406
461
|
with sqlite3.connect(self.db_path) as conn:
|
|
407
|
-
cursor = conn.execute(
|
|
462
|
+
cursor = conn.execute(
|
|
463
|
+
"""
|
|
408
464
|
DELETE FROM ticket_registry
|
|
409
465
|
WHERE status IN ('completed', 'recovered')
|
|
410
466
|
AND updated_at < ?
|
|
411
|
-
""",
|
|
412
|
-
|
|
467
|
+
""",
|
|
468
|
+
(cutoff_date,),
|
|
469
|
+
)
|
|
470
|
+
|
|
413
471
|
deleted_count = cursor.rowcount
|
|
414
472
|
conn.commit()
|
|
415
|
-
|
|
473
|
+
|
|
416
474
|
return deleted_count
|