mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__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.

Files changed (42) hide show
  1. mcp_ticketer/__init__.py +7 -7
  2. mcp_ticketer/__version__.py +4 -2
  3. mcp_ticketer/adapters/__init__.py +4 -4
  4. mcp_ticketer/adapters/aitrackdown.py +66 -49
  5. mcp_ticketer/adapters/github.py +192 -125
  6. mcp_ticketer/adapters/hybrid.py +99 -53
  7. mcp_ticketer/adapters/jira.py +161 -151
  8. mcp_ticketer/adapters/linear.py +396 -246
  9. mcp_ticketer/cache/__init__.py +1 -1
  10. mcp_ticketer/cache/memory.py +15 -16
  11. mcp_ticketer/cli/__init__.py +1 -1
  12. mcp_ticketer/cli/configure.py +69 -93
  13. mcp_ticketer/cli/discover.py +43 -35
  14. mcp_ticketer/cli/main.py +283 -298
  15. mcp_ticketer/cli/mcp_configure.py +39 -15
  16. mcp_ticketer/cli/migrate_config.py +11 -13
  17. mcp_ticketer/cli/queue_commands.py +21 -58
  18. mcp_ticketer/cli/utils.py +121 -66
  19. mcp_ticketer/core/__init__.py +2 -2
  20. mcp_ticketer/core/adapter.py +46 -39
  21. mcp_ticketer/core/config.py +128 -92
  22. mcp_ticketer/core/env_discovery.py +69 -37
  23. mcp_ticketer/core/http_client.py +57 -40
  24. mcp_ticketer/core/mappers.py +98 -54
  25. mcp_ticketer/core/models.py +38 -24
  26. mcp_ticketer/core/project_config.py +145 -80
  27. mcp_ticketer/core/registry.py +16 -16
  28. mcp_ticketer/mcp/__init__.py +1 -1
  29. mcp_ticketer/mcp/server.py +199 -145
  30. mcp_ticketer/queue/__init__.py +2 -2
  31. mcp_ticketer/queue/__main__.py +1 -1
  32. mcp_ticketer/queue/manager.py +30 -26
  33. mcp_ticketer/queue/queue.py +147 -85
  34. mcp_ticketer/queue/run_worker.py +2 -3
  35. mcp_ticketer/queue/worker.py +55 -40
  36. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
  37. mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
  38. mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,18 @@
1
1
  """JIRA adapter implementation using REST API v3."""
2
2
 
3
- import os
4
3
  import asyncio
5
- from typing import List, Optional, Dict, Any, Union, Tuple
4
+ import builtins
5
+ import logging
6
+ import os
6
7
  from datetime import datetime
7
8
  from enum import Enum
8
- import logging
9
+ from typing import Any, Optional, Union
9
10
 
10
11
  import httpx
11
12
  from httpx import AsyncClient, HTTPStatusError, TimeoutException
12
13
 
13
14
  from ..core.adapter import BaseAdapter
14
- from ..core.models import Epic, Task, Comment, SearchQuery, TicketState, Priority
15
+ from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
15
16
  from ..core.registry import AdapterRegistry
16
17
 
17
18
  logger = logging.getLogger(__name__)
@@ -19,6 +20,7 @@ logger = logging.getLogger(__name__)
19
20
 
20
21
  class JiraIssueType(str, Enum):
21
22
  """Common JIRA issue types."""
23
+
22
24
  EPIC = "Epic"
23
25
  STORY = "Story"
24
26
  TASK = "Task"
@@ -30,6 +32,7 @@ class JiraIssueType(str, Enum):
30
32
 
31
33
  class JiraPriority(str, Enum):
32
34
  """Standard JIRA priority levels."""
35
+
33
36
  HIGHEST = "Highest"
34
37
  HIGH = "High"
35
38
  MEDIUM = "Medium"
@@ -40,7 +43,7 @@ class JiraPriority(str, Enum):
40
43
  class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
41
44
  """Adapter for JIRA using REST API v3."""
42
45
 
43
- def __init__(self, config: Dict[str, Any]):
46
+ def __init__(self, config: dict[str, Any]):
44
47
  """Initialize JIRA adapter.
45
48
 
46
49
  Args:
@@ -53,6 +56,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
53
56
  - verify_ssl: Whether to verify SSL certificates (default: True)
54
57
  - timeout: Request timeout in seconds (default: 30)
55
58
  - max_retries: Maximum retry attempts (default: 3)
59
+
56
60
  """
