Skip to content

Commit 7841ce5

Browse files
Rename package to linear-python-client and publish to PyPI
1 parent 80da710 commit 7841ce5

30 files changed

Lines changed: 2308 additions & 177 deletions

.github/workflows/publish.yml

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
name: Build & Release
1+
name: Publish
22

33
on:
4-
# Build + verify on every push/PR; attach artifacts to the GitHub Release when one is published.
4+
# Build + verify on every push/PR; publish to PyPI when a GitHub Release is published.
55
push:
66
branches: [main]
77
pull_request:
@@ -46,31 +46,33 @@ jobs:
4646
run: |
4747
uv venv /tmp/install-check
4848
uv pip install --python /tmp/install-check dist/*.whl
49-
/tmp/install-check/bin/python -c "import linear_python; print('installed', linear_python.__version__)"
49+
/tmp/install-check/bin/python -c "import linear_python_client; print('installed', linear_python_client.__version__)"
5050
5151
- name: Upload distribution artifacts
5252
uses: actions/upload-artifact@v4
5353
with:
5454
name: dist
5555
path: dist/
5656

57-
release-assets:
57+
publish:
5858
needs: [test, build]
5959
if: github.event_name == 'release'
6060
runs-on: ubuntu-latest
61+
environment:
62+
name: pypi
63+
url: https://pypi.org/p/linear-python-client
6164
permissions:
62-
# Required to attach assets to the GitHub Release.
63-
contents: write
65+
# Required for PyPI Trusted Publishing (OIDC); no API token needed.
66+
id-token: write
6467
steps:
68+
- name: Install uv
69+
uses: astral-sh/setup-uv@v5
70+
6571
- name: Download distribution artifacts
6672
uses: actions/download-artifact@v4
6773
with:
6874
name: dist
6975
path: dist/
7076

71-
- name: Attach sdist + wheel to the GitHub Release
72-
env:
73-
GH_TOKEN: ${{ github.token }}
74-
run: >
75-
gh release upload "${{ github.event.release.tag_name }}" dist/*
76-
--repo "${{ github.repository }}" --clobber
77+
- name: Publish to PyPI
78+
run: uv publish --trusted-publishing always

README.md

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# linear-python
1+
# linear-python-client
22

33
A small, pragmatic synchronous Python client for the [Linear](https://linear.app)
44
GraphQL 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

2722
Or for local development of this repo:
@@ -37,7 +32,7 @@ Requires Python 3.14+.
3732
The 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)
4338
client = LinearClient(api_key="lin_api_...")
@@ -61,7 +56,7 @@ with LinearClient() as client:
6156
Each 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)
111106
return 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
117112
resp = 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

146167
Anything 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

175196
try:
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.
222248
prints 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

227269
Build the distributions locally with uv:
@@ -234,23 +276,19 @@ uvx twine check dist/* # validate metadata / README rendering
234276
Releases are automated by [`.github/workflows/publish.yml`](.github/workflows/publish.yml).
235277
On every push and PR it lints, tests (with the coverage gate), builds the sdist + wheel,
236278
validates 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+.

docs/api/client.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Client
22

3-
::: linear_python.client.LinearClient
3+
::: linear_python_client.client.LinearClient

docs/api/errors.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Errors
22

33
Every exception raised by the client subclasses
4-
[`LinearError`][linear_python.errors.LinearError].
4+
[`LinearError`][linear_python_client.errors.LinearError].
55

6-
::: linear_python.errors
6+
::: linear_python_client.errors
77
options:
88
show_root_heading: false
99
show_root_toc_entry: false

docs/api/models.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The Pydantic entity models nested inside responses. Fields are snake_case with
44
camelCase aliases, and everything is optional, so only the fields a query actually
55
requested are populated.
66

7-
::: linear_python.models.entities
7+
::: linear_python_client.models.entities
88
options:
99
show_root_heading: false
1010
show_root_toc_entry: false
@@ -14,7 +14,11 @@ requested are populated.
1414
- User
1515
- Team
1616
- Issue
17+
- IssueDetail
1718
- Project
1819
- Comment
1920
- WorkflowState
2021
- IssueLabel
22+
- Attachment
23+
- Cycle
24+
- IssueRelation

docs/api/requests.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Requests
22

3-
The typed input models. Each [`LinearClient`][linear_python.client.LinearClient]
3+
The typed input models. Each [`LinearClient`][linear_python_client.client.LinearClient]
44
method takes exactly one of these.
55

6-
::: linear_python.models.requests
6+
::: linear_python_client.models.requests
77
options:
88
show_root_heading: false
99
show_root_toc_entry: false
@@ -24,4 +24,8 @@ method takes exactly one of these.
2424
- IssueCreateRequest
2525
- IssueUpdateRequest
2626
- IssueArchiveRequest
27+
- IssueAddLabelRequest
28+
- IssueRemoveLabelRequest
29+
- IssueSetStateRequest
30+
- FindWorkflowStateRequest
2731
- CommentCreateRequest

docs/api/responses.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Responses
22

33
The typed result models returned by each
4-
[`LinearClient`][linear_python.client.LinearClient] method.
4+
[`LinearClient`][linear_python_client.client.LinearClient] method.
55

6-
::: linear_python.models.responses
6+
::: linear_python_client.models.responses
77
options:
88
show_root_heading: false
99
show_root_toc_entry: false
@@ -15,14 +15,18 @@ The typed result models returned by each
1515
- TeamResponse
1616
- TeamsResponse
1717
- IssueResponse
18+
- IssueDetailsResponse
1819
- IssuesResponse
1920
- ProjectResponse
2021
- ProjectsResponse
2122
- CommentResponse
2223
- CommentsResponse
24+
- WorkflowStateResponse
2325
- WorkflowStatesResponse
2426
- IssueLabelsResponse
2527
- CreateIssueResponse
2628
- UpdateIssueResponse
2729
- ArchiveIssueResponse
30+
- AddLabelResponse
31+
- RemoveLabelResponse
2832
- CreateCommentResponse

docs/getting-started.md

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,10 @@
22

33
## Install
44

5-
The package is distributed as assets on its
6-
[GitHub Releases](https://github.com/Hacker0x01/linear-python/releases) (not on
7-
PyPI):
8-
95
```sh
10-
# from a release wheel
11-
uv pip install https://github.com/Hacker0x01/linear-python/releases/download/v0.1.0/linear_python-0.1.0-py3-none-any.whl
12-
13-
# or from a tag (builds from source)
14-
uv pip install "git+https://github.com/Hacker0x01/linear-python@v0.1.0"
6+
uv add linear-python-client
7+
# or
8+
pip install linear-python-client
159
```
1610

1711
Or, working inside a clone of the repository:
@@ -39,15 +33,15 @@ You can authenticate in two ways:
3933
=== "API key"
4034

4135
```python
42-
from linear_python import LinearClient
36+
from linear_python_client import LinearClient
4337

4438
client = LinearClient(api_key="lin_api_...")
4539
```
4640

4741
=== "OAuth token"
4842

4943
```python
50-
from linear_python import LinearClient
44+
from linear_python_client import LinearClient
5145

5246
client = LinearClient(access_token="...")
5347
```
@@ -56,7 +50,7 @@ You can authenticate in two ways:
5650

5751
```python
5852
# Reads LINEAR_API_KEY from the environment.
59-
from linear_python import LinearClient
53+
from linear_python_client import LinearClient
6054

6155
client = LinearClient()
6256
```
@@ -80,7 +74,7 @@ with LinearClient() as client:
8074
```
8175

8276
If the credentials are wrong you'll get a
83-
[`LinearAuthenticationError`][linear_python.LinearAuthenticationError]. See
77+
[`LinearAuthenticationError`][linear_python_client.LinearAuthenticationError]. See
8478
[Error handling](usage.md#error-handling) for the full list.
8579

8680
Next: the [Usage guide](usage.md).

0 commit comments

Comments
 (0)