1- # linear-python
1+ # linear-python-client
22
33A small, pragmatic synchronous Python client for the [ Linear] ( https://linear.app )
44GraphQL API. Linear's official SDK is TypeScript-only — this package gives Python
@@ -13,15 +13,10 @@ Built against the [Linear developer docs](https://linear.app/developers).
1313
1414## Installation
1515
16- The package is distributed as assets on its [ GitHub Releases] ( https://github.com/Hacker0x01/linear-python/releases )
17- (not on PyPI). Install the wheel from a release, or straight from a tag:
18-
1916``` sh
20- # from a release wheel
21- uv pip install https://github.com/Hacker0x01/linear-python/releases/download/v0.1.0/linear_python-0.1.0-py3-none-any.whl
22-
23- # or from a tag (builds from source)
24- uv pip install " git+https://github.com/Hacker0x01/linear-python@v0.1.0"
17+ uv add linear-python-client
18+ # or
19+ pip install linear-python-client
2520```
2621
2722Or for local development of this repo:
@@ -37,7 +32,7 @@ Requires Python 3.14+.
3732The client accepts either a personal API key or an OAuth 2.0 access token.
3833
3934``` python
40- from linear_python import LinearClient
35+ from linear_python_client import LinearClient
4136
4237# Personal API key (sent as the raw `Authorization` header value)
4338client = LinearClient(api_key = " lin_api_..." )
@@ -61,7 +56,7 @@ with LinearClient() as client:
6156Each method takes a ` *Request ` and returns a ` *Response ` :
6257
6358``` python
64- from linear_python import (
59+ from linear_python_client import (
6560 LinearClient,
6661 IssueRequest,
6762 IssueCreateRequest,
@@ -111,7 +106,7 @@ directly to Linear's [filtering syntax](https://linear.app/developers/filtering)
111106return a ` *Response ` that holds ` .nodes ` and ` .page_info ` (and is iterable).
112107
113108``` python
114- from linear_python import IssuesRequest
109+ from linear_python_client import IssuesRequest
115110
116111# First 20 high-priority issues assigned to a specific user
117112resp = client.issues(
@@ -141,6 +136,32 @@ for issue in client.paginate(client.issues, IssuesRequest(filter={"state": {"typ
141136` paginate() ` works with any list method (` client.issues ` , ` client.teams ` ,
142137` client.projects ` , ` client.comments ` , ` client.users ` , …) and its matching request.
143138
139+ ## Labels, status & full details
140+
141+ ``` python
142+ from linear_python_client import (
143+ IssueAddLabelRequest,
144+ IssueRemoveLabelRequest,
145+ IssueSetStateRequest,
146+ FindWorkflowStateRequest,
147+ IssueRequest,
148+ )
149+
150+ # Add / remove a single label without disturbing the issue's other labels
151+ client.add_label(IssueAddLabelRequest(id = issue_id, label_id = label_id))
152+ client.remove_label(IssueRemoveLabelRequest(id = issue_id, label_id = label_id))
153+
154+ # Update status: resolve a state by name, then set it
155+ state = client.find_workflow_state(FindWorkflowStateRequest(team_id = team_id, name = " In Progress" )).state
156+ client.set_issue_state(IssueSetStateRequest(id = issue_id, state_id = state.id))
157+
158+ # Full details: comments, attachments, project, cycle, parent, sub-issues, subscribers, relations
159+ detail = client.issue_details(IssueRequest(id = " ENG-123" )).issue
160+ print (detail.state.name, len (detail.comments), len (detail.attachments))
161+ for child in detail.children:
162+ print (" sub-issue:" , child.identifier, child.title)
163+ ```
164+
144165## Escape hatch: raw GraphQL
145166
146167Anything not covered by a convenience method can be run directly. ` execute() `
@@ -170,7 +191,7 @@ All exceptions subclass `LinearError`:
170191| ` LinearNetworkError ` | The request never produced a usable response |
171192
172193``` python
173- from linear_python import LinearClient, LinearRateLimitError, IssuesRequest
194+ from linear_python_client import LinearClient, LinearRateLimitError, IssuesRequest
174195
175196try :
176197 client.issues(IssuesRequest(first = 100 ))
@@ -190,16 +211,21 @@ Each method maps a `*Request` to a `*Response`:
190211| ` team(...) ` | ` TeamRequest ` | ` TeamResponse ` |
191212| ` teams(...) ` | ` TeamsRequest ` | ` TeamsResponse ` |
192213| ` issue(...) ` | ` IssueRequest ` | ` IssueResponse ` |
214+ | ` issue_details(...) ` | ` IssueRequest ` | ` IssueDetailsResponse ` |
193215| ` issues(...) ` | ` IssuesRequest ` | ` IssuesResponse ` |
194216| ` create_issue(...) ` | ` IssueCreateRequest ` | ` CreateIssueResponse ` |
195217| ` update_issue(...) ` | ` IssueUpdateRequest ` | ` UpdateIssueResponse ` |
196218| ` archive_issue(...) ` | ` IssueArchiveRequest ` | ` ArchiveIssueResponse ` |
219+ | ` add_label(...) ` | ` IssueAddLabelRequest ` | ` AddLabelResponse ` |
220+ | ` remove_label(...) ` | ` IssueRemoveLabelRequest ` | ` RemoveLabelResponse ` |
221+ | ` set_issue_state(...) ` | ` IssueSetStateRequest ` | ` UpdateIssueResponse ` |
197222| ` project(...) ` | ` ProjectRequest ` | ` ProjectResponse ` |
198223| ` projects(...) ` | ` ProjectsRequest ` | ` ProjectsResponse ` |
199224| ` comment(...) ` | ` CommentRequest ` | ` CommentResponse ` |
200225| ` comments(...) ` | ` CommentsRequest ` | ` CommentsResponse ` |
201226| ` create_comment(...) ` | ` CommentCreateRequest ` | ` CreateCommentResponse ` |
202227| ` workflow_states(...) ` | ` WorkflowStatesRequest ` | ` WorkflowStatesResponse ` |
228+ | ` find_workflow_state(...) ` | ` FindWorkflowStateRequest ` | ` WorkflowStateResponse ` |
203229| ` issue_labels(...) ` | ` IssueLabelsRequest ` | ` IssueLabelsResponse ` |
204230| ` execute(query, variables) ` | – | ` dict ` |
205231| ` paginate(method, request) ` | a ` *Request ` | iterator of nodes |
@@ -222,6 +248,22 @@ needed. An optional live smoke test runs only when `LINEAR_API_KEY` is set.
222248prints after each run — add ` --cov-report=html ` for an annotated HTML report in
223249` htmlcov/ ` .
224250
251+ ### Live smoke test
252+
253+ ` scripts/smoke_test.py ` exercises ** every** client method against the real Linear API
254+ and, after each mutation, re-pulls the issue to confirm the change landed (create →
255+ update → set status → add/remove label → comment → full details). It creates one
256+ clearly-labelled test issue and archives it at the end, so it cleans up after itself.
257+
258+ ``` sh
259+ LINEAR_API_KEY=lin_api_... uv run python scripts/smoke_test.py
260+ # optionally pin the team (defaults to the first one):
261+ LINEAR_API_KEY=... LINEAR_TEAM_ID=< uuid> uv run python scripts/smoke_test.py
262+ ```
263+
264+ It prints a ✓/✗ per check and exits non-zero if any fail. Because it writes to your
265+ workspace, it's a manual script — it is not part of ` pytest ` .
266+
225267### Building & releasing
226268
227269Build the distributions locally with uv:
@@ -234,23 +276,19 @@ uvx twine check dist/* # validate metadata / README rendering
234276Releases are automated by [ ` .github/workflows/publish.yml ` ] ( .github/workflows/publish.yml ) .
235277On every push and PR it lints, tests (with the coverage gate), builds the sdist + wheel,
236278validates the metadata, and smoke-tests that the wheel installs and imports. When a
237- ** GitHub Release is published** , it additionally attaches the built sdist + wheel as
238- assets on that release .
279+ ** GitHub Release is published** , it additionally publishes the build to PyPI — after
280+ which ` pip install linear-python-client ` and ` uv add linear-python-client ` work .
239281
240- To cut a release: bump ` version ` in ` pyproject.toml ` , then create a matching GitHub
241- Release (e.g. tag ` v0.1.0 ` ). The workflow uploads ` linear_python-<version>.tar.gz ` and
242- ` linear_python-<version>-py3-none-any.whl ` to the release.
243-
244- Install from a release asset (the package is not published to PyPI):
282+ Publishing uses [ PyPI Trusted Publishing] ( https://docs.pypi.org/trusted-publishers/ )
283+ (OIDC), so no API token or secret is stored. One-time setup:
245284
246- ``` sh
247- # wheel
248- uv pip install https://github.com/Hacker0x01/linear-python/releases/download/v0.1.0/linear_python-0.1.0-py3-none-any.whl
249- pip install https://github.com/Hacker0x01/linear-python/releases/download/v0.1.0/linear_python-0.1.0-py3-none-any.whl
285+ 1 . On PyPI, add a trusted publisher for the project pointing at this repo, workflow
286+ ` publish.yml ` , and environment ` pypi ` .
287+ 2 . In the repo, create a ` pypi ` [ environment ] ( https://docs. github.com/actions/deployment/targeting-different-environments/using-environments-for-deployment )
288+ (Settings → Environments).
250289
251- # or straight from a tag (builds from source)
252- uv pip install " git+https://github.com/Hacker0x01/linear-python@v0.1.0"
253- ```
290+ To cut a release: bump ` version ` in ` pyproject.toml ` , then create a matching GitHub
291+ Release (e.g. tag ` v0.1.0 ` ) — the workflow builds and uploads it to PyPI.
254292
255293> [ !NOTE]
256294> ` requires-python ` is ` >=3.14 ` , so installs require Python 3.14+.
0 commit comments