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,18 +1,15 @@
1
1
  """GitHub adapter implementation using REST API v3 and GraphQL API v4."""
2
2
 
3
+ import builtins
3
4
  import os
4
5
  import re
5
- import asyncio
6
- from typing import List, Optional, Dict, Any, Union, Tuple
7
6
  from datetime import datetime
8
- from enum import Enum
9
- import json
7
+ from typing import Any, Optional
10
8
 
11
9
  import httpx
12
- from pydantic import BaseModel
13
10
 
14
11
  from ..core.adapter import BaseAdapter
15
- from ..core.models import Epic, Task, Comment, SearchQuery, TicketState, Priority
12
+ from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
16
13
  from ..core.registry import AdapterRegistry
17
14
 
18
15
 
@@ -139,7 +136,7 @@ class GitHubGraphQLQueries:
139
136
  class GitHubAdapter(BaseAdapter[Task]):
140
137
  """Adapter for GitHub Issues tracking system."""
141
138
 
142
- def __init__(self, config: Dict[str, Any]):
139
+ def __init__(self, config: dict[str, Any]):
143
140
  """Initialize GitHub adapter.
144
141
 
145
142
  Args:
@@ -150,13 +147,18 @@ class GitHubAdapter(BaseAdapter[Task]):
150
147
  - api_url: Optional API URL for GitHub Enterprise (defaults to github.com)
151
148
  - use_projects_v2: Enable GitHub Projects v2 integration (default: False)
152
149
  - custom_priority_scheme: Custom priority label mapping
150
+
153
151
  """
154
152
  super().__init__(config)
155
153
 
156
154
  # Get authentication token - support both 'api_key' and 'token' for compatibility
157
- self.token = config.get("api_key") or config.get("token") or os.getenv("GITHUB_TOKEN")
155
+ self.token = (
156
+ config.get("api_key") or config.get("token") or os.getenv("GITHUB_TOKEN")
157
+ )
158
158
  if not self.token:
159
- raise ValueError("GitHub token required (config.api_key, config.token or GITHUB_TOKEN env var)")
159
+ raise ValueError(
160
+ "GitHub token required (config.api_key, config.token or GITHUB_TOKEN env var)"
161
+ )
160
162
 
161
163
  # Get repository information
162
164
  self.owner = config.get("owner") or os.getenv("GITHUB_OWNER")
@@ -167,7 +169,11 @@ class GitHubAdapter(BaseAdapter[Task]):
167
169
 
168
170
  # API URLs
169
171
  self.api_url = config.get("api_url", "https://api.github.com")
170
- self.graphql_url = f"{self.api_url}/graphql" if "github.com" in self.api_url else f"{self.api_url}/api/graphql"
172
+ self.graphql_url = (
173
+ f"{self.api_url}/graphql"
174
+ if "github.com" in self.api_url
175
+ else f"{self.api_url}/api/graphql"
176
+ )
171
177
 
172
178
  # Configuration options
173
179
  self.use_projects_v2 = config.get("use_projects_v2", False)
@@ -187,25 +193,35 @@ class GitHubAdapter(BaseAdapter[Task]):
187
193
  )
188
194
 
189
195
  # Cache for labels and milestones
190
- self._labels_cache: Optional[List[Dict[str, Any]]] = None
191
- self._milestones_cache: Optional[List[Dict[str, Any]]] = None
192
- self._rate_limit: Dict[str, Any] = {}
196
+ self._labels_cache: Optional[list[dict[str, Any]]] = None
197
+ self._milestones_cache: Optional[list[dict[str, Any]]] = None
198
+ self._rate_limit: dict[str, Any] = {}
193
199
 
194
200
  def validate_credentials(self) -> tuple[bool, str]:
195
201
  """Validate that required credentials are present.
196
202
 
197
203
  Returns:
198
204
  (is_valid, error_message) - Tuple of validation result and error message
205
+
199
206
  """
