mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,271 @@
1
+ """HTTP client for Jira REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Any
8
+
9
+ import httpx
10
+ from httpx import AsyncClient, HTTPStatusError, TimeoutException
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class JiraClient:
16
+ """HTTP client for JIRA REST API with authentication and retry logic."""
17
+
18
+ def __init__(
19
+ self,
20
+ server: str,
21
+ email: str,
22
+ api_token: str,
23
+ is_cloud: bool = True,
24
+ verify_ssl: bool = True,
25
+ timeout: int = 30,
26
+ max_retries: int = 3,
27
+ ):
28
+ """Initialize JIRA API client.
29
+
30
+ Args:
31
+ ----
32
+ server: JIRA server URL (e.g., https://company.atlassian.net)
33
+ email: User email for authentication
34
+ api_token: API token for authentication
35
+ is_cloud: Whether this is JIRA Cloud (default: True)
36
+ verify_ssl: Whether to verify SSL certificates (default: True)
37
+ timeout: Request timeout in seconds (default: 30)
38
+ max_retries: Maximum retry attempts (default: 3)
39
+
40
+ """
41
+ # Clean up server URL
42
+ self.server = server.rstrip("/")
43
+
44
+ # API base URL
45
+ self.api_base = (
46
+ f"{self.server}/rest/api/3" if is_cloud else f"{self.server}/rest/api/2"
47
+ )
48
+
49
+ # Configuration
50
+ self.email = email
51
+ self.api_token = api_token
52
+ self.verify_ssl = verify_ssl
53
+ self.timeout = timeout
54
+ self.max_retries = max_retries
55
+
56
+ # HTTP client setup
57
+ self.auth = httpx.BasicAuth(self.email, self.api_token)
58
+ self.headers = {
59
+ "Accept": "application/json",
60
+ "Content-Type": "application/json",
61
+ }
62
+
63
+ async def _get_client(self) -> AsyncClient:
64
+ """Get configured async HTTP client.
65
+
66
+ Returns:
67
+ -------
68
+ Configured AsyncClient instance
69
+
70
+ """
71
+ return AsyncClient(
72
+ auth=self.auth,
73
+ headers=self.headers,
74
+ timeout=self.timeout,
75
+ verify=self.verify_ssl,
76
+ )
77
+
78
+ async def request(
79
+ self,
80
+ method: str,
81
+ endpoint: str,
82
+ data: dict[str, Any] | None = None,
83
+ params: dict[str, Any] | None = None,
84
+ retry_count: int = 0,
85
+ ) -> dict[str, Any]:
86
+ """Make HTTP request to JIRA API with retry logic.
87
+
88
+ Args:
89
+ ----
90
+ method: HTTP method (GET, POST, PUT, DELETE)
91
+ endpoint: API endpoint (relative to api_base)
92
+ data: Request body data
93
+ params: Query parameters
94
+ retry_count: Current retry attempt
95
+
96
+ Returns:
97
+ -------
98
+ Response data as dictionary
99
+
100
+ Raises:
101
+ ------
102
+ HTTPStatusError: On API errors
103
+ TimeoutException: On timeout
104
+
105
+ """
106
+ url = f"{self.api_base}/{endpoint.lstrip('/')}"
107
+
108
+ async with await self._get_client() as client:
109
+ try:
110
+ response = await client.request(
111
+ method=method, url=url, json=data, params=params
112
+ )
113
+ response.raise_for_status()
114
+
115
+ # Handle empty responses
116
+ if response.status_code == 204:
117
+ return {}
118
+
119
+ return response.json()
120
+
121
+ except TimeoutException as e:
122
+ if retry_count < self.max_retries:
123
+ await asyncio.sleep(2**retry_count) # Exponential backoff
124
+ return await self.request(
125
+ method, endpoint, data, params, retry_count + 1
126
+ )
127
+ raise e
128
+
129
+ except HTTPStatusError as e:
130
+ # Handle rate limiting
131
+ if e.response.status_code == 429 and retry_count < self.max_retries:
132
+ retry_after = int(e.response.headers.get("Retry-After", 5))
133
+ await asyncio.sleep(retry_after)
134
+ return await self.request(
135
+ method, endpoint, data, params, retry_count + 1
136
+ )
137
+
138
+ # Log error details
139
+ logger.error(
140
+ f"JIRA API error: {e.response.status_code} - {e.response.text}"
141
+ )
142
+ raise e
143
+
144
+ async def get(
145
+ self,
146
+ endpoint: str,
147
+ params: dict[str, Any] | None = None,
148
+ ) -> dict[str, Any]:
149
+ """Make GET request.
150
+
151
+ Args:
152
+ ----
153
+ endpoint: API endpoint
154
+ params: Query parameters
155
+
156
+ Returns:
157
+ -------
158
+ Response data
159
+
160
+ """
161
+ return await self.request("GET", endpoint, params=params)
162
+
163
+ async def post(
164
+ self,
165
+ endpoint: str,
166
+ data: dict[str, Any] | None = None,
167
+ params: dict[str, Any] | None = None,
168
+ ) -> dict[str, Any]:
169
+ """Make POST request.
170
+
171
+ Args:
172
+ ----
173
+ endpoint: API endpoint
174
+ data: Request body data
175
+ params: Query parameters
176
+
177
+ Returns:
178
+ -------
179
+ Response data
180
+
181
+ """
182
+ return await self.request("POST", endpoint, data=data, params=params)
183
+
184
+ async def put(
185
+ self,
186
+ endpoint: str,
187
+ data: dict[str, Any] | None = None,
188
+ ) -> dict[str, Any]:
189
+ """Make PUT request.
190
+
191
+ Args:
192
+ ----
193
+ endpoint: API endpoint
194
+ data: Request body data
195
+
196
+ Returns:
197
+ -------
198
+ Response data
199
+
200
+ """
201
+ return await self.request("PUT", endpoint, data=data)
202
+
203
+ async def delete(
204
+ self,
205
+ endpoint: str,
206
+ ) -> dict[str, Any]:
207
+ """Make DELETE request.
208
+
209
+ Args:
210
+ ----
211
+ endpoint: API endpoint
212
+
213
+ Returns:
214
+ -------
215
+ Response data (usually empty)
216
+
217
+ """
218
+ return await self.request("DELETE", endpoint)
219
+
220
+ async def upload_file(
221
+ self,
222
+ endpoint: str,
223
+ file_path: str,
224
+ filename: str,
225
+ ) -> dict[str, Any]:
226
+ """Upload file with multipart/form-data.
227
+
228
+ Args:
229
+ ----
230
+ endpoint: API endpoint
231
+ file_path: Path to file to upload
232
+ filename: Name for the uploaded file
233
+
234
+ Returns:
235
+ -------
236
+ Response data
237
+
238
+ """
239
+ # JIRA requires special header for attachment upload
240
+ headers = {
241
+ "X-Atlassian-Token": "no-check",
242
+ # Don't set Content-Type - let httpx handle multipart
243
+ }
244
+
245
+ url = f"{self.api_base}/{endpoint.lstrip('/')}"
246
+
247
+ # Prepare multipart file upload
248
+ with open(file_path, "rb") as f:
249
+ files = {"file": (filename, f, "application/octet-stream")}
250
+
251
+ # Use existing client infrastructure
252
+ async with await self._get_client() as client:
253
+ response = await client.post(
254
+ url, files=files, headers={**self.headers, **headers}
255
+ )
256
+ response.raise_for_status()
257
+ return response.json()
258
+
259
+ def get_browse_url(self, issue_key: str) -> str:
260
+ """Get browser URL for an issue.
261
+
262
+ Args:
263
+ ----
264
+ issue_key: Issue key (e.g., PROJ-123)
265
+
266
+ Returns:
267
+ -------
268
+ Full URL to view issue in browser
269
+
270
+ """
271
+ return f"{self.server}/browse/{issue_key}"
@@ -0,0 +1,246 @@
1
+ """Data transformation functions for mapping between Jira and universal models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ...core.models import Epic, Priority, Task
8
+ from .types import (
9
+ JiraIssueType,
10
+ convert_from_adf,
11
+ convert_to_adf,
12
+ map_priority_from_jira,
13
+ map_priority_to_jira,
14
+ map_state_from_jira,
15
+ parse_jira_datetime,
16
+ )
17
+
18
+
19
+ def issue_to_ticket(
20
+ issue: dict[str, Any],
21
+ server: str,
22
+ ) -> Epic | Task:
23
+ """Convert JIRA issue to universal ticket model.
24
+
25
+ Args:
26
+ ----
27
+ issue: JIRA issue dictionary from API
28
+ server: JIRA server URL for constructing browse URLs
29
+
30
+ Returns:
31
+ -------
32
+ Epic or Task object depending on issue type
33
+
34
+ """
35
+ fields = issue.get("fields", {})
36
+
37
+ # Determine ticket type
38
+ issue_type = fields.get("issuetype", {}).get("name", "").lower()
39
+ is_epic = "epic" in issue_type
40
+
41
+ # Extract common fields
42
+ # Convert ADF description back to plain text if needed
43
+ description = convert_from_adf(fields.get("description", ""))
44
+
45
+ base_data = {
46
+ "id": issue.get("key"),
47
+ "title": fields.get("summary", ""),
48
+ "description": description,
49
+ "state": map_state_from_jira(fields.get("status", {})),
50
+ "priority": map_priority_from_jira(fields.get("priority")),
51
+ "tags": [
52
+ label.get("name", "") if isinstance(label, dict) else str(label)
53
+ for label in fields.get("labels", [])
54
+ ],
55
+ "created_at": parse_jira_datetime(fields.get("created")),
56
+ "updated_at": parse_jira_datetime(fields.get("updated")),
57
+ "metadata": {
58
+ "jira": {
59
+ "id": issue.get("id"),
60
+ "key": issue.get("key"),
61
+ "self": issue.get("self"),
62
+ "url": f"{server}/browse/{issue.get('key')}",
63
+ "issue_type": fields.get("issuetype", {}),
64
+ "project": fields.get("project", {}),
65
+ "components": fields.get("components", []),
66
+ "fix_versions": fields.get("fixVersions", []),
67
+ "resolution": fields.get("resolution"),
68
+ }
69
+ },
70
+ }
71
+
72
+ if is_epic:
73
+ # Create Epic
74
+ return Epic(
75
+ **base_data,
76
+ child_issues=[subtask.get("key") for subtask in fields.get("subtasks", [])],
77
+ )
78
+ else:
79
+ # Create Task
80
+ parent = fields.get("parent", {})
81
+ epic_link = fields.get("customfield_10014") # Common epic link field
82
+
83
+ return Task(
84
+ **base_data,
85
+ parent_issue=parent.get("key") if parent else None,
86
+ parent_epic=epic_link if epic_link else None,
87
+ assignee=(
88
+ fields.get("assignee", {}).get("displayName")
89
+ if fields.get("assignee")
90
+ else None
91
+ ),
92
+ estimated_hours=(
93
+ fields.get("timetracking", {}).get("originalEstimateSeconds", 0) / 3600
94
+ if fields.get("timetracking")
95
+ else None
96
+ ),
97
+ actual_hours=(
98
+ fields.get("timetracking", {}).get("timeSpentSeconds", 0) / 3600
99
+ if fields.get("timetracking")
100
+ else None
101
+ ),
102
+ )
103
+
104
+
105
+ def ticket_to_issue_fields(
106
+ ticket: Epic | Task,
107
+ issue_type: str | None = None,
108
+ is_cloud: bool = True,
109
+ project_key: str | None = None,
110
+ ) -> dict[str, Any]:
111
+ """Convert universal ticket to JIRA issue fields.
112
+
113
+ Args:
114
+ ----
115
+ ticket: Epic or Task object
116
+ issue_type: Optional issue type override
117
+ is_cloud: Whether this is JIRA Cloud (affects description format)
118
+ project_key: Project key for new issues
119
+
120
+ Returns:
121
+ -------
122
+ Dictionary of JIRA issue fields
123
+
124
+ """
125
+ # Convert description to ADF format for JIRA Cloud
126
+ description = (
127
+ convert_to_adf(ticket.description or "")
128
+ if is_cloud
129
+ else (ticket.description or "")
130
+ )
131
+
132
+ fields = {
133
+ "summary": ticket.title,
134
+ "description": description,
135
+ "labels": ticket.tags,
136
+ }
137
+
138
+ # Only add priority for Tasks, not Epics (some JIRA configurations don't allow priority on Epics)
139
+ if isinstance(ticket, Task):
140
+ fields["priority"] = {"name": map_priority_to_jira(ticket.priority)}
141
+
142
+ # Add project if creating new issue
143
+ if not ticket.id and project_key:
144
+ fields["project"] = {"key": project_key}
145
+
146
+ # Set issue type
147
+ if issue_type:
148
+ fields["issuetype"] = {"name": issue_type}
149
+ elif isinstance(ticket, Epic):
150
+ fields["issuetype"] = {"name": JiraIssueType.EPIC}
151
+ else:
152
+ fields["issuetype"] = {"name": JiraIssueType.TASK}
153
+
154
+ # Add task-specific fields
155
+ if isinstance(ticket, Task):
156
+ if ticket.assignee:
157
+ # Note: Need to resolve user account ID
158
+ fields["assignee"] = {"accountId": ticket.assignee}
159
+
160
+ if ticket.parent_issue:
161
+ fields["parent"] = {"key": ticket.parent_issue}
162
+
163
+ # Time tracking
164
+ if ticket.estimated_hours:
165
+ fields["timetracking"] = {
166
+ "originalEstimate": f"{int(ticket.estimated_hours)}h"
167
+ }
168
+
169
+ return fields
170
+
171
+
172
+ def map_update_fields(
173
+ updates: dict[str, Any],
174
+ is_cloud: bool = True,
175
+ ) -> dict[str, Any]:
176
+ """Map update dictionary to JIRA field updates.
177
+
178
+ Args:
179
+ ----
180
+ updates: Dictionary of field updates
181
+ is_cloud: Whether this is JIRA Cloud
182
+
183
+ Returns:
184
+ -------
185
+ Dictionary of JIRA field updates
186
+
187
+ """
188
+ fields = {}
189
+
190
+ if "title" in updates:
191
+ fields["summary"] = updates["title"]
192
+ if "description" in updates:
193
+ fields["description"] = updates["description"]
194
+ if "priority" in updates:
195
+ fields["priority"] = {"name": map_priority_to_jira(updates["priority"])}
196
+ if "tags" in updates:
197
+ fields["labels"] = updates["tags"]
198
+ if "assignee" in updates:
199
+ fields["assignee"] = {"accountId": updates["assignee"]}
200
+
201
+ return fields
202
+
203
+
204
+ def map_epic_update_fields(
205
+ updates: dict[str, Any],
206
+ ) -> dict[str, Any]:
207
+ """Map epic update dictionary to JIRA field updates.
208
+
209
+ Args:
210
+ ----
211
+ updates: Dictionary with fields to update:
212
+ - title: Epic title (maps to summary)
213
+ - description: Epic description (auto-converted to ADF)
214
+ - state: TicketState value (transitions via workflow)
215
+ - tags: List of labels
216
+ - priority: Priority level
217
+
218
+ Returns:
219
+ -------
220
+ Dictionary of JIRA field updates
221
+
222
+ """
223
+ fields = {}
224
+
225
+ # Map title to summary
226
+ if "title" in updates:
227
+ fields["summary"] = updates["title"]
228
+
229
+ # Convert description to ADF format
230
+ if "description" in updates:
231
+ fields["description"] = convert_to_adf(updates["description"])
232
+
233
+ # Map tags to labels
234
+ if "tags" in updates:
235
+ fields["labels"] = updates["tags"]
236
+
237
+ # Map priority (some JIRA configs allow priority on Epics)
238
+ if "priority" in updates:
239
+ priority_value = updates["priority"]
240
+ if isinstance(priority_value, Priority):
241
+ fields["priority"] = {"name": map_priority_to_jira(priority_value)}
242
+ else:
243
+ # String priority passed directly
244
+ fields["priority"] = {"name": priority_value}
245
+
246
+ return fields