mcp-ticketer 0.1.21__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.21.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.21.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/top_level.txt +0 -0
@@ -2,17 +2,13 @@
2
2
 
3
3
  import os
4
4
  import re
5
- import asyncio
6
- from typing import List, Optional, Dict, Any, Union, Tuple
7
5
  from datetime import datetime
8
- from enum import Enum
9
- import json
6
+ from typing import Any, Dict, List, Optional
10
7
 
11
8
  import httpx
12
- from pydantic import BaseModel
13
9
 
14
10
  from ..core.adapter import BaseAdapter
15
- from ..core.models import Epic, Task, Comment, SearchQuery, TicketState, Priority
11
+ from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
16
12
  from ..core.registry import AdapterRegistry
17
13
 
18
14
 
@@ -150,13 +146,18 @@ class GitHubAdapter(BaseAdapter[Task]):
150
146
  - api_url: Optional API URL for GitHub Enterprise (defaults to github.com)
151
147
  - use_projects_v2: Enable GitHub Projects v2 integration (default: False)
152
148
  - custom_priority_scheme: Custom priority label mapping
149
+
153
150
  """
154
151
  super().__init__(config)
155
152
 
156
153
  # 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")
154
+ self.token = (
155
+ config.get("api_key") or config.get("token") or os.getenv("GITHUB_TOKEN")
156
+ )
158
157
  if not self.token:
159
- raise ValueError("GitHub token required (config.api_key, config.token or GITHUB_TOKEN env var)")
158
+ raise ValueError(
159
+ "GitHub token required (config.api_key, config.token or GITHUB_TOKEN env var)"
160
+ )
160
161
 
161
162
  # Get repository information
162
163
  self.owner = config.get("owner") or os.getenv("GITHUB_OWNER")
@@ -167,7 +168,11 @@ class GitHubAdapter(BaseAdapter[Task]):
167
168
 
168
169
  # API URLs
169
170
  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"
171
+ self.graphql_url = (
172
+ f"{self.api_url}/graphql"
173
+ if "github.com" in self.api_url
174
+ else f"{self.api_url}/api/graphql"
175
+ )
171
176
 
172
177
  # Configuration options
173
178
  self.use_projects_v2 = config.get("use_projects_v2", False)
@@ -196,13 +201,23 @@ class GitHubAdapter(BaseAdapter[Task]):
196
201
 
197
202
  Returns:
198
203
  (is_valid, error_message) - Tuple of validation result and error message
204
+
199
205
  """
200
206
  if not self.token:
201
- return False, "GITHUB_TOKEN is required but not found. Set it in .env.local or environment."
207
+ return (
208
+ False,
209
+ "GITHUB_TOKEN is required but not found. Set it in .env.local or environment.",
210
+ )
202
211
  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>'"
212
+ return (
213
+ False,
214
+ "GitHub owner is required in configuration. Set GITHUB_OWNER in .env.local or configure with 'mcp-ticketer init --adapter github --github-owner <owner>'",
215
+ )
204
216
  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>'"
217
+ return (
218
+ False,
219
+ "GitHub repo is required in configuration. Set GITHUB_REPO in .env.local or configure with 'mcp-ticketer init --adapter github --github-repo <repo>'",
220
+ )
206
221
  return True, ""
207
222
 
208
223
  def _get_state_mapping(self) -> Dict[TicketState, str]:
@@ -251,7 +266,11 @@ class GitHubAdapter(BaseAdapter[Task]):
251
266
 
252
267
  # Use default labels
253
268
  labels = GitHubStateMapping.PRIORITY_LABELS.get(priority, [])
254
- return labels[0] if labels else f"P{['0', '1', '2', '3'][list(Priority).index(priority)]}"
269
+ return (
270
+ labels[0]
271
+ if labels
272
+ else f"P{['0', '1', '2', '3'][list(Priority).index(priority)]}"
273
+ )
255
274
 
