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,187 @@
1
+ """Pydantic models for Linear API entities.
2
+
3
+ All models use snake_case attribute names with automatically generated camelCase
4
+ aliases, so they parse the API's camelCase payloads (`displayName`, `createdAt`,
5
+ …) and can be serialised back to them with `model_dump(by_alias=True)`. Because
6
+ `populate_by_name=True`, you can construct them with either spelling.
7
+
8
+ Only the fields a given query requested are populated; everything is optional and
9
+ defaults to `None`, and unknown fields are ignored.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import datetime
15
+
16
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
17
+ from pydantic.alias_generators import to_camel
18
+
19
+
20
+ def _unwrap_nodes(value: object) -> object:
21
+ """Accept Linear's `{ nodes: [...] }` connection shape, returning the node list."""
22
+ if isinstance(value, dict) and "nodes" in value:
23
+ return value["nodes"]
24
+ return value
25
+
26
+
27
+ class LinearModel(BaseModel):
28
+ """Base model: camelCase aliases, snake_case attributes, lenient parsing."""
29
+
30
+ model_config = ConfigDict(
31
+ alias_generator=to_camel,
32
+ populate_by_name=True,
33
+ extra="ignore",
34
+ )
35
+
36
+
37
+ class PageInfo(LinearModel):
38
+ """Relay-style pagination metadata for a connection."""
39
+
40
+ has_next_page: bool = False
41
+ has_previous_page: bool = False
42
+ start_cursor: str | None = None
43
+ end_cursor: str | None = None
44
+
45
+
46
+ class User(LinearModel):
47
+ """A Linear user."""
48
+
49
+ id: str | None = None
50
+ name: str | None = None
51
+ display_name: str | None = None
52
+ email: str | None = None
53
+ active: bool | None = None
54
+ admin: bool | None = None
55
+ created_at: datetime | None = None
56
+
57
+
58
+ class Team(LinearModel):
59
+ """A Linear team."""
60
+
61
+ id: str | None = None
62
+ name: str | None = None
63
+ key: str | None = None
64
+ description: str | None = None
65
+ private: bool | None = None
66
+ created_at: datetime | None = None
67
+
68
+
69
+ class WorkflowState(LinearModel):
70
+ """An issue workflow state (e.g. Todo, In Progress, Done)."""
71
+
72
+ id: str | None = None
73
+ name: str | None = None
74
+ type: str | None = None
75
+ color: str | None = None
76
+ position: float | None = None
77
+
78
+
79
+ class IssueLabel(LinearModel):
80
+ """A label that can be applied to issues."""
81
+
82
+ id: str | None = None
83
+ name: str | None = None
84
+ color: str | None = None
85
+
86
+
87
+ class Project(LinearModel):
88
+ """A Linear project."""
89
+
90
+ id: str | None = None
91
+ name: str | None = None
92
+ description: str | None = None
93
+ slug_id: str | None = None
94
+ state: str | None = None
95
+ progress: float | None = None
96
+ created_at: datetime | None = None
97
+
98
+
99
+ class Comment(LinearModel):
100
+ """A comment on an issue."""
101
+
102
+ id: str | None = None
103
+ body: str | None = None
104
+ url: str | None = None
105
+ user: User | None = None
106
+ created_at: datetime | None = None
107
+ updated_at: datetime | None = None
108
+
109
+
110
+ class Issue(LinearModel):
111
+ """A Linear issue, with nested relations populated when requested."""
112
+
113
+ id: str | None = None
114
+ identifier: str | None = None
115
+ title: str | None = None
116
+ description: str | None = None
117
+ url: str | None = None
118
+ priority: int | None = None
119
+ estimate: float | None = None
120
+ branch_name: str | None = None
121
+ created_at: datetime | None = None
122
+ updated_at: datetime | None = None
123
+ completed_at: datetime | None = None
124
+ assignee: User | None = None
125
+ creator: User | None = None
126
+ team: Team | None = None
127
+ state: WorkflowState | None = None
128
+ labels: list[IssueLabel] = Field(default_factory=list)
129
+
130
+ @field_validator("labels", mode="before")
131
+ @classmethod
132
+ def _unwrap_label_nodes(cls, value: object) -> object:
133
+ """Accept Linear's `labels: { nodes: [...] }` connection shape."""
134
+ return _unwrap_nodes(value)
135
+
136
+
137
+ class Attachment(LinearModel):
138
+ """A link or file attached to an issue."""
139
+
140
+ id: str | None = None
141
+ title: str | None = None
142
+ subtitle: str | None = None
143
+ url: str | None = None
144
+ created_at: datetime | None = None
145
+
146
+
147
+ class Cycle(LinearModel):
148
+ """A team cycle (sprint)."""
149
+
150
+ id: str | None = None
151
+ number: int | None = None
152
+ name: str | None = None
153
+ starts_at: datetime | None = None
154
+ ends_at: datetime | None = None
155
+
156
+
157
+ class IssueRelation(LinearModel):
158
+ """A relation from an issue to another (e.g. blocks, related, duplicate)."""
159
+
160
+ type: str | None = None
161
+ related_issue: Issue | None = None
162
+
163
+
164
+ class IssueDetail(Issue):
165
+ """An issue plus its heavier related data, returned by `issue_details`.
166
+
167
+ Inherits every field of [`Issue`][linear_python_client.Issue] and adds the
168
+ related collections. As always, only the fields the query requested are
169
+ populated; `parent`, `children`, and relation targets are shallow issues.
170
+ """
171
+
172
+ comments: list[Comment] = Field(default_factory=list)
173
+ attachments: list[Attachment] = Field(default_factory=list)
174
+ project: Project | None = None
175
+ cycle: Cycle | None = None
176
+ parent: Issue | None = None
177
+ children: list[Issue] = Field(default_factory=list)
178
+ subscribers: list[User] = Field(default_factory=list)
179
+ relations: list[IssueRelation] = Field(default_factory=list)
180
+
181
+ @field_validator(
182
+ "comments", "attachments", "children", "subscribers", "relations", mode="before"
183
+ )
184
+ @classmethod
185
+ def _unwrap_connection_nodes(cls, value: object) -> object:
186
+ """Accept Linear's `{ nodes: [...] }` connection shape for the collections."""
187
+ return _unwrap_nodes(value)
@@ -0,0 +1,260 @@
1
+ """Typed request models for each :class:`~linear_python_client.client.LinearClient` call.
2
+
3
+ Every client method takes exactly one of these. They carry snake_case fields with
4
+ camelCase aliases, and expose helpers (`to_variables`, `to_input`) that serialise
5
+ them into the GraphQL variables the API expects.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from pydantic import ConfigDict, Field
13
+
14
+ from .entities import LinearModel
15
+
16
+ # -- query requests ---------------------------------------------------------
17
+
18
+
19
+ class IssueRequest(LinearModel):
20
+ """Fetch a single issue by id or human identifier (e.g. `"ENG-123"`)."""
21
+
22
+ id: str
23
+
24
+
25
+ class UserRequest(LinearModel):
26
+ """Fetch a single user by UUID."""
27
+
28
+ id: str
29
+
30
+
31
+ class TeamRequest(LinearModel):
32
+ """Fetch a single team by UUID."""
33
+
34
+ id: str
35
+
36
+
37
+ class ProjectRequest(LinearModel):
38
+ """Fetch a single project by UUID."""
39
+
40
+ id: str
41
+
42
+
43
+ class CommentRequest(LinearModel):
44
+ """Fetch a single comment by UUID."""
45
+
46
+ id: str
47
+
48
+
49
+ class PaginatedRequest(LinearModel):
50
+ """Base for list requests: cursor pagination plus an optional filter.
51
+
52
+ Attributes:
53
+ first: Maximum number of results to return (Linear defaults to 50).
54
+ after: Pagination cursor; pass the previous page's `end_cursor`.
55
+ filter: A [Linear filter](https://linear.app/developers/filtering) dict.
56
+ """
57
+
58
+ first: int | None = None
59
+ after: str | None = None
60
+ filter: dict[str, Any] | None = None
61
+
62
+ def to_variables(self) -> dict[str, Any]:
63
+ """Serialise to GraphQL variables (camelCase, omitting unset values)."""
64
+ return self.model_dump(by_alias=True, exclude_none=True)
65
+
66
+
67
+ class IssuesRequest(PaginatedRequest):
68
+ """List issues, optionally filtered and ordered.
69
+
70
+ Attributes:
71
+ order_by: Sort order, either `"createdAt"` (default) or `"updatedAt"`.
72
+ """
73
+
74
+ order_by: str | None = None
75
+
76
+
77
+ class UsersRequest(PaginatedRequest):
78
+ """List users in the workspace."""
79
+
80
+
81
+ class TeamsRequest(PaginatedRequest):
82
+ """List teams in the workspace."""
83
+
84
+
85
+ class ProjectsRequest(PaginatedRequest):
86
+ """List projects in the workspace."""
87
+
88
+
89
+ class IssueLabelsRequest(PaginatedRequest):
90
+ """List issue labels in the workspace."""
91
+
92
+
93
+ class CommentsRequest(PaginatedRequest):
94
+ """List comments, optionally scoped to a single issue.
95
+
96
+ Attributes:
97
+ issue_id: When set, only comments on this issue are returned (merged into
98
+ `filter`). Not sent as a raw variable.
99
+ """
100
+
101
+ issue_id: str | None = Field(default=None, exclude=True)
102
+
103
+
104
+ class WorkflowStatesRequest(PaginatedRequest):
105
+ """List workflow states, optionally scoped to a single team.
106
+
107
+ Attributes:
108
+ team_id: When set, only states belonging to this team are returned (merged
109
+ into `filter`). Not sent as a raw variable.
110
+ """
111
+
112
+ team_id: str | None = Field(default=None, exclude=True)
113
+
114
+
115
+ # -- mutation requests ------------------------------------------------------
116
+
117
+
118
+ class IssueCreateRequest(LinearModel):
119
+ """Input for creating an issue.
120
+
121
+ Any field accepted by Linear's `IssueCreateInput` may also be passed as an
122
+ extra keyword argument using its camelCase API name (e.g. `dueDate="2026-01-01"`).
123
+
124
+ Attributes:
125
+ team_id: UUID of the team the issue belongs to (required).
126
+ title: The issue title (required).
127
+ description: Markdown body for the issue.
128
+ assignee_id: UUID of the user to assign.
129
+ state_id: UUID of the workflow state to set.
130
+ priority: Priority from 0 (none) to 4 (low); 1 is urgent.
131
+ label_ids: UUIDs of labels to attach.
132
+ project_id: UUID of the project to add the issue to.
133
+ """
134
+
135
+ model_config = ConfigDict(extra="allow")
136
+
137
+ team_id: str
138
+ title: str
139
+ description: str | None = None
140
+ assignee_id: str | None = None
141
+ state_id: str | None = None
142
+ priority: int | None = None
143
+ label_ids: list[str] | None = None
144
+ project_id: str | None = None
145
+
146
+ def to_input(self) -> dict[str, Any]:
147
+ """Serialise to an `IssueCreateInput` dict (camelCase, omitting unset values)."""
148
+ return self.model_dump(by_alias=True, exclude_none=True)
149
+
150
+
151
+ class IssueUpdateRequest(LinearModel):
152
+ """Input for updating an issue.
153
+
154
+ At least one field other than `id` must be set. Any field accepted by Linear's
155
+ `IssueUpdateInput` may also be passed as an extra keyword argument using its
156
+ camelCase API name.
157
+
158
+ Attributes:
159
+ id: UUID of the issue to update (required).
160
+ title: New title.
161
+ description: New markdown body.
162
+ assignee_id: UUID of the user to assign.
163
+ state_id: UUID of the workflow state to set.
164
+ priority: Priority from 0 (none) to 4 (low); 1 is urgent.
165
+ label_ids: UUIDs of labels to set.
166
+ project_id: UUID of the project to move the issue to.
167
+ """
168
+
169
+ model_config = ConfigDict(extra="allow")
170
+
171
+ id: str
172
+ title: str | None = None
173
+ description: str | None = None
174
+ assignee_id: str | None = None
175
+ state_id: str | None = None
176
+ priority: int | None = None
177
+ label_ids: list[str] | None = None
178
+ project_id: str | None = None
179
+
180
+ def to_input(self) -> dict[str, Any]:
181
+ """Serialise the update fields to an `IssueUpdateInput` dict (excluding `id`)."""
182
+ data = self.model_dump(by_alias=True, exclude_none=True)
183
+ data.pop("id", None)
184
+ return data
185
+
186
+
187
+ class IssueArchiveRequest(LinearModel):
188
+ """Archive an issue by UUID."""
189
+
190
+ id: str
191
+
192
+
193
+ class IssueAddLabelRequest(LinearModel):
194
+ """Add a single label to an issue, leaving its other labels untouched.
195
+
196
+ Attributes:
197
+ id: UUID of the issue.
198
+ label_id: UUID of the label to add.
199
+ """
200
+
201
+ id: str
202
+ label_id: str
203
+
204
+
205
+ class IssueRemoveLabelRequest(LinearModel):
206
+ """Remove a single label from an issue, leaving its other labels untouched.
207
+
208
+ Attributes:
209
+ id: UUID of the issue.
210
+ label_id: UUID of the label to remove.
211
+ """
212
+
213
+ id: str
214
+ label_id: str
215
+
216
+
217
+ class IssueSetStateRequest(LinearModel):
218
+ """Move an issue to a workflow state (status).
219
+
220
+ Attributes:
221
+ id: UUID of the issue.
222
+ state_id: UUID of the target workflow state. Resolve one by name with
223
+ [`find_workflow_state`][linear_python_client.client.LinearClient.find_workflow_state].
224
+ """
225
+
226
+ id: str
227
+ state_id: str
228
+
229
+
230
+ class FindWorkflowStateRequest(LinearModel):
231
+ """Resolve a workflow state by name within a team.
232
+
233
+ Attributes:
234
+ team_id: UUID of the team that owns the state.
235
+ name: State name to match, case-insensitively (e.g. `"In Progress"`).
236
+ """
237
+
238
+ team_id: str
239
+ name: str
240
+
241
+
242
+ class CommentCreateRequest(LinearModel):
243
+ """Input for adding a comment to an issue.
244
+
245
+ Any field accepted by Linear's `CommentCreateInput` may also be passed as an
246
+ extra keyword argument using its camelCase API name.
247
+
248
+ Attributes:
249
+ issue_id: UUID of the issue to comment on (required).
250
+ body: Markdown body of the comment (required).
251
+ """
252
+
253
+ model_config = ConfigDict(extra="allow")
254
+
255
+ issue_id: str
256
+ body: str
257
+
258
+ def to_input(self) -> dict[str, Any]:
259
+ """Serialise to a `CommentCreateInput` dict (camelCase, omitting unset values)."""
260
+ return self.model_dump(by_alias=True, exclude_none=True)
@@ -0,0 +1,169 @@
1
+ """Typed response models returned by each :class:`~linear_python_client.client.LinearClient` call.
2
+
3
+ Single-entity queries return a wrapper exposing the entity (e.g.
4
+ :class:`IssueResponse.issue`). List queries return a :class:`ConnectionResponse`
5
+ subclass exposing `nodes` and `page_info`. Mutations mirror the GraphQL payload,
6
+ exposing `success` alongside the affected entity.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pydantic import Field
12
+
13
+ from .entities import (
14
+ Comment,
15
+ Issue,
16
+ IssueDetail,
17
+ IssueLabel,
18
+ LinearModel,
19
+ PageInfo,
20
+ Project,
21
+ Team,
22
+ User,
23
+ WorkflowState,
24
+ )
25
+
26
+
27
+ class ConnectionResponse[NodeT](LinearModel):
28
+ """Base for list responses: a page of `nodes` plus its :class:`PageInfo`.
29
+
30
+ Iterable and sized, so you can loop over the response directly or read
31
+ `response.nodes` / `response.page_info`.
32
+ """
33
+
34
+ nodes: list[NodeT] = Field(default_factory=list)
35
+ page_info: PageInfo = Field(default_factory=PageInfo)
36
+
37
+ def __iter__(self):
38
+ """Iterate over `nodes`."""
39
+ return iter(self.nodes)
40
+
41
+ def __len__(self) -> int:
42
+ """Number of nodes on this page."""
43
+ return len(self.nodes)
44
+
45
+
46
+ # -- single-entity query responses -----------------------------------------
47
+
48
+
49
+ class ViewerResponse(LinearModel):
50
+ """Response for [`viewer`][linear_python_client.client.LinearClient.viewer]."""
51
+
52
+ viewer: User | None = None
53
+
54
+
55
+ class UserResponse(LinearModel):
56
+ """Response for [`user`][linear_python_client.client.LinearClient.user]."""
57
+
58
+ user: User | None = None
59
+
60
+
61
+ class TeamResponse(LinearModel):
62
+ """Response for [`team`][linear_python_client.client.LinearClient.team]."""
63
+
64
+ team: Team | None = None
65
+
66
+
67
+ class IssueResponse(LinearModel):
68
+ """Response for [`issue`][linear_python_client.client.LinearClient.issue]."""
69
+
70
+ issue: Issue | None = None
71
+
72
+
73
+ class IssueDetailsResponse(LinearModel):
74
+ """Response for [`issue_details`][linear_python_client.client.LinearClient.issue_details]."""
75
+
76
+ issue: IssueDetail | None = None
77
+
78
+
79
+ class WorkflowStateResponse(LinearModel):
80
+ """Resolved workflow state from `find_workflow_state` (`None` if no match)."""
81
+
82
+ state: WorkflowState | None = None
83
+
84
+
85
+ class ProjectResponse(LinearModel):
86
+ """Response for [`project`][linear_python_client.client.LinearClient.project]."""
87
+
88
+ project: Project | None = None
89
+
90
+
91
+ class CommentResponse(LinearModel):
92
+ """Response for [`comment`][linear_python_client.client.LinearClient.comment]."""
93
+
94
+ comment: Comment | None = None
95
+
96
+
97
+ # -- list query responses ---------------------------------------------------
98
+
99
+
100
+ class UsersResponse(ConnectionResponse[User]):
101
+ """Response for [`users`][linear_python_client.client.LinearClient.users]."""
102
+
103
+
104
+ class TeamsResponse(ConnectionResponse[Team]):
105
+ """Response for [`teams`][linear_python_client.client.LinearClient.teams]."""
106
+
107
+
108
+ class IssuesResponse(ConnectionResponse[Issue]):
109
+ """Response for [`issues`][linear_python_client.client.LinearClient.issues]."""
110
+
111
+
112
+ class ProjectsResponse(ConnectionResponse[Project]):
113
+ """Response for [`projects`][linear_python_client.client.LinearClient.projects]."""
114
+
115
+
116
+ class CommentsResponse(ConnectionResponse[Comment]):
117
+ """Response for [`comments`][linear_python_client.client.LinearClient.comments]."""
118
+
119
+
120
+ class WorkflowStatesResponse(ConnectionResponse[WorkflowState]):
121
+ """[`workflow_states`][linear_python_client.client.LinearClient.workflow_states] response."""
122
+
123
+
124
+ class IssueLabelsResponse(ConnectionResponse[IssueLabel]):
125
+ """Response for [`issue_labels`][linear_python_client.client.LinearClient.issue_labels]."""
126
+
127
+
128
+ # -- mutation responses -----------------------------------------------------
129
+
130
+
131
+ class CreateIssueResponse(LinearModel):
132
+ """Response for [`create_issue`][linear_python_client.client.LinearClient.create_issue]."""
133
+
134
+ success: bool = False
135
+ issue: Issue | None = None
136
+
137
+
138
+ class UpdateIssueResponse(LinearModel):
139
+ """Response for [`update_issue`][linear_python_client.client.LinearClient.update_issue]."""
140
+
141
+ success: bool = False
142
+ issue: Issue | None = None
143
+
144
+
145
+ class ArchiveIssueResponse(LinearModel):
146
+ """Response for [`archive_issue`][linear_python_client.client.LinearClient.archive_issue]."""
147
+
148
+ success: bool = False
149
+
150
+
151
+ class CreateCommentResponse(LinearModel):
152
+ """Response for [`create_comment`][linear_python_client.client.LinearClient.create_comment]."""
153
+
154
+ success: bool = False
155
+ comment: Comment | None = None
156
+
157
+
158
+ class AddLabelResponse(LinearModel):
159
+ """Response for [`add_label`][linear_python_client.client.LinearClient.add_label]."""
160
+
161
+ success: bool = False
162
+ issue: Issue | None = None
163
+
164
+
165
+ class RemoveLabelResponse(LinearModel):
166
+ """Response for [`remove_label`][linear_python_client.client.LinearClient.remove_label]."""
167
+
168
+ success: bool = False
169
+ issue: Issue | None = None
File without changes