mcp-ticketer 0.1.38__py3-none-any.whl → 0.2.0__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.
@@ -0,0 +1,366 @@
1
+ """GraphQL queries and fragments for Linear API."""
2
+
3
+ # GraphQL Fragments for reusable field definitions
4
+
5
+ USER_FRAGMENT = """
6
+ fragment UserFields on User {
7
+ id
8
+ name
9
+ email
10
+ displayName
11
+ avatarUrl
12
+ isMe
13
+ }
14
+ """
15
+
16
+ WORKFLOW_STATE_FRAGMENT = """
17
+ fragment WorkflowStateFields on WorkflowState {
18
+ id
19
+ name
20
+ type
21
+ position
22
+ color
23
+ }
24
+ """
25
+
26
+ TEAM_FRAGMENT = """
27
+ fragment TeamFields on Team {
28
+ id
29
+ name
30
+ key
31
+ description
32
+ }
33
+ """
34
+
35
+ CYCLE_FRAGMENT = """
36
+ fragment CycleFields on Cycle {
37
+ id
38
+ number
39
+ name
40
+ description
41
+ startsAt
42
+ endsAt
43
+ completedAt
44
+ }
45
+ """
46
+
47
+ PROJECT_FRAGMENT = """
48
+ fragment ProjectFields on Project {
49
+ id
50
+ name
51
+ description
52
+ state
53
+ createdAt
54
+ updatedAt
55
+ url
56
+ icon
57
+ color
58
+ targetDate
59
+ startedAt
60
+ completedAt
61
+ teams {
62
+ nodes {
63
+ ...TeamFields
64
+ }
65
+ }
66
+ }
67
+ """
68
+
69
+ LABEL_FRAGMENT = """
70
+ fragment LabelFields on IssueLabel {
71
+ id
72
+ name
73
+ color
74
+ description
75
+ }
76
+ """
77
+
78
+ ATTACHMENT_FRAGMENT = """
79
+ fragment AttachmentFields on Attachment {
80
+ id
81
+ title
82
+ url
83
+ subtitle
84
+ metadata
85
+ createdAt
86
+ updatedAt
87
+ }
88
+ """
89
+
90
+ COMMENT_FRAGMENT = """
91
+ fragment CommentFields on Comment {
92
+ id
93
+ body
94
+ createdAt
95
+ updatedAt
96
+ user {
97
+ ...UserFields
98
+ }
99
+ parent {
100
+ id
101
+ }
102
+ }
103
+ """
104
+
105
+ ISSUE_COMPACT_FRAGMENT = """
106
+ fragment IssueCompactFields on Issue {
107
+ id
108
+ identifier
109
+ title
110
+ description
111
+ priority
112
+ priorityLabel
113
+ estimate
114
+ dueDate
115
+ slaBreachesAt
116
+ slaStartedAt
117
+ createdAt
118
+ updatedAt
119
+ archivedAt
120
+ canceledAt
121
+ completedAt
122
+ startedAt
123
+ startedTriageAt
124
+ triagedAt
125
+ url
126
+ branchName
127
+ customerTicketCount
128
+
129
+ state {
130
+ ...WorkflowStateFields
131
+ }
132
+ assignee {
133
+ ...UserFields
134
+ }
135
+ creator {
136
+ ...UserFields
137
+ }
138
+ labels {
139
+ nodes {
140
+ ...LabelFields
141
+ }
142
+ }
143
+ team {
144
+ ...TeamFields
145
+ }
146
+ cycle {
147
+ ...CycleFields
148
+ }
149
+ project {
150
+ ...ProjectFields
151
+ }
152
+ parent {
153
+ id
154
+ identifier
155
+ title
156
+ }
157
+ children {
158
+ nodes {
159
+ id
160
+ identifier
161
+ title
162
+ }
163
+ }
164
+ attachments {
165
+ nodes {
166
+ ...AttachmentFields
167
+ }
168
+ }
169
+ }
170
+ """
171
+
172
+ ISSUE_FULL_FRAGMENT = """
173
+ fragment IssueFullFields on Issue {
174
+ ...IssueCompactFields
175
+ comments {
176
+ nodes {
177
+ ...CommentFields
178
+ }
179
+ }
180
+ subscribers {
181
+ nodes {
182
+ ...UserFields
183
+ }
184
+ }
185
+ relations {
186
+ nodes {
187
+ id
188
+ type
189
+ relatedIssue {
190
+ id
191
+ identifier
192
+ title
193
+ }
194
+ }
195
+ }
196
+ }
197
+ """
198
+
199
+ # Combine all fragments
200
+ ALL_FRAGMENTS = (
201
+ USER_FRAGMENT
202
+ + WORKFLOW_STATE_FRAGMENT
203
+ + TEAM_FRAGMENT
204
+ + CYCLE_FRAGMENT
205
+ + PROJECT_FRAGMENT
206
+ + LABEL_FRAGMENT
207
+ + ATTACHMENT_FRAGMENT
208
+ + COMMENT_FRAGMENT
209
+ + ISSUE_COMPACT_FRAGMENT
210
+ + ISSUE_FULL_FRAGMENT
211
+ )
212
+
213
+ # Fragments needed for issue list/search (without comments)
214
+ ISSUE_LIST_FRAGMENTS = (
215
+ USER_FRAGMENT
216
+ + WORKFLOW_STATE_FRAGMENT
217
+ + TEAM_FRAGMENT
218
+ + CYCLE_FRAGMENT
219
+ + PROJECT_FRAGMENT
220
+ + LABEL_FRAGMENT
221
+ + ATTACHMENT_FRAGMENT
222
+ + ISSUE_COMPACT_FRAGMENT
223
+ )
224
+
225
+ # Query definitions
226
+
227
+ WORKFLOW_STATES_QUERY = """
228
+ query WorkflowStates($teamId: ID!) {
229
+ workflowStates(filter: { team: { id: { eq: $teamId } } }) {
230
+ nodes {
231
+ id
232
+ name
233
+ type
234
+ position
235
+ color
236
+ }
237
+ }
238
+ }
239
+ """
240
+
241
+ CREATE_ISSUE_MUTATION = ALL_FRAGMENTS + """
242
+ mutation CreateIssue($input: IssueCreateInput!) {
243
+ issueCreate(input: $input) {
244
+ success
245
+ issue {
246
+ ...IssueFullFields
247
+ }
248
+ }
249
+ }
250
+ """
251
+
252
+ UPDATE_ISSUE_MUTATION = ALL_FRAGMENTS + """
253
+ mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
254
+ issueUpdate(id: $id, input: $input) {
255
+ success
256
+ issue {
257
+ ...IssueFullFields
258
+ }
259
+ }
260
+ }
261
+ """
262
+
263
+ LIST_ISSUES_QUERY = ISSUE_LIST_FRAGMENTS + """
264
+ query ListIssues($filter: IssueFilter, $first: Int!) {
265
+ issues(
266
+ filter: $filter
267
+ first: $first
268
+ orderBy: updatedAt
269
+ ) {
270
+ nodes {
271
+ ...IssueCompactFields
272
+ }
273
+ pageInfo {
274
+ hasNextPage
275
+ hasPreviousPage
276
+ }
277
+ }
278
+ }
279
+ """
280
+
281
+ SEARCH_ISSUES_QUERY = ISSUE_LIST_FRAGMENTS + """
282
+ query SearchIssues($filter: IssueFilter, $first: Int!) {
283
+ issues(
284
+ filter: $filter
285
+ first: $first
286
+ orderBy: updatedAt
287
+ ) {
288
+ nodes {
289
+ ...IssueCompactFields
290
+ }
291
+ }
292
+ }
293
+ """
294
+
295
+ GET_CYCLES_QUERY = """
296
+ query GetCycles($filter: CycleFilter) {
297
+ cycles(filter: $filter, orderBy: createdAt) {
298
+ nodes {
299
+ id
300
+ number
301
+ name
302
+ description
303
+ startsAt
304
+ endsAt
305
+ completedAt
306
+ issues {
307
+ nodes {
308
+ id
309
+ identifier
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ """
316
+
317
+ UPDATE_ISSUE_BRANCH_MUTATION = """
318
+ mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
319
+ issueUpdate(id: $id, input: $input) {
320
+ issue {
321
+ id
322
+ identifier
323
+ branchName
324
+ }
325
+ success
326
+ }
327
+ }
328
+ """
329
+
330
+ SEARCH_ISSUE_BY_IDENTIFIER_QUERY = """
331
+ query SearchIssue($identifier: String!) {
332
+ issue(id: $identifier) {
333
+ id
334
+ identifier
335
+ }
336
+ }
337
+ """
338
+
339
+ LIST_PROJECTS_QUERY = PROJECT_FRAGMENT + """
340
+ query ListProjects($filter: ProjectFilter, $first: Int!) {
341
+ projects(filter: $filter, first: $first, orderBy: updatedAt) {
342
+ nodes {
343
+ ...ProjectFields
344
+ }
345
+ }
346
+ }
347
+ """
348
+
349
+ CREATE_SUB_ISSUE_MUTATION = ALL_FRAGMENTS + """
350
+ mutation CreateSubIssue($input: IssueCreateInput!) {
351
+ issueCreate(input: $input) {
352
+ success
353
+ issue {
354
+ ...IssueFullFields
355
+ }
356
+ }
357
+ }
358
+ """
359
+
360
+ GET_CURRENT_USER_QUERY = USER_FRAGMENT + """
361
+ query GetCurrentUser {
362
+ viewer {
363
+ ...UserFields
364
+ }
365
+ }
366
+ """
@@ -0,0 +1,277 @@
1
+ """Linear-specific types and enums."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import Any, Dict
7
+
8
+ from mcp_ticketer.core.models import Priority, TicketState
9
+
10
+
11
+ class LinearPriorityMapping:
12
+ """Mapping between universal Priority and Linear priority values."""
13
+
14
+ # Linear uses numeric priorities: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low
15
+ TO_LINEAR: Dict[Priority, int] = {
16
+ Priority.CRITICAL: 1, # Urgent
17
+ Priority.HIGH: 2, # High
18
+ Priority.MEDIUM: 3, # Medium
19
+ Priority.LOW: 4, # Low
20
+ }
21
+
22
+ FROM_LINEAR: Dict[int, Priority] = {
23
+ 0: Priority.LOW, # No priority -> Low
24
+ 1: Priority.CRITICAL, # Urgent -> Critical
25
+ 2: Priority.HIGH, # High -> High
26
+ 3: Priority.MEDIUM, # Medium -> Medium
27
+ 4: Priority.LOW, # Low -> Low
28
+ }
29
+
30
+
31
+ class LinearStateMapping:
32
+ """Mapping between universal TicketState and Linear workflow state types."""
33
+
34
+ # Linear workflow state types
35
+ TO_LINEAR: Dict[TicketState, str] = {
36
+ TicketState.OPEN: "unstarted",
37
+ TicketState.IN_PROGRESS: "started",
38
+ TicketState.READY: "unstarted", # No direct equivalent, use unstarted
39
+ TicketState.TESTED: "started", # No direct equivalent, use started
40
+ TicketState.DONE: "completed",
41
+ TicketState.CLOSED: "canceled",
42
+ TicketState.WAITING: "unstarted",
43
+ TicketState.BLOCKED: "unstarted",
44
+ }
45
+
46
+ FROM_LINEAR: Dict[str, TicketState] = {
47
+ "backlog": TicketState.OPEN,
48
+ "unstarted": TicketState.OPEN,
49
+ "started": TicketState.IN_PROGRESS,
50
+ "completed": TicketState.DONE,
51
+ "canceled": TicketState.CLOSED,
52
+ }
53
+
54
+
55
+ class LinearWorkflowStateType(Enum):
56
+ """Linear workflow state types."""
57
+
58
+ BACKLOG = "backlog"
59
+ UNSTARTED = "unstarted"
60
+ STARTED = "started"
61
+ COMPLETED = "completed"
62
+ CANCELED = "canceled"
63
+
64
+
65
+ class LinearProjectState(Enum):
66
+ """Linear project states."""
67
+
68
+ PLANNED = "planned"
69
+ STARTED = "started"
70
+ COMPLETED = "completed"
71
+ CANCELED = "canceled"
72
+ PAUSED = "paused"
73
+
74
+
75
+ class LinearIssueRelationType(Enum):
76
+ """Linear issue relation types."""
77
+
78
+ BLOCKS = "blocks"
79
+ BLOCKED_BY = "blockedBy"
80
+ DUPLICATE = "duplicate"
81
+ DUPLICATED_BY = "duplicatedBy"
82
+ RELATES = "relates"
83
+
84
+
85
+ class LinearCommentType(Enum):
86
+ """Linear comment types."""
87
+
88
+ COMMENT = "comment"
89
+ SYSTEM = "system"
90
+
91
+
92
+ def get_linear_priority(priority: Priority) -> int:
93
+ """Convert universal Priority to Linear priority value.
94
+
95
+ Args:
96
+ priority: Universal priority enum
97
+
98
+ Returns:
99
+ Linear priority integer (0-4)
100
+ """
101
+ return LinearPriorityMapping.TO_LINEAR.get(priority, 3) # Default to Medium
102
+
103
+
104
+ def get_universal_priority(linear_priority: int) -> Priority:
105
+ """Convert Linear priority value to universal Priority.
106
+
107
+ Args:
108
+ linear_priority: Linear priority integer (0-4)
109
+
110
+ Returns:
111
+ Universal priority enum
112
+ """
113
+ return LinearPriorityMapping.FROM_LINEAR.get(linear_priority, Priority.MEDIUM)
114
+
115
+
116
+ def get_linear_state_type(state: TicketState) -> str:
117
+ """Convert universal TicketState to Linear workflow state type.
118
+
119
+ Args:
120
+ state: Universal ticket state enum
121
+
122
+ Returns:
123
+ Linear workflow state type string
124
+ """
125
+ return LinearStateMapping.TO_LINEAR.get(state, "unstarted")
126
+
127
+
128
+ def get_universal_state(linear_state_type: str) -> TicketState:
129
+ """Convert Linear workflow state type to universal TicketState.
130
+
131
+ Args:
132
+ linear_state_type: Linear workflow state type string
133
+
134
+ Returns:
135
+ Universal ticket state enum
136
+ """
137
+ return LinearStateMapping.FROM_LINEAR.get(linear_state_type, TicketState.OPEN)
138
+
139
+
140
+ def build_issue_filter(
141
+ state: TicketState | None = None,
142
+ assignee_id: str | None = None,
143
+ priority: Priority | None = None,
144
+ team_id: str | None = None,
145
+ project_id: str | None = None,
146
+ labels: list[str] | None = None,
147
+ created_after: str | None = None,
148
+ updated_after: str | None = None,
149
+ due_before: str | None = None,
150
+ include_archived: bool = False,
151
+ ) -> Dict[str, Any]:
152
+ """Build a Linear issue filter from parameters.
153
+
154
+ Args:
155
+ state: Filter by ticket state
156
+ assignee_id: Filter by assignee Linear user ID
157
+ priority: Filter by priority
158
+ team_id: Filter by team ID
159
+ project_id: Filter by project ID
160
+ labels: Filter by label names
161
+ created_after: Filter by creation date (ISO string)
162
+ updated_after: Filter by update date (ISO string)
163
+ due_before: Filter by due date (ISO string)
164
+ include_archived: Whether to include archived issues
165
+
166
+ Returns:
167
+ Linear GraphQL filter object
168
+ """
169
+ issue_filter: Dict[str, Any] = {}
170
+
171
+ # Team filter (required for most operations)
172
+ if team_id:
173
+ issue_filter["team"] = {"id": {"eq": team_id}}
174
+
175
+ # State filter
176
+ if state:
177
+ state_type = get_linear_state_type(state)
178
+ issue_filter["state"] = {"type": {"eq": state_type}}
179
+
180
+ # Assignee filter
181
+ if assignee_id:
182
+ issue_filter["assignee"] = {"id": {"eq": assignee_id}}
183
+
184
+ # Priority filter
185
+ if priority:
186
+ linear_priority = get_linear_priority(priority)
187
+ issue_filter["priority"] = {"eq": linear_priority}
188
+
189
+ # Project filter
190
+ if project_id:
191
+ issue_filter["project"] = {"id": {"eq": project_id}}
192
+
193
+ # Labels filter
194
+ if labels:
195
+ issue_filter["labels"] = {"some": {"name": {"in": labels}}}
196
+
197
+ # Date filters
198
+ if created_after:
199
+ issue_filter["createdAt"] = {"gte": created_after}
200
+ if updated_after:
201
+ issue_filter["updatedAt"] = {"gte": updated_after}
202
+ if due_before:
203
+ issue_filter["dueDate"] = {"lte": due_before}
204
+
205
+ # Archived filter
206
+ if not include_archived:
207
+ issue_filter["archivedAt"] = {"null": True}
208
+
209
+ return issue_filter
210
+
211
+
212
+ def build_project_filter(
213
+ state: str | None = None,
214
+ team_id: str | None = None,
215
+ include_completed: bool = True,
216
+ ) -> Dict[str, Any]:
217
+ """Build a Linear project filter from parameters.
218
+
219
+ Args:
220
+ state: Filter by project state
221
+ team_id: Filter by team ID
222
+ include_completed: Whether to include completed projects
223
+
224
+ Returns:
225
+ Linear GraphQL filter object
226
+ """
227
+ project_filter: Dict[str, Any] = {}
228
+
229
+ # Team filter
230
+ if team_id:
231
+ project_filter["teams"] = {"some": {"id": {"eq": team_id}}}
232
+
233
+ # State filter
234
+ if state:
235
+ project_filter["state"] = {"eq": state}
236
+ elif not include_completed:
237
+ # Exclude completed projects by default
238
+ project_filter["state"] = {"neq": "completed"}
239
+
240
+ return project_filter
241
+
242
+
243
+ def extract_linear_metadata(issue_data: Dict[str, Any]) -> Dict[str, Any]:
244
+ """Extract Linear-specific metadata from issue data.
245
+
246
+ Args:
247
+ issue_data: Raw Linear issue data from GraphQL
248
+
249
+ Returns:
250
+ Dictionary of Linear-specific metadata
251
+ """
252
+ metadata = {}
253
+
254
+ # Extract Linear-specific fields
255
+ if "dueDate" in issue_data and issue_data["dueDate"]:
256
+ metadata["due_date"] = issue_data["dueDate"]
257
+
258
+ if "cycle" in issue_data and issue_data["cycle"]:
259
+ metadata["cycle_id"] = issue_data["cycle"]["id"]
260
+ metadata["cycle_name"] = issue_data["cycle"]["name"]
261
+
262
+ if "estimate" in issue_data and issue_data["estimate"]:
263
+ metadata["estimate"] = issue_data["estimate"]
264
+
265
+ if "branchName" in issue_data and issue_data["branchName"]:
266
+ metadata["branch_name"] = issue_data["branchName"]
267
+
268
+ if "url" in issue_data:
269
+ metadata["linear_url"] = issue_data["url"]
270
+
271
+ if "slaBreachesAt" in issue_data and issue_data["slaBreachesAt"]:
272
+ metadata["sla_breaches_at"] = issue_data["slaBreachesAt"]
273
+
274
+ if "customerTicketCount" in issue_data:
275
+ metadata["customer_ticket_count"] = issue_data["customerTicketCount"]
276
+
277
+ return metadata