256
275
  def _extract_state_from_issue(self, issue: Dict[str, Any]) -> TicketState:
257
276
  """Extract ticket state from GitHub issue data."""
@@ -263,8 +282,10 @@ class GitHubAdapter(BaseAdapter[Task]):
263
282
  labels = []
264
283
  if "labels" in issue:
265
284
  if isinstance(issue["labels"], list):
266
- labels = [label.get("name", "") if isinstance(label, dict) else str(label)
267
- for label in issue["labels"]]
285
+ labels = [
286
+ label.get("name", "") if isinstance(label, dict) else str(label)
287
+ for label in issue["labels"]
288
+ ]
268
289
  elif isinstance(issue["labels"], dict) and "nodes" in issue["labels"]:
269
290
  labels = [label["name"] for label in issue["labels"]["nodes"]]
270
291
 
@@ -283,8 +304,10 @@ class GitHubAdapter(BaseAdapter[Task]):
283
304
  labels = []
284
305
  if "labels" in issue:
285
306
  if isinstance(issue["labels"], list):
286
- labels = [label.get("name", "") if isinstance(label, dict) else str(label)
287
- for label in issue["labels"]]
307
+ labels = [
308
+ label.get("name", "") if isinstance(label, dict) else str(label)
309
+ for label in issue["labels"]
310
+ ]
288
311
  elif isinstance(issue["labels"], dict) and "nodes" in issue["labels"]:
289
312
  labels = [label["name"] for label in issue["labels"]["nodes"]]
290
313
 
@@ -314,22 +337,34 @@ class GitHubAdapter(BaseAdapter[Task]):
314
337
  # Parse dates
315
338
  created_at = None
316
339
  if issue.get("created_at"):
317
- created_at = datetime.fromisoformat(issue["created_at"].replace("Z", "+00:00"))
340
+ created_at = datetime.fromisoformat(
341
+ issue["created_at"].replace("Z", "+00:00")
342
+ )
318
343
  elif issue.get("createdAt"):
319
- created_at = datetime.fromisoformat(issue["createdAt"].replace("Z", "+00:00"))
344
+ created_at = datetime.fromisoformat(
345
+ issue["createdAt"].replace("Z", "+00:00")
346
+ )
320
347
 
321
348
  updated_at = None
322
349
  if issue.get("updated_at"):
323
- updated_at = datetime.fromisoformat(issue["updated_at"].replace("Z", "+00:00"))
350
+ updated_at = datetime.fromisoformat(
351
+ issue["updated_at"].replace("Z", "+00:00")
352
+ )
324
353
  elif issue.get("updatedAt"):
325
- updated_at = datetime.fromisoformat(issue["updatedAt"].replace("Z", "+00:00"))
354
+ updated_at = datetime.fromisoformat(
355
+ issue["updatedAt"].replace("Z", "+00:00")
356
+ )
326
357
 
327
358
  # Build metadata
328
359
  metadata = {
329
360
  "github": {
330
361
  "number": issue.get("number"),
331
362
  "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"),
363
+ "author": (
364
+ issue.get("user", {}).get("login")
365
+ if "user" in issue
366
+ else issue.get("author", {}).get("login")
367
+ ),
333
368
  "labels": labels,
334
369
  }
335
370
  }
@@ -359,7 +394,9 @@ class GitHubAdapter(BaseAdapter[Task]):
359
394
  metadata=metadata,
360
395
  )
361
396
 
362
- async def _ensure_label_exists(self, label_name: str, color: str = "0366d6") -> None:
397
+ async def _ensure_label_exists(
398
+ self, label_name: str, color: str = "0366d6"
399
+ ) -> None:
363
400
  """Ensure a label exists in the repository."""
364
401
  if not self._labels_cache:
365
402
  response = await self.client.get(f"/repos/{self.owner}/{self.repo}/labels")
@@ -372,16 +409,17 @@ class GitHubAdapter(BaseAdapter[Task]):
372
409
  # Create the label
