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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {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, Dict, List, Optional
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: Optional[Path] = None):
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
- conn.execute("""
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
- conn.execute("""
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: Dict[str, Any]
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
- queue_id,
105
- adapter,
106
- operation,
107
- title,
108
- "queued",
109
- datetime.now().isoformat(),
110
- datetime.now().isoformat(),
111
- json.dumps(ticket_data),
112
- 0
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: Optional[str] = None,
121
- result_data: Optional[Dict[str, Any]] = None,
122
- error_message: Optional[str] = None,
123
- retry_count: Optional[int] = None
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(f"""
171
+
172
+ conn.execute(
173
+ f"""
159
174
  UPDATE ticket_registry
160
175
  SET {', '.join(update_fields)}
161
176
  WHERE queue_id = ?
162
- """, values)
177
+ """,
178
+ values,
179
+ )
163
180
  conn.commit()
164
-
165
- def get_ticket_info(self, queue_id: str) -> Optional[Dict[str, Any]]:
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
- """, (queue_id,))
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) -> List[Dict[str, Any]]:
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
- SELECT * FROM ticket_registry
226
+ cursor = conn.execute(
227
+ """
228
+ SELECT * FROM ticket_registry
206
229
  WHERE ticket_id = ?
207
230
  ORDER BY created_at DESC
208
- """, (ticket_id,))
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) -> List[Dict[str, Any]]:
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
- SELECT * FROM ticket_registry
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
- """, (limit,))
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) -> List[Dict[str, Any]]:
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
- SELECT * FROM ticket_registry
268
- WHERE ticket_id IS NOT NULL
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) -> Dict[str, Any]:
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(queue_id, "recovered",
314
- result_data={"recovery": "marked_completed"})
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"] = "Marked as completed based on existing ticket ID"
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"] = "No ticket ID available to mark as completed"
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(queue_id, "queued",
324
- error_message=None,
325
- retry_count=ticket_info.get("retry_count", 0))
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(queue_id, recovery_type, recovery_data, recovery_data["success"])
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: Dict[str, Any],
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
- queue_id,
360
- recovery_type,
361
- json.dumps(recovery_data),
362
- datetime.now().isoformat(),
363
- success
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) -> List[Dict[str, Any]]:
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
- SELECT * FROM recovery_log
426
+ cursor = conn.execute(
427
+ """
428
+ SELECT * FROM recovery_log
379
429
  WHERE queue_id = ?
380
430
  ORDER BY timestamp DESC
381
- """, (queue_id,))
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(recovery_info["recovery_data"])
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
- """, (cutoff_date,))
412
-
467
+ """,
468
+ (cutoff_date,),
469
+ )
470
+
413
471
  deleted_count = cursor.rowcount
414
472
  conn.commit()
415
-
473
+
416
474
  return deleted_count