mcp-ticketer 0.3.2__py3-none-any.whl → 0.3.4__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/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +152 -21
- mcp_ticketer/adapters/github.py +4 -4
- mcp_ticketer/adapters/jira.py +6 -6
- mcp_ticketer/adapters/linear/adapter.py +121 -17
- mcp_ticketer/adapters/linear/client.py +7 -7
- mcp_ticketer/adapters/linear/mappers.py +9 -10
- mcp_ticketer/adapters/linear/types.py +10 -10
- mcp_ticketer/cli/adapter_diagnostics.py +2 -2
- mcp_ticketer/cli/codex_configure.py +6 -6
- mcp_ticketer/cli/diagnostics.py +17 -18
- mcp_ticketer/cli/main.py +15 -0
- mcp_ticketer/cli/simple_health.py +5 -10
- mcp_ticketer/core/env_loader.py +13 -13
- mcp_ticketer/core/exceptions.py +5 -5
- mcp_ticketer/core/models.py +30 -2
- mcp_ticketer/mcp/constants.py +58 -0
- mcp_ticketer/mcp/dto.py +195 -0
- mcp_ticketer/mcp/response_builder.py +206 -0
- mcp_ticketer/mcp/server.py +311 -1287
- mcp_ticketer/queue/health_monitor.py +14 -14
- mcp_ticketer/queue/manager.py +59 -15
- mcp_ticketer/queue/queue.py +9 -2
- mcp_ticketer/queue/ticket_registry.py +15 -15
- mcp_ticketer/queue/worker.py +25 -18
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.4.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.4.dist-info}/RECORD +31 -28
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.4.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.4.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""MCP server constants and configuration."""
|
|
2
|
+
|
|
3
|
+
# JSON-RPC Protocol
|
|
4
|
+
JSONRPC_VERSION = "2.0"
|
|
5
|
+
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
6
|
+
|
|
7
|
+
# Server Info
|
|
8
|
+
SERVER_NAME = "mcp-ticketer"
|
|
9
|
+
SERVER_VERSION = "0.3.2"
|
|
10
|
+
|
|
11
|
+
# Status Values
|
|
12
|
+
STATUS_COMPLETED = "completed"
|
|
13
|
+
STATUS_ERROR = "error"
|
|
14
|
+
STATUS_NOT_IMPLEMENTED = "not_implemented"
|
|
15
|
+
|
|
16
|
+
# Error Codes
|
|
17
|
+
ERROR_PARSE = -32700
|
|
18
|
+
ERROR_INVALID_REQUEST = -32600
|
|
19
|
+
ERROR_METHOD_NOT_FOUND = -32601
|
|
20
|
+
ERROR_INVALID_PARAMS = -32602
|
|
21
|
+
ERROR_INTERNAL = -32603
|
|
22
|
+
|
|
23
|
+
# Default Values
|
|
24
|
+
DEFAULT_LIMIT = 10
|
|
25
|
+
DEFAULT_OFFSET = 0
|
|
26
|
+
DEFAULT_PRIORITY = "medium"
|
|
27
|
+
DEFAULT_MAX_DEPTH = 3
|
|
28
|
+
DEFAULT_BASE_PATH = ".aitrackdown"
|
|
29
|
+
|
|
30
|
+
# Response Messages
|
|
31
|
+
MSG_TICKET_NOT_FOUND = "Ticket {ticket_id} not found"
|
|
32
|
+
MSG_UPDATE_FAILED = "Ticket {ticket_id} not found or update failed"
|
|
33
|
+
MSG_TRANSITION_FAILED = "Ticket {ticket_id} not found or transition failed"
|
|
34
|
+
MSG_EPIC_NOT_FOUND = "Epic {epic_id} not found"
|
|
35
|
+
MSG_MISSING_PARENT_ID = "Tasks must have a parent_id (issue identifier)"
|
|
36
|
+
MSG_UNKNOWN_OPERATION = "Unknown comment operation: {operation}"
|
|
37
|
+
MSG_UNKNOWN_METHOD = "Method not found: {method}"
|
|
38
|
+
MSG_INTERNAL_ERROR = "Internal error: {error}"
|
|
39
|
+
MSG_NO_TICKETS_PROVIDED = "No tickets provided for bulk creation"
|
|
40
|
+
MSG_NO_UPDATES_PROVIDED = "No updates provided for bulk operation"
|
|
41
|
+
MSG_MISSING_TITLE = "Ticket {index} missing required 'title' field"
|
|
42
|
+
MSG_MISSING_TICKET_ID = "Update {index} missing required 'ticket_id' field"
|
|
43
|
+
MSG_TICKET_ID_REQUIRED = "ticket_id is required"
|
|
44
|
+
MSG_PR_URL_REQUIRED = "pr_url is required"
|
|
45
|
+
MSG_ATTACHMENT_NOT_IMPLEMENTED = "Attachment functionality not yet implemented"
|
|
46
|
+
MSG_PR_NOT_SUPPORTED = "PR creation not supported for adapter: {adapter}"
|
|
47
|
+
MSG_PR_LINK_NOT_SUPPORTED = "PR linking not supported for adapter: {adapter}"
|
|
48
|
+
MSG_UNKNOWN_TOOL = "Unknown tool: {tool}"
|
|
49
|
+
MSG_GITHUB_CONFIG_REQUIRED = "GitHub owner and repo are required for Linear PR creation"
|
|
50
|
+
|
|
51
|
+
# Attachment Alternative Messages
|
|
52
|
+
ATTACHMENT_ALTERNATIVES = [
|
|
53
|
+
"Add file URLs in comments",
|
|
54
|
+
"Use external file storage",
|
|
55
|
+
]
|
|
56
|
+
ATTACHMENT_NOT_IMPLEMENTED_REASON = (
|
|
57
|
+
"File attachments require adapter-specific implementation"
|
|
58
|
+
)
|
mcp_ticketer/mcp/dto.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Data Transfer Objects for MCP requests and responses."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Request DTOs
|
|
9
|
+
class CreateTicketRequest(BaseModel):
|
|
10
|
+
"""Request to create a ticket."""
|
|
11
|
+
|
|
12
|
+
title: str = Field(..., min_length=1, description="Ticket title")
|
|
13
|
+
description: Optional[str] = Field(None, description="Ticket description")
|
|
14
|
+
priority: str = Field("medium", description="Ticket priority")
|
|
15
|
+
tags: list[str] = Field(default_factory=list, description="Ticket tags")
|
|
16
|
+
assignee: Optional[str] = Field(None, description="Ticket assignee")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreateEpicRequest(BaseModel):
|
|
20
|
+
"""Request to create an epic."""
|
|
21
|
+
|
|
22
|
+
title: str = Field(..., min_length=1, description="Epic title")
|
|
23
|
+
description: Optional[str] = Field(None, description="Epic description")
|
|
24
|
+
child_issues: list[str] = Field(default_factory=list, description="Child issue IDs")
|
|
25
|
+
target_date: Optional[str] = Field(None, description="Target completion date")
|
|
26
|
+
lead_id: Optional[str] = Field(None, description="Epic lead/owner ID")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CreateIssueRequest(BaseModel):
|
|
30
|
+
"""Request to create an issue."""
|
|
31
|
+
|
|
32
|
+
title: str = Field(..., min_length=1, description="Issue title")
|
|
33
|
+
description: Optional[str] = Field(None, description="Issue description")
|
|
34
|
+
epic_id: Optional[str] = Field(None, description="Parent epic ID")
|
|
35
|
+
priority: str = Field("medium", description="Issue priority")
|
|
36
|
+
assignee: Optional[str] = Field(None, description="Issue assignee")
|
|
37
|
+
tags: list[str] = Field(default_factory=list, description="Issue tags")
|
|
38
|
+
estimated_hours: Optional[float] = Field(
|
|
39
|
+
None, description="Estimated hours to complete"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CreateTaskRequest(BaseModel):
|
|
44
|
+
"""Request to create a task."""
|
|
45
|
+
|
|
46
|
+
title: str = Field(..., min_length=1, description="Task title")
|
|
47
|
+
parent_id: str = Field(..., description="Parent issue ID")
|
|
48
|
+
description: Optional[str] = Field(None, description="Task description")
|
|
49
|
+
priority: str = Field("medium", description="Task priority")
|
|
50
|
+
assignee: Optional[str] = Field(None, description="Task assignee")
|
|
51
|
+
tags: list[str] = Field(default_factory=list, description="Task tags")
|
|
52
|
+
estimated_hours: Optional[float] = Field(
|
|
53
|
+
None, description="Estimated hours to complete"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ReadTicketRequest(BaseModel):
|
|
58
|
+
"""Request to read a ticket."""
|
|
59
|
+
|
|
60
|
+
ticket_id: str = Field(..., description="Ticket ID to read")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class UpdateTicketRequest(BaseModel):
|
|
64
|
+
"""Request to update a ticket."""
|
|
65
|
+
|
|
66
|
+
ticket_id: str = Field(..., description="Ticket ID to update")
|
|
67
|
+
updates: dict[str, Any] = Field(..., description="Fields to update")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TransitionRequest(BaseModel):
|
|
71
|
+
"""Request to transition ticket state."""
|
|
72
|
+
|
|
73
|
+
ticket_id: str = Field(..., description="Ticket ID")
|
|
74
|
+
target_state: str = Field(..., description="Target state")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SearchRequest(BaseModel):
|
|
78
|
+
"""Request to search tickets."""
|
|
79
|
+
|
|
80
|
+
query: Optional[str] = Field(None, description="Search query text")
|
|
81
|
+
state: Optional[str] = Field(None, description="Filter by ticket state")
|
|
82
|
+
priority: Optional[str] = Field(None, description="Filter by priority")
|
|
83
|
+
assignee: Optional[str] = Field(None, description="Filter by assignee")
|
|
84
|
+
tags: Optional[list[str]] = Field(None, description="Filter by tags")
|
|
85
|
+
limit: int = Field(10, description="Maximum number of results")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ListRequest(BaseModel):
|
|
89
|
+
"""Request to list tickets."""
|
|
90
|
+
|
|
91
|
+
limit: int = Field(10, description="Maximum number of tickets to return")
|
|
92
|
+
offset: int = Field(0, description="Number of tickets to skip")
|
|
93
|
+
filters: Optional[dict[str, Any]] = Field(None, description="Additional filters")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DeleteTicketRequest(BaseModel):
|
|
97
|
+
"""Request to delete a ticket."""
|
|
98
|
+
|
|
99
|
+
ticket_id: str = Field(..., description="Ticket ID to delete")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CommentRequest(BaseModel):
|
|
103
|
+
"""Request for comment operations."""
|
|
104
|
+
|
|
105
|
+
operation: str = Field("add", description="Operation: 'add' or 'list'")
|
|
106
|
+
ticket_id: str = Field(..., description="Ticket ID")
|
|
107
|
+
content: Optional[str] = Field(None, description="Comment content (for add)")
|
|
108
|
+
author: Optional[str] = Field(None, description="Comment author (for add)")
|
|
109
|
+
limit: int = Field(10, description="Max comments to return (for list)")
|
|
110
|
+
offset: int = Field(0, description="Number of comments to skip (for list)")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CreatePRRequest(BaseModel):
|
|
114
|
+
"""Request to create a pull request."""
|
|
115
|
+
|
|
116
|
+
ticket_id: str = Field(..., description="Ticket ID")
|
|
117
|
+
base_branch: str = Field("main", description="Base branch")
|
|
118
|
+
head_branch: Optional[str] = Field(None, description="Head branch")
|
|
119
|
+
title: Optional[str] = Field(None, description="PR title")
|
|
120
|
+
body: Optional[str] = Field(None, description="PR body")
|
|
121
|
+
draft: bool = Field(False, description="Create as draft PR")
|
|
122
|
+
github_owner: Optional[str] = Field(None, description="GitHub owner (for Linear)")
|
|
123
|
+
github_repo: Optional[str] = Field(None, description="GitHub repo (for Linear)")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class LinkPRRequest(BaseModel):
|
|
127
|
+
"""Request to link an existing PR to a ticket."""
|
|
128
|
+
|
|
129
|
+
ticket_id: str = Field(..., description="Ticket ID")
|
|
130
|
+
pr_url: str = Field(..., description="Pull request URL")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class EpicListRequest(BaseModel):
|
|
134
|
+
"""Request to list epics."""
|
|
135
|
+
|
|
136
|
+
limit: int = Field(10, description="Maximum number of epics to return")
|
|
137
|
+
offset: int = Field(0, description="Number of epics to skip")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class EpicIssuesRequest(BaseModel):
|
|
141
|
+
"""Request to list issues in an epic."""
|
|
142
|
+
|
|
143
|
+
epic_id: str = Field(..., description="Epic ID")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class IssueTasksRequest(BaseModel):
|
|
147
|
+
"""Request to list tasks in an issue."""
|
|
148
|
+
|
|
149
|
+
issue_id: str = Field(..., description="Issue ID")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class HierarchyTreeRequest(BaseModel):
|
|
153
|
+
"""Request to get hierarchy tree."""
|
|
154
|
+
|
|
155
|
+
epic_id: Optional[str] = Field(None, description="Specific epic ID (optional)")
|
|
156
|
+
max_depth: int = Field(3, description="Maximum depth of tree")
|
|
157
|
+
limit: int = Field(10, description="Max epics to return (if no epic_id)")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class BulkCreateRequest(BaseModel):
|
|
161
|
+
"""Request to bulk create tickets."""
|
|
162
|
+
|
|
163
|
+
tickets: list[dict[str, Any]] = Field(..., description="List of ticket data")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class BulkUpdateRequest(BaseModel):
|
|
167
|
+
"""Request to bulk update tickets."""
|
|
168
|
+
|
|
169
|
+
updates: list[dict[str, Any]] = Field(..., description="List of update data")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class SearchHierarchyRequest(BaseModel):
|
|
173
|
+
"""Request to search with hierarchy context."""
|
|
174
|
+
|
|
175
|
+
query: str = Field("", description="Search query")
|
|
176
|
+
state: Optional[str] = Field(None, description="Filter by state")
|
|
177
|
+
priority: Optional[str] = Field(None, description="Filter by priority")
|
|
178
|
+
include_children: bool = Field(True, description="Include child tickets")
|
|
179
|
+
include_parents: bool = Field(True, description="Include parent tickets")
|
|
180
|
+
limit: int = Field(50, description="Maximum number of results")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class AttachRequest(BaseModel):
|
|
184
|
+
"""Request to attach file to ticket."""
|
|
185
|
+
|
|
186
|
+
ticket_id: str = Field(..., description="Ticket ID")
|
|
187
|
+
file_path: Optional[str] = Field(None, description="File path to attach")
|
|
188
|
+
file_content: Optional[str] = Field(None, description="File content (base64)")
|
|
189
|
+
file_name: Optional[str] = Field(None, description="File name")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ListAttachmentsRequest(BaseModel):
|
|
193
|
+
"""Request to list ticket attachments."""
|
|
194
|
+
|
|
195
|
+
ticket_id: str = Field(..., description="Ticket ID")
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Response builder utility for consistent MCP responses."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .constants import JSONRPC_VERSION, STATUS_COMPLETED
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResponseBuilder:
|
|
9
|
+
"""Build consistent JSON-RPC and MCP responses."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def success(
|
|
13
|
+
request_id: Any,
|
|
14
|
+
result: dict[str, Any],
|
|
15
|
+
status: str = STATUS_COMPLETED,
|
|
16
|
+
) -> dict[str, Any]:
|
|
17
|
+
"""Build successful response.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
request_id: Request ID
|
|
21
|
+
result: Result data
|
|
22
|
+
status: Status value
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
JSON-RPC response
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
return {
|
|
29
|
+
"jsonrpc": JSONRPC_VERSION,
|
|
30
|
+
"result": {"status": status, **result},
|
|
31
|
+
"id": request_id,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def error(
|
|
36
|
+
request_id: Any,
|
|
37
|
+
code: int,
|
|
38
|
+
message: str,
|
|
39
|
+
data: Optional[dict[str, Any]] = None,
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
"""Build error response.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
request_id: Request ID
|
|
45
|
+
code: Error code
|
|
46
|
+
message: Error message
|
|
47
|
+
data: Additional error data
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
JSON-RPC error response
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
error_obj = {"code": code, "message": message}
|
|
54
|
+
if data:
|
|
55
|
+
error_obj["data"] = data
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
"jsonrpc": JSONRPC_VERSION,
|
|
59
|
+
"error": error_obj,
|
|
60
|
+
"id": request_id,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def status_result(status: str, **kwargs: Any) -> dict[str, Any]:
|
|
65
|
+
"""Build status result with additional fields.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
status: Status value
|
|
69
|
+
**kwargs: Additional fields
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Result dictionary
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
return {"status": status, **kwargs}
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def ticket_result(ticket: Any) -> dict[str, Any]:
|
|
79
|
+
"""Build ticket result.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
ticket: Ticket object
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Result dictionary with ticket data
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
return {"ticket": ticket.model_dump()}
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def tickets_result(tickets: list[Any]) -> dict[str, Any]:
|
|
92
|
+
"""Build tickets list result.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
tickets: List of ticket objects
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Result dictionary with tickets data
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
return {"tickets": [t.model_dump() for t in tickets]}
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def comment_result(comment: Any) -> dict[str, Any]:
|
|
105
|
+
"""Build comment result.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
comment: Comment object
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Result dictionary with comment data
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
return {"comment": comment.model_dump()}
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def comments_result(comments: list[Any]) -> dict[str, Any]:
|
|
118
|
+
"""Build comments list result.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
comments: List of comment objects
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Result dictionary with comments data
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
return {"comments": [c.model_dump() for c in comments]}
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def deletion_result(ticket_id: str, success: bool) -> dict[str, Any]:
|
|
131
|
+
"""Build deletion result.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
ticket_id: ID of deleted ticket
|
|
135
|
+
success: Whether deletion was successful
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Result dictionary with deletion status
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
return {"success": success, "ticket_id": ticket_id}
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def bulk_result(results: list[dict[str, Any]]) -> dict[str, Any]:
|
|
145
|
+
"""Build bulk operation result.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
results: List of operation results
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Result dictionary with bulk operation data
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
return {"results": results, "count": len(results)}
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def epics_result(epics: list[Any]) -> dict[str, Any]:
|
|
158
|
+
"""Build epics list result.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
epics: List of epic objects
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Result dictionary with epics data
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
return {"epics": [epic.model_dump() for epic in epics]}
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def issues_result(issues: list[Any]) -> dict[str, Any]:
|
|
171
|
+
"""Build issues list result.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
issues: List of issue objects
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Result dictionary with issues data
|
|
178
|
+
|
|
179
|
+
"""
|
|
180
|
+
return {"issues": [issue.model_dump() for issue in issues]}
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def tasks_result(tasks: list[Any]) -> dict[str, Any]:
|
|
184
|
+
"""Build tasks list result.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
tasks: List of task objects
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Result dictionary with tasks data
|
|
191
|
+
|
|
192
|
+
"""
|
|
193
|
+
return {"tasks": [task.model_dump() for task in tasks]}
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def attachments_result(attachments: list[Any]) -> dict[str, Any]:
|
|
197
|
+
"""Build attachments list result.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
attachments: List of attachment objects
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Result dictionary with attachments data
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
return {"attachments": attachments}
|