373
410
  response = await self.client.post(
374
411
  f"/repos/{self.owner}/{self.repo}/labels",
375
- json={"name": label_name, "color": color}
412
+ json={"name": label_name, "color": color},
376
413
  )
377
414
  if response.status_code == 201:
378
415
  self._labels_cache.append(response.json())
379
416
 
380
- async def _graphql_request(self, query: str, variables: Dict[str, Any]) -> Dict[str, Any]:
417
+ async def _graphql_request(
418
+ self, query: str, variables: Dict[str, Any]
419
+ ) -> Dict[str, Any]:
381
420
  """Execute a GraphQL query."""
382
421
  response = await self.client.post(
383
- self.graphql_url,
384
- json={"query": query, "variables": variables}
422
+ self.graphql_url, json={"query": query, "variables": variables}
385
423
  )
386
424
  response.raise_for_status()
387
425
 
@@ -448,8 +486,7 @@ class GitHubAdapter(BaseAdapter[Task]):
448
486
 
449
487
  # Create the issue
450
488
  response = await self.client.post(
451
- f"/repos/{self.owner}/{self.repo}/issues",
452
- json=issue_data
489
+ f"/repos/{self.owner}/{self.repo}/issues", json=issue_data
453
490
  )
454
491
  response.raise_for_status()
455
492
 
@@ -459,7 +496,7 @@ class GitHubAdapter(BaseAdapter[Task]):
459
496
  if ticket.state in [TicketState.DONE, TicketState.CLOSED]:
460
497
  await self.client.patch(
461
498
  f"/repos/{self.owner}/{self.repo}/issues/{created_issue['number']}",
462
- json={"state": "closed"}
499
+ json={"state": "closed"},
463
500
  )
464
501
  created_issue["state"] = "closed"
465
502
 
@@ -530,8 +567,10 @@ class GitHubAdapter(BaseAdapter[Task]):
530
567
 
531
568
  # Remove old state labels
532
569
  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()]
570
+ label
571
+ for label in current_labels
572
+ if label.lower()
573
+ not in [sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()]
535
574
  ]
536
575
 
537
576
  # Add new state label if needed
@@ -561,8 +600,10 @@ class GitHubAdapter(BaseAdapter[Task]):
561
600
  all_priority_labels.extend([l.lower() for l in labels])
562
601
 
563
602
  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)
603
+ label
604
+ for label in labels_to_update
605
+ if label.lower() not in all_priority_labels
606
+ and not re.match(r"^P[0-3]$", label, re.IGNORECASE)
566
607
  ]
567
608
 
568
609
  # Add new priority label
@@ -584,12 +625,16 @@ class GitHubAdapter(BaseAdapter[Task]):
584
625
  # Preserve state and priority labels
585
626
  preserved_labels = []
586
627
  for label in current_labels:
587
- if label.lower() in [sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()]:
628
+ if label.lower() in [
629
+ sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()
630
+ ]:
588
631
  preserved_labels.append(label)
589
- elif any(label.lower() in [pl.lower() for pl in labels]
590
- for labels in GitHubStateMapping.PRIORITY_LABELS.values()):
632
+ elif any(
633
+ label.lower() in [pl.lower() for pl in labels]
634
+ for labels in GitHubStateMapping.PRIORITY_LABELS.values()
635
+ ):
591
636
  preserved_labels.append(label)
592
- elif re.match(r'^P[0-3]$', label, re.IGNORECASE):
637
+ elif re.match(r"^P[0-3]$", label, re.IGNORECASE):
593
638
  preserved_labels.append(label)
594
639
 
595
640
  # Add new tags
@@ -602,7 +647,7 @@ class GitHubAdapter(BaseAdapter[Task]):
602
647
  if update_data:
603
648
  response = await self.client.patch(
604
649
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}",
605
- json=update_data
650
+ json=update_data,
606
651
  )
607
652
  response.raise_for_status()
608
653
 
@@ -626,7 +671,7 @@ class GitHubAdapter(BaseAdapter[Task]):
626
671
  try:
