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.
- mcp_ticketer/__init__.py +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +66 -49
- mcp_ticketer/adapters/github.py +192 -125
- mcp_ticketer/adapters/hybrid.py +99 -53
- mcp_ticketer/adapters/jira.py +161 -151
- mcp_ticketer/adapters/linear.py +396 -246
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +15 -16
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +283 -298
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +11 -13
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +121 -66
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +46 -39
- mcp_ticketer/core/config.py +128 -92
- mcp_ticketer/core/env_discovery.py +69 -37
- mcp_ticketer/core/http_client.py +57 -40
- mcp_ticketer/core/mappers.py +98 -54
- mcp_ticketer/core/models.py +38 -24
- mcp_ticketer/core/project_config.py +145 -80
- mcp_ticketer/core/registry.py +16 -16
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +199 -145
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +30 -26
- mcp_ticketer/queue/queue.py +147 -85
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +55 -40
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
mcp_ticketer/adapters/github.py
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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 =
|
|
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(
|
|
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 =
|
|
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[
|
|
191
|
-
self._milestones_cache: Optional[
|
|
192
|
-
self._rate_limit:
|
|
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
|
|
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
|
|
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
|
|
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) ->
|
|
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:
|
|
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
|
|
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:
|
|
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 = [
|
|
267
|
-
|
|
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:
|
|
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 = [
|
|
287
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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":
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
534
|
-
|
|
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
|
|
565
|
-
|
|
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 [
|
|
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(
|
|
590
|
-
|
|
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
|
|
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
|
-
|
|
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) ->
|
|
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 = (
|
|
746
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
"
|
|
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) ->
|
|
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=
|
|
906
|
-
|
|
907
|
-
|
|
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=
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
|
|
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(
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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)
|