mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -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 +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- 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/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- 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 +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- 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/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +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 +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"""Base adapter abstract class for ticket systems."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import builtins
|
|
4
6
|
from abc import ABC, abstractmethod
|
|
5
|
-
from typing import Any, Generic,
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
6
8
|
|
|
7
9
|
from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
|
|
10
|
+
from .state_matcher import get_state_matcher
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .models import Attachment
|
|
8
14
|
|
|
9
15
|
# Generic type for tickets
|
|
10
16
|
T = TypeVar("T", Epic, Task)
|
|
@@ -17,17 +23,52 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
17
23
|
"""Initialize adapter with configuration.
|
|
18
24
|
|
|
19
25
|
Args:
|
|
26
|
+
----
|
|
20
27
|
config: Adapter-specific configuration dictionary
|
|
21
28
|
|
|
22
29
|
"""
|
|
23
30
|
self.config = config
|
|
24
31
|
self._state_mapping = self._get_state_mapping()
|
|
25
32
|
|
|
33
|
+
@property
|
|
34
|
+
def adapter_type(self) -> str:
|
|
35
|
+
"""Return lowercase adapter type identifier.
|
|
36
|
+
|
|
37
|
+
This identifier is used in MCP responses to show which adapter
|
|
38
|
+
handled the operation (e.g., "linear", "github", "jira", "asana").
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
-------
|
|
42
|
+
Lowercase adapter type (e.g., "linear", "github")
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
# Extract adapter type from class name
|
|
46
|
+
# LinearAdapter -> linear, GitHubAdapter -> github
|
|
47
|
+
class_name = self.__class__.__name__
|
|
48
|
+
if class_name.endswith("Adapter"):
|
|
49
|
+
adapter_name = class_name[: -len("Adapter")]
|
|
50
|
+
else:
|
|
51
|
+
adapter_name = class_name
|
|
52
|
+
|
|
53
|
+
return adapter_name.lower()
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def adapter_display_name(self) -> str:
|
|
57
|
+
"""Return human-readable adapter name.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
-------
|
|
61
|
+
Title-cased adapter name (e.g., "Linear", "Github", "Jira")
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
return self.adapter_type.title()
|
|
65
|
+
|
|
26
66
|
@abstractmethod
|
|
27
67
|
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
28
68
|
"""Get mapping from universal states to system-specific states.
|
|
29
69
|
|
|
30
70
|
Returns:
|
|
71
|
+
-------
|
|
31
72
|
Dictionary mapping TicketState to system-specific state strings
|
|
32
73
|
|
|
33
74
|
"""
|
|
@@ -38,6 +79,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
38
79
|
"""Validate that required credentials are present.
|
|
39
80
|
|
|
40
81
|
Returns:
|
|
82
|
+
-------
|
|
41
83
|
(is_valid, error_message) - Tuple of validation result and error message
|
|
42
84
|
|
|
43
85
|
"""
|
|
@@ -48,36 +90,42 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
48
90
|
"""Create a new ticket.
|
|
49
91
|
|
|
50
92
|
Args:
|
|
93
|
+
----
|
|
51
94
|
ticket: Ticket to create (Epic or Task)
|
|
52
95
|
|
|
53
96
|
Returns:
|
|
97
|
+
-------
|
|
54
98
|
Created ticket with ID populated
|
|
55
99
|
|
|
56
100
|
"""
|
|
57
101
|
pass
|
|
58
102
|
|
|
59
103
|
@abstractmethod
|
|
60
|
-
async def read(self, ticket_id: str) ->
|
|
104
|
+
async def read(self, ticket_id: str) -> T | None:
|
|
61
105
|
"""Read a ticket by ID.
|
|
62
106
|
|
|
63
107
|
Args:
|
|
108
|
+
----
|
|
64
109
|
ticket_id: Unique ticket identifier
|
|
65
110
|
|
|
66
111
|
Returns:
|
|
112
|
+
-------
|
|
67
113
|
Ticket if found, None otherwise
|
|
68
114
|
|
|
69
115
|
"""
|
|
70
116
|
pass
|
|
71
117
|
|
|
72
118
|
@abstractmethod
|
|
73
|
-
async def update(self, ticket_id: str, updates: dict[str, Any]) ->
|
|
119
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> T | None:
|
|
74
120
|
"""Update a ticket.
|
|
75
121
|
|
|
76
122
|
Args:
|
|
123
|
+
----
|
|
77
124
|
ticket_id: Ticket identifier
|
|
78
125
|
updates: Fields to update
|
|
79
126
|
|
|
80
127
|
Returns:
|
|
128
|
+
-------
|
|
81
129
|
Updated ticket if successful, None otherwise
|
|
82
130
|
|
|
83
131
|
"""
|
|
@@ -88,9 +136,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
88
136
|
"""Delete a ticket.
|
|
89
137
|
|
|
90
138
|
Args:
|
|
139
|
+
----
|
|
91
140
|
ticket_id: Ticket identifier
|
|
92
141
|
|
|
93
142
|
Returns:
|
|
143
|
+
-------
|
|
94
144
|
True if deleted, False otherwise
|
|
95
145
|
|
|
96
146
|
"""
|
|
@@ -98,16 +148,18 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
98
148
|
|
|
99
149
|
@abstractmethod
|
|
100
150
|
async def list(
|
|
101
|
-
self, limit: int = 10, offset: int = 0, filters:
|
|
151
|
+
self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None
|
|
102
152
|
) -> list[T]:
|
|
103
153
|
"""List tickets with pagination and filters.
|
|
104
154
|
|
|
105
155
|
Args:
|
|
156
|
+
----
|
|
106
157
|
limit: Maximum number of tickets
|
|
107
158
|
offset: Skip this many tickets
|
|
108
159
|
filters: Optional filter criteria
|
|
109
160
|
|
|
110
161
|
Returns:
|
|
162
|
+
-------
|
|
111
163
|
List of tickets matching criteria
|
|
112
164
|
|
|
113
165
|
"""
|
|
@@ -118,9 +170,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
118
170
|
"""Search tickets using advanced query.
|
|
119
171
|
|
|
120
172
|
Args:
|
|
173
|
+
----
|
|
121
174
|
query: Search parameters
|
|
122
175
|
|
|
123
176
|
Returns:
|
|
177
|
+
-------
|
|
124
178
|
List of tickets matching search criteria
|
|
125
179
|
|
|
126
180
|
"""
|
|
@@ -129,14 +183,16 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
129
183
|
@abstractmethod
|
|
130
184
|
async def transition_state(
|
|
131
185
|
self, ticket_id: str, target_state: TicketState
|
|
132
|
-
) ->
|
|
186
|
+
) -> T | None:
|
|
133
187
|
"""Transition ticket to a new state.
|
|
134
188
|
|
|
135
189
|
Args:
|
|
190
|
+
----
|
|
136
191
|
ticket_id: Ticket identifier
|
|
137
192
|
target_state: Target state
|
|
138
193
|
|
|
139
194
|
Returns:
|
|
195
|
+
-------
|
|
140
196
|
Updated ticket if transition successful, None otherwise
|
|
141
197
|
|
|
142
198
|
"""
|
|
@@ -147,9 +203,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
147
203
|
"""Add a comment to a ticket.
|
|
148
204
|
|
|
149
205
|
Args:
|
|
206
|
+
----
|
|
150
207
|
comment: Comment to add
|
|
151
208
|
|
|
152
209
|
Returns:
|
|
210
|
+
-------
|
|
153
211
|
Created comment with ID populated
|
|
154
212
|
|
|
155
213
|
"""
|
|
@@ -162,11 +220,13 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
162
220
|
"""Get comments for a ticket.
|
|
163
221
|
|
|
164
222
|
Args:
|
|
223
|
+
----
|
|
165
224
|
ticket_id: Ticket identifier
|
|
166
225
|
limit: Maximum number of comments
|
|
167
226
|
offset: Skip this many comments
|
|
168
227
|
|
|
169
228
|
Returns:
|
|
229
|
+
-------
|
|
170
230
|
List of comments for the ticket
|
|
171
231
|
|
|
172
232
|
"""
|
|
@@ -176,9 +236,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
176
236
|
"""Map universal state to system-specific state.
|
|
177
237
|
|
|
178
238
|
Args:
|
|
239
|
+
----
|
|
179
240
|
state: Universal ticket state
|
|
180
241
|
|
|
181
242
|
Returns:
|
|
243
|
+
-------
|
|
182
244
|
System-specific state string
|
|
183
245
|
|
|
184
246
|
"""
|
|
@@ -188,31 +250,88 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
188
250
|
"""Map system-specific state to universal state.
|
|
189
251
|
|
|
190
252
|
Args:
|
|
253
|
+
----
|
|
191
254
|
system_state: System-specific state string
|
|
192
255
|
|
|
193
256
|
Returns:
|
|
257
|
+
-------
|
|
194
258
|
Universal ticket state
|
|
195
259
|
|
|
196
260
|
"""
|
|
197
261
|
reverse_mapping = {v: k for k, v in self._state_mapping.items()}
|
|
198
262
|
return reverse_mapping.get(system_state, TicketState.OPEN)
|
|
199
263
|
|
|
264
|
+
def get_available_states(self) -> list[str]:
|
|
265
|
+
"""Get list of adapter-specific available states.
|
|
266
|
+
|
|
267
|
+
Returns adapter-specific state names that can be used for
|
|
268
|
+
semantic state matching. Override in subclasses to provide
|
|
269
|
+
platform-specific state names.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
-------
|
|
273
|
+
List of adapter-specific state names
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
-------
|
|
277
|
+
>>> # Linear adapter override
|
|
278
|
+
>>> def get_available_states(self):
|
|
279
|
+
... return ["Backlog", "Todo", "In Progress", "Done", "Canceled"]
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
# Default: return universal state values
|
|
283
|
+
return [state.value for state in TicketState]
|
|
284
|
+
|
|
285
|
+
def resolve_state(self, user_input: str) -> TicketState:
|
|
286
|
+
"""Resolve user input to universal state using semantic matcher.
|
|
287
|
+
|
|
288
|
+
Uses the semantic state matcher to interpret natural language
|
|
289
|
+
inputs and resolve them to universal TicketState values.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
----
|
|
293
|
+
user_input: Natural language state input (e.g., "working on it")
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
-------
|
|
297
|
+
Resolved universal TicketState
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
-------
|
|
301
|
+
>>> adapter = get_adapter()
|
|
302
|
+
>>> state = adapter.resolve_state("working on it")
|
|
303
|
+
>>> print(state)
|
|
304
|
+
TicketState.IN_PROGRESS
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
matcher = get_state_matcher()
|
|
308
|
+
adapter_states = self.get_available_states()
|
|
309
|
+
result = matcher.match_state(user_input, adapter_states)
|
|
310
|
+
return result.state
|
|
311
|
+
|
|
200
312
|
async def validate_transition(
|
|
201
313
|
self, ticket_id: str, target_state: TicketState
|
|
202
314
|
) -> bool:
|
|
203
315
|
"""Validate if state transition is allowed.
|
|
204
316
|
|
|
317
|
+
Validates both workflow rules and parent/child state constraints:
|
|
318
|
+
- Parent issues must remain at least as complete as their most complete child
|
|
319
|
+
- Standard workflow transitions must be valid
|
|
320
|
+
|
|
205
321
|
Args:
|
|
322
|
+
----
|
|
206
323
|
ticket_id: Ticket identifier
|
|
207
324
|
target_state: Target state
|
|
208
325
|
|
|
209
326
|
Returns:
|
|
327
|
+
-------
|
|
210
328
|
True if transition is valid
|
|
211
329
|
|
|
212
330
|
"""
|
|
213
331
|
ticket = await self.read(ticket_id)
|
|
214
332
|
if not ticket:
|
|
215
333
|
return False
|
|
334
|
+
|
|
216
335
|
# Handle case where state might be stored as string due to use_enum_values=True
|
|
217
336
|
current_state = ticket.state
|
|
218
337
|
if isinstance(current_state, str):
|
|
@@ -220,21 +339,51 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
220
339
|
current_state = TicketState(current_state)
|
|
221
340
|
except ValueError:
|
|
222
341
|
return False
|
|
223
|
-
|
|
342
|
+
|
|
343
|
+
# Check workflow transition validity
|
|
344
|
+
if not current_state.can_transition_to(target_state):
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
# Check parent/child state constraint
|
|
348
|
+
# If this ticket has children, ensure target state >= max child state
|
|
349
|
+
if isinstance(ticket, Task):
|
|
350
|
+
# Get all children
|
|
351
|
+
children = await self.list_tasks_by_issue(ticket_id)
|
|
352
|
+
if children:
|
|
353
|
+
# Find max child completion level
|
|
354
|
+
max_child_level = 0
|
|
355
|
+
for child in children:
|
|
356
|
+
child_state = child.state
|
|
357
|
+
if isinstance(child_state, str):
|
|
358
|
+
try:
|
|
359
|
+
child_state = TicketState(child_state)
|
|
360
|
+
except ValueError:
|
|
361
|
+
continue
|
|
362
|
+
max_child_level = max(
|
|
363
|
+
max_child_level, child_state.completion_level()
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Target state must be at least as complete as most complete child
|
|
367
|
+
if target_state.completion_level() < max_child_level:
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
return True
|
|
224
371
|
|
|
225
372
|
# Epic/Issue/Task Hierarchy Methods
|
|
226
373
|
|
|
227
374
|
async def create_epic(
|
|
228
|
-
self, title: str, description:
|
|
229
|
-
) ->
|
|
375
|
+
self, title: str, description: str | None = None, **kwargs: Any
|
|
376
|
+
) -> Epic | None:
|
|
230
377
|
"""Create epic (top-level grouping).
|
|
231
378
|
|
|
232
379
|
Args:
|
|
380
|
+
----
|
|
233
381
|
title: Epic title
|
|
234
382
|
description: Epic description
|
|
235
383
|
**kwargs: Additional adapter-specific fields
|
|
236
384
|
|
|
237
385
|
Returns:
|
|
386
|
+
-------
|
|
238
387
|
Created epic or None if failed
|
|
239
388
|
|
|
240
389
|
"""
|
|
@@ -249,13 +398,15 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
249
398
|
return result
|
|
250
399
|
return None
|
|
251
400
|
|
|
252
|
-
async def get_epic(self, epic_id: str) ->
|
|
401
|
+
async def get_epic(self, epic_id: str) -> Epic | None:
|
|
253
402
|
"""Get epic by ID.
|
|
254
403
|
|
|
255
404
|
Args:
|
|
405
|
+
----
|
|
256
406
|
epic_id: Epic identifier
|
|
257
407
|
|
|
258
408
|
Returns:
|
|
409
|
+
-------
|
|
259
410
|
Epic if found, None otherwise
|
|
260
411
|
|
|
261
412
|
"""
|
|
@@ -265,13 +416,15 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
265
416
|
return result
|
|
266
417
|
return None
|
|
267
418
|
|
|
268
|
-
async def list_epics(self, **kwargs) -> builtins.list[Epic]:
|
|
419
|
+
async def list_epics(self, **kwargs: Any) -> builtins.list[Epic]:
|
|
269
420
|
"""List all epics.
|
|
270
421
|
|
|
271
422
|
Args:
|
|
423
|
+
----
|
|
272
424
|
**kwargs: Adapter-specific filter parameters
|
|
273
425
|
|
|
274
426
|
Returns:
|
|
427
|
+
-------
|
|
275
428
|
List of epics
|
|
276
429
|
|
|
277
430
|
"""
|
|
@@ -284,19 +437,21 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
284
437
|
async def create_issue(
|
|
285
438
|
self,
|
|
286
439
|
title: str,
|
|
287
|
-
description:
|
|
288
|
-
epic_id:
|
|
289
|
-
**kwargs,
|
|
290
|
-
) ->
|
|
440
|
+
description: str | None = None,
|
|
441
|
+
epic_id: str | None = None,
|
|
442
|
+
**kwargs: Any,
|
|
443
|
+
) -> Task | None:
|
|
291
444
|
"""Create issue, optionally linked to epic.
|
|
292
445
|
|
|
293
446
|
Args:
|
|
447
|
+
----
|
|
294
448
|
title: Issue title
|
|
295
449
|
description: Issue description
|
|
296
450
|
epic_id: Optional parent epic ID
|
|
297
451
|
**kwargs: Additional adapter-specific fields
|
|
298
452
|
|
|
299
453
|
Returns:
|
|
454
|
+
-------
|
|
300
455
|
Created issue or None if failed
|
|
301
456
|
|
|
302
457
|
"""
|
|
@@ -313,9 +468,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
313
468
|
"""List all issues in epic.
|
|
314
469
|
|
|
315
470
|
Args:
|
|
471
|
+
----
|
|
316
472
|
epic_id: Epic identifier
|
|
317
473
|
|
|
318
474
|
Returns:
|
|
475
|
+
-------
|
|
319
476
|
List of issues belonging to epic
|
|
320
477
|
|
|
321
478
|
"""
|
|
@@ -325,20 +482,23 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
325
482
|
return [r for r in results if isinstance(r, Task) and r.is_issue()]
|
|
326
483
|
|
|
327
484
|
async def create_task(
|
|
328
|
-
self, title: str, parent_id: str, description:
|
|
329
|
-
) ->
|
|
485
|
+
self, title: str, parent_id: str, description: str | None = None, **kwargs: Any
|
|
486
|
+
) -> Task | None:
|
|
330
487
|
"""Create task as sub-ticket of parent issue.
|
|
331
488
|
|
|
332
489
|
Args:
|
|
490
|
+
----
|
|
333
491
|
title: Task title
|
|
334
492
|
parent_id: Required parent issue ID
|
|
335
493
|
description: Task description
|
|
336
494
|
**kwargs: Additional adapter-specific fields
|
|
337
495
|
|
|
338
496
|
Returns:
|
|
497
|
+
-------
|
|
339
498
|
Created task or None if failed
|
|
340
499
|
|
|
341
500
|
Raises:
|
|
501
|
+
------
|
|
342
502
|
ValueError: If parent_id is not provided
|
|
343
503
|
|
|
344
504
|
"""
|
|
@@ -364,9 +524,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
364
524
|
"""List all tasks under an issue.
|
|
365
525
|
|
|
366
526
|
Args:
|
|
527
|
+
----
|
|
367
528
|
issue_id: Issue identifier
|
|
368
529
|
|
|
369
530
|
Returns:
|
|
531
|
+
-------
|
|
370
532
|
List of tasks belonging to issue
|
|
371
533
|
|
|
372
534
|
"""
|
|
@@ -375,6 +537,78 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
375
537
|
results = await self.list(filters=filters)
|
|
376
538
|
return [r for r in results if isinstance(r, Task) and r.is_task()]
|
|
377
539
|
|
|
540
|
+
# Attachment methods
|
|
541
|
+
async def add_attachment(
|
|
542
|
+
self,
|
|
543
|
+
ticket_id: str,
|
|
544
|
+
file_path: str,
|
|
545
|
+
description: str | None = None,
|
|
546
|
+
) -> Attachment:
|
|
547
|
+
"""Attach a file to a ticket.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
----
|
|
551
|
+
ticket_id: Ticket identifier
|
|
552
|
+
file_path: Local file path to upload
|
|
553
|
+
description: Optional attachment description
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
-------
|
|
557
|
+
Created Attachment with metadata
|
|
558
|
+
|
|
559
|
+
Raises:
|
|
560
|
+
------
|
|
561
|
+
NotImplementedError: If adapter doesn't support attachments
|
|
562
|
+
FileNotFoundError: If file doesn't exist
|
|
563
|
+
ValueError: If ticket doesn't exist or upload fails
|
|
564
|
+
|
|
565
|
+
"""
|
|
566
|
+
raise NotImplementedError(
|
|
567
|
+
f"{self.__class__.__name__} does not support file attachments. "
|
|
568
|
+
"Use comments to reference external files instead."
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
async def get_attachments(self, ticket_id: str) -> list[Attachment]:
|
|
572
|
+
"""Get all attachments for a ticket.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
----
|
|
576
|
+
ticket_id: Ticket identifier
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
-------
|
|
580
|
+
List of attachments (empty if none or not supported)
|
|
581
|
+
|
|
582
|
+
"""
|
|
583
|
+
raise NotImplementedError(
|
|
584
|
+
f"{self.__class__.__name__} does not support file attachments."
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
async def delete_attachment(
|
|
588
|
+
self,
|
|
589
|
+
ticket_id: str,
|
|
590
|
+
attachment_id: str,
|
|
591
|
+
) -> bool:
|
|
592
|
+
"""Delete an attachment (optional implementation).
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
----
|
|
596
|
+
ticket_id: Ticket identifier
|
|
597
|
+
attachment_id: Attachment identifier
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
-------
|
|
601
|
+
True if deleted, False otherwise
|
|
602
|
+
|
|
603
|
+
Raises:
|
|
604
|
+
------
|
|
605
|
+
NotImplementedError: If adapter doesn't support deletion
|
|
606
|
+
|
|
607
|
+
"""
|
|
608
|
+
raise NotImplementedError(
|
|
609
|
+
f"{self.__class__.__name__} does not support attachment deletion."
|
|
610
|
+
)
|
|
611
|
+
|
|
378
612
|
async def close(self) -> None:
|
|
379
613
|
"""Close adapter and cleanup resources."""
|
|
380
614
|
pass
|