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,338 @@
1
+ Metadata-Version: 2.4
2
+ Name: linear-python-client
3
+ Version: 0.1.0
4
+ Summary: Pragmatic Python client for the Linear GraphQL API
5
+ Keywords: linear,linear.app,graphql,api,client,sdk
6
+ Author: eli-the-wizard
7
+ Author-email: eli-the-wizard <ewinter@hackerone.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: jupyter>=1.1.1
19
+ Requires-Dist: pydantic>=2.7
20
+ Requires-Python: >=3.14
21
+ Project-URL: Homepage, https://github.com/Hacker0x01/linear-python
22
+ Project-URL: Documentation, https://hacker0x01.github.io/linear-python/
23
+ Project-URL: Repository, https://github.com/Hacker0x01/linear-python
24
+ Project-URL: Issues, https://github.com/Hacker0x01/linear-python/issues
25
+ Description-Content-Type: text/markdown
26
+
27
+ # linear-python-client
28
+
29
+ A small, pragmatic synchronous Python client for the [Linear](https://linear.app)
30
+ GraphQL API. Linear's official SDK is TypeScript-only — this package gives Python
31
+ the same ergonomics, built on [Pydantic](https://docs.pydantic.dev/): every call
32
+ takes a typed **`*Request`** model and returns a dedicated **`*Response`** model, so
33
+ inputs and outputs are explicit and validated. A generic `execute()` escape hatch
34
+ covers anything the typed methods don't.
35
+
36
+ Built against the [Linear developer docs](https://linear.app/developers).
37
+
38
+ 📖 **Full documentation:** <https://hacker0x01.github.io/linear-python/>
39
+
40
+ ## Installation
41
+
42
+ ```sh
43
+ uv add linear-python-client
44
+ # or
45
+ pip install linear-python-client
46
+ ```
47
+
48
+ Or for local development of this repo:
49
+
50
+ ```sh
51
+ uv sync
52
+ ```
53
+
54
+ Requires Python 3.14+.
55
+
56
+ ## Authentication
57
+
58
+ The client accepts either a personal API key or an OAuth 2.0 access token.
59
+
60
+ ```python
61
+ from linear_python_client import LinearClient
62
+
63
+ # Personal API key (sent as the raw `Authorization` header value)
64
+ client = LinearClient(api_key="lin_api_...")
65
+
66
+ # OAuth 2.0 access token (sent as `Authorization: Bearer <token>`)
67
+ client = LinearClient(access_token="...")
68
+
69
+ # Or set LINEAR_API_KEY in the environment and call LinearClient()
70
+ client = LinearClient()
71
+ ```
72
+
73
+ Use it as a context manager to close the underlying HTTP client automatically:
74
+
75
+ ```python
76
+ with LinearClient() as client:
77
+ print(client.viewer().viewer.name)
78
+ ```
79
+
80
+ ## Quickstart
81
+
82
+ Each method takes a `*Request` and returns a `*Response`:
83
+
84
+ ```python
85
+ from linear_python_client import (
86
+ LinearClient,
87
+ IssueRequest,
88
+ IssueCreateRequest,
89
+ IssueUpdateRequest,
90
+ IssueArchiveRequest,
91
+ CommentCreateRequest,
92
+ )
93
+
94
+ with LinearClient() as client:
95
+ # The authenticated user
96
+ me = client.viewer().viewer
97
+ print(me.name, me.email)
98
+
99
+ # Fetch a single issue by id or identifier
100
+ issue = client.issue(IssueRequest(id="ENG-123")).issue
101
+ print(issue.title, issue.state.name)
102
+
103
+ # Create an issue
104
+ created = client.create_issue(
105
+ IssueCreateRequest(
106
+ team_id="9cfb482a-81e3-4154-b5b9-2c805e70a02d",
107
+ title="New exception",
108
+ description="More detailed error report in **markdown**",
109
+ priority=2,
110
+ )
111
+ )
112
+ print(created.success, created.issue.identifier)
113
+
114
+ # Update it
115
+ client.update_issue(IssueUpdateRequest(id=created.issue.id, title="Renamed", priority=1))
116
+
117
+ # Comment on it
118
+ client.create_comment(CommentCreateRequest(issue_id=created.issue.id, body="On it 👍"))
119
+
120
+ # Archive it
121
+ client.archive_issue(IssueArchiveRequest(id=created.issue.id))
122
+ ```
123
+
124
+ Field names are Pythonic snake_case with camelCase aliases, so `IssueCreateRequest`
125
+ accepts `team_id=` (or `teamId=`) and the parsed models expose `issue.created_at`,
126
+ `issue.assignee.display_name`, and so on.
127
+
128
+ ## Listing, filtering & pagination
129
+
130
+ List methods take a `*Request` (with `first`, `after`, and a `filter` dict that maps
131
+ directly to Linear's [filtering syntax](https://linear.app/developers/filtering)) and
132
+ return a `*Response` that holds `.nodes` and `.page_info` (and is iterable).
133
+
134
+ ```python
135
+ from linear_python_client import IssuesRequest
136
+
137
+ # First 20 high-priority issues assigned to a specific user
138
+ resp = client.issues(
139
+ IssuesRequest(
140
+ first=20,
141
+ filter={
142
+ "priority": {"eq": 1},
143
+ "assignee": {"email": {"eq": "you@example.com"}},
144
+ },
145
+ order_by="updatedAt",
146
+ )
147
+ )
148
+ for issue in resp.nodes:
149
+ print(issue.identifier, issue.title)
150
+
151
+ print(resp.page_info.has_next_page, resp.page_info.end_cursor)
152
+ ```
153
+
154
+ Use `paginate()` to transparently follow the cursor across every page. Pass the list
155
+ method and a starting request:
156
+
157
+ ```python
158
+ for issue in client.paginate(client.issues, IssuesRequest(filter={"state": {"type": {"eq": "started"}}})):
159
+ print(issue.identifier, issue.title)
160
+ ```
161
+
162
+ `paginate()` works with any list method (`client.issues`, `client.teams`,
163
+ `client.projects`, `client.comments`, `client.users`, …) and its matching request.
164
+
165
+ ## Labels, status & full details
166
+
167
+ ```python
168
+ from linear_python_client import (
169
+ IssueAddLabelRequest,
170
+ IssueRemoveLabelRequest,
171
+ IssueSetStateRequest,
172
+ FindWorkflowStateRequest,
173
+ IssueRequest,
174
+ )
175
+
176
+ # Add / remove a single label without disturbing the issue's other labels
177
+ client.add_label(IssueAddLabelRequest(id=issue_id, label_id=label_id))
178
+ client.remove_label(IssueRemoveLabelRequest(id=issue_id, label_id=label_id))
179
+
180
+ # Update status: resolve a state by name, then set it
181
+ state = client.find_workflow_state(FindWorkflowStateRequest(team_id=team_id, name="In Progress")).state
182
+ client.set_issue_state(IssueSetStateRequest(id=issue_id, state_id=state.id))
183
+
184
+ # Full details: comments, attachments, project, cycle, parent, sub-issues, subscribers, relations
185
+ detail = client.issue_details(IssueRequest(id="ENG-123")).issue
186
+ print(detail.state.name, len(detail.comments), len(detail.attachments))
187
+ for child in detail.children:
188
+ print("sub-issue:", child.identifier, child.title)
189
+ ```
190
+
191
+ ## Escape hatch: raw GraphQL
192
+
193
+ Anything not covered by a convenience method can be run directly. `execute()`
194
+ returns the `data` object and raises on errors.
195
+
196
+ ```python
197
+ data = client.execute(
198
+ """
199
+ query($id: String!) {
200
+ issue(id: $id) { id title attachments { nodes { url title } } }
201
+ }
202
+ """,
203
+ {"id": "ENG-123"},
204
+ )
205
+ print(data["issue"]["attachments"]["nodes"])
206
+ ```
207
+
208
+ ## Errors
209
+
210
+ All exceptions subclass `LinearError`:
211
+
212
+ | Exception | Raised when |
213
+ |-----------|-------------|
214
+ | `LinearAuthenticationError` | Credentials are rejected (HTTP 401/403 or auth error code) |
215
+ | `LinearRateLimitError` | A rate limit is hit (`RATELIMITED`); carries the `X-RateLimit-*` header values |
216
+ | `LinearGraphQLError` | The API returns GraphQL `errors`; exposes `.errors` and `.code` |
217
+ | `LinearNetworkError` | The request never produced a usable response |
218
+
219
+ ```python
220
+ from linear_python_client import LinearClient, LinearRateLimitError, IssuesRequest
221
+
222
+ try:
223
+ client.issues(IssuesRequest(first=100))
224
+ except LinearRateLimitError as exc:
225
+ print("Rate limited; resets at", exc.requests_reset)
226
+ ```
227
+
228
+ ## Available client methods
229
+
230
+ Each method maps a `*Request` to a `*Response`:
231
+
232
+ | Method | Request | Response |
233
+ |--------|---------|----------|
234
+ | `viewer()` | – | `ViewerResponse` |
235
+ | `user(...)` | `UserRequest` | `UserResponse` |
236
+ | `users(...)` | `UsersRequest` | `UsersResponse` |
237
+ | `team(...)` | `TeamRequest` | `TeamResponse` |
238
+ | `teams(...)` | `TeamsRequest` | `TeamsResponse` |
239
+ | `issue(...)` | `IssueRequest` | `IssueResponse` |
240
+ | `issue_details(...)` | `IssueRequest` | `IssueDetailsResponse` |
241
+ | `issues(...)` | `IssuesRequest` | `IssuesResponse` |
242
+ | `create_issue(...)` | `IssueCreateRequest` | `CreateIssueResponse` |
243
+ | `update_issue(...)` | `IssueUpdateRequest` | `UpdateIssueResponse` |
244
+ | `archive_issue(...)` | `IssueArchiveRequest` | `ArchiveIssueResponse` |
245
+ | `add_label(...)` | `IssueAddLabelRequest` | `AddLabelResponse` |
246
+ | `remove_label(...)` | `IssueRemoveLabelRequest` | `RemoveLabelResponse` |
247
+ | `set_issue_state(...)` | `IssueSetStateRequest` | `UpdateIssueResponse` |
248
+ | `project(...)` | `ProjectRequest` | `ProjectResponse` |
249
+ | `projects(...)` | `ProjectsRequest` | `ProjectsResponse` |
250
+ | `comment(...)` | `CommentRequest` | `CommentResponse` |
251
+ | `comments(...)` | `CommentsRequest` | `CommentsResponse` |
252
+ | `create_comment(...)` | `CommentCreateRequest` | `CreateCommentResponse` |
253
+ | `workflow_states(...)` | `WorkflowStatesRequest` | `WorkflowStatesResponse` |
254
+ | `find_workflow_state(...)` | `FindWorkflowStateRequest` | `WorkflowStateResponse` |
255
+ | `issue_labels(...)` | `IssueLabelsRequest` | `IssueLabelsResponse` |
256
+ | `execute(query, variables)` | – | `dict` |
257
+ | `paginate(method, request)` | a `*Request` | iterator of nodes |
258
+
259
+ List requests are optional (e.g. `client.issues()` returns the first page unfiltered).
260
+
261
+ ## Development
262
+
263
+ ```sh
264
+ uv sync # install deps + dev tools
265
+ uv run pytest # run the mocked unit tests with coverage (no network)
266
+ uv run ruff check
267
+ ```
268
+
269
+ The test suite mocks the GraphQL endpoint, so no credentials or network access are
270
+ needed. An optional live smoke test runs only when `LINEAR_API_KEY` is set.
271
+
272
+ `pytest` runs with coverage by default and **fails under 90%** (configured in
273
+ `pyproject.toml`); the suite currently covers ~99% of the package. A coverage summary
274
+ prints after each run — add `--cov-report=html` for an annotated HTML report in
275
+ `htmlcov/`.
276
+
277
+ ### Live smoke test
278
+
279
+ `scripts/smoke_test.py` exercises **every** client method against the real Linear API
280
+ and, after each mutation, re-pulls the issue to confirm the change landed (create →
281
+ update → set status → add/remove label → comment → full details). It creates one
282
+ clearly-labelled test issue and archives it at the end, so it cleans up after itself.
283
+
284
+ ```sh
285
+ LINEAR_API_KEY=lin_api_... uv run python scripts/smoke_test.py
286
+ # optionally pin the team (defaults to the first one):
287
+ LINEAR_API_KEY=... LINEAR_TEAM_ID=<uuid> uv run python scripts/smoke_test.py
288
+ ```
289
+
290
+ It prints a ✓/✗ per check and exits non-zero if any fail. Because it writes to your
291
+ workspace, it's a manual script — it is not part of `pytest`.
292
+
293
+ ### Building & releasing
294
+
295
+ Build the distributions locally with uv:
296
+
297
+ ```sh
298
+ uv build # writes sdist + wheel to ./dist
299
+ uvx twine check dist/* # validate metadata / README rendering
300
+ ```
301
+
302
+ Releases are automated by [`.github/workflows/publish.yml`](.github/workflows/publish.yml).
303
+ On every push and PR it lints, tests (with the coverage gate), builds the sdist + wheel,
304
+ validates the metadata, and smoke-tests that the wheel installs and imports. When a
305
+ **GitHub Release is published**, it additionally publishes the build to PyPI — after
306
+ which `pip install linear-python-client` and `uv add linear-python-client` work.
307
+
308
+ Publishing uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
309
+ (OIDC), so no API token or secret is stored. One-time setup:
310
+
311
+ 1. On PyPI, add a trusted publisher for the project pointing at this repo, workflow
312
+ `publish.yml`, and environment `pypi`.
313
+ 2. In the repo, create a `pypi` [environment](https://docs.github.com/actions/deployment/targeting-different-environments/using-environments-for-deployment)
314
+ (Settings → Environments).
315
+
316
+ To cut a release: bump `version` in `pyproject.toml`, then create a matching GitHub
317
+ Release (e.g. tag `v0.1.0`) — the workflow builds and uploads it to PyPI.
318
+
319
+ > [!NOTE]
320
+ > `requires-python` is `>=3.14`, so installs require Python 3.14+.
321
+
322
+ ### Documentation
323
+
324
+ The docs are built with [MkDocs](https://www.mkdocs.org/) +
325
+ [Material](https://squidfunk.github.io/mkdocs-material/) and the API reference is
326
+ generated automatically from docstrings via
327
+ [mkdocstrings](https://mkdocstrings.github.io/).
328
+
329
+ ```sh
330
+ uv run --group docs mkdocs serve # live preview at http://127.0.0.1:8000
331
+ uv run --group docs mkdocs build --strict # production build into ./site
332
+ ```
333
+
334
+ They deploy to GitHub Pages automatically on every push to `main` via
335
+ [`.github/workflows/docs.yml`](.github/workflows/docs.yml). To enable publishing,
336
+ set **Settings → Pages → Build and deployment → Source** to **GitHub Actions** in the
337
+ repository once. Update the `site_url`/`repo_url` in `mkdocs.yml` if the repo lives
338
+ under a different owner.
@@ -0,0 +1,14 @@
1
+ linear_python_client/__init__.py,sha256=2L9lnijqqbAytIC5fp_OnvZvhQji9RxOhNubbW7816Q,3135
2
+ linear_python_client/client.py,sha256=oMWwM94I_46iYQVKfpnmAyICvPAUkyKPdv--QXOYG7I,24233
3
+ linear_python_client/errors.py,sha256=TBdv1mqPIQJjSUf42YQakAS4A9GN7PQKPnzdptTR7-k,2838
4
+ linear_python_client/graphql/__init__.py,sha256=7Km7YkHvWrRMD4GRJo7PNRD-4271e3jPDNqFy5zam1w,228
5
+ linear_python_client/graphql/queries.py,sha256=3ckrYY4yTRSDtLHfahUOpZsezEfNntFjMk7Kz2KO2o0,7431
6
+ linear_python_client/models/__init__.py,sha256=zJM7GsvUwPuGibRwdugerSyJkTH8opWjVJD8yus6Rxk,2831
7
+ linear_python_client/models/entities.py,sha256=mE1ejvrXjhHFXnjU3ctlVAbmWL9TpLL2vz317ve5w1M,5450
8
+ linear_python_client/models/requests.py,sha256=Yss-zSo6gWWMqX4Qh1-P_n1sWbe176tL04LECnyASv4,7282
9
+ linear_python_client/models/responses.py,sha256=lLsJz35XruT0g4qnGF9g7sc_lNWcPNzy-QP5tW9tIUA,4830
10
+ linear_python_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ linear_python_client-0.1.0.dist-info/licenses/LICENSE,sha256=CcQjmF-Tw7VtkjYGvej9Df6Dz-AztG1aTRtMAiLoL7w,1066
12
+ linear_python_client-0.1.0.dist-info/WHEEL,sha256=wXwAVsgVaOZ_pwDFqQm5Rd6PID-Fc74nkLc8X8gHiDo,81
13
+ linear_python_client-0.1.0.dist-info/METADATA,sha256=t0dlnHz0hHBKvHfEawnH0cCgc00k_GHppLTdhBg0b0s,12355
14
+ linear_python_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.19
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HackerOne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.