57
61
  super().__init__(config)
58
62
 
@@ -60,7 +64,9 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
60
64
  self.server = config.get("server") or os.getenv("JIRA_SERVER", "")
61
65
  self.email = config.get("email") or os.getenv("JIRA_EMAIL", "")
62
66
  self.api_token = config.get("api_token") or os.getenv("JIRA_API_TOKEN", "")
63
- self.project_key = config.get("project_key") or os.getenv("JIRA_PROJECT_KEY", "")
67
+ self.project_key = config.get("project_key") or os.getenv(
68
+ "JIRA_PROJECT_KEY", ""
69
+ )
64
70
  self.is_cloud = config.get("cloud", True)
65
71
  self.verify_ssl = config.get("verify_ssl", True)
66
72
  self.timeout = config.get("timeout", 30)
@@ -74,36 +80,50 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
74
80
  self.server = self.server.rstrip("/")
75
81
 
76
82
  # API base URL
77
- self.api_base = f"{self.server}/rest/api/3" if self.is_cloud else f"{self.server}/rest/api/2"
83
+ self.api_base = (
84
+ f"{self.server}/rest/api/3"
85
+ if self.is_cloud
86
+ else f"{self.server}/rest/api/2"
87
+ )
78
88
 
79
89
  # HTTP client setup
80
90
  self.auth = httpx.BasicAuth(self.email, self.api_token)
81
91
  self.headers = {
82
92
  "Accept": "application/json",
83
- "Content-Type": "application/json"
93
+ "Content-Type": "application/json",
84
94
  }
85
95
 
86
96
  # Cache for workflow states and transitions
87
- self._workflow_cache: Dict[str, Any] = {}
88
- self._priority_cache: List[Dict[str, Any]] = []
89
- self._issue_types_cache: Dict[str, Any] = {}
90
- self._custom_fields_cache: Dict[str, Any] = {}
97
+ self._workflow_cache: dict[str, Any] = {}
98
+ self._priority_cache: list[dict[str, Any]] = []
99
+ self._issue_types_cache: dict[str, Any] = {}
100
+ self._custom_fields_cache: dict[str, Any] = {}
91
101
 
92
102
  def validate_credentials(self) -> tuple[bool, str]:
93
103
  """Validate that required credentials are present.
94
104
 
95
105
  Returns:
96
106
  (is_valid, error_message) - Tuple of validation result and error message
107
+
97
108
  """
98
109
  if not self.server:
99
- return False, "JIRA_SERVER is required but not found. Set it in .env.local or environment."
110
+ return (
111
+ False,
112
+ "JIRA_SERVER is required but not found. Set it in .env.local or environment.",
113
+ )
100
114
  if not self.email:
101
- return False, "JIRA_EMAIL is required but not found. Set it in .env.local or environment."
115
+ return (
116
+ False,
117
+ "JIRA_EMAIL is required but not found. Set it in .env.local or environment.",
118
+ )
102
119
  if not self.api_token:
103
- return False, "JIRA_API_TOKEN is required but not found. Set it in .env.local or environment."
120
+ return (
121
+ False,
122
+ "JIRA_API_TOKEN is required but not found. Set it in .env.local or environment.",
123
+ )
104
124
  return True, ""
105
125
 
106
- def _get_state_mapping(self) -> Dict[TicketState, str]:
126
+ def _get_state_mapping(self) -> dict[TicketState, str]:
107
127
  """Map universal states to common JIRA workflow states."""
108
128
  return {
109
129
  TicketState.OPEN: "To Do",
@@ -122,17 +142,17 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
122
142
  auth=self.auth,
123
143
  headers=self.headers,
124
144
  timeout=self.timeout,
125
- verify=self.verify_ssl
145
+ verify=self.verify_ssl,
126
146
  )
127
147
 