200
207
  if not self.token:
201
- return False, "GITHUB_TOKEN is required but not found. Set it in .env.local or environment."
208
+ return (
209
+ False,
210
+ "GITHUB_TOKEN is required but not found. Set it in .env.local or environment.",
211
+ )
202
212
  if not self.owner:
203
- return False, "GitHub owner is required in configuration. Set GITHUB_OWNER in .env.local or configure with 'mcp-ticketer init --adapter github --github-owner <owner>'"
213
+ return (
214
+ False,
215
+ "GitHub owner is required in configuration. Set GITHUB_OWNER in .env.local or configure with 'mcp-ticketer init --adapter github --github-owner <owner>'",
216
+ )
204
217
  if not self.repo:
205
- return False, "GitHub repo is required in configuration. Set GITHUB_REPO in .env.local or configure with 'mcp-ticketer init --adapter github --github-repo <repo>'"
218
+ return (
219
+ False,
220
+ "GitHub repo is required in configuration. Set GITHUB_REPO in .env.local or configure with 'mcp-ticketer init --adapter github --github-repo <repo>'",
221
+ )
206
222
  return True, ""
207
223
 
208
- def _get_state_mapping(self) -> Dict[TicketState, str]:
224
+ def _get_state_mapping(self) -> dict[TicketState, str]:
209
225
  """Map universal states to GitHub states."""