627
672
  response = await self.client.patch(
628
673
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}",
629
- json={"state": "closed", "state_reason": "not_planned"}
674
+ json={"state": "closed", "state_reason": "not_planned"},
630
675
  )
631
676
  response.raise_for_status()
632
677
  return True
@@ -634,10 +679,7 @@ class GitHubAdapter(BaseAdapter[Task]):
634
679
  return False
635
680
 
636
681
  async def list(
637
- self,
638
- limit: int = 10,
639
- offset: int = 0,
640
- filters: Optional[Dict[str, Any]] = None
682
+ self, limit: int = 10, offset: int = 0, filters: Optional[Dict[str, Any]] = None
641
683
  ) -> List[Task]:
642
684
  """List GitHub issues with filters."""
643
685
  # Build query parameters
@@ -683,8 +725,7 @@ class GitHubAdapter(BaseAdapter[Task]):
683
725
  params["milestone"] = filters["parent_epic"]
684
726
 
685
727
  response = await self.client.get(
686
- f"/repos/{self.owner}/{self.repo}/issues",
687
- params=params
728
+ f"/repos/{self.owner}/{self.repo}/issues", params=params
688
729
  )
689
730
  response.raise_for_status()
690
731
 
@@ -742,8 +783,9 @@ class GitHubAdapter(BaseAdapter[Task]):
742
783
  github_query = " ".join(search_parts)
743
784
 
744
785
  # Use GraphQL for better search capabilities
745
- full_query = (GitHubGraphQLQueries.ISSUE_FRAGMENT +
746
- GitHubGraphQLQueries.SEARCH_ISSUES)
786
+ full_query = (
787
+ GitHubGraphQLQueries.ISSUE_FRAGMENT + GitHubGraphQLQueries.SEARCH_ISSUES
788
+ )
747
789
 
748
790
  variables = {
749
791
  "query": github_query,
@@ -788,9 +830,7 @@ class GitHubAdapter(BaseAdapter[Task]):
788
830
  return issues
789
831
 
790
832
  async def transition_state(
791
- self,
792
- ticket_id: str,
793
- target_state: TicketState
833
+ self, ticket_id: str, target_state: TicketState
794
834
  ) -> Optional[Task]:
795
835
  """Transition GitHub issue to a new state."""
796
836
  # Validate transition
@@ -810,7 +850,7 @@ class GitHubAdapter(BaseAdapter[Task]):
810
850
  # Create comment
811
851
  response = await self.client.post(
812
852
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
813
- json={"body": comment.content}
853
+ json={"body": comment.content},
814
854
  )
815
855
  response.raise_for_status()
816
856
 
@@ -821,7 +861,9 @@ class GitHubAdapter(BaseAdapter[Task]):
821
861
  ticket_id=comment.ticket_id,
822
862
  author=created_comment["user"]["login"],
823
863
  content=created_comment["body"],
824
- created_at=datetime.fromisoformat(created_comment["created_at"].replace("Z", "+00:00")),
864
+ created_at=datetime.fromisoformat(
865
+ created_comment["created_at"].replace("Z", "+00:00")
866
+ ),
825
867
  metadata={
826
868
  "github": {
827
869
  "id": created_comment["id"],
@@ -832,10 +874,7 @@ class GitHubAdapter(BaseAdapter[Task]):
832
874
  )
833
875
 
834
876
  async def get_comments(
835
- self,
836
- ticket_id: str,
837
- limit: int = 10,
838
- offset: int = 0
877
+ self, ticket_id: str, limit: int = 10, offset: int = 0
839
878
  ) -> List[Comment]:
840
879
  """Get comments for a GitHub issue."""
841
880
  try:
@@ -851,26 +890,30 @@ class GitHubAdapter(BaseAdapter[Task]):
851
890
  try:
852
891
  response = await self.client.get(
853
892
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
854
- params=params
893
+ params=params,
855
894
  )
856
895
  response.raise_for_status()
857
896
 
858
897
  comments = []
859
898
  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
- ))
899
+ comments.append(
900
+ Comment(
901
+ id=str(comment_data["id"]),
902
+ ticket_id=ticket_id,
903
+ author=comment_data["user"]["login"],
904
+ content=comment_data["body"],
905
+ created_at=datetime.fromisoformat(
906
+ comment_data["created_at"].replace("Z", "+00:00")
907
+ ),
908
+ metadata={
909
+ "github": {
910
+ "id": comment_data["id"],
911
+ "url": comment_data["html_url"],
912
+ "author_avatar": comment_data["user"]["avatar_url"],
913
+ }
914
+ },
915
+ )
916
+ )
874
917
 