128
148
  async def _make_request(
129
149
  self,
130
150
  method: str,
131
151
  endpoint: str,
132
- data: Optional[Dict[str, Any]] = None,
133
- params: Optional[Dict[str, Any]] = None,
134
- retry_count: int = 0
135
- ) -> Dict[str, Any]:
152
+ data: Optional[dict[str, Any]] = None,
153
+ params: Optional[dict[str, Any]] = None,
154
+ retry_count: int = 0,
155
+ ) -> dict[str, Any]:
136
156
  """Make HTTP request to JIRA API with retry logic.
137
157
 
138
158
  Args:
@@ -148,16 +168,14 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
148
168
  Raises:
149
169
  HTTPStatusError: On API errors
150
170
  TimeoutException: On timeout
171
+
151
172
  """
152
173
  url = f"{self.api_base}/{endpoint.lstrip('/')}"
153
174
 
154
175
  async with await self._get_client() as client:
155
176
  try:
156
177
  response = await client.request(
157
- method=method,
158
- url=url,
159
- json=data,
160
- params=params
178
+ method=method, url=url, json=data, params=params
161
179
  )
162
180
  response.raise_for_status()
163
181
 
@@ -169,7 +187,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
169
187
 
170
188
  except TimeoutException as e:
171
189
  if retry_count < self.max_retries:
172
- await asyncio.sleep(2 ** retry_count) # Exponential backoff
190
+ await asyncio.sleep(2**retry_count) # Exponential backoff
173
191
  return await self._make_request(
174
192
  method, endpoint, data, params, retry_count + 1
175
193
  )
@@ -185,16 +203,20 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
185
203
  )
186
204
 
187
205
  # Log error details
188
- logger.error(f"JIRA API error: {e.response.status_code} - {e.response.text}")
206
+ logger.error(
207
+ f"JIRA API error: {e.response.status_code} - {e.response.text}"
208
+ )
189
209
  raise e
190
210
 
191
- async def _get_priorities(self) -> List[Dict[str, Any]]:
211
+ async def _get_priorities(self) -> list[dict[str, Any]]:
192
212
  """Get available priorities from JIRA."""
193
213
  if not self._priority_cache:
194
214
  self._priority_cache = await self._make_request("GET", "priority")
195
215
  return self._priority_cache
196
216
 
197
- async def _get_issue_types(self, project_key: Optional[str] = None) -> List[Dict[str, Any]]:
217
+ async def _get_issue_types(
218
+ self, project_key: Optional[str] = None
219
+ ) -> list[dict[str, Any]]:
198
220
  """Get available issue types for a project."""
199
221
  key = project_key or self.project_key
200
222
  if key not in self._issue_types_cache:
@@ -202,12 +224,12 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
202
224
  self._issue_types_cache[key] = data.get("issueTypes", [])
203
225
  return self._issue_types_cache[key]
204
226
 
205
- async def _get_transitions(self, issue_key: str) -> List[Dict[str, Any]]:
227
+ async def _get_transitions(self, issue_key: str) -> list[dict[str, Any]]:
206
228
  """Get available transitions for an issue."""
207
229
  data = await self._make_request("GET", f"issue/{issue_key}/transitions")
208
230
  return data.get("transitions", [])
209
231
 
210
- async def _get_custom_fields(self) -> Dict[str, str]:
232
+ async def _get_custom_fields(self) -> dict[str, str]:
211
233
  """Get custom field definitions."""
212
234
  if not self._custom_fields_cache:
213
235
  fields = await self._make_request("GET", "field")
@@ -253,45 +275,28 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
253
275
 
254
276
  return "\n".join(lines)
255
277
 
256
- def _convert_to_adf(self, text: str) -> Dict[str, Any]:
278
+ def _convert_to_adf(self, text: str) -> dict[str, Any]:
257
279
  """Convert plain text to Atlassian Document Format (ADF).
258
280
 
259
281
  ADF is required for JIRA Cloud description fields.
260
282
  This creates a simple document with paragraphs for each line.
261
283
  """
262
284
  if not text:
263
- return {
264
- "type": "doc",
265
- "version": 1,
266
- "content": []
267
- }
285
+ return {"type": "doc", "version": 1, "content": []}
268
286
 
269
287
  # Split text into lines and create paragraphs
270
- lines = text.split('\n')
288
+ lines = text.split("\n")
271
289
  content = []
272
290
 
273
291
  for line in lines:
274
292
  if line.strip(): # Non-empty line