210
226
  return {
211
227
  TicketState.OPEN: GitHubStateMapping.OPEN,
@@ -222,7 +238,7 @@ class GitHubAdapter(BaseAdapter[Task]):
222
238
  """Get the label name for extended states."""
223
239
  return GitHubStateMapping.STATE_LABELS.get(state)
224
240
 
225
- def _get_priority_from_labels(self, labels: List[str]) -> Priority:
241
+ def _get_priority_from_labels(self, labels: list[str]) -> Priority:
226
242
  """Extract priority from issue labels."""
227
243
  label_names = [label.lower() for label in labels]
228
244
 
@@ -251,9 +267,13 @@ class GitHubAdapter(BaseAdapter[Task]):
251
267
 
252
268
  # Use default labels
253
269
  labels = GitHubStateMapping.PRIORITY_LABELS.get(priority, [])
254
- return labels[0] if labels else f"P{['0', '1', '2', '3'][list(Priority).index(priority)]}"
270
+ return (
271
+ labels[0]
272
+ if labels
273
+ else f"P{['0', '1', '2', '3'][list(Priority).index(priority)]}"
274
+ )
255
275
 
256
- def _extract_state_from_issue(self, issue: Dict[str, Any]) -> TicketState:
276
+ def _extract_state_from_issue(self, issue: dict[str, Any]) -> TicketState:
257
277
  """Extract ticket state from GitHub issue data."""
258
278
  # Check if closed
259
279
  if issue["state"] == "closed":
@@ -263,8 +283,10 @@ class GitHubAdapter(BaseAdapter[Task]):
263
283
  labels = []
264
284
  if "labels" in issue:
265
285
  if isinstance(issue["labels"], list):
266
- labels = [label.get("name", "") if isinstance(label, dict) else str(label)
267
- for label in issue["labels"]]
286
+ labels = [
287
+ label.get("name", "") if isinstance(label, dict) else str(label)
288
+ for label in issue["labels"]
289
+ ]
268
290
  elif isinstance(issue["labels"], dict) and "nodes" in issue["labels"]:
269
291
  labels = [label["name"] for label in issue["labels"]["nodes"]]
270
292
 
@@ -277,14 +299,16 @@ class GitHubAdapter(BaseAdapter[Task]):
277
299
 
278
300
  return TicketState.OPEN
279
301
 
280
- def _task_from_github_issue(self, issue: Dict[str, Any]) -> Task:
302
+ def _task_from_github_issue(self, issue: dict[str, Any]) -> Task:
281
303
  """Convert GitHub issue to universal Task."""
282
304
  # Extract labels
283
305
  labels = []
284
306
  if "labels" in issue:
285
307
  if isinstance(issue["labels"], list):
286
- labels = [label.get("name", "") if isinstance(label, dict) else str(label)
287
- for label in issue["labels"]]
308
+ labels = [
309
+ label.get("name", "") if isinstance(label, dict) else str(label)
310
+ for label in issue["labels"]
311
+ ]
288
312
  elif isinstance(issue["labels"], dict) and "nodes" in issue["labels"]:
289
313
  labels = [label["name"] for label in issue["labels"]["nodes"]]
290
314
 
@@ -314,22 +338,34 @@ class GitHubAdapter(BaseAdapter[Task]):
314
338
  # Parse dates
315
339
  created_at = None
316
340
  if issue.get("created_at"):
317
- created_at = datetime.fromisoformat(issue["created_at"].replace("Z", "+00:00"))
341
+ created_at = datetime.fromisoformat(
342
+ issue["created_at"].replace("Z", "+00:00")
343
+ )
318
344
  elif issue.get("createdAt"):
319
- created_at = datetime.fromisoformat(issue["createdAt"].replace("Z", "+00:00"))
345
+ created_at = datetime.fromisoformat(
346
+ issue["createdAt"].replace("Z", "+00:00")
347
+ )
320
348
 
321
349
  updated_at = None
322
350
  if issue.get("updated_at"):
323
- updated_at = datetime.fromisoformat(issue["updated_at"].replace("Z", "+00:00"))
351
+ updated_at = datetime.fromisoformat(
352
+ issue["updated_at"].replace("Z", "+00:00")
353
+ )
324
354
  elif issue.get("updatedAt"):
325
- updated_at = datetime.fromisoformat(issue["updatedAt"].replace("Z", "+00:00"))
355
+ updated_at = datetime.fromisoformat(
356
+ issue["updatedAt"].replace("Z", "+00:00")
357
+ )
326
358
 
327
359
  # Build metadata
328
360
  metadata = {
329
361
  "github": {
330
362
  "number": issue.get("number"),
331
363
  "url": issue.get("url") or issue.get("html_url"),
332
- "author": issue.get("user", {}).get("login") if "user" in issue else issue.get("author", {}).get("login"),
364
+ "author": (
365
+ issue.get("user", {}).get("login")
366
+ if "user" in issue
367
+ else issue.get("author", {}).get("login")
368
+ ),
333
369
  "labels": labels,
334
370
  }
335
371
  }
@@ -359,7 +395,9 @@ class GitHubAdapter(BaseAdapter[Task]):
359
395
  metadata=metadata,
360
396
  )
361
397
 
362
- async def _ensure_label_exists(self, label_name: str, color: str = "0366d6") -> None:
398
+ async def _ensure_label_exists(
399
+ self, label_name: str, color: str = "0366d6"
400
+ ) -> None:
363
401
  """Ensure a label exists in the repository."""
364
402
  if not self._labels_cache:
365
403
  response = await self.client.get(f"/repos/{self.owner}/{self.repo}/labels")
@@ -372,16 +410,17 @@ class GitHubAdapter(BaseAdapter[Task]):
372
410
  # Create the label
373
411
  response = await self.client.post(
374
412
  f"/repos/{self.owner}/{self.repo}/labels",
375
- json={"name": label_name, "color": color}
413
+ json={"name": label_name, "color": color},
376
414
  )
377
415
  if response.status_code == 201:
378
416
  self._labels_cache.append(response.json())
379
417
 
380
- async def _graphql_request(self, query: str, variables: Dict[str, Any]) -> Dict[str, Any]:
418
+ async def _graphql_request(
419
+ self, query: str, variables: dict[str, Any]
420
+ ) -> dict[str, Any]:
381
421
  """Execute a GraphQL query."""