875
918
  return comments
876
919
  except httpx.HTTPError:
@@ -891,8 +934,7 @@ class GitHubAdapter(BaseAdapter[Task]):
891
934
  }
892
935
 
893
936
  response = await self.client.post(
894
- f"/repos/{self.owner}/{self.repo}/milestones",
895
- json=milestone_data
937
+ f"/repos/{self.owner}/{self.repo}/milestones", json=milestone_data
896
938
  )
897
939
  response.raise_for_status()
898
940
 
@@ -902,9 +944,17 @@ class GitHubAdapter(BaseAdapter[Task]):
902
944
  id=str(created_milestone["number"]),
903
945
  title=created_milestone["title"],
904
946
  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")),
947
+ state=(
948
+ TicketState.OPEN
949
+ if created_milestone["state"] == "open"
950
+ else TicketState.CLOSED
951
+ ),
952
+ created_at=datetime.fromisoformat(
953
+ created_milestone["created_at"].replace("Z", "+00:00")
954
+ ),
955
+ updated_at=datetime.fromisoformat(
956
+ created_milestone["updated_at"].replace("Z", "+00:00")
957
+ ),
908
958
  metadata={
909
959
  "github": {
910
960
  "number": created_milestone["number"],
@@ -931,9 +981,17 @@ class GitHubAdapter(BaseAdapter[Task]):
931
981
  id=str(milestone["number"]),
932
982
  title=milestone["title"],
933
983
  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")),
984
+ state=(
985
+ TicketState.OPEN
986
+ if milestone["state"] == "open"
987
+ else TicketState.CLOSED
988
+ ),
989
+ created_at=datetime.fromisoformat(
990
+ milestone["created_at"].replace("Z", "+00:00")
991
+ ),
992
+ updated_at=datetime.fromisoformat(
993
+ milestone["updated_at"].replace("Z", "+00:00")
994
+ ),
937
995
  metadata={
938
996
  "github": {
939
997
  "number": milestone["number"],
@@ -947,10 +1005,7 @@ class GitHubAdapter(BaseAdapter[Task]):
947
1005
  return None
948
1006
 
949
1007
  async def list_milestones(
950
- self,
951
- state: str = "open",
952
- limit: int = 10,
953
- offset: int = 0
1008
+ self, state: str = "open", limit: int = 10, offset: int = 0
954
1009
  ) -> List[Epic]:
955
1010
  """List GitHub milestones as Epics."""
956
1011
  params = {
@@ -960,29 +1015,38 @@ class GitHubAdapter(BaseAdapter[Task]):
960
1015
  }
961
1016
 
962
1017
  response = await self.client.get(
963
- f"/repos/{self.owner}/{self.repo}/milestones",
964
- params=params
1018
+ f"/repos/{self.owner}/{self.repo}/milestones", params=params
965
1019
  )
966
1020
  response.raise_for_status()
967
1021
 
968
1022
  epics = []
969
1023
  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
- ))
1024
+ epics.append(
1025
+ Epic(
1026
+ id=str(milestone["number"]),
1027
+ title=milestone["title"],
1028
+ description=milestone["description"],
1029
+ state=(
1030
+ TicketState.OPEN
1031
+ if milestone["state"] == "open"
1032
+ else TicketState.CLOSED
1033
+ ),
1034
+ created_at=datetime.fromisoformat(
1035
+ milestone["created_at"].replace("Z", "+00:00")
1036
+ ),
1037
+ updated_at=datetime.fromisoformat(
1038
+ milestone["updated_at"].replace("Z", "+00:00")
1039
+ ),
1040
+ metadata={
1041
+ "github": {
1042
+ "number": milestone["number"],
1043
+ "url": milestone["html_url"],
1044
+ "open_issues": milestone["open_issues"],
1045
+ "closed_issues": milestone["closed_issues"],
1046
+ }
1047
+ },
1048
+ )
1049
+ )
986
1050
 
987
1051
  return epics
988
1052
 
@@ -994,7 +1058,7 @@ class GitHubAdapter(BaseAdapter[Task]):
994
1058
 
995
1059
  response = await self.client.post(
996
1060
  f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
997
- json={"body": comment}
1061
+ json={"body": comment},
998
1062
  )
999
1063
 
1000
1064
  return response.status_code == 201
@@ -1020,6 +1084,7 @@ class GitHubAdapter(BaseAdapter[Task]):
1020
1084
 
1021
1085
  Returns:
1022
1086
  Dictionary with PR details including number, url, and branch
1087
+
1023
1088
  """
1024
1089
  try:
1025
1090
  issue_number = int(ticket_id)
@@ -1105,7 +1170,7 @@ Fixes #{issue_number}
1105
1170
  json={
1106
1171
  "ref": f"refs/heads/{head_branch}",
1107
1172
  "sha": base_sha,
1108
- }
1173
+ },
1109
1174
  )
1110
1175
 
1111
1176
  if ref_response.status_code != 201:
@@ -1122,8 +1187,7 @@ Fixes #{issue_number}
1122
1187
  }
1123
1188
 
1124
1189
  pr_response = await self.client.post(
1125
- f"/repos/{self.owner}/{self.repo}/pulls",
1126
- json=pr_data
1190
+ f"/repos/{self.owner}/{self.repo}/pulls", json=pr_data
1127
1191
  )
1128
1192
 
1129
1193
  if pr_response.status_code == 422:
@@ -1134,7 +1198,7 @@ Fixes #{issue_number}
1134
1198
  "head": f"{self.owner}:{head_branch}",
1135
1199
  "base": base_branch,
1136
1200
  "state": "open",
1137
- }
1201
+ },
1138
1202
  )
1139
1203
 
1140
1204
  if search_response.status_code == 200:
@@ -1191,6 +1255,7 @@ Fixes #{issue_number}
1191
1255
 
1192
1256
  Returns:
1193
1257
  Dictionary with link status and PR details
1258
+
1194
1259
  """
1195
1260
  try:
1196
1261
  issue_number = int(ticket_id)
@@ -1200,6 +1265,7 @@ Fixes #{issue_number}
1200
1265
  # Parse PR URL to extract owner, repo, and PR number
1201
1266
  # Expected format: https://github.com/owner/repo/pull/123
1202
1267
  import re
1268
+
1203
1269
  pr_pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
1204
1270
  match = re.search(pr_pattern, pr_url)
1205
1271
 
@@ -1239,7 +1305,7 @@ Fixes #{issue_number}
1239
1305
  # Update the PR
1240
1306
  update_response = await self.client.patch(
1241
1307
  f"/repos/{self.owner}/{self.repo}/pulls/{pr_number}",
1242
- json={"body": updated_body}
1308
+ json={"body": updated_body},
1243
1309
  )
1244
1310
  update_response.raise_for_status()
1245
1311
 
@@ -1268,4 +1334,4 @@ Fixes #{issue_number}
1268
1334
 
1269
1335
 
1270
1336
  # Register the adapter
1271
- AdapterRegistry.register("github", GitHubAdapter)
1337
+ AdapterRegistry.register("github", GitHubAdapter)