mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
9
|
+
|
|
10
|
+
from .models import (
|
|
11
|
+
Comment,
|
|
12
|
+
Epic,
|
|
13
|
+
Milestone,
|
|
14
|
+
Project,
|
|
15
|
+
ProjectScope,
|
|
16
|
+
ProjectState,
|
|
17
|
+
ProjectStatistics,
|
|
18
|
+
SearchQuery,
|
|
19
|
+
Task,
|
|
20
|
+
TicketState,
|
|
21
|
+
TicketType,
|
|
22
|
+
)
|
|
23
|
+
from .state_matcher import get_state_matcher
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from .models import Attachment
|
|
8
27
|
|
|
9
28
|
# Generic type for tickets
|
|
10
29
|
T = TypeVar("T", Epic, Task)
|
|
@@ -17,17 +36,52 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
17
36
|
"""Initialize adapter with configuration.
|
|
18
37
|
|
|
19
38
|
Args:
|
|
39
|
+
----
|
|
20
40
|
config: Adapter-specific configuration dictionary
|
|
21
41
|
|
|
22
42
|
"""
|
|
23
43
|
self.config = config
|
|
24
44
|
self._state_mapping = self._get_state_mapping()
|
|
25
45
|
|
|
46
|
+
@property
|
|
47
|
+
def adapter_type(self) -> str:
|
|
48
|
+
"""Return lowercase adapter type identifier.
|
|
49
|
+
|
|
50
|
+
This identifier is used in MCP responses to show which adapter
|
|
51
|
+
handled the operation (e.g., "linear", "github", "jira", "asana").
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
-------
|
|
55
|
+
Lowercase adapter type (e.g., "linear", "github")
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
# Extract adapter type from class name
|
|
59
|
+
# LinearAdapter -> linear, GitHubAdapter -> github
|
|
60
|
+
class_name = self.__class__.__name__
|
|
61
|
+
if class_name.endswith("Adapter"):
|
|
62
|
+
adapter_name = class_name[: -len("Adapter")]
|
|
63
|
+
else:
|
|
64
|
+
adapter_name = class_name
|
|
65
|
+
|
|
66
|
+
return adapter_name.lower()
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def adapter_display_name(self) -> str:
|
|
70
|
+
"""Return human-readable adapter name.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
-------
|
|
74
|
+
Title-cased adapter name (e.g., "Linear", "Github", "Jira")
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
return self.adapter_type.title()
|
|
78
|
+
|
|
26
79
|
@abstractmethod
|
|
27
80
|
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
28
81
|
"""Get mapping from universal states to system-specific states.
|
|
29
82
|
|
|
30
83
|
Returns:
|
|
84
|
+
-------
|
|
31
85
|
Dictionary mapping TicketState to system-specific state strings
|
|
32
86
|
|
|
33
87
|
"""
|
|
@@ -38,6 +92,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
38
92
|
"""Validate that required credentials are present.
|
|
39
93
|
|
|
40
94
|
Returns:
|
|
95
|
+
-------
|
|
41
96
|
(is_valid, error_message) - Tuple of validation result and error message
|
|
42
97
|
|
|
43
98
|
"""
|
|
@@ -48,36 +103,42 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
48
103
|
"""Create a new ticket.
|
|
49
104
|
|
|
50
105
|
Args:
|
|
106
|
+
----
|
|
51
107
|
ticket: Ticket to create (Epic or Task)
|
|
52
108
|
|
|
53
109
|
Returns:
|
|
110
|
+
-------
|
|
54
111
|
Created ticket with ID populated
|
|
55
112
|
|
|
56
113
|
"""
|
|
57
114
|
pass
|
|
58
115
|
|
|
59
116
|
@abstractmethod
|
|
60
|
-
async def read(self, ticket_id: str) ->
|
|
117
|
+
async def read(self, ticket_id: str) -> T | None:
|
|
61
118
|
"""Read a ticket by ID.
|
|
62
119
|
|
|
63
120
|
Args:
|
|
121
|
+
----
|
|
64
122
|
ticket_id: Unique ticket identifier
|
|
65
123
|
|
|
66
124
|
Returns:
|
|
125
|
+
-------
|
|
67
126
|
Ticket if found, None otherwise
|
|
68
127
|
|
|
69
128
|
"""
|
|
70
129
|
pass
|
|
71
130
|
|
|
72
131
|
@abstractmethod
|
|
73
|
-
async def update(self, ticket_id: str, updates: dict[str, Any]) ->
|
|
132
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> T | None:
|
|
74
133
|
"""Update a ticket.
|
|
75
134
|
|
|
76
135
|
Args:
|
|
136
|
+
----
|
|
77
137
|
ticket_id: Ticket identifier
|
|
78
138
|
updates: Fields to update
|
|
79
139
|
|
|
80
140
|
Returns:
|
|
141
|
+
-------
|
|
81
142
|
Updated ticket if successful, None otherwise
|
|
82
143
|
|
|
83
144
|
"""
|
|
@@ -88,9 +149,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
88
149
|
"""Delete a ticket.
|
|
89
150
|
|
|
90
151
|
Args:
|
|
152
|
+
----
|
|
91
153
|
ticket_id: Ticket identifier
|
|
92
154
|
|
|
93
155
|
Returns:
|
|
156
|
+
-------
|
|
94
157
|
True if deleted, False otherwise
|
|
95
158
|
|
|
96
159
|
"""
|
|
@@ -98,16 +161,18 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
98
161
|
|
|
99
162
|
@abstractmethod
|
|
100
163
|
async def list(
|
|
101
|
-
self, limit: int = 10, offset: int = 0, filters:
|
|
164
|
+
self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None
|
|
102
165
|
) -> list[T]:
|
|
103
166
|
"""List tickets with pagination and filters.
|
|
104
167
|
|
|
105
168
|
Args:
|
|
169
|
+
----
|
|
106
170
|
limit: Maximum number of tickets
|
|
107
171
|
offset: Skip this many tickets
|
|
108
172
|
filters: Optional filter criteria
|
|
109
173
|
|
|
110
174
|
Returns:
|
|
175
|
+
-------
|
|
111
176
|
List of tickets matching criteria
|
|
112
177
|
|
|
113
178
|
"""
|
|
@@ -118,9 +183,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
118
183
|
"""Search tickets using advanced query.
|
|
119
184
|
|
|
120
185
|
Args:
|
|
186
|
+
----
|
|
121
187
|
query: Search parameters
|
|
122
188
|
|
|
123
189
|
Returns:
|
|
190
|
+
-------
|
|
124
191
|
List of tickets matching search criteria
|
|
125
192
|
|
|
126
193
|
"""
|
|
@@ -129,14 +196,16 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
129
196
|
@abstractmethod
|
|
130
197
|
async def transition_state(
|
|
131
198
|
self, ticket_id: str, target_state: TicketState
|
|
132
|
-
) ->
|
|
199
|
+
) -> T | None:
|
|
133
200
|
"""Transition ticket to a new state.
|
|
134
201
|
|
|
135
202
|
Args:
|
|
203
|
+
----
|
|
136
204
|
ticket_id: Ticket identifier
|
|
137
205
|
target_state: Target state
|
|
138
206
|
|
|
139
207
|
Returns:
|
|
208
|
+
-------
|
|
140
209
|
Updated ticket if transition successful, None otherwise
|
|
141
210
|
|
|
142
211
|
"""
|
|
@@ -147,9 +216,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
147
216
|
"""Add a comment to a ticket.
|
|
148
217
|
|
|
149
218
|
Args:
|
|
219
|
+
----
|
|
150
220
|
comment: Comment to add
|
|
151
221
|
|
|
152
222
|
Returns:
|
|
223
|
+
-------
|
|
153
224
|
Created comment with ID populated
|
|
154
225
|
|
|
155
226
|
"""
|
|
@@ -162,11 +233,13 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
162
233
|
"""Get comments for a ticket.
|
|
163
234
|
|
|
164
235
|
Args:
|
|
236
|
+
----
|
|
165
237
|
ticket_id: Ticket identifier
|
|
166
238
|
limit: Maximum number of comments
|
|
167
239
|
offset: Skip this many comments
|
|
168
240
|
|
|
169
241
|
Returns:
|
|
242
|
+
-------
|
|
170
243
|
List of comments for the ticket
|
|
171
244
|
|
|
172
245
|
"""
|
|
@@ -176,9 +249,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
176
249
|
"""Map universal state to system-specific state.
|
|
177
250
|
|
|
178
251
|
Args:
|
|
252
|
+
----
|
|
179
253
|
state: Universal ticket state
|
|
180
254
|
|
|
181
255
|
Returns:
|
|
256
|
+
-------
|
|
182
257
|
System-specific state string
|
|
183
258
|
|
|
184
259
|
"""
|
|
@@ -188,31 +263,88 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
188
263
|
"""Map system-specific state to universal state.
|
|
189
264
|
|
|
190
265
|
Args:
|
|
266
|
+
----
|
|
191
267
|
system_state: System-specific state string
|
|
192
268
|
|
|
193
269
|
Returns:
|
|
270
|
+
-------
|
|
194
271
|
Universal ticket state
|
|
195
272
|
|
|
196
273
|
"""
|
|
197
274
|
reverse_mapping = {v: k for k, v in self._state_mapping.items()}
|
|
198
275
|
return reverse_mapping.get(system_state, TicketState.OPEN)
|
|
199
276
|
|
|
277
|
+
def get_available_states(self) -> list[str]:
|
|
278
|
+
"""Get list of adapter-specific available states.
|
|
279
|
+
|
|
280
|
+
Returns adapter-specific state names that can be used for
|
|
281
|
+
semantic state matching. Override in subclasses to provide
|
|
282
|
+
platform-specific state names.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
-------
|
|
286
|
+
List of adapter-specific state names
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
-------
|
|
290
|
+
>>> # Linear adapter override
|
|
291
|
+
>>> def get_available_states(self):
|
|
292
|
+
... return ["Backlog", "Todo", "In Progress", "Done", "Canceled"]
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
# Default: return universal state values
|
|
296
|
+
return [state.value for state in TicketState]
|
|
297
|
+
|
|
298
|
+
def resolve_state(self, user_input: str) -> TicketState:
|
|
299
|
+
"""Resolve user input to universal state using semantic matcher.
|
|
300
|
+
|
|
301
|
+
Uses the semantic state matcher to interpret natural language
|
|
302
|
+
inputs and resolve them to universal TicketState values.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
----
|
|
306
|
+
user_input: Natural language state input (e.g., "working on it")
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
-------
|
|
310
|
+
Resolved universal TicketState
|
|
311
|
+
|
|
312
|
+
Example:
|
|
313
|
+
-------
|
|
314
|
+
>>> adapter = get_adapter()
|
|
315
|
+
>>> state = adapter.resolve_state("working on it")
|
|
316
|
+
>>> print(state)
|
|
317
|
+
TicketState.IN_PROGRESS
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
matcher = get_state_matcher()
|
|
321
|
+
adapter_states = self.get_available_states()
|
|
322
|
+
result = matcher.match_state(user_input, adapter_states)
|
|
323
|
+
return result.state
|
|
324
|
+
|
|
200
325
|
async def validate_transition(
|
|
201
326
|
self, ticket_id: str, target_state: TicketState
|
|
202
327
|
) -> bool:
|
|
203
328
|
"""Validate if state transition is allowed.
|
|
204
329
|
|
|
330
|
+
Validates both workflow rules and parent/child state constraints:
|
|
331
|
+
- Parent issues must remain at least as complete as their most complete child
|
|
332
|
+
- Standard workflow transitions must be valid
|
|
333
|
+
|
|
205
334
|
Args:
|
|
335
|
+
----
|
|
206
336
|
ticket_id: Ticket identifier
|
|
207
337
|
target_state: Target state
|
|
208
338
|
|
|
209
339
|
Returns:
|
|
340
|
+
-------
|
|
210
341
|
True if transition is valid
|
|
211
342
|
|
|
212
343
|
"""
|
|
213
344
|
ticket = await self.read(ticket_id)
|
|
214
345
|
if not ticket:
|
|
215
346
|
return False
|
|
347
|
+
|
|
216
348
|
# Handle case where state might be stored as string due to use_enum_values=True
|
|
217
349
|
current_state = ticket.state
|
|
218
350
|
if isinstance(current_state, str):
|
|
@@ -220,21 +352,51 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
220
352
|
current_state = TicketState(current_state)
|
|
221
353
|
except ValueError:
|
|
222
354
|
return False
|
|
223
|
-
|
|
355
|
+
|
|
356
|
+
# Check workflow transition validity
|
|
357
|
+
if not current_state.can_transition_to(target_state):
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
# Check parent/child state constraint
|
|
361
|
+
# If this ticket has children, ensure target state >= max child state
|
|
362
|
+
if isinstance(ticket, Task):
|
|
363
|
+
# Get all children
|
|
364
|
+
children = await self.list_tasks_by_issue(ticket_id)
|
|
365
|
+
if children:
|
|
366
|
+
# Find max child completion level
|
|
367
|
+
max_child_level = 0
|
|
368
|
+
for child in children:
|
|
369
|
+
child_state = child.state
|
|
370
|
+
if isinstance(child_state, str):
|
|
371
|
+
try:
|
|
372
|
+
child_state = TicketState(child_state)
|
|
373
|
+
except ValueError:
|
|
374
|
+
continue
|
|
375
|
+
max_child_level = max(
|
|
376
|
+
max_child_level, child_state.completion_level()
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Target state must be at least as complete as most complete child
|
|
380
|
+
if target_state.completion_level() < max_child_level:
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
return True
|
|
224
384
|
|
|
225
385
|
# Epic/Issue/Task Hierarchy Methods
|
|
226
386
|
|
|
227
387
|
async def create_epic(
|
|
228
|
-
self, title: str, description:
|
|
229
|
-
) ->
|
|
388
|
+
self, title: str, description: str | None = None, **kwargs: Any
|
|
389
|
+
) -> Epic | None:
|
|
230
390
|
"""Create epic (top-level grouping).
|
|
231
391
|
|
|
232
392
|
Args:
|
|
393
|
+
----
|
|
233
394
|
title: Epic title
|
|
234
395
|
description: Epic description
|
|
235
396
|
**kwargs: Additional adapter-specific fields
|
|
236
397
|
|
|
237
398
|
Returns:
|
|
399
|
+
-------
|
|
238
400
|
Created epic or None if failed
|
|
239
401
|
|
|
240
402
|
"""
|
|
@@ -249,13 +411,15 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
249
411
|
return result
|
|
250
412
|
return None
|
|
251
413
|
|
|
252
|
-
async def get_epic(self, epic_id: str) ->
|
|
414
|
+
async def get_epic(self, epic_id: str) -> Epic | None:
|
|
253
415
|
"""Get epic by ID.
|
|
254
416
|
|
|
255
417
|
Args:
|
|
418
|
+
----
|
|
256
419
|
epic_id: Epic identifier
|
|
257
420
|
|
|
258
421
|
Returns:
|
|
422
|
+
-------
|
|
259
423
|
Epic if found, None otherwise
|
|
260
424
|
|
|
261
425
|
"""
|
|
@@ -265,13 +429,15 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
265
429
|
return result
|
|
266
430
|
return None
|
|
267
431
|
|
|
268
|
-
async def list_epics(self, **kwargs) -> builtins.list[Epic]:
|
|
432
|
+
async def list_epics(self, **kwargs: Any) -> builtins.list[Epic]:
|
|
269
433
|
"""List all epics.
|
|
270
434
|
|
|
271
435
|
Args:
|
|
436
|
+
----
|
|
272
437
|
**kwargs: Adapter-specific filter parameters
|
|
273
438
|
|
|
274
439
|
Returns:
|
|
440
|
+
-------
|
|
275
441
|
List of epics
|
|
276
442
|
|
|
277
443
|
"""
|
|
@@ -284,19 +450,21 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
284
450
|
async def create_issue(
|
|
285
451
|
self,
|
|
286
452
|
title: str,
|
|
287
|
-
description:
|
|
288
|
-
epic_id:
|
|
289
|
-
**kwargs,
|
|
290
|
-
) ->
|
|
453
|
+
description: str | None = None,
|
|
454
|
+
epic_id: str | None = None,
|
|
455
|
+
**kwargs: Any,
|
|
456
|
+
) -> Task | None:
|
|
291
457
|
"""Create issue, optionally linked to epic.
|
|
292
458
|
|
|
293
459
|
Args:
|
|
460
|
+
----
|
|
294
461
|
title: Issue title
|
|
295
462
|
description: Issue description
|
|
296
463
|
epic_id: Optional parent epic ID
|
|
297
464
|
**kwargs: Additional adapter-specific fields
|
|
298
465
|
|
|
299
466
|
Returns:
|
|
467
|
+
-------
|
|
300
468
|
Created issue or None if failed
|
|
301
469
|
|
|
302
470
|
"""
|
|
@@ -313,9 +481,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
313
481
|
"""List all issues in epic.
|
|
314
482
|
|
|
315
483
|
Args:
|
|
484
|
+
----
|
|
316
485
|
epic_id: Epic identifier
|
|
317
486
|
|
|
318
487
|
Returns:
|
|
488
|
+
-------
|
|
319
489
|
List of issues belonging to epic
|
|
320
490
|
|
|
321
491
|
"""
|
|
@@ -325,20 +495,23 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
325
495
|
return [r for r in results if isinstance(r, Task) and r.is_issue()]
|
|
326
496
|
|
|
327
497
|
async def create_task(
|
|
328
|
-
self, title: str, parent_id: str, description:
|
|
329
|
-
) ->
|
|
498
|
+
self, title: str, parent_id: str, description: str | None = None, **kwargs: Any
|
|
499
|
+
) -> Task | None:
|
|
330
500
|
"""Create task as sub-ticket of parent issue.
|
|
331
501
|
|
|
332
502
|
Args:
|
|
503
|
+
----
|
|
333
504
|
title: Task title
|
|
334
505
|
parent_id: Required parent issue ID
|
|
335
506
|
description: Task description
|
|
336
507
|
**kwargs: Additional adapter-specific fields
|
|
337
508
|
|
|
338
509
|
Returns:
|
|
510
|
+
-------
|
|
339
511
|
Created task or None if failed
|
|
340
512
|
|
|
341
513
|
Raises:
|
|
514
|
+
------
|
|
342
515
|
ValueError: If parent_id is not provided
|
|
343
516
|
|
|
344
517
|
"""
|
|
@@ -364,9 +537,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
364
537
|
"""List all tasks under an issue.
|
|
365
538
|
|
|
366
539
|
Args:
|
|
540
|
+
----
|
|
367
541
|
issue_id: Issue identifier
|
|
368
542
|
|
|
369
543
|
Returns:
|
|
544
|
+
-------
|
|
370
545
|
List of tasks belonging to issue
|
|
371
546
|
|
|
372
547
|
"""
|
|
@@ -375,6 +550,431 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
375
550
|
results = await self.list(filters=filters)
|
|
376
551
|
return [r for r in results if isinstance(r, Task) and r.is_task()]
|
|
377
552
|
|
|
553
|
+
# Attachment methods
|
|
554
|
+
async def add_attachment(
|
|
555
|
+
self,
|
|
556
|
+
ticket_id: str,
|
|
557
|
+
file_path: str,
|
|
558
|
+
description: str | None = None,
|
|
559
|
+
) -> Attachment:
|
|
560
|
+
"""Attach a file to a ticket.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
----
|
|
564
|
+
ticket_id: Ticket identifier
|
|
565
|
+
file_path: Local file path to upload
|
|
566
|
+
description: Optional attachment description
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
-------
|
|
570
|
+
Created Attachment with metadata
|
|
571
|
+
|
|
572
|
+
Raises:
|
|
573
|
+
------
|
|
574
|
+
NotImplementedError: If adapter doesn't support attachments
|
|
575
|
+
FileNotFoundError: If file doesn't exist
|
|
576
|
+
ValueError: If ticket doesn't exist or upload fails
|
|
577
|
+
|
|
578
|
+
"""
|
|
579
|
+
raise NotImplementedError(
|
|
580
|
+
f"{self.__class__.__name__} does not support file attachments. "
|
|
581
|
+
"Use comments to reference external files instead."
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
async def get_attachments(self, ticket_id: str) -> list[Attachment]:
|
|
585
|
+
"""Get all attachments for a ticket.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
----
|
|
589
|
+
ticket_id: Ticket identifier
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
-------
|
|
593
|
+
List of attachments (empty if none or not supported)
|
|
594
|
+
|
|
595
|
+
"""
|
|
596
|
+
raise NotImplementedError(
|
|
597
|
+
f"{self.__class__.__name__} does not support file attachments."
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
async def delete_attachment(
|
|
601
|
+
self,
|
|
602
|
+
ticket_id: str,
|
|
603
|
+
attachment_id: str,
|
|
604
|
+
) -> bool:
|
|
605
|
+
"""Delete an attachment (optional implementation).
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
----
|
|
609
|
+
ticket_id: Ticket identifier
|
|
610
|
+
attachment_id: Attachment identifier
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
-------
|
|
614
|
+
True if deleted, False otherwise
|
|
615
|
+
|
|
616
|
+
Raises:
|
|
617
|
+
------
|
|
618
|
+
NotImplementedError: If adapter doesn't support deletion
|
|
619
|
+
|
|
620
|
+
"""
|
|
621
|
+
raise NotImplementedError(
|
|
622
|
+
f"{self.__class__.__name__} does not support attachment deletion."
|
|
623
|
+
)
|
|
624
|
+
|
|
378
625
|
async def close(self) -> None:
|
|
379
626
|
"""Close adapter and cleanup resources."""
|
|
380
627
|
pass
|
|
628
|
+
|
|
629
|
+
# Milestone Operations (Phase 1 - Abstract methods)
|
|
630
|
+
|
|
631
|
+
@abstractmethod
|
|
632
|
+
async def milestone_create(
|
|
633
|
+
self,
|
|
634
|
+
name: str,
|
|
635
|
+
target_date: datetime | None = None,
|
|
636
|
+
labels: list[str] | None = None,
|
|
637
|
+
description: str = "",
|
|
638
|
+
project_id: str | None = None,
|
|
639
|
+
) -> Milestone:
|
|
640
|
+
"""Create a new milestone.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
----
|
|
644
|
+
name: Milestone name
|
|
645
|
+
target_date: Target completion date (ISO format: YYYY-MM-DD)
|
|
646
|
+
labels: Labels that define this milestone
|
|
647
|
+
description: Milestone description
|
|
648
|
+
project_id: Associated project ID
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
-------
|
|
652
|
+
Created Milestone object
|
|
653
|
+
|
|
654
|
+
"""
|
|
655
|
+
pass
|
|
656
|
+
|
|
657
|
+
@abstractmethod
|
|
658
|
+
async def milestone_get(self, milestone_id: str) -> Milestone | None:
|
|
659
|
+
"""Get milestone by ID with progress calculation.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
----
|
|
663
|
+
milestone_id: Milestone identifier
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
-------
|
|
667
|
+
Milestone object with calculated progress, None if not found
|
|
668
|
+
|
|
669
|
+
"""
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
@abstractmethod
|
|
673
|
+
async def milestone_list(
|
|
674
|
+
self,
|
|
675
|
+
project_id: str | None = None,
|
|
676
|
+
state: str | None = None,
|
|
677
|
+
) -> builtins.list[Milestone]:
|
|
678
|
+
"""List milestones with optional filters.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
----
|
|
682
|
+
project_id: Filter by project
|
|
683
|
+
state: Filter by state (open, active, completed, closed)
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
-------
|
|
687
|
+
List of Milestone objects
|
|
688
|
+
|
|
689
|
+
"""
|
|
690
|
+
pass
|
|
691
|
+
|
|
692
|
+
@abstractmethod
|
|
693
|
+
async def milestone_update(
|
|
694
|
+
self,
|
|
695
|
+
milestone_id: str,
|
|
696
|
+
name: str | None = None,
|
|
697
|
+
target_date: datetime | None = None,
|
|
698
|
+
state: str | None = None,
|
|
699
|
+
labels: list[str] | None = None,
|
|
700
|
+
description: str | None = None,
|
|
701
|
+
) -> Milestone | None:
|
|
702
|
+
"""Update milestone properties.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
----
|
|
706
|
+
milestone_id: Milestone identifier
|
|
707
|
+
name: New name (optional)
|
|
708
|
+
target_date: New target date (optional)
|
|
709
|
+
state: New state (optional)
|
|
710
|
+
labels: New labels (optional)
|
|
711
|
+
description: New description (optional)
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
-------
|
|
715
|
+
Updated Milestone object, None if not found
|
|
716
|
+
|
|
717
|
+
"""
|
|
718
|
+
pass
|
|
719
|
+
|
|
720
|
+
@abstractmethod
|
|
721
|
+
async def milestone_delete(self, milestone_id: str) -> bool:
|
|
722
|
+
"""Delete milestone.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
----
|
|
726
|
+
milestone_id: Milestone identifier
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
-------
|
|
730
|
+
True if deleted successfully, False otherwise
|
|
731
|
+
|
|
732
|
+
"""
|
|
733
|
+
pass
|
|
734
|
+
|
|
735
|
+
@abstractmethod
|
|
736
|
+
async def milestone_get_issues(
|
|
737
|
+
self,
|
|
738
|
+
milestone_id: str,
|
|
739
|
+
state: str | None = None,
|
|
740
|
+
) -> builtins.list[Task]:
|
|
741
|
+
"""Get issues associated with milestone.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
----
|
|
745
|
+
milestone_id: Milestone identifier
|
|
746
|
+
state: Filter by issue state (optional)
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
-------
|
|
750
|
+
List of Task objects (issues)
|
|
751
|
+
|
|
752
|
+
"""
|
|
753
|
+
pass
|
|
754
|
+
|
|
755
|
+
# Project Operations (Phase 1 - Abstract methods)
|
|
756
|
+
# These methods are optional - adapters that don't support projects
|
|
757
|
+
# can raise NotImplementedError with a helpful message
|
|
758
|
+
|
|
759
|
+
async def project_list(
|
|
760
|
+
self,
|
|
761
|
+
scope: ProjectScope | None = None,
|
|
762
|
+
state: ProjectState | None = None,
|
|
763
|
+
limit: int = 50,
|
|
764
|
+
offset: int = 0,
|
|
765
|
+
) -> builtins.list[Project]:
|
|
766
|
+
"""List projects with optional filters.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
----
|
|
770
|
+
scope: Filter by project scope (user, team, org, repo)
|
|
771
|
+
state: Filter by project state
|
|
772
|
+
limit: Maximum results (default: 50)
|
|
773
|
+
offset: Pagination offset (default: 0)
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
-------
|
|
777
|
+
List of Project objects
|
|
778
|
+
|
|
779
|
+
Raises:
|
|
780
|
+
------
|
|
781
|
+
NotImplementedError: If adapter doesn't support projects
|
|
782
|
+
|
|
783
|
+
"""
|
|
784
|
+
raise NotImplementedError(
|
|
785
|
+
f"{self.__class__.__name__} does not support project operations. "
|
|
786
|
+
"Use Epic operations for this adapter."
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
async def project_get(self, project_id: str) -> Project | None:
|
|
790
|
+
"""Get project by ID.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
----
|
|
794
|
+
project_id: Project identifier (platform-specific or unified)
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
-------
|
|
798
|
+
Project object if found, None otherwise
|
|
799
|
+
|
|
800
|
+
Raises:
|
|
801
|
+
------
|
|
802
|
+
NotImplementedError: If adapter doesn't support projects
|
|
803
|
+
|
|
804
|
+
"""
|
|
805
|
+
raise NotImplementedError(
|
|
806
|
+
f"{self.__class__.__name__} does not support project operations. "
|
|
807
|
+
"Use get_epic() for this adapter."
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
async def project_create(
|
|
811
|
+
self,
|
|
812
|
+
name: str,
|
|
813
|
+
description: str | None = None,
|
|
814
|
+
state: ProjectState = ProjectState.PLANNED,
|
|
815
|
+
target_date: datetime | None = None,
|
|
816
|
+
**kwargs: Any,
|
|
817
|
+
) -> Project:
|
|
818
|
+
"""Create new project.
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
----
|
|
822
|
+
name: Project name (required)
|
|
823
|
+
description: Project description
|
|
824
|
+
state: Initial project state (default: PLANNED)
|
|
825
|
+
target_date: Target completion date
|
|
826
|
+
**kwargs: Platform-specific additional fields
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
-------
|
|
830
|
+
Created Project object
|
|
831
|
+
|
|
832
|
+
Raises:
|
|
833
|
+
------
|
|
834
|
+
NotImplementedError: If adapter doesn't support projects
|
|
835
|
+
|
|
836
|
+
"""
|
|
837
|
+
raise NotImplementedError(
|
|
838
|
+
f"{self.__class__.__name__} does not support project operations. "
|
|
839
|
+
"Use create_epic() for this adapter."
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
async def project_update(
|
|
843
|
+
self,
|
|
844
|
+
project_id: str,
|
|
845
|
+
name: str | None = None,
|
|
846
|
+
description: str | None = None,
|
|
847
|
+
state: ProjectState | None = None,
|
|
848
|
+
**kwargs: Any,
|
|
849
|
+
) -> Project | None:
|
|
850
|
+
"""Update project properties.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
----
|
|
854
|
+
project_id: Project identifier
|
|
855
|
+
name: New name (optional)
|
|
856
|
+
description: New description (optional)
|
|
857
|
+
state: New state (optional)
|
|
858
|
+
**kwargs: Platform-specific fields to update
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
-------
|
|
862
|
+
Updated Project object, None if not found
|
|
863
|
+
|
|
864
|
+
Raises:
|
|
865
|
+
------
|
|
866
|
+
NotImplementedError: If adapter doesn't support projects
|
|
867
|
+
|
|
868
|
+
"""
|
|
869
|
+
raise NotImplementedError(
|
|
870
|
+
f"{self.__class__.__name__} does not support project operations."
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
async def project_delete(self, project_id: str) -> bool:
|
|
874
|
+
"""Delete or archive project.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
----
|
|
878
|
+
project_id: Project identifier
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
-------
|
|
882
|
+
True if deleted successfully, False otherwise
|
|
883
|
+
|
|
884
|
+
Raises:
|
|
885
|
+
------
|
|
886
|
+
NotImplementedError: If adapter doesn't support projects
|
|
887
|
+
|
|
888
|
+
"""
|
|
889
|
+
raise NotImplementedError(
|
|
890
|
+
f"{self.__class__.__name__} does not support project operations."
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
async def project_get_issues(
|
|
894
|
+
self, project_id: str, state: TicketState | None = None
|
|
895
|
+
) -> builtins.list[Task]:
|
|
896
|
+
"""Get all issues in project.
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
----
|
|
900
|
+
project_id: Project identifier
|
|
901
|
+
state: Filter by issue state (optional)
|
|
902
|
+
|
|
903
|
+
Returns:
|
|
904
|
+
-------
|
|
905
|
+
List of Task objects (issues in project)
|
|
906
|
+
|
|
907
|
+
Raises:
|
|
908
|
+
------
|
|
909
|
+
NotImplementedError: If adapter doesn't support projects
|
|
910
|
+
|
|
911
|
+
"""
|
|
912
|
+
raise NotImplementedError(
|
|
913
|
+
f"{self.__class__.__name__} does not support project operations. "
|
|
914
|
+
"Use list_issues_by_epic() for this adapter."
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
async def project_add_issue(self, project_id: str, issue_id: str) -> bool:
|
|
918
|
+
"""Add issue to project.
|
|
919
|
+
|
|
920
|
+
Args:
|
|
921
|
+
----
|
|
922
|
+
project_id: Project identifier
|
|
923
|
+
issue_id: Issue identifier to add
|
|
924
|
+
|
|
925
|
+
Returns:
|
|
926
|
+
-------
|
|
927
|
+
True if added successfully, False otherwise
|
|
928
|
+
|
|
929
|
+
Raises:
|
|
930
|
+
------
|
|
931
|
+
NotImplementedError: If adapter doesn't support projects
|
|
932
|
+
|
|
933
|
+
"""
|
|
934
|
+
raise NotImplementedError(
|
|
935
|
+
f"{self.__class__.__name__} does not support project operations."
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
async def project_remove_issue(self, project_id: str, issue_id: str) -> bool:
|
|
939
|
+
"""Remove issue from project.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
----
|
|
943
|
+
project_id: Project identifier
|
|
944
|
+
issue_id: Issue identifier to remove
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
-------
|
|
948
|
+
True if removed successfully, False otherwise
|
|
949
|
+
|
|
950
|
+
Raises:
|
|
951
|
+
------
|
|
952
|
+
NotImplementedError: If adapter doesn't support projects
|
|
953
|
+
|
|
954
|
+
"""
|
|
955
|
+
raise NotImplementedError(
|
|
956
|
+
f"{self.__class__.__name__} does not support project operations."
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
async def project_get_statistics(self, project_id: str) -> ProjectStatistics:
|
|
960
|
+
"""Get project statistics and metrics.
|
|
961
|
+
|
|
962
|
+
Calculates or retrieves statistics including issue counts by state,
|
|
963
|
+
progress percentage, and velocity metrics.
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
----
|
|
967
|
+
project_id: Project identifier
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
-------
|
|
971
|
+
ProjectStatistics object with calculated metrics
|
|
972
|
+
|
|
973
|
+
Raises:
|
|
974
|
+
------
|
|
975
|
+
NotImplementedError: If adapter doesn't support projects
|
|
976
|
+
|
|
977
|
+
"""
|
|
978
|
+
raise NotImplementedError(
|
|
979
|
+
f"{self.__class__.__name__} does not support project statistics."
|
|
980
|
+
)
|