382
422
  response = await self.client.post(
383
- self.graphql_url,
384
- json={"query": query, "variables": variables}
423
+ self.graphql_url, json={"query": query, "variables": variables}
385
424
  )
386
425
  response.raise_for_status()
387
426
 
@@ -448,8 +487,7 @@ class GitHubAdapter(BaseAdapter[Task]):
448
487
 
449
488
  # Create the issue
450
489
  response = await self.client.post(
451
- f"/repos/{self.owner}/{self.repo}/issues",
452
- json=issue_data
490
+ f"/repos/{self.owner}/{self.repo}/issues", json=issue_data
453
491
  )
454
492
  response.raise_for_status()
455
493
 
@@ -459,7 +497,7 @@ class GitHubAdapter(BaseAdapter[Task]):
459
497
  if ticket.state in [TicketState.DONE, TicketState.CLOSED]:
460
498
  await self.client.patch(
461
499
  f"/repos/{self.owner}/{self.repo}/issues/{created_issue['number']}",
462
- json={"state": "closed"}
500
+ json={"state": "closed"},
463
501
  )
464
502
  created_issue["state"] = "closed"
465
503
 
@@ -490,7 +528,7 @@ class GitHubAdapter(BaseAdapter[Task]):
490
528
  except httpx.HTTPError:
491
529
  return None
492
530
 
493
- async def update(self, ticket_id: str, updates: Dict[str, Any]) -> Optional[Task]:
531
+ async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[Task]:
494
532
  """Update a GitHub issue."""
495
533
  # Validate credentials before attempting operation
496
534
  is_valid, error_message = self.validate_credentials()
@@ -530,8 +568,10 @@ class GitHubAdapter(BaseAdapter[Task]):
530
568
 
531
569
  # Remove old state labels
532
570
  labels_to_update = [
533
- label for label in current_labels
534
- if label.lower() not in [sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()]
571
+ label
572
+ for label in current_labels
573
+ if label.lower()
574
+ not in [sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()]
535
575
  ]
536
576
 
537
577
  # Add new state label if needed
@@ -561,8 +601,10 @@ class GitHubAdapter(BaseAdapter[Task]):
561
601
  all_priority_labels.extend([l.lower() for l in labels])
562
602
 
563
603
  labels_to_update = [
564
- label for label in labels_to_update
565
- if label.lower() not in all_priority_labels and not re.match(r'^P[0-3]$', label, re.IGNORECASE)
604
+ label
605
+ for label in labels_to_update
606
+ if label.lower() not in all_priority_labels
607
+ and not re.match(r"^P[0-3]$", label, re.IGNORECASE)
566
608
  ]
567
609
 
568
610
  # Add new priority label
@@ -584,12 +626,16 @@ class GitHubAdapter(BaseAdapter[Task]):
584
626
  # Preserve state and priority labels
585
627
  preserved_labels = []
586
628
  for label in current_labels:
587
- if label.lower() in [sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()]:
629
+ if label.lower() in [
630
+ sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()
631
+ ]:
588
632
  preserved_labels.append(label)
589
- elif any(label.lower() in [pl.lower() for pl in labels]
590
- for labels in GitHubStateMapping.PRIORITY_LABELS.values()):
633
+ elif any(
634
+ label.lower() in [pl.lower() for pl in labels]
635
+ for labels in GitHubStateMapping.PRIORITY_LABELS.values()
636
+ ):
591
637
  preserved_labels.append(label)
592
- elif re.match(r'^P[0-3]$', label, re.IGNORECASE):
638
+ elif re.match(r"^P[0-3]$", label, re.IGNORECASE):
593
639
  preserved_labels.append(label)
594
640
 
595
641
  # Add new tags
@@ -602,7 +648,7 @@ class GitHubAdapter(BaseAdapter[Task]):
602
648
  if update_data:
603
649
  response = await self.client.patch(
604
650
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}",
605
- json=update_data
651
+ json=update_data,
606
652
  )
607
653
  response.raise_for_status()
608
654
 
