mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__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 +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- 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/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- 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/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- 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 +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
mcp_ticketer/adapters/jira.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""JIRA adapter implementation using REST API v3."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import asyncio
|
|
4
6
|
import builtins
|
|
5
7
|
import logging
|
|
@@ -59,9 +61,11 @@ def extract_text_from_adf(adf_content: str | dict[str, Any]) -> str:
|
|
|
59
61
|
"""Extract plain text from Atlassian Document Format (ADF).
|
|
60
62
|
|
|
61
63
|
Args:
|
|
64
|
+
----
|
|
62
65
|
adf_content: Either a string (already plain text) or ADF document dict
|
|
63
66
|
|
|
64
67
|
Returns:
|
|
68
|
+
-------
|
|
65
69
|
Plain text string extracted from the ADF content
|
|
66
70
|
|
|
67
71
|
"""
|
|
@@ -123,6 +127,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
123
127
|
"""Initialize JIRA adapter.
|
|
124
128
|
|
|
125
129
|
Args:
|
|
130
|
+
----
|
|
126
131
|
config: Configuration with:
|
|
127
132
|
- server: JIRA server URL (e.g., https://company.atlassian.net)
|
|
128
133
|
- email: User email for authentication
|
|
@@ -183,6 +188,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
183
188
|
"""Validate that required credentials are present.
|
|
184
189
|
|
|
185
190
|
Returns:
|
|
191
|
+
-------
|
|
186
192
|
(is_valid, error_message) - Tuple of validation result and error message
|
|
187
193
|
|
|
188
194
|
"""
|
|
@@ -236,6 +242,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
236
242
|
"""Make HTTP request to JIRA API with retry logic.
|
|
237
243
|
|
|
238
244
|
Args:
|
|
245
|
+
----
|
|
239
246
|
method: HTTP method
|
|
240
247
|
endpoint: API endpoint
|
|
241
248
|
data: Request body data
|
|
@@ -243,9 +250,11 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
243
250
|
retry_count: Current retry attempt
|
|
244
251
|
|
|
245
252
|
Returns:
|
|
253
|
+
-------
|
|
246
254
|
Response data
|
|
247
255
|
|
|
248
256
|
Raises:
|
|
257
|
+
------
|
|
249
258
|
HTTPStatusError: On API errors
|
|
250
259
|
TimeoutException: On timeout
|
|
251
260
|
|
|
@@ -890,10 +899,12 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
890
899
|
"""Execute a raw JQL query.
|
|
891
900
|
|
|
892
901
|
Args:
|
|
902
|
+
----
|
|
893
903
|
jql: JIRA Query Language string
|
|
894
904
|
limit: Maximum number of results
|
|
895
905
|
|
|
896
906
|
Returns:
|
|
907
|
+
-------
|
|
897
908
|
List of matching tickets
|
|
898
909
|
|
|
899
910
|
"""
|
|
@@ -917,9 +928,11 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
917
928
|
"""Get active sprints for a board (requires JIRA Software).
|
|
918
929
|
|
|
919
930
|
Args:
|
|
931
|
+
----
|
|
920
932
|
board_id: Agile board ID
|
|
921
933
|
|
|
922
934
|
Returns:
|
|
935
|
+
-------
|
|
923
936
|
List of sprint information
|
|
924
937
|
|
|
925
938
|
"""
|
|
@@ -1010,6 +1023,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
1010
1023
|
recent issues and extract unique labels from them.
|
|
1011
1024
|
|
|
1012
1025
|
Returns:
|
|
1026
|
+
-------
|
|
1013
1027
|
List of label dictionaries with 'id' and 'name' fields
|
|
1014
1028
|
|
|
1015
1029
|
"""
|
|
@@ -1045,10 +1059,506 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
1045
1059
|
# Fallback: return empty list if query fails
|
|
1046
1060
|
return []
|
|
1047
1061
|
|
|
1062
|
+
async def create_issue_label(
|
|
1063
|
+
self, name: str, color: str | None = None
|
|
1064
|
+
) -> dict[str, Any]:
|
|
1065
|
+
"""Create a new issue label in JIRA.
|
|
1066
|
+
|
|
1067
|
+
Note: JIRA doesn't have a dedicated label creation API. Labels are
|
|
1068
|
+
created automatically when first used on an issue. This method
|
|
1069
|
+
validates the label name and returns a success response.
|
|
1070
|
+
|
|
1071
|
+
Args:
|
|
1072
|
+
----
|
|
1073
|
+
name: Label name to create
|
|
1074
|
+
color: Optional color (JIRA doesn't support colors natively, ignored)
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
-------
|
|
1078
|
+
Dict with label details:
|
|
1079
|
+
- id: Label name (same as name in JIRA)
|
|
1080
|
+
- name: Label name
|
|
1081
|
+
- status: "ready" indicating the label can be used
|
|
1082
|
+
|
|
1083
|
+
Raises:
|
|
1084
|
+
------
|
|
1085
|
+
ValueError: If credentials are invalid or label name is invalid
|
|
1086
|
+
|
|
1087
|
+
"""
|
|
1088
|
+
# Validate credentials before attempting operation
|
|
1089
|
+
is_valid, error_message = self.validate_credentials()
|
|
1090
|
+
if not is_valid:
|
|
1091
|
+
raise ValueError(error_message)
|
|
1092
|
+
|
|
1093
|
+
# Validate label name
|
|
1094
|
+
if not name or not name.strip():
|
|
1095
|
+
raise ValueError("Label name cannot be empty")
|
|
1096
|
+
|
|
1097
|
+
# JIRA label names must not contain spaces
|
|
1098
|
+
if " " in name:
|
|
1099
|
+
raise ValueError(
|
|
1100
|
+
"JIRA label names cannot contain spaces. Use underscores or hyphens instead."
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
# Return success response
|
|
1104
|
+
# The label will be created automatically when first used on an issue
|
|
1105
|
+
return {"id": name, "name": name, "status": "ready"}
|
|
1106
|
+
|
|
1107
|
+
async def list_project_labels(
|
|
1108
|
+
self, project_key: str | None = None, limit: int = 100
|
|
1109
|
+
) -> builtins.list[dict[str, Any]]:
|
|
1110
|
+
"""List all labels used in a JIRA project.
|
|
1111
|
+
|
|
1112
|
+
JIRA doesn't have a dedicated endpoint for listing project labels.
|
|
1113
|
+
This method queries recent issues and extracts unique labels.
|
|
1114
|
+
|
|
1115
|
+
Args:
|
|
1116
|
+
----
|
|
1117
|
+
project_key: JIRA project key (e.g., 'PROJ'). If None, uses configured project.
|
|
1118
|
+
limit: Maximum number of labels to return (default: 100)
|
|
1119
|
+
|
|
1120
|
+
Returns:
|
|
1121
|
+
-------
|
|
1122
|
+
List of label dictionaries with 'id', 'name', and 'usage_count' fields
|
|
1123
|
+
|
|
1124
|
+
Raises:
|
|
1125
|
+
------
|
|
1126
|
+
ValueError: If credentials are invalid or project key not available
|
|
1127
|
+
|
|
1128
|
+
"""
|
|
1129
|
+
# Validate credentials before attempting operation
|
|
1130
|
+
is_valid, error_message = self.validate_credentials()
|
|
1131
|
+
if not is_valid:
|
|
1132
|
+
raise ValueError(error_message)
|
|
1133
|
+
|
|
1134
|
+
# Use configured project if not specified
|
|
1135
|
+
key = project_key or self.project_key
|
|
1136
|
+
if not key:
|
|
1137
|
+
raise ValueError("Project key is required")
|
|
1138
|
+
|
|
1139
|
+
try:
|
|
1140
|
+
# Query recent issues to get labels in use
|
|
1141
|
+
jql = f"project = {key} ORDER BY updated DESC"
|
|
1142
|
+
data = await self._make_request(
|
|
1143
|
+
"GET",
|
|
1144
|
+
"search/jql",
|
|
1145
|
+
params={
|
|
1146
|
+
"jql": jql,
|
|
1147
|
+
"maxResults": 500, # Sample from more issues for better coverage
|
|
1148
|
+
"fields": "labels",
|
|
1149
|
+
},
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
# Collect labels with usage count
|
|
1153
|
+
label_counts: dict[str, int] = {}
|
|
1154
|
+
for issue in data.get("issues", []):
|
|
1155
|
+
labels = issue.get("fields", {}).get("labels", [])
|
|
1156
|
+
for label in labels:
|
|
1157
|
+
label_name = (
|
|
1158
|
+
label.get("name", "") if isinstance(label, dict) else str(label)
|
|
1159
|
+
)
|
|
1160
|
+
if label_name:
|
|
1161
|
+
label_counts[label_name] = label_counts.get(label_name, 0) + 1
|
|
1162
|
+
|
|
1163
|
+
# Transform to standardized format with usage counts
|
|
1164
|
+
result = [
|
|
1165
|
+
{"id": label, "name": label, "usage_count": count}
|
|
1166
|
+
for label, count in sorted(
|
|
1167
|
+
label_counts.items(), key=lambda x: x[1], reverse=True
|
|
1168
|
+
)
|
|
1169
|
+
]
|
|
1170
|
+
|
|
1171
|
+
return result[:limit]
|
|
1172
|
+
|
|
1173
|
+
except Exception as e:
|
|
1174
|
+
logger.error(f"Failed to list project labels: {e}")
|
|
1175
|
+
raise ValueError(f"Failed to list project labels: {e}") from e
|
|
1176
|
+
|
|
1177
|
+
async def list_cycles(
|
|
1178
|
+
self, board_id: str | None = None, state: str | None = None, limit: int = 50
|
|
1179
|
+
) -> builtins.list[dict[str, Any]]:
|
|
1180
|
+
"""List JIRA sprints (cycles) for a board.
|
|
1181
|
+
|
|
1182
|
+
Requires JIRA Agile/Software. Falls back to empty list if not available.
|
|
1183
|
+
|
|
1184
|
+
Args:
|
|
1185
|
+
----
|
|
1186
|
+
board_id: JIRA Agile board ID. If None, finds first board for project.
|
|
1187
|
+
state: Filter by state ('active', 'closed', 'future'). If None, returns all.
|
|
1188
|
+
limit: Maximum number of sprints to return (default: 50)
|
|
1189
|
+
|
|
1190
|
+
Returns:
|
|
1191
|
+
-------
|
|
1192
|
+
List of sprint dictionaries with fields:
|
|
1193
|
+
- id: Sprint ID
|
|
1194
|
+
- name: Sprint name
|
|
1195
|
+
- state: Sprint state (active, closed, future)
|
|
1196
|
+
- startDate: Start date (ISO format)
|
|
1197
|
+
- endDate: End date (ISO format)
|
|
1198
|
+
- completeDate: Completion date (ISO format, None if not completed)
|
|
1199
|
+
- goal: Sprint goal
|
|
1200
|
+
|
|
1201
|
+
Raises:
|
|
1202
|
+
------
|
|
1203
|
+
ValueError: If credentials are invalid
|
|
1204
|
+
|
|
1205
|
+
"""
|
|
1206
|
+
# Validate credentials before attempting operation
|
|
1207
|
+
is_valid, error_message = self.validate_credentials()
|
|
1208
|
+
if not is_valid:
|
|
1209
|
+
raise ValueError(error_message)
|
|
1210
|
+
|
|
1211
|
+
try:
|
|
1212
|
+
# If no board_id provided, try to find a board for the project
|
|
1213
|
+
if not board_id:
|
|
1214
|
+
boards_data = await self._make_request(
|
|
1215
|
+
"GET",
|
|
1216
|
+
"/rest/agile/1.0/board",
|
|
1217
|
+
params={"projectKeyOrId": self.project_key, "maxResults": 1},
|
|
1218
|
+
)
|
|
1219
|
+
boards = boards_data.get("values", [])
|
|
1220
|
+
if not boards:
|
|
1221
|
+
logger.warning(
|
|
1222
|
+
f"No Agile boards found for project {self.project_key}"
|
|
1223
|
+
)
|
|
1224
|
+
return []
|
|
1225
|
+
board_id = str(boards[0]["id"])
|
|
1226
|
+
|
|
1227
|
+
# Get sprints for the board
|
|
1228
|
+
params = {"maxResults": limit}
|
|
1229
|
+
if state:
|
|
1230
|
+
params["state"] = state
|
|
1231
|
+
|
|
1232
|
+
sprints_data = await self._make_request(
|
|
1233
|
+
"GET", f"/rest/agile/1.0/board/{board_id}/sprint", params=params
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
sprints = sprints_data.get("values", [])
|
|
1237
|
+
|
|
1238
|
+
# Transform to standardized format
|
|
1239
|
+
return [
|
|
1240
|
+
{
|
|
1241
|
+
"id": sprint.get("id"),
|
|
1242
|
+
"name": sprint.get("name"),
|
|
1243
|
+
"state": sprint.get("state"),
|
|
1244
|
+
"startDate": sprint.get("startDate"),
|
|
1245
|
+
"endDate": sprint.get("endDate"),
|
|
1246
|
+
"completeDate": sprint.get("completeDate"),
|
|
1247
|
+
"goal": sprint.get("goal", ""),
|
|
1248
|
+
}
|
|
1249
|
+
for sprint in sprints
|
|
1250
|
+
]
|
|
1251
|
+
|
|
1252
|
+
except HTTPStatusError as e:
|
|
1253
|
+
if e.response.status_code == 404:
|
|
1254
|
+
logger.warning("JIRA Agile API not available (404)")
|
|
1255
|
+
return []
|
|
1256
|
+
logger.error(f"Failed to list sprints: {e}")
|
|
1257
|
+
raise ValueError(f"Failed to list sprints: {e}") from e
|
|
1258
|
+
except Exception as e:
|
|
1259
|
+
logger.warning(f"JIRA Agile may not be available: {e}")
|
|
1260
|
+
return []
|
|
1261
|
+
|
|
1262
|
+
async def list_issue_statuses(
|
|
1263
|
+
self, project_key: str | None = None
|
|
1264
|
+
) -> builtins.list[dict[str, Any]]:
|
|
1265
|
+
"""List all workflow statuses in JIRA.
|
|
1266
|
+
|
|
1267
|
+
Args:
|
|
1268
|
+
----
|
|
1269
|
+
project_key: Optional project key to filter statuses.
|
|
1270
|
+
If None, returns all statuses.
|
|
1271
|
+
|
|
1272
|
+
Returns:
|
|
1273
|
+
-------
|
|
1274
|
+
List of status dictionaries with fields:
|
|
1275
|
+
- id: Status ID
|
|
1276
|
+
- name: Status name (e.g., "To Do", "In Progress", "Done")
|
|
1277
|
+
- category: Status category key (e.g., "new", "indeterminate", "done")
|
|
1278
|
+
- categoryName: Human-readable category name
|
|
1279
|
+
- description: Status description
|
|
1280
|
+
|
|
1281
|
+
Raises:
|
|
1282
|
+
------
|
|
1283
|
+
ValueError: If credentials are invalid
|
|
1284
|
+
|
|
1285
|
+
"""
|
|
1286
|
+
# Validate credentials before attempting operation
|
|
1287
|
+
is_valid, error_message = self.validate_credentials()
|
|
1288
|
+
if not is_valid:
|
|
1289
|
+
raise ValueError(error_message)
|
|
1290
|
+
|
|
1291
|
+
try:
|
|
1292
|
+
# Use project-specific statuses if project key provided
|
|
1293
|
+
if project_key:
|
|
1294
|
+
# Get statuses for the project
|
|
1295
|
+
data = await self._make_request(
|
|
1296
|
+
"GET", f"project/{project_key}/statuses"
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
# Extract unique statuses from all issue types
|
|
1300
|
+
status_map: dict[str, dict[str, Any]] = {}
|
|
1301
|
+
for issue_type_data in data:
|
|
1302
|
+
for status in issue_type_data.get("statuses", []):
|
|
1303
|
+
status_id = status.get("id")
|
|
1304
|
+
if status_id not in status_map:
|
|
1305
|
+
status_map[status_id] = status
|
|
1306
|
+
|
|
1307
|
+
statuses = list(status_map.values())
|
|
1308
|
+
else:
|
|
1309
|
+
# Get all statuses
|
|
1310
|
+
statuses = await self._make_request("GET", "status")
|
|
1311
|
+
|
|
1312
|
+
# Transform to standardized format
|
|
1313
|
+
return [
|
|
1314
|
+
{
|
|
1315
|
+
"id": status.get("id"),
|
|
1316
|
+
"name": status.get("name"),
|
|
1317
|
+
"category": status.get("statusCategory", {}).get("key", ""),
|
|
1318
|
+
"categoryName": status.get("statusCategory", {}).get("name", ""),
|
|
1319
|
+
"description": status.get("description", ""),
|
|
1320
|
+
}
|
|
1321
|
+
for status in statuses
|
|
1322
|
+
]
|
|
1323
|
+
|
|
1324
|
+
except Exception as e:
|
|
1325
|
+
logger.error(f"Failed to list issue statuses: {e}")
|
|
1326
|
+
raise ValueError(f"Failed to list issue statuses: {e}") from e
|
|
1327
|
+
|
|
1328
|
+
async def get_issue_status(self, issue_key: str) -> dict[str, Any] | None:
|
|
1329
|
+
"""Get rich status information for an issue.
|
|
1330
|
+
|
|
1331
|
+
Args:
|
|
1332
|
+
----
|
|
1333
|
+
issue_key: JIRA issue key (e.g., 'PROJ-123')
|
|
1334
|
+
|
|
1335
|
+
Returns:
|
|
1336
|
+
-------
|
|
1337
|
+
Dict with status details and available transitions:
|
|
1338
|
+
- id: Status ID
|
|
1339
|
+
- name: Status name
|
|
1340
|
+
- category: Status category key
|
|
1341
|
+
- categoryName: Human-readable category name
|
|
1342
|
+
- description: Status description
|
|
1343
|
+
- transitions: List of available transitions with:
|
|
1344
|
+
- id: Transition ID
|
|
1345
|
+
- name: Transition name
|
|
1346
|
+
- to: Target status info (id, name, category)
|
|
1347
|
+
Returns None if issue not found.
|
|
1348
|
+
|
|
1349
|
+
Raises:
|
|
1350
|
+
------
|
|
1351
|
+
ValueError: If credentials are invalid
|
|
1352
|
+
|
|
1353
|
+
"""
|
|
1354
|
+
# Validate credentials before attempting operation
|
|
1355
|
+
is_valid, error_message = self.validate_credentials()
|
|
1356
|
+
if not is_valid:
|
|
1357
|
+
raise ValueError(error_message)
|
|
1358
|
+
|
|
1359
|
+
try:
|
|
1360
|
+
# Get issue with status field
|
|
1361
|
+
issue = await self._make_request(
|
|
1362
|
+
"GET", f"issue/{issue_key}", params={"fields": "status"}
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
if not issue:
|
|
1366
|
+
return None
|
|
1367
|
+
|
|
1368
|
+
status = issue.get("fields", {}).get("status", {})
|
|
1369
|
+
|
|
1370
|
+
# Get available transitions
|
|
1371
|
+
transitions_data = await self._make_request(
|
|
1372
|
+
"GET", f"issue/{issue_key}/transitions"
|
|
1373
|
+
)
|
|
1374
|
+
transitions = transitions_data.get("transitions", [])
|
|
1375
|
+
|
|
1376
|
+
# Transform transitions to simplified format
|
|
1377
|
+
transition_list = [
|
|
1378
|
+
{
|
|
1379
|
+
"id": trans.get("id"),
|
|
1380
|
+
"name": trans.get("name"),
|
|
1381
|
+
"to": {
|
|
1382
|
+
"id": trans.get("to", {}).get("id"),
|
|
1383
|
+
"name": trans.get("to", {}).get("name"),
|
|
1384
|
+
"category": trans.get("to", {})
|
|
1385
|
+
.get("statusCategory", {})
|
|
1386
|
+
.get("key", ""),
|
|
1387
|
+
},
|
|
1388
|
+
}
|
|
1389
|
+
for trans in transitions
|
|
1390
|
+
]
|
|
1391
|
+
|
|
1392
|
+
return {
|
|
1393
|
+
"id": status.get("id"),
|
|
1394
|
+
"name": status.get("name"),
|
|
1395
|
+
"category": status.get("statusCategory", {}).get("key", ""),
|
|
1396
|
+
"categoryName": status.get("statusCategory", {}).get("name", ""),
|
|
1397
|
+
"description": status.get("description", ""),
|
|
1398
|
+
"transitions": transition_list,
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
except HTTPStatusError as e:
|
|
1402
|
+
if e.response.status_code == 404:
|
|
1403
|
+
return None
|
|
1404
|
+
logger.error(f"Failed to get issue status: {e}")
|
|
1405
|
+
raise ValueError(f"Failed to get issue status: {e}") from e
|
|
1406
|
+
except Exception as e:
|
|
1407
|
+
logger.error(f"Failed to get issue status: {e}")
|
|
1408
|
+
raise ValueError(f"Failed to get issue status: {e}") from e
|
|
1409
|
+
|
|
1410
|
+
async def create_epic(
|
|
1411
|
+
self,
|
|
1412
|
+
title: str,
|
|
1413
|
+
description: str = "",
|
|
1414
|
+
priority: Priority = Priority.MEDIUM,
|
|
1415
|
+
tags: list[str] | None = None,
|
|
1416
|
+
**kwargs: Any,
|
|
1417
|
+
) -> Epic:
|
|
1418
|
+
"""Create a new JIRA Epic.
|
|
1419
|
+
|
|
1420
|
+
Args:
|
|
1421
|
+
----
|
|
1422
|
+
title: Epic title
|
|
1423
|
+
description: Epic description
|
|
1424
|
+
priority: Priority level
|
|
1425
|
+
tags: List of labels
|
|
1426
|
+
**kwargs: Additional fields (reserved for future use)
|
|
1427
|
+
|
|
1428
|
+
Returns:
|
|
1429
|
+
-------
|
|
1430
|
+
Created Epic with ID populated
|
|
1431
|
+
|
|
1432
|
+
Raises:
|
|
1433
|
+
------
|
|
1434
|
+
ValueError: If credentials are invalid or creation fails
|
|
1435
|
+
|
|
1436
|
+
"""
|
|
1437
|
+
# Validate credentials
|
|
1438
|
+
is_valid, error_message = self.validate_credentials()
|
|
1439
|
+
if not is_valid:
|
|
1440
|
+
raise ValueError(error_message)
|
|
1441
|
+
|
|
1442
|
+
# Build epic input
|
|
1443
|
+
epic = Epic(
|
|
1444
|
+
id="", # Will be populated by JIRA
|
|
1445
|
+
title=title,
|
|
1446
|
+
description=description,
|
|
1447
|
+
priority=priority,
|
|
1448
|
+
tags=tags or [],
|
|
1449
|
+
state=TicketState.OPEN,
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
# Create using base create method with Epic type
|
|
1453
|
+
created_epic = await self.create(epic)
|
|
1454
|
+
|
|
1455
|
+
if not isinstance(created_epic, Epic):
|
|
1456
|
+
raise ValueError("Created ticket is not an Epic")
|
|
1457
|
+
|
|
1458
|
+
return created_epic
|
|
1459
|
+
|
|
1460
|
+
async def get_epic(self, epic_id: str) -> Epic | None:
|
|
1461
|
+
"""Get a JIRA Epic by key or ID.
|
|
1462
|
+
|
|
1463
|
+
Args:
|
|
1464
|
+
----
|
|
1465
|
+
epic_id: Epic identifier (key like PROJ-123)
|
|
1466
|
+
|
|
1467
|
+
Returns:
|
|
1468
|
+
-------
|
|
1469
|
+
Epic object if found and is an Epic type, None otherwise
|
|
1470
|
+
|
|
1471
|
+
Raises:
|
|
1472
|
+
------
|
|
1473
|
+
ValueError: If credentials are invalid
|
|
1474
|
+
|
|
1475
|
+
"""
|
|
1476
|
+
# Validate credentials
|
|
1477
|
+
is_valid, error_message = self.validate_credentials()
|
|
1478
|
+
if not is_valid:
|
|
1479
|
+
raise ValueError(error_message)
|
|
1480
|
+
|
|
1481
|
+
# Read issue
|
|
1482
|
+
ticket = await self.read(epic_id)
|
|
1483
|
+
|
|
1484
|
+
if not ticket:
|
|
1485
|
+
return None
|
|
1486
|
+
|
|
1487
|
+
# Verify it's an Epic
|
|
1488
|
+
if not isinstance(ticket, Epic):
|
|
1489
|
+
return None
|
|
1490
|
+
|
|
1491
|
+
return ticket
|
|
1492
|
+
|
|
1493
|
+
async def list_epics(
|
|
1494
|
+
self, limit: int = 50, offset: int = 0, state: str | None = None, **kwargs: Any
|
|
1495
|
+
) -> builtins.list[Epic]:
|
|
1496
|
+
"""List JIRA Epics with pagination.
|
|
1497
|
+
|
|
1498
|
+
Args:
|
|
1499
|
+
----
|
|
1500
|
+
limit: Maximum number of epics to return (default: 50)
|
|
1501
|
+
offset: Number of epics to skip for pagination (default: 0)
|
|
1502
|
+
state: Filter by state/status name (e.g., "To Do", "In Progress", "Done")
|
|
1503
|
+
**kwargs: Additional filter parameters (reserved for future use)
|
|
1504
|
+
|
|
1505
|
+
Returns:
|
|
1506
|
+
-------
|
|
1507
|
+
List of Epic objects
|
|
1508
|
+
|
|
1509
|
+
Raises:
|
|
1510
|
+
------
|
|
1511
|
+
ValueError: If credentials are invalid or query fails
|
|
1512
|
+
|
|
1513
|
+
"""
|
|
1514
|
+
# Validate credentials
|
|
1515
|
+
is_valid, error_message = self.validate_credentials()
|
|
1516
|
+
if not is_valid:
|
|
1517
|
+
raise ValueError(error_message)
|
|
1518
|
+
|
|
1519
|
+
# Build JQL query for epics
|
|
1520
|
+
jql_parts = [f"project = {self.project_key}", 'issuetype = "Epic"']
|
|
1521
|
+
|
|
1522
|
+
# Add state filter if provided
|
|
1523
|
+
if state:
|
|
1524
|
+
jql_parts.append(f'status = "{state}"')
|
|
1525
|
+
|
|
1526
|
+
jql = " AND ".join(jql_parts) + " ORDER BY updated DESC"
|
|
1527
|
+
|
|
1528
|
+
try:
|
|
1529
|
+
# Execute search
|
|
1530
|
+
data = await self._make_request(
|
|
1531
|
+
"GET",
|
|
1532
|
+
"search/jql",
|
|
1533
|
+
params={
|
|
1534
|
+
"jql": jql,
|
|
1535
|
+
"startAt": offset,
|
|
1536
|
+
"maxResults": limit,
|
|
1537
|
+
"fields": "*all",
|
|
1538
|
+
"expand": "renderedFields",
|
|
1539
|
+
},
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
# Convert issues to tickets
|
|
1543
|
+
issues = data.get("issues", [])
|
|
1544
|
+
epics = []
|
|
1545
|
+
|
|
1546
|
+
for issue in issues:
|
|
1547
|
+
ticket = self._issue_to_ticket(issue)
|
|
1548
|
+
# Only include if it's actually an Epic
|
|
1549
|
+
if isinstance(ticket, Epic):
|
|
1550
|
+
epics.append(ticket)
|
|
1551
|
+
|
|
1552
|
+
return epics
|
|
1553
|
+
|
|
1554
|
+
except Exception as e:
|
|
1555
|
+
raise ValueError(f"Failed to list JIRA epics: {e}") from e
|
|
1556
|
+
|
|
1048
1557
|
async def update_epic(self, epic_id: str, updates: dict[str, Any]) -> Epic | None:
|
|
1049
1558
|
"""Update a JIRA Epic with epic-specific field handling.
|
|
1050
1559
|
|
|
1051
1560
|
Args:
|
|
1561
|
+
----
|
|
1052
1562
|
epic_id: Epic identifier (key like PROJ-123 or ID)
|
|
1053
1563
|
updates: Dictionary with fields to update:
|
|
1054
1564
|
- title: Epic title (maps to summary)
|
|
@@ -1058,9 +1568,11 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
1058
1568
|
- priority: Priority level
|
|
1059
1569
|
|
|
1060
1570
|
Returns:
|
|
1571
|
+
-------
|
|
1061
1572
|
Updated Epic object or None if not found
|
|
1062
1573
|
|
|
1063
1574
|
Raises:
|
|
1575
|
+
------
|
|
1064
1576
|
ValueError: If no fields provided for update
|
|
1065
1577
|
HTTPStatusError: If update fails
|
|
1066
1578
|
|
|
@@ -1110,14 +1622,17 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
1110
1622
|
"""Attach file to JIRA issue (including Epic).
|
|
1111
1623
|
|
|
1112
1624
|
Args:
|
|
1625
|
+
----
|
|
1113
1626
|
ticket_id: Issue key (e.g., PROJ-123) or ID
|
|
1114
1627
|
file_path: Path to file to attach
|
|
1115
1628
|
description: Optional description (stored in metadata, not used by JIRA directly)
|
|
1116
1629
|
|
|
1117
1630
|
Returns:
|
|
1631
|
+
-------
|
|
1118
1632
|
Attachment object with metadata
|
|
1119
1633
|
|
|
1120
1634
|
Raises:
|
|
1635
|
+
------
|
|
1121
1636
|
FileNotFoundError: If file doesn't exist
|
|
1122
1637
|
ValueError: If credentials invalid
|
|
1123
1638
|
HTTPStatusError: If upload fails
|
|
@@ -1173,12 +1688,15 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
1173
1688
|
"""Get all attachments for a JIRA issue.
|
|
1174
1689
|
|
|
1175
1690
|
Args:
|
|
1691
|
+
----
|
|
1176
1692
|
ticket_id: Issue key or ID
|
|
1177
1693
|
|
|
1178
1694
|
Returns:
|
|
1695
|
+
-------
|
|
1179
1696
|
List of Attachment objects
|
|
1180
1697
|
|
|
1181
1698
|
Raises:
|
|
1699
|
+
------
|
|
1182
1700
|
ValueError: If credentials invalid
|
|
1183
1701
|
HTTPStatusError: If request fails
|
|
1184
1702
|
|
|
@@ -1215,13 +1733,16 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
1215
1733
|
"""Delete an attachment from a JIRA issue.
|
|
1216
1734
|
|
|
1217
1735
|
Args:
|
|
1736
|
+
----
|
|
1218
1737
|
ticket_id: Issue key or ID (for validation/context)
|
|
1219
1738
|
attachment_id: Attachment ID to delete
|
|
1220
1739
|
|
|
1221
1740
|
Returns:
|
|
1741
|
+
-------
|
|
1222
1742
|
True if deleted successfully, False otherwise
|
|
1223
1743
|
|
|
1224
1744
|
Raises:
|
|
1745
|
+
------
|
|
1225
1746
|
ValueError: If credentials invalid
|
|
1226
1747
|
|
|
1227
1748
|
"""
|