275
- content.append({
276
- "type": "paragraph",
277
- "content": [
278
- {
279
- "type": "text",
280
- "text": line
281
- }
282
- ]
283
- })
293
+ content.append(
294
+ {"type": "paragraph", "content": [{"type": "text", "text": line}]}
295
+ )
284
296
  else: # Empty line becomes empty paragraph
285
- content.append({
286
- "type": "paragraph",
287
- "content": []
288
- })
297
+ content.append({"type": "paragraph", "content": []})
289
298
 
290
- return {
291
- "type": "doc",
292
- "version": 1,
293
- "content": content
294
- }
299
+ return {"type": "doc", "version": 1, "content": content}
295
300
 
296
301
  def _map_priority_to_jira(self, priority: Priority) -> str:
297
302
  """Map universal priority to JIRA priority."""
@@ -303,7 +308,9 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
303
308
  }
304
309
  return mapping.get(priority, JiraPriority.MEDIUM)
305
310
 
306
- def _map_priority_from_jira(self, jira_priority: Optional[Dict[str, Any]]) -> Priority:
311
+ def _map_priority_from_jira(
312
+ self, jira_priority: Optional[dict[str, Any]]
313
+ ) -> Priority:
307
314
  """Map JIRA priority to universal priority."""
308
315
  if not jira_priority:
309
316
  return Priority.MEDIUM
@@ -319,7 +326,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
319
326
  else:
320
327
  return Priority.MEDIUM
321
328
 
322
- def _map_state_from_jira(self, status: Dict[str, Any]) -> TicketState:
329
+ def _map_state_from_jira(self, status: dict[str, Any]) -> TicketState:
323
330
  """Map JIRA status to universal state."""
324
331
  if not status:
325
332
  return TicketState.OPEN
@@ -353,7 +360,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
353
360
  else:
354
361
  return TicketState.OPEN
355
362
 
356
- def _issue_to_ticket(self, issue: Dict[str, Any]) -> Union[Epic, Task]:
363
+ def _issue_to_ticket(self, issue: dict[str, Any]) -> Union[Epic, Task]:
357
364
  """Convert JIRA issue to universal ticket model."""
358
365
  fields = issue.get("fields", {})
359
366
 
@@ -375,12 +382,16 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
375
382
  label.get("name", "") if isinstance(label, dict) else str(label)
376
383
  for label in fields.get("labels", [])
377
384
  ],
378
- "created_at": datetime.fromisoformat(
379
- fields.get("created", "").replace("Z", "+00:00")
380
- ) if fields.get("created") else None,
381
- "updated_at": datetime.fromisoformat(
382
- fields.get("updated", "").replace("Z", "+00:00")
383
- ) if fields.get("updated") else None,
385
+ "created_at": (
386
+ datetime.fromisoformat(fields.get("created", "").replace("Z", "+00:00"))
387
+ if fields.get("created")
388
+ else None
389
+ ),
390
+ "updated_at": (
391
+ datetime.fromisoformat(fields.get("updated", "").replace("Z", "+00:00"))
392
+ if fields.get("updated")
393
+ else None
394
+ ),
384
395
  "metadata": {
385
396
  "jira": {
386
397
  "id": issue.get("id"),
@@ -393,7 +404,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
393
404
  "fix_versions": fields.get("fixVersions", []),
394
405
  "resolution": fields.get("resolution"),
395
406
  }
396
- }
407
+ },
397
408
  }
398
409
 
399
410
  if is_epic:
@@ -401,9 +412,8 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
401
412
  return Epic(
402
413
  **base_data,
403
414
  child_issues=[
404
- subtask.get("key")
405
- for subtask in fields.get("subtasks", [])
406
- ]
415
+ subtask.get("key") for subtask in fields.get("subtasks", [])
416
+ ],
407
417
  )
408
418
  else:
409
419
  # Create Task
@@ -414,24 +424,34 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
414
424
  **base_data,
415
425
  parent_issue=parent.get("key") if parent else None,
416
426
  parent_epic=epic_link if epic_link else None,