@@ -626,7 +672,7 @@ class GitHubAdapter(BaseAdapter[Task]):
626
672
  try:
627
673
  response = await self.client.patch(
628
674
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}",
629
- json={"state": "closed", "state_reason": "not_planned"}
675
+ json={"state": "closed", "state_reason": "not_planned"},
630
676
  )
631
677
  response.raise_for_status()
632
678
  return True
@@ -634,11 +680,8 @@ class GitHubAdapter(BaseAdapter[Task]):
634
680
  return False
635
681
 
636
682
  async def list(
637
- self,
638
- limit: int = 10,
639
- offset: int = 0,
640
- filters: Optional[Dict[str, Any]] = None
641
- ) -> List[Task]:
683
+ self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
684
+ ) -> list[Task]:
642
685
  """List GitHub issues with filters."""
643
686
  # Build query parameters
644
687
  params = {
@@ -683,8 +726,7 @@ class GitHubAdapter(BaseAdapter[Task]):
683
726
  params["milestone"] = filters["parent_epic"]
684
727
 
685
728
  response = await self.client.get(
686
- f"/repos/{self.owner}/{self.repo}/issues",
687
- params=params
729
+ f"/repos/{self.owner}/{self.repo}/issues", params=params
688
730
  )
689
731
  response.raise_for_status()
690
732
 
@@ -702,7 +744,7 @@ class GitHubAdapter(BaseAdapter[Task]):
702
744
 
703
745
  return [self._task_from_github_issue(issue) for issue in issues]
704
746
 
705
- async def search(self, query: SearchQuery) -> List[Task]:
747
+ async def search(self, query: SearchQuery) -> builtins.list[Task]:
706
748
  """Search GitHub issues using advanced search syntax."""
707
749
  # Build GitHub search query
708
750
  search_parts = [f"repo:{self.owner}/{self.repo}", "is:issue"]
@@ -742,8 +784,9 @@ class GitHubAdapter(BaseAdapter[Task]):
742
784
  github_query = " ".join(search_parts)
743
785
 
744
786
  # Use GraphQL for better search capabilities
745
- full_query = (GitHubGraphQLQueries.ISSUE_FRAGMENT +
746
- GitHubGraphQLQueries.SEARCH_ISSUES)
787
+ full_query = (
788
+ GitHubGraphQLQueries.ISSUE_FRAGMENT + GitHubGraphQLQueries.SEARCH_ISSUES
789
+ )
747
790
 
748
791
  variables = {
749
792
  "query": github_query,
@@ -788,9 +831,7 @@ class GitHubAdapter(BaseAdapter[Task]):
788
831
  return issues
789
832
 
790
833
  async def transition_state(
791
- self,
792
- ticket_id: str,
793
- target_state: TicketState
834
+ self, ticket_id: str, target_state: TicketState
794
835
  ) -> Optional[Task]:
795
836
  """Transition GitHub issue to a new state."""
796
837
  # Validate transition
@@ -810,7 +851,7 @@ class GitHubAdapter(BaseAdapter[Task]):
810
851
  # Create comment
811
852
  response = await self.client.post(
812
853
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
813
- json={"body": comment.content}
854
+ json={"body": comment.content},
814
855
  )
815
856
  response.raise_for_status()
816
857
 
@@ -821,7 +862,9 @@ class GitHubAdapter(BaseAdapter[Task]):
821
862
  ticket_id=comment.ticket_id,
822
863
  author=created_comment["user"]["login"],
823
864
  content=created_comment["body"],
824
- created_at=datetime.fromisoformat(created_comment["created_at"].replace("Z", "+00:00")),
865
+ created_at=datetime.fromisoformat(
866
+ created_comment["created_at"].replace("Z", "+00:00")
867
+ ),
825
868
  metadata={
826
869
  "github": {
827
870
  "id": created_comment["id"],
@@ -832,11 +875,8 @@ class GitHubAdapter(BaseAdapter[Task]):
832
875
  )
833
876
 
834
877
  async def get_comments(
835
- self,
836
- ticket_id: str,
837
- limit: int = 10,
838
- offset: int = 0
839
- ) -> List[Comment]:
878
+ self, ticket_id: str, limit: int = 10, offset: int = 0
879
+ ) -> builtins.list[Comment]:
840
880
  """Get comments for a GitHub issue."""
841
881
  try:
842
882
  issue_number = int(ticket_id)
@@ -851,32 +891,36 @@ class GitHubAdapter(BaseAdapter[Task]):
851
891
  try:
852
892
  response = await self.client.get(
853
893
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
854
- params=params
894
+ params=params,
855
895
  )
856
896
  response.raise_for_status()
857
897
 
858
898
  comments = []
859
899
  for comment_data in response.json():
860
- comments.append(Comment(
861
- id=str(comment_data["id"]),
862
- ticket_id=ticket_id,
863
- author=comment_data["user"]["login"],
864
- content=comment_data["body"],
865
- created_at=datetime.fromisoformat(comment_data["created_at"].replace("Z", "+00:00")),
866
- metadata={
867
- "github": {
868
- "id": comment_data["id"],
869
- "url": comment_data["html_url"],
870
- "author_avatar": comment_data["user"]["avatar_url"],
871
- }
872
- },
873
- ))
900
+ comments.append(
901
+ Comment(
902
+ id=str(comment_data["id"]),
903
+ ticket_id=ticket_id,
904
+ author=comment_data["user"]["login"],
905
+ content=comment_data["body"],
906
+ created_at=datetime.fromisoformat(
907
+ comment_data["created_at"].replace("Z", "+00:00")
908
+ ),
909
+ metadata={
910
+ "github": {
911
+ "id": comment_data["id"],
912
+ "url": comment_data["html_url"],
913
+ "author_avatar": comment_data["user"]["avatar_url"],
914
+ }
915
+ },
916
+ )
917
+ )
874
918
 
875
919
  return comments
876
920
  except httpx.HTTPError:
877
921
  return []
878
922
 
879
- async def get_rate_limit(self) -> Dict[str, Any]:
923
+ async def get_rate_limit(self) -> dict[str, Any]:
880
924
  """Get current rate limit status."""
881
925
  response = await self.client.get("/rate_limit")
882
926
  response.raise_for_status()
@@ -891,8 +935,7 @@ class GitHubAdapter(BaseAdapter[Task]):
891
935
  }
892
936
 
893
937
  response = await self.client.post(
894
- f"/repos/{self.owner}/{self.repo}/milestones",
895
- json=milestone_data
938
+ f"/repos/{self.owner}/{self.repo}/milestones", json=milestone_data
896
939
  )
897
940
  response.raise_for_status()
898
941
 
@@ -902,9 +945,17 @@ class GitHubAdapter(BaseAdapter[Task]):
902
945
  id=str(created_milestone["number"]),
903
946
  title=created_milestone["title"],
904
947
  description=created_milestone["description"],
905
- state=TicketState.OPEN if created_milestone["state"] == "open" else TicketState.CLOSED,
906
- created_at=datetime.fromisoformat(created_milestone["created_at"].replace("Z", "+00:00")),
907
- updated_at=datetime.fromisoformat(created_milestone["updated_at"].replace("Z", "+00:00")),
948
+ state=(
949
+ TicketState.OPEN
950
+ if created_milestone["state"] == "open"
951
+ else TicketState.CLOSED
952
+ ),
953
+ created_at=datetime.fromisoformat(
954
+ created_milestone["created_at"].replace("Z", "+00:00")
955
+ ),
956
+ updated_at=datetime.fromisoformat(
957
+ created_milestone["updated_at"].replace("Z", "+00:00")
958
+ ),
908
959
  metadata={
909
960
  "github": {
910
961
  "number": created_milestone["number"],
@@ -931,9 +982,17 @@ class GitHubAdapter(BaseAdapter[Task]):
931
982
  id=str(milestone["number"]),
932
983
  title=milestone["title"],
933
984
  description=milestone["description"],
934
- state=TicketState.OPEN if milestone["state"] == "open" else TicketState.CLOSED,
935
- created_at=datetime.fromisoformat(milestone["created_at"].replace("Z", "+00:00")),
936
- updated_at=datetime.fromisoformat(milestone["updated_at"].replace("Z", "+00:00")),
985
+ state=(
986
+ TicketState.OPEN
987
+ if milestone["state"] == "open"
988
+ else TicketState.CLOSED
989
+ ),
990
+ created_at=datetime.fromisoformat(
991
+ milestone["created_at"].replace("Z", "+00:00")
992
+ ),
993
+ updated_at=datetime.fromisoformat(
994
+ milestone["updated_at"].replace("Z", "+00:00")
995
+ ),
937
996
  metadata={
938
997
  "github": {
939
998
  "number": milestone["number"],
@@ -947,11 +1006,8 @@ class GitHubAdapter(BaseAdapter[Task]):
947
1006
  return None
948
1007
 
949
1008
  async def list_milestones(
950
- self,
951
- state: str = "open",
952
- limit: int = 10,
953
- offset: int = 0
954
- ) -> List[Epic]:
1009
+ self, state: str = "open", limit: int = 10, offset: int = 0
1010
+ ) -> builtins.list[Epic]:
955
1011
  """List GitHub milestones as Epics."""
956
1012
  params = {
957
1013
  "state": state,
@@ -960,29 +1016,38 @@ class GitHubAdapter(BaseAdapter[Task]):
960
1016
  }
961
1017
 
962
1018
  response = await self.client.get(
963
- f"/repos/{self.owner}/{self.repo}/milestones",
964
- params=params
1019
+ f"/repos/{self.owner}/{self.repo}/milestones", params=params
965
1020
  )
966
1021
  response.raise_for_status()
967
1022
 
968
1023
  epics = []
969
1024
  for milestone in response.json():
970
- epics.append(Epic(
971
- id=str(milestone["number"]),
972
- title=milestone["title"],
973
- description=milestone["description"],
974
- state=TicketState.OPEN if milestone["state"] == "open" else TicketState.CLOSED,
975
- created_at=datetime.fromisoformat(milestone["created_at"].replace("Z", "+00:00")),
976
- updated_at=datetime.fromisoformat(milestone["updated_at"].replace("Z", "+00:00")),
977
- metadata={
978
- "github": {
979
- "number": milestone["number"],
980
- "url": milestone["html_url"],
981
- "open_issues": milestone["open_issues"],
982
- "closed_issues": milestone["closed_issues"],
983
- }
984
- },
985
- ))
1025
+ epics.append(
1026
+ Epic(
1027
+ id=str(milestone["number"]),
1028
+ title=milestone["title"],
1029
+ description=milestone["description"],
1030
+ state=(
1031
+ TicketState.OPEN
1032
+ if milestone["state"] == "open"
1033
+ else TicketState.CLOSED
1034
+ ),
1035
+ created_at=datetime.fromisoformat(
1036
+ milestone["created_at"].replace("Z", "+00:00")
1037
+ ),
1038
+ updated_at=datetime.fromisoformat(
1039
+ milestone["updated_at"].replace("Z", "+00:00")
1040
+ ),
1041
+ metadata={
1042
+ "github": {
1043
+ "number": milestone["number"],
1044
+ "url": milestone["html_url"],
1045
+ "open_issues": milestone["open_issues"],
1046
+ "closed_issues": milestone["closed_issues"],
1047
+ }
1048
+ },
1049
+ )
1050
+ )
986
1051
 
987
1052
  return epics
988
1053
 
@@ -994,7 +1059,7 @@ class GitHubAdapter(BaseAdapter[Task]):
994
1059
 
995
1060
  response = await self.client.post(
996
1061
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
997
- json={"body": comment}
1062
+ json={"body": comment},
998
1063
  )
999
1064
 
1000
1065
  return response.status_code == 201
@@ -1007,7 +1072,7 @@ class GitHubAdapter(BaseAdapter[Task]):
1007
1072
  title: Optional[str] = None,
1008
1073
  body: Optional[str] = None,
1009
1074
  draft: bool = False,
1010
- ) -> Dict[str, Any]:
1075
+ ) -> dict[str, Any]:
1011
1076
  """Create a pull request linked to an issue.
1012
1077
 
1013
1078
  Args:
@@ -1020,6 +1085,7 @@ class GitHubAdapter(BaseAdapter[Task]):
1020
1085
 
1021
1086
  Returns:
1022
1087
  Dictionary with PR details including number, url, and branch
1088
+
1023
1089
  """
1024
1090
  try:
1025
1091
  issue_number = int(ticket_id)
@@ -1105,7 +1171,7 @@ Fixes #{issue_number}
1105
1171
  json={
1106
1172
  "ref": f"refs/heads/{head_branch}",
1107
1173
  "sha": base_sha,
1108
- }
1174
+ },
1109
1175
  )
