linear-python-client 0.1.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,78 @@
1
+ """Exception types raised by the Linear client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class LinearError(Exception):
9
+ """Base class for all errors raised by ``linear_python_client``."""
10
+
11
+
12
+ class LinearAuthenticationError(LinearError):
13
+ """Raised when the API rejects the supplied credentials (HTTP 401/403)."""
14
+
15
+
16
+ class LinearRateLimitError(LinearError):
17
+ """Raised when a request is rejected for exceeding a rate limit.
18
+
19
+ Linear signals rate limiting with an HTTP 400 response whose GraphQL error
20
+ carries the `RATELIMITED` code. The relevant `X-RateLimit-*` headers are
21
+ exposed as attributes when present (otherwise `None`).
22
+
23
+ Attributes:
24
+ requests_limit: Max requests allowed in the current window.
25
+ requests_remaining: Requests left in the current window.
26
+ requests_reset: Window reset time (UTC epoch milliseconds).
27
+ complexity_limit: Max complexity points allowed in the current window.
28
+ complexity_remaining: Complexity points left in the current window.
29
+ complexity_reset: Complexity window reset time (UTC epoch milliseconds).
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ message: str,
35
+ *,
36
+ requests_limit: int | None = None,
37
+ requests_remaining: int | None = None,
38
+ requests_reset: int | None = None,
39
+ complexity_limit: int | None = None,
40
+ complexity_remaining: int | None = None,
41
+ complexity_reset: int | None = None,
42
+ ) -> None:
43
+ super().__init__(message)
44
+ self.requests_limit = requests_limit
45
+ self.requests_remaining = requests_remaining
46
+ self.requests_reset = requests_reset
47
+ self.complexity_limit = complexity_limit
48
+ self.complexity_remaining = complexity_remaining
49
+ self.complexity_reset = complexity_reset
50
+
51
+
52
+ class LinearGraphQLError(LinearError):
53
+ """Raised when the API returns one or more GraphQL `errors`.
54
+
55
+ The raw error list returned by the API is available as `errors`, and the
56
+ code of the first error carrying one (if any) is exposed via `code`.
57
+
58
+ Attributes:
59
+ errors: The raw GraphQL error objects returned by the API.
60
+ """
61
+
62
+ def __init__(self, message: str, *, errors: list[dict[str, Any]] | None = None) -> None:
63
+ super().__init__(message)
64
+ self.errors: list[dict[str, Any]] = errors or []
65
+
66
+ @property
67
+ def code(self) -> str | None:
68
+ """The error code of the first error that carries one, else `None`."""
69
+ for error in self.errors:
70
+ extensions = error.get("extensions") or {}
71
+ code = extensions.get("code") or error.get("code")
72
+ if code:
73
+ return str(code)
74
+ return None
75
+
76
+
77
+ class LinearNetworkError(LinearError):
78
+ """Raised when the request fails to reach the API or returns an unexpected response."""
@@ -0,0 +1,10 @@
1
+ """GraphQL operation strings used by the client.
2
+
3
+ Import the query/mutation module with ``from linear_python_client.graphql import queries``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from . import queries
9
+
10
+ __all__ = ["queries"]
@@ -0,0 +1,405 @@
1
+ """GraphQL query and mutation strings used by :class:`linear_python_client.client.LinearClient`.
2
+
3
+ Field selections are factored into reusable fragments so the operations below stay
4
+ readable and the requested fields line up with the dataclasses in ``models.py``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ # --- Field fragments -------------------------------------------------------
10
+
11
+ USER_FIELDS = """
12
+ fragment UserFields on User {
13
+ id
14
+ name
15
+ displayName
16
+ email
17
+ active
18
+ admin
19
+ createdAt
20
+ }
21
+ """
22
+
23
+ TEAM_FIELDS = """
24
+ fragment TeamFields on Team {
25
+ id
26
+ name
27
+ key
28
+ description
29
+ private
30
+ createdAt
31
+ }
32
+ """
33
+
34
+ STATE_FIELDS = """
35
+ fragment StateFields on WorkflowState {
36
+ id
37
+ name
38
+ type
39
+ color
40
+ position
41
+ }
42
+ """
43
+
44
+ LABEL_FIELDS = """
45
+ fragment LabelFields on IssueLabel {
46
+ id
47
+ name
48
+ color
49
+ }
50
+ """
51
+
52
+ PROJECT_FIELDS = """
53
+ fragment ProjectFields on Project {
54
+ id
55
+ name
56
+ description
57
+ slugId
58
+ state
59
+ progress
60
+ createdAt
61
+ }
62
+ """
63
+
64
+ COMMENT_FIELDS = """
65
+ fragment CommentFields on Comment {
66
+ id
67
+ body
68
+ url
69
+ createdAt
70
+ updatedAt
71
+ user { ...UserFields }
72
+ }
73
+ """
74
+
75
+ ISSUE_FIELDS = """
76
+ fragment IssueFields on Issue {
77
+ id
78
+ identifier
79
+ title
80
+ description
81
+ url
82
+ priority
83
+ estimate
84
+ branchName
85
+ createdAt
86
+ updatedAt
87
+ completedAt
88
+ assignee { ...UserFields }
89
+ creator { ...UserFields }
90
+ team { ...TeamFields }
91
+ state { ...StateFields }
92
+ labels { nodes { ...LabelFields } }
93
+ }
94
+ """
95
+
96
+ ATTACHMENT_FIELDS = """
97
+ fragment AttachmentFields on Attachment {
98
+ id
99
+ title
100
+ subtitle
101
+ url
102
+ createdAt
103
+ }
104
+ """
105
+
106
+ CYCLE_FIELDS = """
107
+ fragment CycleFields on Cycle {
108
+ id
109
+ number
110
+ name
111
+ startsAt
112
+ endsAt
113
+ }
114
+ """
115
+
116
+ # A shallow issue projection used for parent / sub-issues / related issues so the
117
+ # detailed query stays bounded (no recursion into their relations).
118
+ ISSUE_SUMMARY_FIELDS = """
119
+ fragment IssueSummaryFields on Issue {
120
+ id
121
+ identifier
122
+ title
123
+ url
124
+ priority
125
+ state { ...StateFields }
126
+ }
127
+ """
128
+
129
+ # Full issue detail: the base fields plus the heavier related collections.
130
+ ISSUE_DETAIL_FIELDS = """
131
+ fragment IssueDetailFields on Issue {
132
+ ...IssueFields
133
+ comments { nodes { ...CommentFields } }
134
+ attachments { nodes { ...AttachmentFields } }
135
+ project { ...ProjectFields }
136
+ cycle { ...CycleFields }
137
+ parent { ...IssueSummaryFields }
138
+ children { nodes { ...IssueSummaryFields } }
139
+ subscribers { nodes { ...UserFields } }
140
+ relations { nodes { type relatedIssue { ...IssueSummaryFields } } }
141
+ }
142
+ """
143
+
144
+
145
+ def _compose(*parts: str) -> str:
146
+ """Join an operation body with the fragments it depends on."""
147
+ return "\n".join(part.strip() for part in parts)
148
+
149
+
150
+ # --- Queries ---------------------------------------------------------------
151
+
152
+ VIEWER = _compose(
153
+ USER_FIELDS,
154
+ """
155
+ query Viewer {
156
+ viewer { ...UserFields }
157
+ }
158
+ """,
159
+ )
160
+
161
+ USER = _compose(
162
+ USER_FIELDS,
163
+ """
164
+ query User($id: String!) {
165
+ user(id: $id) { ...UserFields }
166
+ }
167
+ """,
168
+ )
169
+
170
+ USERS = _compose(
171
+ USER_FIELDS,
172
+ """
173
+ query Users($first: Int, $after: String, $filter: UserFilter) {
174
+ users(first: $first, after: $after, filter: $filter) {
175
+ nodes { ...UserFields }
176
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
177
+ }
178
+ }
179
+ """,
180
+ )
181
+
182
+ TEAM = _compose(
183
+ TEAM_FIELDS,
184
+ """
185
+ query Team($id: String!) {
186
+ team(id: $id) { ...TeamFields }
187
+ }
188
+ """,
189
+ )
190
+
191
+ TEAMS = _compose(
192
+ TEAM_FIELDS,
193
+ """
194
+ query Teams($first: Int, $after: String, $filter: TeamFilter) {
195
+ teams(first: $first, after: $after, filter: $filter) {
196
+ nodes { ...TeamFields }
197
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
198
+ }
199
+ }
200
+ """,
201
+ )
202
+
203
+ ISSUE = _compose(
204
+ ISSUE_FIELDS,
205
+ USER_FIELDS,
206
+ TEAM_FIELDS,
207
+ STATE_FIELDS,
208
+ LABEL_FIELDS,
209
+ """
210
+ query Issue($id: String!) {
211
+ issue(id: $id) { ...IssueFields }
212
+ }
213
+ """,
214
+ )
215
+
216
+ ISSUE_DETAILS = _compose(
217
+ ISSUE_DETAIL_FIELDS,
218
+ ISSUE_FIELDS,
219
+ ISSUE_SUMMARY_FIELDS,
220
+ USER_FIELDS,
221
+ TEAM_FIELDS,
222
+ STATE_FIELDS,
223
+ LABEL_FIELDS,
224
+ PROJECT_FIELDS,
225
+ COMMENT_FIELDS,
226
+ ATTACHMENT_FIELDS,
227
+ CYCLE_FIELDS,
228
+ """
229
+ query IssueDetails($id: String!) {
230
+ issue(id: $id) { ...IssueDetailFields }
231
+ }
232
+ """,
233
+ )
234
+
235
+ ISSUES = _compose(
236
+ ISSUE_FIELDS,
237
+ USER_FIELDS,
238
+ TEAM_FIELDS,
239
+ STATE_FIELDS,
240
+ LABEL_FIELDS,
241
+ """
242
+ query Issues($first: Int, $after: String, $filter: IssueFilter, $orderBy: PaginationOrderBy) {
243
+ issues(first: $first, after: $after, filter: $filter, orderBy: $orderBy) {
244
+ nodes { ...IssueFields }
245
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
246
+ }
247
+ }
248
+ """,
249
+ )
250
+
251
+ PROJECT = _compose(
252
+ PROJECT_FIELDS,
253
+ """
254
+ query Project($id: String!) {
255
+ project(id: $id) { ...ProjectFields }
256
+ }
257
+ """,
258
+ )
259
+
260
+ PROJECTS = _compose(
261
+ PROJECT_FIELDS,
262
+ """
263
+ query Projects($first: Int, $after: String, $filter: ProjectFilter) {
264
+ projects(first: $first, after: $after, filter: $filter) {
265
+ nodes { ...ProjectFields }
266
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
267
+ }
268
+ }
269
+ """,
270
+ )
271
+
272
+ COMMENT = _compose(
273
+ COMMENT_FIELDS,
274
+ USER_FIELDS,
275
+ """
276
+ query Comment($id: String!) {
277
+ comment(id: $id) { ...CommentFields }
278
+ }
279
+ """,
280
+ )
281
+
282
+ COMMENTS = _compose(
283
+ COMMENT_FIELDS,
284
+ USER_FIELDS,
285
+ """
286
+ query Comments($first: Int, $after: String, $filter: CommentFilter) {
287
+ comments(first: $first, after: $after, filter: $filter) {
288
+ nodes { ...CommentFields }
289
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
290
+ }
291
+ }
292
+ """,
293
+ )
294
+
295
+ WORKFLOW_STATES = _compose(
296
+ STATE_FIELDS,
297
+ """
298
+ query WorkflowStates($first: Int, $after: String, $filter: WorkflowStateFilter) {
299
+ workflowStates(first: $first, after: $after, filter: $filter) {
300
+ nodes { ...StateFields }
301
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
302
+ }
303
+ }
304
+ """,
305
+ )
306
+
307
+ ISSUE_LABELS = _compose(
308
+ LABEL_FIELDS,
309
+ """
310
+ query IssueLabels($first: Int, $after: String, $filter: IssueLabelFilter) {
311
+ issueLabels(first: $first, after: $after, filter: $filter) {
312
+ nodes { ...LabelFields }
313
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
314
+ }
315
+ }
316
+ """,
317
+ )
318
+
319
+
320
+ # --- Mutations -------------------------------------------------------------
321
+
322
+ ISSUE_CREATE = _compose(
323
+ ISSUE_FIELDS,
324
+ USER_FIELDS,
325
+ TEAM_FIELDS,
326
+ STATE_FIELDS,
327
+ LABEL_FIELDS,
328
+ """
329
+ mutation IssueCreate($input: IssueCreateInput!) {
330
+ issueCreate(input: $input) {
331
+ success
332
+ issue { ...IssueFields }
333
+ }
334
+ }
335
+ """,
336
+ )
337
+
338
+ ISSUE_UPDATE = _compose(
339
+ ISSUE_FIELDS,
340
+ USER_FIELDS,
341
+ TEAM_FIELDS,
342
+ STATE_FIELDS,
343
+ LABEL_FIELDS,
344
+ """
345
+ mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
346
+ issueUpdate(id: $id, input: $input) {
347
+ success
348
+ issue { ...IssueFields }
349
+ }
350
+ }
351
+ """,
352
+ )
353
+
354
+ ISSUE_ARCHIVE = """
355
+ mutation IssueArchive($id: String!) {
356
+ issueArchive(id: $id) {
357
+ success
358
+ }
359
+ }
360
+ """.strip()
361
+
362
+ COMMENT_CREATE = _compose(
363
+ COMMENT_FIELDS,
364
+ USER_FIELDS,
365
+ """
366
+ mutation CommentCreate($input: CommentCreateInput!) {
367
+ commentCreate(input: $input) {
368
+ success
369
+ comment { ...CommentFields }
370
+ }
371
+ }
372
+ """,
373
+ )
374
+
375
+ ISSUE_ADD_LABEL = _compose(
376
+ ISSUE_FIELDS,
377
+ USER_FIELDS,
378
+ TEAM_FIELDS,
379
+ STATE_FIELDS,
380
+ LABEL_FIELDS,
381
+ """
382
+ mutation IssueAddLabel($id: String!, $labelId: String!) {
383
+ issueAddLabel(id: $id, labelId: $labelId) {
384
+ success
385
+ issue { ...IssueFields }
386
+ }
387
+ }
388
+ """,
389
+ )
390
+
391
+ ISSUE_REMOVE_LABEL = _compose(
392
+ ISSUE_FIELDS,
393
+ USER_FIELDS,
394
+ TEAM_FIELDS,
395
+ STATE_FIELDS,
396
+ LABEL_FIELDS,
397
+ """
398
+ mutation IssueRemoveLabel($id: String!, $labelId: String!) {
399
+ issueRemoveLabel(id: $id, labelId: $labelId) {
400
+ success
401
+ issue { ...IssueFields }
402
+ }
403
+ }
404
+ """,
405
+ )
@@ -0,0 +1,132 @@
1
+ """Data models: entities, request inputs, and response outputs.
2
+
3
+ Re-exports everything so you can import from the package directly, e.g.
4
+ ``from linear_python_client.models import Issue, IssueCreateRequest, IssuesResponse``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .entities import (
10
+ Attachment,
11
+ Comment,
12
+ Cycle,
13
+ Issue,
14
+ IssueDetail,
15
+ IssueLabel,
16
+ IssueRelation,
17
+ LinearModel,
18
+ PageInfo,
19
+ Project,
20
+ Team,
21
+ User,
22
+ WorkflowState,
23
+ )
24
+ from .requests import (
25
+ CommentCreateRequest,
26
+ CommentRequest,
27
+ CommentsRequest,
28
+ FindWorkflowStateRequest,
29
+ IssueAddLabelRequest,
30
+ IssueArchiveRequest,
31
+ IssueCreateRequest,
32
+ IssueLabelsRequest,
33
+ IssueRemoveLabelRequest,
34
+ IssueRequest,
35
+ IssueSetStateRequest,
36
+ IssuesRequest,
37
+ IssueUpdateRequest,
38
+ PaginatedRequest,
39
+ ProjectRequest,
40
+ ProjectsRequest,
41
+ TeamRequest,
42
+ TeamsRequest,
43
+ UserRequest,
44
+ UsersRequest,
45
+ WorkflowStatesRequest,
46
+ )
47
+ from .responses import (
48
+ AddLabelResponse,
49
+ ArchiveIssueResponse,
50
+ CommentResponse,
51
+ CommentsResponse,
52
+ ConnectionResponse,
53
+ CreateCommentResponse,
54
+ CreateIssueResponse,
55
+ IssueDetailsResponse,
56
+ IssueLabelsResponse,
57
+ IssueResponse,
58
+ IssuesResponse,
59
+ ProjectResponse,
60
+ ProjectsResponse,
61
+ RemoveLabelResponse,
62
+ TeamResponse,
63
+ TeamsResponse,
64
+ UpdateIssueResponse,
65
+ UserResponse,
66
+ UsersResponse,
67
+ ViewerResponse,
68
+ WorkflowStateResponse,
69
+ WorkflowStatesResponse,
70
+ )
71
+
72
+ __all__ = [
73
+ # entities
74
+ "LinearModel",
75
+ "Attachment",
76
+ "Comment",
77
+ "Cycle",
78
+ "Issue",
79
+ "IssueDetail",
80
+ "IssueLabel",
81
+ "IssueRelation",
82
+ "PageInfo",
83
+ "Project",
84
+ "Team",
85
+ "User",
86
+ "WorkflowState",
87
+ # requests
88
+ "PaginatedRequest",
89
+ "UserRequest",
90
+ "UsersRequest",
91
+ "TeamRequest",
92
+ "TeamsRequest",
93
+ "IssueRequest",
94
+ "IssuesRequest",
95
+ "IssueCreateRequest",
96
+ "IssueUpdateRequest",
97
+ "IssueArchiveRequest",
98
+ "IssueAddLabelRequest",
99
+ "IssueRemoveLabelRequest",
100
+ "IssueSetStateRequest",
101
+ "FindWorkflowStateRequest",
102
+ "ProjectRequest",
103
+ "ProjectsRequest",
104
+ "CommentRequest",
105
+ "CommentsRequest",
106
+ "CommentCreateRequest",
107
+ "WorkflowStatesRequest",
108
+ "IssueLabelsRequest",
109
+ # responses
110
+ "ConnectionResponse",
111
+ "ViewerResponse",
112
+ "UserResponse",
113
+ "UsersResponse",
114
+ "TeamResponse",
115
+ "TeamsResponse",
116
+ "IssueResponse",
117
+ "IssueDetailsResponse",
118
+ "IssuesResponse",
119
+ "CreateIssueResponse",
120
+ "UpdateIssueResponse",
121
+ "ArchiveIssueResponse",
122
+ "AddLabelResponse",
123
+ "RemoveLabelResponse",
124
+ "ProjectResponse",
125
+ "ProjectsResponse",
126
+ "CommentResponse",
127
+ "CommentsResponse",
128
+ "CreateCommentResponse",
129
+ "WorkflowStateResponse",
130
+ "WorkflowStatesResponse",
131
+ "IssueLabelsResponse",
132
+ ]