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