mcp-ticketer 0.4.11__py3-none-any.whl → 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +9 -3
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +313 -96
- mcp_ticketer/adapters/jira.py +251 -1
- mcp_ticketer/adapters/linear/adapter.py +524 -22
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +1 -1
- mcp_ticketer/cli/codex_configure.py +80 -1
- mcp_ticketer/cli/configure.py +33 -43
- mcp_ticketer/cli/diagnostics.py +18 -16
- mcp_ticketer/cli/discover.py +288 -21
- mcp_ticketer/cli/gemini_configure.py +1 -1
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +1199 -227
- mcp_ticketer/cli/mcp_configure.py +1 -1
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +14 -13
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +12 -0
- mcp_ticketer/core/adapter.py +4 -4
- mcp_ticketer/core/config.py +17 -10
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +1 -1
- mcp_ticketer/core/models.py +1 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +17 -1
- mcp_ticketer/core/registry.py +1 -1
- 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/main.py +82 -69
- mcp_ticketer/mcp/server/tools/__init__.py +9 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -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 +14 -12
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""User-specific ticket management tools.
|
|
2
|
+
|
|
3
|
+
This module provides tools for managing tickets from a user's perspective,
|
|
4
|
+
including listing assigned tickets and transitioning tickets through workflow
|
|
5
|
+
states with validation.
|
|
6
|
+
|
|
7
|
+
Design Decision: Workflow State Validation
|
|
8
|
+
------------------------------------------
|
|
9
|
+
State transitions are validated using TicketState.can_transition_to() to ensure
|
|
10
|
+
tickets follow the defined workflow. This prevents invalid state changes that
|
|
11
|
+
could break integrations or confuse team members.
|
|
12
|
+
|
|
13
|
+
Valid workflow transitions:
|
|
14
|
+
- OPEN → IN_PROGRESS, WAITING, BLOCKED, CLOSED
|
|
15
|
+
- IN_PROGRESS → READY, WAITING, BLOCKED, OPEN
|
|
16
|
+
- READY → TESTED, IN_PROGRESS, BLOCKED
|
|
17
|
+
- TESTED → DONE, IN_PROGRESS
|
|
18
|
+
- DONE → CLOSED
|
|
19
|
+
- WAITING/BLOCKED → OPEN, IN_PROGRESS, CLOSED
|
|
20
|
+
- CLOSED → (no transitions, terminal state)
|
|
21
|
+
|
|
22
|
+
Performance Considerations:
|
|
23
|
+
- get_my_tickets uses adapter's native filtering when available
|
|
24
|
+
- Falls back to client-side filtering for adapters without assignee filter
|
|
25
|
+
- State transition validation is O(1) lookup in predefined state machine
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from ....core.models import TicketState
|
|
32
|
+
from ....core.project_config import ConfigResolver, TicketerConfig
|
|
33
|
+
from ..server_sdk import get_adapter, mcp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_config_resolver() -> ConfigResolver:
|
|
37
|
+
"""Get configuration resolver for current project.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
ConfigResolver instance for current working directory
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
return ConfigResolver(project_path=Path.cwd())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp.tool()
|
|
47
|
+
async def get_my_tickets(
|
|
48
|
+
state: str | None = None,
|
|
49
|
+
limit: int = 10,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
"""Get tickets assigned to the configured default user.
|
|
52
|
+
|
|
53
|
+
Retrieves tickets assigned to the user specified in default_user configuration.
|
|
54
|
+
Requires default_user to be set via config_set_default_user().
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
state: Optional state filter - must be one of: open, in_progress, ready,
|
|
58
|
+
tested, done, closed, waiting, blocked
|
|
59
|
+
limit: Maximum number of tickets to return (default: 10, max: 100)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dictionary containing:
|
|
63
|
+
- status: "completed" or "error"
|
|
64
|
+
- tickets: List of ticket objects assigned to user
|
|
65
|
+
- count: Number of tickets returned
|
|
66
|
+
- user: User ID that was queried
|
|
67
|
+
- state_filter: State filter applied (if any)
|
|
68
|
+
- error: Error details (if failed)
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> result = await get_my_tickets(state="in_progress", limit=5)
|
|
72
|
+
>>> print(result)
|
|
73
|
+
{
|
|
74
|
+
"status": "completed",
|
|
75
|
+
"tickets": [
|
|
76
|
+
{"id": "TICKET-1", "title": "Fix bug", "state": "in_progress"},
|
|
77
|
+
{"id": "TICKET-2", "title": "Add feature", "state": "in_progress"}
|
|
78
|
+
],
|
|
79
|
+
"count": 2,
|
|
80
|
+
"user": "user@example.com",
|
|
81
|
+
"state_filter": "in_progress"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Error Conditions:
|
|
85
|
+
- No default user configured: Returns error with setup instructions
|
|
86
|
+
- Invalid state: Returns error with valid state options
|
|
87
|
+
- Adapter query failure: Returns error with details
|
|
88
|
+
|
|
89
|
+
Usage Notes:
|
|
90
|
+
- Requires default_user to be set in configuration
|
|
91
|
+
- Use config_set_default_user() to configure the user first
|
|
92
|
+
- Limit is capped at 100 to prevent performance issues
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
# Validate limit
|
|
97
|
+
if limit > 100:
|
|
98
|
+
limit = 100
|
|
99
|
+
|
|
100
|
+
# Load configuration to get default user
|
|
101
|
+
resolver = get_config_resolver()
|
|
102
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
103
|
+
|
|
104
|
+
if not config.default_user:
|
|
105
|
+
return {
|
|
106
|
+
"status": "error",
|
|
107
|
+
"error": "No default user configured. Use config_set_default_user() to set a default user first.",
|
|
108
|
+
"setup_command": "config_set_default_user",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Validate state if provided
|
|
112
|
+
state_filter = None
|
|
113
|
+
if state is not None:
|
|
114
|
+
try:
|
|
115
|
+
state_filter = TicketState(state.lower())
|
|
116
|
+
except ValueError:
|
|
117
|
+
valid_states = [s.value for s in TicketState]
|
|
118
|
+
return {
|
|
119
|
+
"status": "error",
|
|
120
|
+
"error": f"Invalid state '{state}'. Must be one of: {', '.join(valid_states)}",
|
|
121
|
+
"valid_states": valid_states,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Build filters
|
|
125
|
+
filters: dict[str, Any] = {"assignee": config.default_user}
|
|
126
|
+
if state_filter:
|
|
127
|
+
filters["state"] = state_filter
|
|
128
|
+
|
|
129
|
+
# Query adapter
|
|
130
|
+
adapter = get_adapter()
|
|
131
|
+
tickets = await adapter.list(limit=limit, offset=0, filters=filters)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"status": "completed",
|
|
135
|
+
"tickets": [ticket.model_dump() for ticket in tickets],
|
|
136
|
+
"count": len(tickets),
|
|
137
|
+
"user": config.default_user,
|
|
138
|
+
"state_filter": state if state else "all",
|
|
139
|
+
"limit": limit,
|
|
140
|
+
}
|
|
141
|
+
except Exception as e:
|
|
142
|
+
return {
|
|
143
|
+
"status": "error",
|
|
144
|
+
"error": f"Failed to retrieve tickets: {str(e)}",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@mcp.tool()
|
|
149
|
+
async def get_available_transitions(ticket_id: str) -> dict[str, Any]:
|
|
150
|
+
"""Get valid next states for a ticket based on workflow rules.
|
|
151
|
+
|
|
152
|
+
Retrieves the ticket's current state and returns all valid target states
|
|
153
|
+
according to the defined workflow state machine. This helps AI agents and
|
|
154
|
+
users understand which state transitions are allowed.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
ticket_id: Unique identifier of the ticket
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Dictionary containing:
|
|
161
|
+
- status: "completed" or "error"
|
|
162
|
+
- ticket_id: ID of the queried ticket
|
|
163
|
+
- current_state: Current workflow state
|
|
164
|
+
- available_transitions: List of valid target states
|
|
165
|
+
- transition_descriptions: Human-readable descriptions of each transition
|
|
166
|
+
- error: Error details (if failed)
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
>>> result = await get_available_transitions("TICKET-123")
|
|
170
|
+
>>> print(result)
|
|
171
|
+
{
|
|
172
|
+
"status": "completed",
|
|
173
|
+
"ticket_id": "TICKET-123",
|
|
174
|
+
"current_state": "in_progress",
|
|
175
|
+
"available_transitions": ["ready", "waiting", "blocked", "open"],
|
|
176
|
+
"transition_descriptions": {
|
|
177
|
+
"ready": "Mark work as complete and ready for review",
|
|
178
|
+
"waiting": "Pause work while waiting for external dependency",
|
|
179
|
+
"blocked": "Work is blocked by an impediment",
|
|
180
|
+
"open": "Move back to backlog"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Error Conditions:
|
|
185
|
+
- Ticket not found: Returns error with ticket ID
|
|
186
|
+
- Adapter query failure: Returns error with details
|
|
187
|
+
- Terminal state (CLOSED): Returns empty transitions list
|
|
188
|
+
|
|
189
|
+
Usage Notes:
|
|
190
|
+
- CLOSED is a terminal state with no valid transitions
|
|
191
|
+
- Use this before ticket_transition() to validate intended state change
|
|
192
|
+
- Transition validation prevents workflow violations
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
# Get ticket from adapter
|
|
197
|
+
adapter = get_adapter()
|
|
198
|
+
ticket = await adapter.read(ticket_id)
|
|
199
|
+
|
|
200
|
+
if ticket is None:
|
|
201
|
+
return {
|
|
202
|
+
"status": "error",
|
|
203
|
+
"error": f"Ticket {ticket_id} not found",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Get current state
|
|
207
|
+
current_state = ticket.state
|
|
208
|
+
|
|
209
|
+
# Get valid transitions from state machine
|
|
210
|
+
valid_transitions = TicketState.valid_transitions()
|
|
211
|
+
# Handle both TicketState enum and string values
|
|
212
|
+
if isinstance(current_state, str):
|
|
213
|
+
current_state = TicketState(current_state)
|
|
214
|
+
available = valid_transitions.get(current_state, [])
|
|
215
|
+
|
|
216
|
+
# Create human-readable descriptions
|
|
217
|
+
descriptions = {
|
|
218
|
+
TicketState.OPEN: "Move to backlog (not yet started)",
|
|
219
|
+
TicketState.IN_PROGRESS: "Begin active work on ticket",
|
|
220
|
+
TicketState.READY: "Mark as complete and ready for review/testing",
|
|
221
|
+
TicketState.TESTED: "Mark as tested and verified",
|
|
222
|
+
TicketState.DONE: "Mark as complete and accepted",
|
|
223
|
+
TicketState.WAITING: "Pause work while waiting for external dependency",
|
|
224
|
+
TicketState.BLOCKED: "Work is blocked by an impediment",
|
|
225
|
+
TicketState.CLOSED: "Close and archive ticket (final state)",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
transition_descriptions = {
|
|
229
|
+
state.value: descriptions.get(state, "") for state in available
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"status": "completed",
|
|
234
|
+
"ticket_id": ticket_id,
|
|
235
|
+
"current_state": current_state.value,
|
|
236
|
+
"available_transitions": [state.value for state in available],
|
|
237
|
+
"transition_descriptions": transition_descriptions,
|
|
238
|
+
"is_terminal": len(available) == 0,
|
|
239
|
+
}
|
|
240
|
+
except Exception as e:
|
|
241
|
+
return {
|
|
242
|
+
"status": "error",
|
|
243
|
+
"error": f"Failed to get available transitions: {str(e)}",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@mcp.tool()
|
|
248
|
+
async def ticket_transition(
|
|
249
|
+
ticket_id: str,
|
|
250
|
+
to_state: str,
|
|
251
|
+
comment: str | None = None,
|
|
252
|
+
) -> dict[str, Any]:
|
|
253
|
+
"""Move ticket through workflow with validation and optional comment.
|
|
254
|
+
|
|
255
|
+
Transitions a ticket to a new state, validating the transition against the
|
|
256
|
+
defined workflow rules. Optionally adds a comment explaining the transition.
|
|
257
|
+
|
|
258
|
+
Workflow State Machine:
|
|
259
|
+
OPEN → IN_PROGRESS, WAITING, BLOCKED, CLOSED
|
|
260
|
+
IN_PROGRESS → READY, WAITING, BLOCKED, OPEN
|
|
261
|
+
READY → TESTED, IN_PROGRESS, BLOCKED
|
|
262
|
+
TESTED → DONE, IN_PROGRESS
|
|
263
|
+
DONE → CLOSED
|
|
264
|
+
WAITING → OPEN, IN_PROGRESS, CLOSED
|
|
265
|
+
BLOCKED → OPEN, IN_PROGRESS, CLOSED
|
|
266
|
+
CLOSED → (no transitions)
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
ticket_id: Unique identifier of the ticket to transition
|
|
270
|
+
to_state: Target state - must be valid for current state
|
|
271
|
+
comment: Optional comment explaining the transition reason
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Dictionary containing:
|
|
275
|
+
- status: "completed" or "error"
|
|
276
|
+
- ticket: Updated ticket object with new state
|
|
277
|
+
- previous_state: State before transition
|
|
278
|
+
- new_state: State after transition
|
|
279
|
+
- comment_added: Whether a comment was added (if applicable)
|
|
280
|
+
- error: Error details (if failed)
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> result = await ticket_transition(
|
|
284
|
+
... "TICKET-123",
|
|
285
|
+
... "ready",
|
|
286
|
+
... "Work complete, ready for code review"
|
|
287
|
+
... )
|
|
288
|
+
>>> print(result)
|
|
289
|
+
{
|
|
290
|
+
"status": "completed",
|
|
291
|
+
"ticket": {"id": "TICKET-123", "state": "ready", ...},
|
|
292
|
+
"previous_state": "in_progress",
|
|
293
|
+
"new_state": "ready",
|
|
294
|
+
"comment_added": True
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
Error Conditions:
|
|
298
|
+
- Ticket not found: Returns error with ticket ID
|
|
299
|
+
- Invalid transition: Returns error with valid options
|
|
300
|
+
- Invalid state name: Returns error with valid states
|
|
301
|
+
- Adapter update failure: Returns error with details
|
|
302
|
+
|
|
303
|
+
Usage Notes:
|
|
304
|
+
- Use get_available_transitions() first to see valid options
|
|
305
|
+
- Comments are adapter-dependent (some may not support them)
|
|
306
|
+
- Validation prevents workflow violations
|
|
307
|
+
- Terminal state (CLOSED) has no valid transitions
|
|
308
|
+
|
|
309
|
+
"""
|
|
310
|
+
try:
|
|
311
|
+
# Get ticket from adapter
|
|
312
|
+
adapter = get_adapter()
|
|
313
|
+
ticket = await adapter.read(ticket_id)
|
|
314
|
+
|
|
315
|
+
if ticket is None:
|
|
316
|
+
return {
|
|
317
|
+
"status": "error",
|
|
318
|
+
"error": f"Ticket {ticket_id} not found",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Validate target state
|
|
322
|
+
try:
|
|
323
|
+
target_state = TicketState(to_state.lower())
|
|
324
|
+
except ValueError:
|
|
325
|
+
valid_states = [s.value for s in TicketState]
|
|
326
|
+
return {
|
|
327
|
+
"status": "error",
|
|
328
|
+
"error": f"Invalid state '{to_state}'. Must be one of: {', '.join(valid_states)}",
|
|
329
|
+
"valid_states": valid_states,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Store current state for response
|
|
333
|
+
current_state = ticket.state
|
|
334
|
+
# Handle both TicketState enum and string values
|
|
335
|
+
if isinstance(current_state, str):
|
|
336
|
+
current_state = TicketState(current_state)
|
|
337
|
+
|
|
338
|
+
# Validate transition
|
|
339
|
+
if not current_state.can_transition_to(target_state):
|
|
340
|
+
valid_transitions = TicketState.valid_transitions().get(current_state, [])
|
|
341
|
+
valid_values = [s.value for s in valid_transitions]
|
|
342
|
+
return {
|
|
343
|
+
"status": "error",
|
|
344
|
+
"error": f"Invalid transition from '{current_state.value}' to '{target_state.value}'",
|
|
345
|
+
"current_state": current_state.value,
|
|
346
|
+
"valid_transitions": valid_values,
|
|
347
|
+
"message": f"Cannot transition from {current_state.value} to {target_state.value}. "
|
|
348
|
+
f"Valid transitions: {', '.join(valid_values) if valid_values else 'none (terminal state)'}",
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Update ticket state
|
|
352
|
+
updated = await adapter.update(ticket_id, {"state": target_state})
|
|
353
|
+
|
|
354
|
+
if updated is None:
|
|
355
|
+
return {
|
|
356
|
+
"status": "error",
|
|
357
|
+
"error": f"Failed to update ticket {ticket_id}",
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# Add comment if provided and adapter supports it
|
|
361
|
+
comment_added = False
|
|
362
|
+
if comment and hasattr(adapter, "add_comment"):
|
|
363
|
+
try:
|
|
364
|
+
await adapter.add_comment(ticket_id, comment)
|
|
365
|
+
comment_added = True
|
|
366
|
+
except Exception:
|
|
367
|
+
# Log but don't fail the transition
|
|
368
|
+
comment_added = False
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
"status": "completed",
|
|
372
|
+
"ticket": updated.model_dump(),
|
|
373
|
+
"previous_state": current_state.value,
|
|
374
|
+
"new_state": target_state.value,
|
|
375
|
+
"comment_added": comment_added,
|
|
376
|
+
"message": f"Ticket {ticket_id} transitioned from {current_state.value} to {target_state.value}",
|
|
377
|
+
}
|
|
378
|
+
except Exception as e:
|
|
379
|
+
return {
|
|
380
|
+
"status": "error",
|
|
381
|
+
"error": f"Failed to transition ticket: {str(e)}",
|
|
382
|
+
}
|
mcp_ticketer/queue/manager.py
CHANGED
|
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
class WorkerManager:
|
|
20
20
|
"""Manages worker process with file-based locking."""
|
|
21
21
|
|
|
22
|
-
def __init__(self):
|
|
22
|
+
def __init__(self) -> None:
|
|
23
23
|
"""Initialize worker manager."""
|
|
24
24
|
# Lazy import to avoid circular dependency
|
|
25
25
|
from .queue import Queue
|
|
@@ -54,7 +54,7 @@ class WorkerManager:
|
|
|
54
54
|
# Lock already held
|
|
55
55
|
return False
|
|
56
56
|
|
|
57
|
-
def _release_lock(self):
|
|
57
|
+
def _release_lock(self) -> None:
|
|
58
58
|
"""Release worker lock."""
|
|
59
59
|
if hasattr(self, "lock_fd"):
|
|
60
60
|
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
|
|
@@ -287,7 +287,7 @@ class WorkerManager:
|
|
|
287
287
|
is_running = self.is_running()
|
|
288
288
|
pid = self._get_pid() if is_running else None
|
|
289
289
|
|
|
290
|
-
status = {"running": is_running, "pid": pid}
|
|
290
|
+
status: dict[str, Any] = {"running": is_running, "pid": pid}
|
|
291
291
|
|
|
292
292
|
# Add process info if running
|
|
293
293
|
if is_running and pid:
|
|
@@ -326,7 +326,7 @@ class WorkerManager:
|
|
|
326
326
|
except (OSError, ValueError):
|
|
327
327
|
return None
|
|
328
328
|
|
|
329
|
-
def _cleanup(self):
|
|
329
|
+
def _cleanup(self) -> None:
|
|
330
330
|
"""Clean up lock and PID files."""
|
|
331
331
|
self._release_lock()
|
|
332
332
|
if self.pid_file.exists():
|
mcp_ticketer/queue/queue.py
CHANGED
|
@@ -83,7 +83,7 @@ class Queue:
|
|
|
83
83
|
self._lock = threading.Lock()
|
|
84
84
|
self._init_database()
|
|
85
85
|
|
|
86
|
-
def _init_database(self):
|
|
86
|
+
def _init_database(self) -> None:
|
|
87
87
|
"""Initialize database schema."""
|
|
88
88
|
with sqlite3.connect(self.db_path) as conn:
|
|
89
89
|
conn.execute(
|
|
@@ -462,7 +462,7 @@ class Queue:
|
|
|
462
462
|
|
|
463
463
|
return cursor.fetchone()[0]
|
|
464
464
|
|
|
465
|
-
def cleanup_old(self, days: int = 7):
|
|
465
|
+
def cleanup_old(self, days: int = 7) -> None:
|
|
466
466
|
"""Clean up old completed/failed items.
|
|
467
467
|
|
|
468
468
|
Args:
|
|
@@ -487,7 +487,7 @@ class Queue:
|
|
|
487
487
|
)
|
|
488
488
|
conn.commit()
|
|
489
489
|
|
|
490
|
-
def reset_stuck_items(self, timeout_minutes: int = 30):
|
|
490
|
+
def reset_stuck_items(self, timeout_minutes: int = 30) -> None:
|
|
491
491
|
"""Reset items stuck in processing state.
|
|
492
492
|
|
|
493
493
|
Args:
|
mcp_ticketer/queue/run_worker.py
CHANGED
|
@@ -27,7 +27,7 @@ class TicketRegistry:
|
|
|
27
27
|
self._lock = threading.Lock()
|
|
28
28
|
self._init_database()
|
|
29
29
|
|
|
30
|
-
def _init_database(self):
|
|
30
|
+
def _init_database(self) -> None:
|
|
31
31
|
"""Initialize database schema."""
|
|
32
32
|
with sqlite3.connect(self.db_path) as conn:
|
|
33
33
|
# Ticket registry table
|
|
@@ -149,7 +149,7 @@ class TicketRegistry:
|
|
|
149
149
|
with self._lock:
|
|
150
150
|
with sqlite3.connect(self.db_path) as conn:
|
|
151
151
|
update_fields = ["status = ?", "updated_at = ?"]
|
|
152
|
-
values = [status, datetime.now().isoformat()]
|
|
152
|
+
values: list[Any] = [status, datetime.now().isoformat()]
|
|
153
153
|
|
|
154
154
|
if ticket_id is not None:
|
|
155
155
|
update_fields.append("ticket_id = ?")
|
mcp_ticketer/queue/worker.py
CHANGED
|
@@ -97,12 +97,12 @@ class Worker:
|
|
|
97
97
|
f"Worker initialized with batch_size={batch_size}, max_concurrent={max_concurrent}"
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
-
def _signal_handler(self, signum, frame):
|
|
100
|
+
def _signal_handler(self, signum: int, frame: Any) -> None:
|
|
101
101
|
"""Handle shutdown signals."""
|
|
102
102
|
logger.info(f"Received signal {signum}, shutting down...")
|
|
103
103
|
self.stop()
|
|
104
104
|
|
|
105
|
-
def start(self, daemon: bool = True):
|
|
105
|
+
def start(self, daemon: bool = True) -> None:
|
|
106
106
|
"""Start the worker.
|
|
107
107
|
|
|
108
108
|
Args:
|
|
@@ -126,14 +126,14 @@ class Worker:
|
|
|
126
126
|
# Run in main thread
|
|
127
127
|
self._run_loop()
|
|
128
128
|
|
|
129
|
-
def stop(self):
|
|
129
|
+
def stop(self) -> None:
|
|
130
130
|
"""Stop the worker."""
|
|
131
131
|
logger.info("Stopping worker...")
|
|
132
132
|
self.running = False
|
|
133
133
|
self.stop_event.set()
|
|
134
134
|
|
|
135
|
-
def _run_loop(self):
|
|
136
|
-
"""
|
|
135
|
+
def _run_loop(self) -> None:
|
|
136
|
+
"""Run main worker loop with batch processing."""
|
|
137
137
|
logger.info("Worker loop started")
|
|
138
138
|
|
|
139
139
|
# Reset any stuck items on startup
|
|
@@ -174,7 +174,7 @@ class Worker:
|
|
|
174
174
|
break
|
|
175
175
|
return batch
|
|
176
176
|
|
|
177
|
-
async def _process_batch(self, batch: list[QueueItem]):
|
|
177
|
+
async def _process_batch(self, batch: list[QueueItem]) -> None:
|
|
178
178
|
"""Process a batch of queue items with concurrency control.
|
|
179
179
|
|
|
180
180
|
Args:
|
|
@@ -199,7 +199,9 @@ class Worker:
|
|
|
199
199
|
# Wait for all adapter groups to complete
|
|
200
200
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
201
201
|
|
|
202
|
-
async def _process_adapter_group(
|
|
202
|
+
async def _process_adapter_group(
|
|
203
|
+
self, adapter: str, items: list[QueueItem]
|
|
204
|
+
) -> None:
|
|
203
205
|
"""Process items for a specific adapter with concurrency control.
|
|
204
206
|
|
|
205
207
|
Args:
|
|
@@ -216,7 +218,7 @@ class Worker:
|
|
|
216
218
|
semaphore = self.adapter_semaphores[adapter]
|
|
217
219
|
|
|
218
220
|
# Process items with concurrency control
|
|
219
|
-
async def process_with_semaphore(item):
|
|
221
|
+
async def process_with_semaphore(item: QueueItem) -> None:
|
|
220
222
|
async with semaphore:
|
|
221
223
|
await self._process_item(item)
|
|
222
224
|
|
|
@@ -226,7 +228,7 @@ class Worker:
|
|
|
226
228
|
# Process with concurrency control
|
|
227
229
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
228
230
|
|
|
229
|
-
async def _process_item(self, item: QueueItem):
|
|
231
|
+
async def _process_item(self, item: QueueItem) -> None:
|
|
230
232
|
"""Process a single queue item.
|
|
231
233
|
|
|
232
234
|
Args:
|
|
@@ -333,7 +335,7 @@ class Worker:
|
|
|
333
335
|
self.stats["items_failed"] += 1
|
|
334
336
|
logger.error(f"Max retries exceeded for {item.id}, marking as failed")
|
|
335
337
|
|
|
336
|
-
async def _check_rate_limit(self, adapter: str):
|
|
338
|
+
async def _check_rate_limit(self, adapter: str) -> None:
|
|
337
339
|
"""Check and enforce rate limits.
|
|
338
340
|
|
|
339
341
|
Args:
|
|
@@ -357,7 +359,7 @@ class Worker:
|
|
|
357
359
|
|
|
358
360
|
self.last_request_times[adapter] = datetime.now()
|
|
359
361
|
|
|
360
|
-
def _get_adapter(self, item: QueueItem):
|
|
362
|
+
def _get_adapter(self, item: QueueItem) -> Any:
|
|
361
363
|
"""Get adapter instance for item.
|
|
362
364
|
|
|
363
365
|
Args:
|
|
@@ -442,7 +444,7 @@ class Worker:
|
|
|
442
444
|
|
|
443
445
|
return adapter
|
|
444
446
|
|
|
445
|
-
async def _execute_operation(self, adapter, item: QueueItem) -> dict[str, Any]:
|
|
447
|
+
async def _execute_operation(self, adapter: Any, item: QueueItem) -> dict[str, Any]:
|
|
446
448
|
"""Execute the queued operation.
|
|
447
449
|
|
|
448
450
|
Args:
|