417
- assignee=fields.get("assignee", {}).get("displayName")
418
- if fields.get("assignee") else None,
419
- estimated_hours=fields.get("timetracking", {}).get(
420
- "originalEstimateSeconds", 0
421
- ) / 3600 if fields.get("timetracking") else None,
422
- actual_hours=fields.get("timetracking", {}).get(
423
- "timeSpentSeconds", 0
424
- ) / 3600 if fields.get("timetracking") else None,
427
+ assignee=(
428
+ fields.get("assignee", {}).get("displayName")
429
+ if fields.get("assignee")
430
+ else None
431
+ ),
432
+ estimated_hours=(
433
+ fields.get("timetracking", {}).get("originalEstimateSeconds", 0)
434
+ / 3600
435
+ if fields.get("timetracking")
436
+ else None
437
+ ),
438
+ actual_hours=(
439
+ fields.get("timetracking", {}).get("timeSpentSeconds", 0) / 3600
440
+ if fields.get("timetracking")
441
+ else None
442
+ ),
425
443
  )
426
444
 
427
445
  def _ticket_to_issue_fields(
428
- self,
429
- ticket: Union[Epic, Task],
430
- issue_type: Optional[str] = None
431
- ) -> Dict[str, Any]:
446
+ self, ticket: Union[Epic, Task], issue_type: Optional[str] = None
447
+ ) -> dict[str, Any]:
432
448
  """Convert universal ticket to JIRA issue fields."""
433
449
  # Convert description to ADF format for JIRA Cloud
434
- description = self._convert_to_adf(ticket.description or "") if self.is_cloud else (ticket.description or "")
450
+ description = (
451
+ self._convert_to_adf(ticket.description or "")
452
+ if self.is_cloud
453
+ else (ticket.description or "")
454
+ )
435
455
 
436
456
  fields = {
437
457
  "summary": ticket.title,
@@ -480,11 +500,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
480
500
  fields = self._ticket_to_issue_fields(ticket)
481
501
 
482
502
  # Create issue
483
- data = await self._make_request(
484
- "POST",
485
- "issue",
486
- data={"fields": fields}
487
- )
503
+ data = await self._make_request("POST", "issue", data={"fields": fields})
488
504
 
489
505
  # Set the ID and fetch full issue data
490
506
  ticket.id = data.get("key")
@@ -502,9 +518,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
502
518
 
503
519
  try:
504
520
  issue = await self._make_request(
505
- "GET",
506
- f"issue/{ticket_id}",
507
- params={"expand": "renderedFields"}
521
+ "GET", f"issue/{ticket_id}", params={"expand": "renderedFields"}
508
522
  )
509
523
  return self._issue_to_ticket(issue)
510
524
  except HTTPStatusError as e:
@@ -513,9 +527,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
513
527
  raise
514
528
 
515
529
  async def update(
516
- self,
517
- ticket_id: str,
518
- updates: Dict[str, Any]
530
+ self, ticket_id: str, updates: dict[str, Any]
519
531
  ) -> Optional[Union[Epic, Task]]:
520
532
  """Update a JIRA issue."""
521
533
  # Validate credentials before attempting operation
@@ -536,7 +548,9 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
536
548
  if "description" in updates:
537
549
  fields["description"] = updates["description"]
538
550
  if "priority" in updates:
539
- fields["priority"] = {"name": self._map_priority_to_jira(updates["priority"])}
551
+ fields["priority"] = {
552
+ "name": self._map_priority_to_jira(updates["priority"])
553
+ }
540
554
  if "tags" in updates:
541
555
  fields["labels"] = updates["tags"]
542
556
  if "assignee" in updates:
@@ -545,9 +559,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
545
559
  # Apply update
546
560
  if fields:
547
561
  await self._make_request(
548
- "PUT",
549
- f"issue/{ticket_id}",
550
- data={"fields": fields}
562
+ "PUT", f"issue/{ticket_id}", data={"fields": fields}
551
563
  )
552
564
 
553
565
  # Handle state transitions separately
@@ -573,11 +585,8 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
573
585
  raise
574
586
 
575
587
  async def list(
576
- self,
577
- limit: int = 10,
578
- offset: int = 0,
579
- filters: Optional[Dict[str, Any]] = None
580
- ) -> List[Union[Epic, Task]]:
588
+ self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
589
+ ) -> list[Union[Epic, Task]]:
581
590
  """List JIRA issues with pagination."""
582
591
  # Build JQL query
583
592
  jql_parts = []
@@ -608,15 +617,15 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
608
617
  "startAt": offset,
609
618
  "maxResults": limit,
610
619
  "fields": ["*all"],
