mcp-ticketer 0.1.20__py3-none-any.whl → 0.1.22__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 +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +54 -38
- mcp_ticketer/adapters/github.py +175 -109
- mcp_ticketer/adapters/hybrid.py +90 -45
- mcp_ticketer/adapters/jira.py +139 -130
- mcp_ticketer/adapters/linear.py +374 -225
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +14 -15
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +250 -293
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +10 -12
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +115 -60
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +36 -30
- mcp_ticketer/core/config.py +113 -77
- mcp_ticketer/core/env_discovery.py +51 -19
- mcp_ticketer/core/http_client.py +46 -29
- mcp_ticketer/core/mappers.py +79 -35
- mcp_ticketer/core/models.py +29 -15
- mcp_ticketer/core/project_config.py +131 -66
- mcp_ticketer/core/registry.py +12 -12
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +183 -129
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +29 -25
- mcp_ticketer/queue/queue.py +144 -82
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +48 -33
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.22.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.20.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/top_level.txt +0 -0
mcp_ticketer/queue/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Async queue system for mcp-ticketer."""
|
|
2
2
|
|
|
3
|
+
from .manager import WorkerManager
|
|
3
4
|
from .queue import Queue, QueueItem, QueueStatus
|
|
4
5
|
from .worker import Worker
|
|
5
|
-
from .manager import WorkerManager
|
|
6
6
|
|
|
7
|
-
__all__ = ["Queue", "QueueItem", "QueueStatus", "Worker", "WorkerManager"]
|
|
7
|
+
__all__ = ["Queue", "QueueItem", "QueueStatus", "Worker", "WorkerManager"]
|
mcp_ticketer/queue/__main__.py
CHANGED
mcp_ticketer/queue/manager.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"""Worker manager with file-based locking for single instance."""
|
|
2
2
|
|
|
3
|
+
import fcntl
|
|
4
|
+
import logging
|
|
3
5
|
import os
|
|
4
|
-
import psutil
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
import time
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
10
|
-
|
|
11
|
-
import
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import psutil
|
|
12
13
|
|
|
13
14
|
from .queue import Queue
|
|
14
|
-
from .worker import Worker
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -31,6 +31,7 @@ class WorkerManager:
|
|
|
31
31
|
|
|
32
32
|
Returns:
|
|
33
33
|
True if lock acquired, False otherwise
|
|
34
|
+
|
|
34
35
|
"""
|
|
35
36
|
try:
|
|
36
37
|
# Create lock file if it doesn't exist
|
|
@@ -46,13 +47,13 @@ class WorkerManager:
|
|
|
46
47
|
self.lock_fd.flush()
|
|
47
48
|
|
|
48
49
|
return True
|
|
49
|
-
except
|
|
50
|
+
except OSError:
|
|
50
51
|
# Lock already held
|
|
51
52
|
return False
|
|
52
53
|
|
|
53
54
|
def _release_lock(self):
|
|
54
55
|
"""Release worker lock."""
|
|
55
|
-
if hasattr(self,
|
|
56
|
+
if hasattr(self, "lock_fd"):
|
|
56
57
|
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
|
|
57
58
|
self.lock_fd.close()
|
|
58
59
|
|
|
@@ -65,6 +66,7 @@ class WorkerManager:
|
|
|
65
66
|
|
|
66
67
|
Returns:
|
|
67
68
|
True if worker started or already running, False otherwise
|
|
69
|
+
|
|
68
70
|
"""
|
|
69
71
|
# Check if worker is already running
|
|
70
72
|
if self.is_running():
|
|
@@ -84,6 +86,7 @@ class WorkerManager:
|
|
|
84
86
|
|
|
85
87
|
Returns:
|
|
86
88
|
True if started successfully, False otherwise
|
|
89
|
+
|
|
87
90
|
"""
|
|
88
91
|
# Check if already running
|
|
89
92
|
if self.is_running():
|
|
@@ -97,18 +100,14 @@ class WorkerManager:
|
|
|
97
100
|
|
|
98
101
|
try:
|
|
99
102
|
# Start worker in subprocess
|
|
100
|
-
cmd = [
|
|
101
|
-
sys.executable,
|
|
102
|
-
"-m",
|
|
103
|
-
"mcp_ticketer.queue.run_worker"
|
|
104
|
-
]
|
|
103
|
+
cmd = [sys.executable, "-m", "mcp_ticketer.queue.run_worker"]
|
|
105
104
|
|
|
106
105
|
# Start as background process
|
|
107
106
|
process = subprocess.Popen(
|
|
108
107
|
cmd,
|
|
109
108
|
stdout=subprocess.DEVNULL,
|
|
110
109
|
stderr=subprocess.DEVNULL,
|
|
111
|
-
start_new_session=True
|
|
110
|
+
start_new_session=True,
|
|
112
111
|
)
|
|
113
112
|
|
|
114
113
|
# Save PID
|
|
@@ -116,6 +115,7 @@ class WorkerManager:
|
|
|
116
115
|
|
|
117
116
|
# Give the process a moment to start
|
|
118
117
|
import time
|
|
118
|
+
|
|
119
119
|
time.sleep(0.5)
|
|
120
120
|
|
|
121
121
|
# Verify process is running
|
|
@@ -137,6 +137,7 @@ class WorkerManager:
|
|
|
137
137
|
|
|
138
138
|
Returns:
|
|
139
139
|
True if stopped successfully, False otherwise
|
|
140
|
+
|
|
140
141
|
"""
|
|
141
142
|
pid = self._get_pid()
|
|
142
143
|
if not pid:
|
|
@@ -177,6 +178,7 @@ class WorkerManager:
|
|
|
177
178
|
|
|
178
179
|
Returns:
|
|
179
180
|
True if restarted successfully, False otherwise
|
|
181
|
+
|
|
180
182
|
"""
|
|
181
183
|
logger.info("Restarting worker...")
|
|
182
184
|
self.stop()
|
|
@@ -188,6 +190,7 @@ class WorkerManager:
|
|
|
188
190
|
|
|
189
191
|
Returns:
|
|
190
192
|
True if running, False otherwise
|
|
193
|
+
|
|
191
194
|
"""
|
|
192
195
|
pid = self._get_pid()
|
|
193
196
|
if not pid:
|
|
@@ -209,25 +212,25 @@ class WorkerManager:
|
|
|
209
212
|
|
|
210
213
|
Returns:
|
|
211
214
|
Status information
|
|
215
|
+
|
|
212
216
|
"""
|
|
213
217
|
is_running = self.is_running()
|
|
214
218
|
pid = self._get_pid() if is_running else None
|
|
215
219
|
|
|
216
|
-
status = {
|
|
217
|
-
"running": is_running,
|
|
218
|
-
"pid": pid
|
|
219
|
-
}
|
|
220
|
+
status = {"running": is_running, "pid": pid}
|
|
220
221
|
|
|
221
222
|
# Add process info if running
|
|
222
223
|
if is_running and pid:
|
|
223
224
|
try:
|
|
224
225
|
process = psutil.Process(pid)
|
|
225
|
-
status.update(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
status.update(
|
|
227
|
+
{
|
|
228
|
+
"cpu_percent": process.cpu_percent(),
|
|
229
|
+
"memory_mb": process.memory_info().rss / 1024 / 1024,
|
|
230
|
+
"create_time": process.create_time(),
|
|
231
|
+
"status": process.status(),
|
|
232
|
+
}
|
|
233
|
+
)
|
|
231
234
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
232
235
|
pass
|
|
233
236
|
|
|
@@ -242,6 +245,7 @@ class WorkerManager:
|
|
|
242
245
|
|
|
243
246
|
Returns:
|
|
244
247
|
Process ID or None if not found
|
|
248
|
+
|
|
245
249
|
"""
|
|
246
250
|
if not self.pid_file.exists():
|
|
247
251
|
return None
|
|
@@ -249,7 +253,7 @@ class WorkerManager:
|
|
|
249
253
|
try:
|
|
250
254
|
pid_text = self.pid_file.read_text().strip()
|
|
251
255
|
return int(pid_text)
|
|
252
|
-
except (
|
|
256
|
+
except (OSError, ValueError):
|
|
253
257
|
return None
|
|
254
258
|
|
|
255
259
|
def _cleanup(self):
|
|
@@ -258,4 +262,4 @@ class WorkerManager:
|
|
|
258
262
|
if self.pid_file.exists():
|
|
259
263
|
self.pid_file.unlink()
|
|
260
264
|
if self.lock_file.exists():
|
|
261
|
-
self.lock_file.unlink()
|
|
265
|
+
self.lock_file.unlink()
|
mcp_ticketer/queue/queue.py
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
"""SQLite-based queue system for async ticket operations."""
|
|
2
2
|
|
|
3
|
-
import sqlite3
|
|
4
3
|
import json
|
|
4
|
+
import sqlite3
|
|
5
5
|
import threading
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
6
8
|
from datetime import datetime, timedelta
|
|
7
9
|
from enum import Enum
|
|
8
10
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
10
|
-
from dataclasses import dataclass, asdict
|
|
11
|
-
import uuid
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class QueueStatus(str, Enum):
|
|
15
15
|
"""Queue item status values."""
|
|
16
|
+
|
|
16
17
|
PENDING = "pending"
|
|
17
18
|
PROCESSING = "processing"
|
|
18
19
|
COMPLETED = "completed"
|
|
@@ -22,6 +23,7 @@ class QueueStatus(str, Enum):
|
|
|
22
23
|
@dataclass
|
|
23
24
|
class QueueItem:
|
|
24
25
|
"""Represents a queued operation."""
|
|
26
|
+
|
|
25
27
|
id: str
|
|
26
28
|
ticket_data: Dict[str, Any]
|
|
27
29
|
adapter: str
|
|
@@ -37,9 +39,9 @@ class QueueItem:
|
|
|
37
39
|
def to_dict(self) -> dict:
|
|
38
40
|
"""Convert to dictionary for storage."""
|
|
39
41
|
data = asdict(self)
|
|
40
|
-
data[
|
|
42
|
+
data["created_at"] = self.created_at.isoformat()
|
|
41
43
|
if self.processed_at:
|
|
42
|
-
data[
|
|
44
|
+
data["processed_at"] = self.processed_at.isoformat()
|
|
43
45
|
return data
|
|
44
46
|
|
|
45
47
|
@classmethod
|
|
@@ -56,7 +58,7 @@ class QueueItem:
|
|
|
56
58
|
error_message=row[7],
|
|
57
59
|
retry_count=row[8],
|
|
58
60
|
result=json.loads(row[9]) if row[9] else None,
|
|
59
|
-
project_dir=row[10] if len(row) > 10 else None
|
|
61
|
+
project_dir=row[10] if len(row) > 10 else None,
|
|
60
62
|
)
|
|
61
63
|
|
|
62
64
|
|
|
@@ -68,6 +70,7 @@ class Queue:
|
|
|
68
70
|
|
|
69
71
|
Args:
|
|
70
72
|
db_path: Path to SQLite database. Defaults to ~/.mcp-ticketer/queue.db
|
|
73
|
+
|
|
71
74
|
"""
|
|
72
75
|
if db_path is None:
|
|
73
76
|
db_dir = Path.home() / ".mcp-ticketer"
|
|
@@ -81,7 +84,8 @@ class Queue:
|
|
|
81
84
|
def _init_database(self):
|
|
82
85
|
"""Initialize database schema."""
|
|
83
86
|
with sqlite3.connect(self.db_path) as conn:
|
|
84
|
-
conn.execute(
|
|
87
|
+
conn.execute(
|
|
88
|
+
"""
|
|
85
89
|
CREATE TABLE IF NOT EXISTS queue (
|
|
86
90
|
id TEXT PRIMARY KEY,
|
|
87
91
|
ticket_data TEXT NOT NULL,
|
|
@@ -95,35 +99,44 @@ class Queue:
|
|
|
95
99
|
result TEXT,
|
|
96
100
|
CHECK (status IN ('pending', 'processing', 'completed', 'failed'))
|
|
97
101
|
)
|
|
98
|
-
|
|
102
|
+
"""
|
|
103
|
+
)
|
|
99
104
|
|
|
100
105
|
# Create indices for efficient queries
|
|
101
|
-
conn.execute(
|
|
106
|
+
conn.execute(
|
|
107
|
+
"""
|
|
102
108
|
CREATE INDEX IF NOT EXISTS idx_queue_status
|
|
103
109
|
ON queue(status)
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
"""
|
|
111
|
+
)
|
|
112
|
+
conn.execute(
|
|
113
|
+
"""
|
|
106
114
|
CREATE INDEX IF NOT EXISTS idx_queue_created
|
|
107
115
|
ON queue(created_at)
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
"""
|
|
117
|
+
)
|
|
118
|
+
conn.execute(
|
|
119
|
+
"""
|
|
110
120
|
CREATE INDEX IF NOT EXISTS idx_queue_adapter
|
|
111
121
|
ON queue(adapter)
|
|
112
|
-
|
|
122
|
+
"""
|
|
123
|
+
)
|
|
113
124
|
|
|
114
125
|
# Migration: Add project_dir column if it doesn't exist
|
|
115
126
|
cursor = conn.execute("PRAGMA table_info(queue)")
|
|
116
127
|
columns = [row[1] for row in cursor.fetchall()]
|
|
117
|
-
if
|
|
118
|
-
conn.execute(
|
|
128
|
+
if "project_dir" not in columns:
|
|
129
|
+
conn.execute("ALTER TABLE queue ADD COLUMN project_dir TEXT")
|
|
119
130
|
|
|
120
131
|
conn.commit()
|
|
121
132
|
|
|
122
|
-
def add(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
def add(
|
|
134
|
+
self,
|
|
135
|
+
ticket_data: Dict[str, Any],
|
|
136
|
+
adapter: str,
|
|
137
|
+
operation: str,
|
|
138
|
+
project_dir: Optional[str] = None,
|
|
139
|
+
) -> str:
|
|
127
140
|
"""Add item to queue.
|
|
128
141
|
|
|
129
142
|
Args:
|
|
@@ -134,6 +147,7 @@ class Queue:
|
|
|
134
147
|
|
|
135
148
|
Returns:
|
|
136
149
|
Queue ID for tracking
|
|
150
|
+
|
|
137
151
|
"""
|
|
138
152
|
queue_id = f"Q-{uuid.uuid4().hex[:8].upper()}"
|
|
139
153
|
|
|
@@ -143,21 +157,24 @@ class Queue:
|
|
|
143
157
|
|
|
144
158
|
with self._lock:
|
|
145
159
|
with sqlite3.connect(self.db_path) as conn:
|
|
146
|
-
conn.execute(
|
|
160
|
+
conn.execute(
|
|
161
|
+
"""
|
|
147
162
|
INSERT INTO queue (
|
|
148
163
|
id, ticket_data, adapter, operation,
|
|
149
164
|
status, created_at, retry_count, project_dir
|
|
150
165
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
""",
|
|
167
|
+
(
|
|
168
|
+
queue_id,
|
|
169
|
+
json.dumps(ticket_data),
|
|
170
|
+
adapter,
|
|
171
|
+
operation,
|
|
172
|
+
QueueStatus.PENDING.value,
|
|
173
|
+
datetime.now().isoformat(),
|
|
174
|
+
0,
|
|
175
|
+
project_dir,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
161
178
|
conn.commit()
|
|
162
179
|
|
|
163
180
|
return queue_id
|
|
@@ -167,25 +184,32 @@ class Queue:
|
|
|
167
184
|
|
|
168
185
|
Returns:
|
|
169
186
|
Next pending QueueItem or None if queue is empty
|
|
187
|
+
|
|
170
188
|
"""
|
|
171
189
|
with self._lock:
|
|
172
190
|
with sqlite3.connect(self.db_path) as conn:
|
|
173
191
|
# Get next pending item ordered by creation time
|
|
174
|
-
cursor = conn.execute(
|
|
192
|
+
cursor = conn.execute(
|
|
193
|
+
"""
|
|
175
194
|
SELECT * FROM queue
|
|
176
195
|
WHERE status = ?
|
|
177
196
|
ORDER BY created_at
|
|
178
197
|
LIMIT 1
|
|
179
|
-
|
|
198
|
+
""",
|
|
199
|
+
(QueueStatus.PENDING.value,),
|
|
200
|
+
)
|
|
180
201
|
|
|
181
202
|
row = cursor.fetchone()
|
|
182
203
|
if row:
|
|
183
204
|
# Mark as processing
|
|
184
|
-
conn.execute(
|
|
205
|
+
conn.execute(
|
|
206
|
+
"""
|
|
185
207
|
UPDATE queue
|
|
186
208
|
SET status = ?
|
|
187
209
|
WHERE id = ?
|
|
188
|
-
|
|
210
|
+
""",
|
|
211
|
+
(QueueStatus.PROCESSING.value, row[0]),
|
|
212
|
+
)
|
|
189
213
|
conn.commit()
|
|
190
214
|
|
|
191
215
|
# Create QueueItem from row and update status
|
|
@@ -195,11 +219,13 @@ class Queue:
|
|
|
195
219
|
|
|
196
220
|
return None
|
|
197
221
|
|
|
198
|
-
def update_status(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
222
|
+
def update_status(
|
|
223
|
+
self,
|
|
224
|
+
queue_id: str,
|
|
225
|
+
status: QueueStatus,
|
|
226
|
+
error_message: Optional[str] = None,
|
|
227
|
+
result: Optional[Dict[str, Any]] = None,
|
|
228
|
+
):
|
|
203
229
|
"""Update queue item status.
|
|
204
230
|
|
|
205
231
|
Args:
|
|
@@ -207,25 +233,31 @@ class Queue:
|
|
|
207
233
|
status: New status
|
|
208
234
|
error_message: Error message if failed
|
|
209
235
|
result: Result data if completed
|
|
236
|
+
|
|
210
237
|
"""
|
|
211
238
|
with self._lock:
|
|
212
239
|
with sqlite3.connect(self.db_path) as conn:
|
|
213
|
-
processed_at =
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
processed_at = (
|
|
241
|
+
datetime.now().isoformat()
|
|
242
|
+
if status in [QueueStatus.COMPLETED, QueueStatus.FAILED]
|
|
243
|
+
else None
|
|
244
|
+
)
|
|
216
245
|
|
|
217
|
-
conn.execute(
|
|
246
|
+
conn.execute(
|
|
247
|
+
"""
|
|
218
248
|
UPDATE queue
|
|
219
249
|
SET status = ?, processed_at = ?,
|
|
220
250
|
error_message = ?, result = ?
|
|
221
251
|
WHERE id = ?
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
252
|
+
""",
|
|
253
|
+
(
|
|
254
|
+
status.value,
|
|
255
|
+
processed_at,
|
|
256
|
+
error_message,
|
|
257
|
+
json.dumps(result) if result else None,
|
|
258
|
+
queue_id,
|
|
259
|
+
),
|
|
260
|
+
)
|
|
229
261
|
conn.commit()
|
|
230
262
|
|
|
231
263
|
def increment_retry(self, queue_id: str) -> int:
|
|
@@ -236,16 +268,20 @@ class Queue:
|
|
|
236
268
|
|
|
237
269
|
Returns:
|
|
238
270
|
New retry count
|
|
271
|
+
|
|
239
272
|
"""
|
|
240
273
|
with self._lock:
|
|
241
274
|
with sqlite3.connect(self.db_path) as conn:
|
|
242
|
-
cursor = conn.execute(
|
|
275
|
+
cursor = conn.execute(
|
|
276
|
+
"""
|
|
243
277
|
UPDATE queue
|
|
244
278
|
SET retry_count = retry_count + 1,
|
|
245
279
|
status = ?
|
|
246
280
|
WHERE id = ?
|
|
247
281
|
RETURNING retry_count
|
|
248
|
-
|
|
282
|
+
""",
|
|
283
|
+
(QueueStatus.PENDING.value, queue_id),
|
|
284
|
+
)
|
|
249
285
|
|
|
250
286
|
result = cursor.fetchone()
|
|
251
287
|
conn.commit()
|
|
@@ -259,18 +295,22 @@ class Queue:
|
|
|
259
295
|
|
|
260
296
|
Returns:
|
|
261
297
|
QueueItem or None if not found
|
|
298
|
+
|
|
262
299
|
"""
|
|
263
300
|
with sqlite3.connect(self.db_path) as conn:
|
|
264
|
-
cursor = conn.execute(
|
|
301
|
+
cursor = conn.execute(
|
|
302
|
+
"""
|
|
265
303
|
SELECT * FROM queue WHERE id = ?
|
|
266
|
-
|
|
304
|
+
""",
|
|
305
|
+
(queue_id,),
|
|
306
|
+
)
|
|
267
307
|
|
|
268
308
|
row = cursor.fetchone()
|
|
269
309
|
return QueueItem.from_row(row) if row else None
|
|
270
310
|
|
|
271
|
-
def list_items(
|
|
272
|
-
|
|
273
|
-
|
|
311
|
+
def list_items(
|
|
312
|
+
self, status: Optional[QueueStatus] = None, limit: int = 50
|
|
313
|
+
) -> List[QueueItem]:
|
|
274
314
|
"""List queue items.
|
|
275
315
|
|
|
276
316
|
Args:
|
|
@@ -279,21 +319,28 @@ class Queue:
|
|
|
279
319
|
|
|
280
320
|
Returns:
|
|
281
321
|
List of QueueItems
|
|
322
|
+
|
|
282
323
|
"""
|
|
283
324
|
with sqlite3.connect(self.db_path) as conn:
|
|
284
325
|
if status:
|
|
285
|
-
cursor = conn.execute(
|
|
326
|
+
cursor = conn.execute(
|
|
327
|
+
"""
|
|
286
328
|
SELECT * FROM queue
|
|
287
329
|
WHERE status = ?
|
|
288
330
|
ORDER BY created_at DESC
|
|
289
331
|
LIMIT ?
|
|
290
|
-
|
|
332
|
+
""",
|
|
333
|
+
(status.value, limit),
|
|
334
|
+
)
|
|
291
335
|
else:
|
|
292
|
-
cursor = conn.execute(
|
|
336
|
+
cursor = conn.execute(
|
|
337
|
+
"""
|
|
293
338
|
SELECT * FROM queue
|
|
294
339
|
ORDER BY created_at DESC
|
|
295
340
|
LIMIT ?
|
|
296
|
-
|
|
341
|
+
""",
|
|
342
|
+
(limit,),
|
|
343
|
+
)
|
|
297
344
|
|
|
298
345
|
return [QueueItem.from_row(row) for row in cursor.fetchall()]
|
|
299
346
|
|
|
@@ -302,12 +349,16 @@ class Queue:
|
|
|
302
349
|
|
|
303
350
|
Returns:
|
|
304
351
|
Number of pending items
|
|
352
|
+
|
|
305
353
|
"""
|
|
306
354
|
with sqlite3.connect(self.db_path) as conn:
|
|
307
|
-
cursor = conn.execute(
|
|
355
|
+
cursor = conn.execute(
|
|
356
|
+
"""
|
|
308
357
|
SELECT COUNT(*) FROM queue
|
|
309
358
|
WHERE status = ?
|
|
310
|
-
|
|
359
|
+
""",
|
|
360
|
+
(QueueStatus.PENDING.value,),
|
|
361
|
+
)
|
|
311
362
|
|
|
312
363
|
return cursor.fetchone()[0]
|
|
313
364
|
|
|
@@ -316,20 +367,24 @@ class Queue:
|
|
|
316
367
|
|
|
317
368
|
Args:
|
|
318
369
|
days: Delete items older than this many days
|
|
370
|
+
|
|
319
371
|
"""
|
|
320
372
|
cutoff_date = (datetime.now() - timedelta(days=days)).isoformat()
|
|
321
373
|
|
|
322
374
|
with self._lock:
|
|
323
375
|
with sqlite3.connect(self.db_path) as conn:
|
|
324
|
-
conn.execute(
|
|
376
|
+
conn.execute(
|
|
377
|
+
"""
|
|
325
378
|
DELETE FROM queue
|
|
326
379
|
WHERE status IN (?, ?)
|
|
327
380
|
AND processed_at < ?
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
381
|
+
""",
|
|
382
|
+
(
|
|
383
|
+
QueueStatus.COMPLETED.value,
|
|
384
|
+
QueueStatus.FAILED.value,
|
|
385
|
+
cutoff_date,
|
|
386
|
+
),
|
|
387
|
+
)
|
|
333
388
|
conn.commit()
|
|
334
389
|
|
|
335
390
|
def reset_stuck_items(self, timeout_minutes: int = 30):
|
|
@@ -337,22 +392,26 @@ class Queue:
|
|
|
337
392
|
|
|
338
393
|
Args:
|
|
339
394
|
timeout_minutes: Consider items stuck after this many minutes
|
|
395
|
+
|
|
340
396
|
"""
|
|
341
397
|
cutoff_time = (datetime.now() - timedelta(minutes=timeout_minutes)).isoformat()
|
|
342
398
|
|
|
343
399
|
with self._lock:
|
|
344
400
|
with sqlite3.connect(self.db_path) as conn:
|
|
345
|
-
conn.execute(
|
|
401
|
+
conn.execute(
|
|
402
|
+
"""
|
|
346
403
|
UPDATE queue
|
|
347
404
|
SET status = ?, error_message = ?
|
|
348
405
|
WHERE status = ?
|
|
349
406
|
AND created_at < ?
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
407
|
+
""",
|
|
408
|
+
(
|
|
409
|
+
QueueStatus.PENDING.value,
|
|
410
|
+
"Reset from stuck processing state",
|
|
411
|
+
QueueStatus.PROCESSING.value,
|
|
412
|
+
cutoff_time,
|
|
413
|
+
),
|
|
414
|
+
)
|
|
356
415
|
conn.commit()
|
|
357
416
|
|
|
358
417
|
def get_stats(self) -> Dict[str, int]:
|
|
@@ -360,16 +419,19 @@ class Queue:
|
|
|
360
419
|
|
|
361
420
|
Returns:
|
|
362
421
|
Dictionary with counts by status
|
|
422
|
+
|
|
363
423
|
"""
|
|
364
424
|
with sqlite3.connect(self.db_path) as conn:
|
|
365
|
-
cursor = conn.execute(
|
|
425
|
+
cursor = conn.execute(
|
|
426
|
+
"""
|
|
366
427
|
SELECT status, COUNT(*)
|
|
367
428
|
FROM queue
|
|
368
429
|
GROUP BY status
|
|
369
|
-
|
|
430
|
+
"""
|
|
431
|
+
)
|
|
370
432
|
|
|
371
433
|
stats = {status.value: 0 for status in QueueStatus}
|
|
372
434
|
for status, count in cursor.fetchall():
|
|
373
435
|
stats[status] = count
|
|
374
436
|
|
|
375
|
-
return stats
|
|
437
|
+
return stats
|
mcp_ticketer/queue/run_worker.py
CHANGED
|
@@ -8,8 +8,7 @@ from .worker import Worker
|
|
|
8
8
|
|
|
9
9
|
# Set up logging
|
|
10
10
|
logging.basicConfig(
|
|
11
|
-
level=logging.INFO,
|
|
12
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
11
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
13
12
|
)
|
|
14
13
|
logger = logging.getLogger(__name__)
|
|
15
14
|
|
|
@@ -35,4 +34,4 @@ def main():
|
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
if __name__ == "__main__":
|
|
38
|
-
main()
|
|
37
|
+
main()
|