1110
1176
 
1111
1177
  if ref_response.status_code != 201:
@@ -1122,8 +1188,7 @@ Fixes #{issue_number}
1122
1188
  }
1123
1189
 
1124
1190
  pr_response = await self.client.post(
1125
- f"/repos/{self.owner}/{self.repo}/pulls",
1126
- json=pr_data
1191
+ f"/repos/{self.owner}/{self.repo}/pulls", json=pr_data
1127
1192
  )
1128
1193
 
1129
1194
  if pr_response.status_code == 422:
@@ -1134,7 +1199,7 @@ Fixes #{issue_number}
1134
1199
  "head": f"{self.owner}:{head_branch}",
1135
1200
  "base": base_branch,
1136
1201
  "state": "open",
1137
- }
1202
+ },
1138
1203
  )
1139
1204
 
1140
1205
  if search_response.status_code == 200:
@@ -1182,7 +1247,7 @@ Fixes #{issue_number}
1182
1247
  self,
1183
1248
  ticket_id: str,
1184
1249
  pr_url: str,
1185
- ) -> Dict[str, Any]:
1250
+ ) -> dict[str, Any]:
1186
1251
  """Link an existing pull request to a ticket.
1187
1252
 
1188
1253
  Args:
@@ -1191,6 +1256,7 @@ Fixes #{issue_number}
1191
1256
 
1192
1257
  Returns:
1193
1258
  Dictionary with link status and PR details
1259
+
1194
1260
  """
1195
1261
  try:
1196
1262
  issue_number = int(ticket_id)
@@ -1200,6 +1266,7 @@ Fixes #{issue_number}
1200
1266
  # Parse PR URL to extract owner, repo, and PR number
1201
1267
  # Expected format: https://github.com/owner/repo/pull/123
1202
1268
  import re
1269
+
1203
1270
  pr_pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
1204
1271
  match = re.search(pr_pattern, pr_url)
1205
1272
 
@@ -1239,7 +1306,7 @@ Fixes #{issue_number}
1239
1306
  # Update the PR
1240
1307
  update_response = await self.client.patch(
1241
1308
  f"/repos/{self.owner}/{self.repo}/pulls/{pr_number}",
1242
- json={"body": updated_body}
1309
+ json={"body": updated_body},
1243
1310
  )
1244
1311
  update_response.raise_for_status()
1245
1312
 
@@ -1268,4 +1335,4 @@ Fixes #{issue_number}
1268
1335
 
1269
1336
 
1270
1337
  # Register the adapter
1271
- AdapterRegistry.register("github", GitHubAdapter)
1338
+ AdapterRegistry.register("github", GitHubAdapter)