611
- "expand": ["renderedFields"]
612
- }
620
+ "expand": ["renderedFields"],
621
+ },
613
622
  )
614
623
 
615
624
  # Convert issues
616
625
  issues = data.get("issues", [])
617
626
  return [self._issue_to_ticket(issue) for issue in issues]
618
627
 
619
- async def search(self, query: SearchQuery) -> List[Union[Epic, Task]]:
628
+ async def search(self, query: SearchQuery) -> builtins.list[Union[Epic, Task]]:
620
629
  """Search JIRA issues using JQL."""
621
630
  # Build JQL query
622
631
  jql_parts = []
@@ -658,8 +667,8 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
658
667
  "startAt": query.offset,
659
668
  "maxResults": query.limit,
660
669
  "fields": ["*all"],
661
- "expand": ["renderedFields"]
662
- }
670
+ "expand": ["renderedFields"],
671
+ },
663
672
  )
664
673
 
665
674
  # Convert and return results
@@ -667,9 +676,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
667
676
  return [self._issue_to_ticket(issue) for issue in issues]
668
677
 
669
678
  async def transition_state(
670
- self,
671
- ticket_id: str,
672
- target_state: TicketState
679
+ self, ticket_id: str, target_state: TicketState
673
680
  ) -> Optional[Union[Epic, Task]]:
674
681
  """Transition JIRA issue to a new state."""
675
682
  # Get available transitions
@@ -688,10 +695,17 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
688
695
  if not transition:
689
696
  # Try to find by status category
690
697
  for trans in transitions:
691
- category = trans.get("to", {}).get("statusCategory", {}).get("key", "").lower()
692
- if (target_state == TicketState.DONE and category == "done") or \
693
- (target_state == TicketState.IN_PROGRESS and category == "indeterminate") or \
694
- (target_state == TicketState.OPEN and category == "new"):
698
+ category = (
699
+ trans.get("to", {}).get("statusCategory", {}).get("key", "").lower()
700
+ )
701
+ if (
702
+ (target_state == TicketState.DONE and category == "done")
703
+ or (
704
+ target_state == TicketState.IN_PROGRESS
705
+ and category == "indeterminate"
706
+ )
707
+ or (target_state == TicketState.OPEN and category == "new")
708
+ ):
695
709
  transition = trans
696
710
  break
697
711
 
@@ -706,7 +720,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
706
720
  await self._make_request(
707
721
  "POST",
708
722
  f"issue/{ticket_id}/transitions",
709
- data={"transition": {"id": transition["id"]}}
723
+ data={"transition": {"id": transition["id"]}},
710
724
  )
711
725
 
712
726
  # Return updated issue
@@ -715,51 +729,39 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
715
729
  async def add_comment(self, comment: Comment) -> Comment:
716
730
  """Add a comment to a JIRA issue."""
717
731
  # Prepare comment data
718
- data = {
719
- "body": comment.content
720
- }
732
+ data = {"body": comment.content}
721
733
 
722
734
  # Add comment
723
735
  result = await self._make_request(
724
- "POST",
725
- f"issue/{comment.ticket_id}/comment",
726
- data=data
736
+ "POST", f"issue/{comment.ticket_id}/comment", data=data
727
737
  )
728
738
 
729
739
  # Update comment with JIRA data
730
740
  comment.id = result.get("id")
731
- comment.created_at = datetime.fromisoformat(
732
- result.get("created", "").replace("Z", "+00:00")
733
- ) if result.get("created") else datetime.now()
741
+ comment.created_at = (
742
+ datetime.fromisoformat(result.get("created", "").replace("Z", "+00:00"))
743
+ if result.get("created")
744
+ else datetime.now()
745
+ )
734
746
  comment.author = result.get("author", {}).get("displayName", comment.author)
735
747
  comment.metadata["jira"] = result
736
748
 
737
749
  return comment
738
750
 
739
751
  async def get_comments(
740
- self,
741
- ticket_id: str,
742
- limit: int = 10,
743
- offset: int = 0
744
- ) -> List[Comment]:
752
+ self, ticket_id: str, limit: int = 10, offset: int = 0
753
+ ) -> builtins.list[Comment]:
745
754
  """Get comments for a JIRA issue."""
746
755
  # Fetch issue with comments
747
- params = {
748
- "expand": "comments",
749
- "fields": "comment"
750
- }
756
+ params = {"expand": "comments", "fields": "comment"}
751
757
 
752
- issue = await self._make_request(
753
- "GET",
754
- f"issue/{ticket_id}",
755
- params=params
756
- )
758
+ issue = await self._make_request("GET", f"issue/{ticket_id}", params=params)
757
759
 
758
760
  # Extract comments
759
761
  comments_data = issue.get("fields", {}).get("comment", {}).get("comments", [])
760
762
 
761
763
  # Apply pagination
762
- paginated = comments_data[offset:offset + limit]
764
+ paginated = comments_data[offset : offset + limit]
763
765
 
764
766
  # Convert to Comment objects
765
767
  comments = []
@@ -769,16 +771,22 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
769
771
  ticket_id=ticket_id,
770
772
  author=comment_data.get("author", {}).get("displayName", "Unknown"),
771
773
  content=comment_data.get("body", ""),
772
- created_at=datetime.fromisoformat(
773
- comment_data.get("created", "").replace("Z", "+00:00")
774
- ) if comment_data.get("created") else None,
775
- metadata={"jira": comment_data}
774
+ created_at=(
775
+ datetime.fromisoformat(
776
+ comment_data.get("created", "").replace("Z", "+00:00")
777
+ )
778
+ if comment_data.get("created")
779
+ else None
780
+ ),
781
+ metadata={"jira": comment_data},
776
782
  )
777
783
  comments.append(comment)
778
784
 
779
785
  return comments
780
786
 
781
- async def get_project_info(self, project_key: Optional[str] = None) -> Dict[str, Any]:
787
+ async def get_project_info(
788
+ self, project_key: Optional[str] = None
789
+ ) -> dict[str, Any]:
782
790
  """Get JIRA project information including workflows and fields."""
783
791
  key = project_key or self.project_key
784
792
  if not key:
@@ -798,7 +806,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
798
806
  "custom_fields": custom_fields,
799
807
  }
800
808
 
801
- async def execute_jql(self, jql: str, limit: int = 50) -> List[Union[Epic, Task]]:
809
+ async def execute_jql(self, jql: str, limit: int = 50) -> builtins.list[Union[Epic, Task]]:
802
810
  """Execute a raw JQL query.
803
811
 
804
812
  Args:
@@ -807,6 +815,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
807
815
 
808
816
  Returns:
809
817
  List of matching tickets
818
+
810
819
  """
811
820
  data = await self._make_request(
812
821
  "POST",
@@ -816,13 +825,13 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
816
825
  "startAt": 0,
817
826
  "maxResults": limit,
818
827
  "fields": ["*all"],
819
- }
828
+ },
820
829
  )
821
830
 
822
831
  issues = data.get("issues", [])
823
832
  return [self._issue_to_ticket(issue) for issue in issues]
824
833
 
825
- async def get_sprints(self, board_id: Optional[int] = None) -> List[Dict[str, Any]]:
834
+ async def get_sprints(self, board_id: Optional[int] = None) -> builtins.list[dict[str, Any]]:
826
835
  """Get active sprints for a board (requires JIRA Software).
827
836
 
828
837
  Args:
@@ -830,13 +839,14 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
830
839
 
831
840
  Returns:
832
841
  List of sprint information
842
+
833
843
  """
834
844
  if not board_id:
835
845
  # Try to find a board for the project
836
846
  boards_data = await self._make_request(
837
847
  "GET",
838
- f"/rest/agile/1.0/board",
839
- params={"projectKeyOrId": self.project_key}
848
+ "/rest/agile/1.0/board",
849
+ params={"projectKeyOrId": self.project_key},
840
850
  )
841
851
  boards = boards_data.get("values", [])
842
852
  if not boards:
@@ -847,7 +857,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
847
857
  sprints_data = await self._make_request(
848
858
  "GET",
849
859
  f"/rest/agile/1.0/board/{board_id}/sprint",
850
- params={"state": "active,future"}
860
+ params={"state": "active,future"},
851
861
  )
852
862
 
853
863
  return sprints_data.get("values", [])
@@ -862,4 +872,4 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
862
872
 
863
873
 
864
874
  # Register the adapter
865
- AdapterRegistry.register("jira", JiraAdapter)
875
+ AdapterRegistry.register("jira", JiraAdapter)