sinatra-dev 0.1.0__tar.gz

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.
Files changed (51) hide show
  1. sinatra_dev-0.1.0/.gitignore +12 -0
  2. sinatra_dev-0.1.0/LICENSE +21 -0
  3. sinatra_dev-0.1.0/PKG-INFO +184 -0
  4. sinatra_dev-0.1.0/README.md +158 -0
  5. sinatra_dev-0.1.0/pyproject.toml +78 -0
  6. sinatra_dev-0.1.0/src/sinatra_dev/__init__.py +71 -0
  7. sinatra_dev-0.1.0/src/sinatra_dev/_client.py +391 -0
  8. sinatra_dev-0.1.0/src/sinatra_dev/_errors.py +53 -0
  9. sinatra_dev-0.1.0/src/sinatra_dev/_generated/__init__.py +8 -0
  10. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/__init__.py +1 -0
  11. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/me/__init__.py +1 -0
  12. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/me/me_get.py +156 -0
  13. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/me/me_revoke_token.py +173 -0
  14. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/messages/__init__.py +1 -0
  15. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/messages/messages_create.py +323 -0
  16. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/messages/messages_list.py +332 -0
  17. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/tasks/__init__.py +1 -0
  18. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/tasks/tasks_create.py +337 -0
  19. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/tasks/tasks_get.py +202 -0
  20. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/tasks/tasks_list.py +252 -0
  21. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/tasks/tasks_stop.py +231 -0
  22. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/tickets/__init__.py +1 -0
  23. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/tickets/tickets_delegate.py +273 -0
  24. sinatra_dev-0.1.0/src/sinatra_dev/_generated/api/tickets/tickets_search.py +265 -0
  25. sinatra_dev-0.1.0/src/sinatra_dev/_generated/client.py +280 -0
  26. sinatra_dev-0.1.0/src/sinatra_dev/_generated/errors.py +16 -0
  27. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/__init__.py +47 -0
  28. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/create_message_request.py +62 -0
  29. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/create_message_response.py +62 -0
  30. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/create_task_request.py +106 -0
  31. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/create_task_response.py +76 -0
  32. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/delegate_github_ticket.py +86 -0
  33. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/delegate_linear_ticket.py +78 -0
  34. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/delegate_ticket_response.py +62 -0
  35. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/error.py +168 -0
  36. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/me.py +86 -0
  37. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/me_user.py +98 -0
  38. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/message.py +113 -0
  39. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/message_list.py +91 -0
  40. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/pull_request.py +70 -0
  41. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/stop_task_response.py +62 -0
  42. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/task.py +201 -0
  43. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/task_list.py +91 -0
  44. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/ticket.py +147 -0
  45. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/ticket_search_result.py +97 -0
  46. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/ticket_search_result_github.py +77 -0
  47. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/ticket_search_result_linear.py +79 -0
  48. sinatra_dev-0.1.0/src/sinatra_dev/_generated/models/workspace.py +78 -0
  49. sinatra_dev-0.1.0/src/sinatra_dev/_generated/types.py +54 -0
  50. sinatra_dev-0.1.0/src/sinatra_dev/_retry.py +72 -0
  51. sinatra_dev-0.1.0/src/sinatra_dev/py.typed +0 -0
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .tools/
11
+ release-state/
12
+ scripts/prism/node_modules/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sinatra
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.
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.5
2
+ Name: sinatra-dev
3
+ Version: 0.1.0
4
+ Summary: Python client for the Sinatra API: start, follow and stop Sinatra tasks, and delegate Linear and GitHub issues.
5
+ Project-URL: Homepage, https://sinatra.dev
6
+ Project-URL: Documentation, https://docs.sinatra.dev/docs/api-reference
7
+ Project-URL: Repository, https://github.com/sinatra-dev/sinatra-python
8
+ Project-URL: Issues, https://github.com/sinatra-dev/sinatra-python/issues
9
+ Author-email: Sinatra <contact@sinatra.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-client,coding-agent,sdk,sinatra,sinatra.dev
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: attrs>=22.2.0
24
+ Requires-Dist: httpx<0.29.0,>=0.23.1
25
+ Description-Content-Type: text/markdown
26
+
27
+ # sinatra-dev
28
+
29
+ A Python client for the [Sinatra API](https://docs.sinatra.dev/docs/api-reference): start, follow and stop Sinatra tasks, and delegate Linear and GitHub issues.
30
+
31
+ It runs on Python 3.11 and later, is typed (`py.typed`), and depends only on `httpx` and `attrs`. The client is generated from the API's [OpenAPI spec](https://api.sinatra.dev/openapi.json) and released automatically when the spec changes.
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ pip install sinatra-dev
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ```python
42
+ import time
43
+
44
+ from sinatra_dev import Sinatra
45
+
46
+ # The turn is done when the task leaves these statuses (for example, it reaches replied or pr_open).
47
+ WORKING = {"pending", "investigating", "implementing", "rework"}
48
+
49
+ with Sinatra() as sinatra: # reads SINATRA_TOKEN
50
+ me = sinatra.me.get()
51
+ if not me.workspaces:
52
+ raise SystemExit("The token's user isn't in any workspace yet.")
53
+ workspace = me.workspaces[0]
54
+
55
+ task = sinatra.tasks.create(
56
+ workspace.id, "Fix the flaky login test", repo="acme/web", idempotency_key="fix-flaky-login-1"
57
+ ).task
58
+
59
+ after = None
60
+ while True:
61
+ status = sinatra.tasks.get(workspace.id, task.id).status
62
+ for m in sinatra.messages.list_all(workspace.id, task.id, after=after):
63
+ print(f"{m.role}: {m.body}")
64
+ after = m.id
65
+ if status not in WORKING:
66
+ break
67
+ time.sleep(3)
68
+ print(f"Task {task.id} is {status}.")
69
+ ```
70
+
71
+ The API has no streaming. To wait for a reply, read the conversation after the last entry you've seen, and check the task's `status`: the agent posts one reply per turn, and the turn is done when the task stops working. The loop reads the status before the conversation, so it never stops before printing the reply that ended the turn. An unknown status counts as done, so the loop can't wait forever on a status added after this was written. See [How replies work](https://docs.sinatra.dev/docs/cli-and-api#how-replies-work).
72
+
73
+ ## Authentication
74
+
75
+ Create a personal access token on the [Access tokens](https://app.sinatra.dev/tokens) page (**CLI & API** in the dashboard). It starts with `sntr_` and acts as you in every workspace you belong to. The client reads it from the `SINATRA_TOKEN` environment variable, or you can pass it:
76
+
77
+ ```python
78
+ sinatra = Sinatra(token=os.environ["MY_TOKEN"])
79
+ ```
80
+
81
+ The constructor raises `ValueError` if it finds no token. Other options:
82
+
83
+ - `base_url`: defaults to `https://api.sinatra.dev`.
84
+ - `timeout`: seconds each attempt may wait to connect, and for each chunk of the response, before httpx raises `httpx.TimeoutException`. Defaults to 60; `None` turns it off. An `httpx.Timeout` works too.
85
+ - `http_client`: an `httpx.Client` to send requests with, for example one with a proxy. The client doesn't close it.
86
+
87
+ Every method takes a `timeout` keyword that overrides the client's for that call:
88
+
89
+ ```python
90
+ sinatra.tasks.get(workspace_id, task_id, timeout=5)
91
+ ```
92
+
93
+ Use the client as a context manager, or call `sinatra.close()`, to release its connections.
94
+
95
+ ## Operations
96
+
97
+ The namespaces mirror the API's operation ids:
98
+
99
+ | Method | Request |
100
+ | --- | --- |
101
+ | `me.get()` | `GET /v1/me` |
102
+ | `me.revoke_token()` | `DELETE /v1/me/token` |
103
+ | `tasks.list(workspace_id, status=, limit=, cursor=)` | `GET /v1/workspaces/{workspaceId}/tasks` |
104
+ | `tasks.get(workspace_id, task_id)` | `GET /v1/workspaces/{workspaceId}/tasks/{taskId}` |
105
+ | `tasks.create(workspace_id, prompt, title=, repo=, idempotency_key=)` | `POST /v1/workspaces/{workspaceId}/tasks` |
106
+ | `tasks.stop(workspace_id, task_id)` | `POST /v1/workspaces/{workspaceId}/tasks/{taskId}/stop` |
107
+ | `messages.create(workspace_id, task_id, text, idempotency_key=)` | `POST /v1/workspaces/{workspaceId}/tasks/{taskId}/messages` |
108
+ | `messages.list(workspace_id, task_id, after=, limit=)` | `GET /v1/workspaces/{workspaceId}/tasks/{taskId}/messages` |
109
+ | `tickets.search(workspace_id, q=, repo=)` | `GET /v1/workspaces/{workspaceId}/tickets` |
110
+ | `tickets.delegate(workspace_id, source="linear", id=)` or `(workspace_id, source="github", repo=, number=)` | `POST /v1/workspaces/{workspaceId}/tickets/delegate` |
111
+
112
+ Responses are typed objects with snake_case attributes (`task.pull_request`, `page.next_cursor`). Fields the API added after this release are kept in each object's `additional_properties`.
113
+
114
+ `tasks.list_all` and `messages.list_all` page through everything for you:
115
+
116
+ ```python
117
+ for task in sinatra.tasks.list_all(workspace_id, status="pr_open"):
118
+ print(task.id, task.title)
119
+ ```
120
+
121
+ Fields such as `status`, `source`, `role`, `kind` and the error `code` are plain strings, and the API adds new values over time. Handle values you don't recognise.
122
+
123
+ ## Retries and idempotency
124
+
125
+ `tasks.create` and `messages.create` always send an `Idempotency-Key`. If you don't pass one, the client makes a random UUID. Every retry of a call sends the same key, so a retried start never starts a second task. Pass your own key to make a retry safe across process restarts too.
126
+
127
+ The client retries a request up to 3 times when it's safe to repeat:
128
+
129
+ - `429`: after the `Retry-After` the server sends, at most 60 seconds.
130
+ - `503`: after 0.5s, then 1s, then 2s.
131
+
132
+ GETs, `me.revoke_token`, `tasks.create` and `messages.create` retry on both. `tasks.stop` retries on `503` only. `tickets.delegate` takes no idempotency key and is never retried. Other errors, timeouts and network failures are never retried.
133
+
134
+ ## Errors
135
+
136
+ An error response raises a `SinatraError`:
137
+
138
+ ```python
139
+ from sinatra_dev import SinatraError
140
+
141
+ try:
142
+ sinatra.tasks.stop(workspace_id, task_id)
143
+ except SinatraError as err:
144
+ if err.code != "not_running": # not_running: the task had already stopped
145
+ raise
146
+ ```
147
+
148
+ | Attribute | |
149
+ | --- | --- |
150
+ | `status` | The HTTP status. |
151
+ | `code` | The API's `error` code, such as `capacity_full`. `http_error` when the response carried none, such as an HTML error page from a proxy. |
152
+ | `reason` | A sentence for a person. Don't branch on it. |
153
+ | `request_id` | The request's id. Include it when you contact support. |
154
+ | `body` | The response body: the parsed JSON, or the text. Refusals carry extra fields here, such as `billingUrl`. |
155
+ | `headers` | The response headers. |
156
+
157
+ Network failures and timeouts are not wrapped: they raise the `httpx.HTTPError` (for example `httpx.TimeoutException`) that httpx raised.
158
+
159
+ The [API reference](https://docs.sinatra.dev/docs/api-reference) lists every operation's error codes.
160
+
161
+ ## Versions
162
+
163
+ Releases follow semver. While the version is `0.x`, a breaking API change bumps the minor version and anything else bumps the patch. From `1.0`, breaking changes bump the major version and additions bump the minor.
164
+
165
+ ## Development
166
+
167
+ Needs [uv](https://docs.astral.sh/uv/), and Node 24 for the contract tests.
168
+
169
+ ```sh
170
+ uv sync
171
+ bash scripts/install-oasdiff.sh # installs the pinned oasdiff into .tools/bin
172
+ bash scripts/generate.sh # regenerates src/sinatra_dev/_generated from openapi.json
173
+ uv run ruff check . && uv run ruff format --check . && uv run mypy
174
+ uv run pytest # wrapper tests against a local stub server
175
+ bash scripts/contract.sh # every operation against a Prism mock of openapi.json
176
+ PATH="$PWD/.tools/bin:$PATH" bash scripts/next-version.test.sh
177
+ bash scripts/build.sh # sdist and wheel in dist/, build backend pinned by hash
178
+ ```
179
+
180
+ `src/sinatra_dev/_generated` is written by `openapi-python-client` and never edited by hand; CI fails if it differs from what `openapi.json` generates (`bash scripts/check-generated.sh`). `openapi.json` is updated by the release workflow. See [RELEASING.md](RELEASING.md).
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,158 @@
1
+ # sinatra-dev
2
+
3
+ A Python client for the [Sinatra API](https://docs.sinatra.dev/docs/api-reference): start, follow and stop Sinatra tasks, and delegate Linear and GitHub issues.
4
+
5
+ It runs on Python 3.11 and later, is typed (`py.typed`), and depends only on `httpx` and `attrs`. The client is generated from the API's [OpenAPI spec](https://api.sinatra.dev/openapi.json) and released automatically when the spec changes.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install sinatra-dev
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```python
16
+ import time
17
+
18
+ from sinatra_dev import Sinatra
19
+
20
+ # The turn is done when the task leaves these statuses (for example, it reaches replied or pr_open).
21
+ WORKING = {"pending", "investigating", "implementing", "rework"}
22
+
23
+ with Sinatra() as sinatra: # reads SINATRA_TOKEN
24
+ me = sinatra.me.get()
25
+ if not me.workspaces:
26
+ raise SystemExit("The token's user isn't in any workspace yet.")
27
+ workspace = me.workspaces[0]
28
+
29
+ task = sinatra.tasks.create(
30
+ workspace.id, "Fix the flaky login test", repo="acme/web", idempotency_key="fix-flaky-login-1"
31
+ ).task
32
+
33
+ after = None
34
+ while True:
35
+ status = sinatra.tasks.get(workspace.id, task.id).status
36
+ for m in sinatra.messages.list_all(workspace.id, task.id, after=after):
37
+ print(f"{m.role}: {m.body}")
38
+ after = m.id
39
+ if status not in WORKING:
40
+ break
41
+ time.sleep(3)
42
+ print(f"Task {task.id} is {status}.")
43
+ ```
44
+
45
+ The API has no streaming. To wait for a reply, read the conversation after the last entry you've seen, and check the task's `status`: the agent posts one reply per turn, and the turn is done when the task stops working. The loop reads the status before the conversation, so it never stops before printing the reply that ended the turn. An unknown status counts as done, so the loop can't wait forever on a status added after this was written. See [How replies work](https://docs.sinatra.dev/docs/cli-and-api#how-replies-work).
46
+
47
+ ## Authentication
48
+
49
+ Create a personal access token on the [Access tokens](https://app.sinatra.dev/tokens) page (**CLI & API** in the dashboard). It starts with `sntr_` and acts as you in every workspace you belong to. The client reads it from the `SINATRA_TOKEN` environment variable, or you can pass it:
50
+
51
+ ```python
52
+ sinatra = Sinatra(token=os.environ["MY_TOKEN"])
53
+ ```
54
+
55
+ The constructor raises `ValueError` if it finds no token. Other options:
56
+
57
+ - `base_url`: defaults to `https://api.sinatra.dev`.
58
+ - `timeout`: seconds each attempt may wait to connect, and for each chunk of the response, before httpx raises `httpx.TimeoutException`. Defaults to 60; `None` turns it off. An `httpx.Timeout` works too.
59
+ - `http_client`: an `httpx.Client` to send requests with, for example one with a proxy. The client doesn't close it.
60
+
61
+ Every method takes a `timeout` keyword that overrides the client's for that call:
62
+
63
+ ```python
64
+ sinatra.tasks.get(workspace_id, task_id, timeout=5)
65
+ ```
66
+
67
+ Use the client as a context manager, or call `sinatra.close()`, to release its connections.
68
+
69
+ ## Operations
70
+
71
+ The namespaces mirror the API's operation ids:
72
+
73
+ | Method | Request |
74
+ | --- | --- |
75
+ | `me.get()` | `GET /v1/me` |
76
+ | `me.revoke_token()` | `DELETE /v1/me/token` |
77
+ | `tasks.list(workspace_id, status=, limit=, cursor=)` | `GET /v1/workspaces/{workspaceId}/tasks` |
78
+ | `tasks.get(workspace_id, task_id)` | `GET /v1/workspaces/{workspaceId}/tasks/{taskId}` |
79
+ | `tasks.create(workspace_id, prompt, title=, repo=, idempotency_key=)` | `POST /v1/workspaces/{workspaceId}/tasks` |
80
+ | `tasks.stop(workspace_id, task_id)` | `POST /v1/workspaces/{workspaceId}/tasks/{taskId}/stop` |
81
+ | `messages.create(workspace_id, task_id, text, idempotency_key=)` | `POST /v1/workspaces/{workspaceId}/tasks/{taskId}/messages` |
82
+ | `messages.list(workspace_id, task_id, after=, limit=)` | `GET /v1/workspaces/{workspaceId}/tasks/{taskId}/messages` |
83
+ | `tickets.search(workspace_id, q=, repo=)` | `GET /v1/workspaces/{workspaceId}/tickets` |
84
+ | `tickets.delegate(workspace_id, source="linear", id=)` or `(workspace_id, source="github", repo=, number=)` | `POST /v1/workspaces/{workspaceId}/tickets/delegate` |
85
+
86
+ Responses are typed objects with snake_case attributes (`task.pull_request`, `page.next_cursor`). Fields the API added after this release are kept in each object's `additional_properties`.
87
+
88
+ `tasks.list_all` and `messages.list_all` page through everything for you:
89
+
90
+ ```python
91
+ for task in sinatra.tasks.list_all(workspace_id, status="pr_open"):
92
+ print(task.id, task.title)
93
+ ```
94
+
95
+ Fields such as `status`, `source`, `role`, `kind` and the error `code` are plain strings, and the API adds new values over time. Handle values you don't recognise.
96
+
97
+ ## Retries and idempotency
98
+
99
+ `tasks.create` and `messages.create` always send an `Idempotency-Key`. If you don't pass one, the client makes a random UUID. Every retry of a call sends the same key, so a retried start never starts a second task. Pass your own key to make a retry safe across process restarts too.
100
+
101
+ The client retries a request up to 3 times when it's safe to repeat:
102
+
103
+ - `429`: after the `Retry-After` the server sends, at most 60 seconds.
104
+ - `503`: after 0.5s, then 1s, then 2s.
105
+
106
+ GETs, `me.revoke_token`, `tasks.create` and `messages.create` retry on both. `tasks.stop` retries on `503` only. `tickets.delegate` takes no idempotency key and is never retried. Other errors, timeouts and network failures are never retried.
107
+
108
+ ## Errors
109
+
110
+ An error response raises a `SinatraError`:
111
+
112
+ ```python
113
+ from sinatra_dev import SinatraError
114
+
115
+ try:
116
+ sinatra.tasks.stop(workspace_id, task_id)
117
+ except SinatraError as err:
118
+ if err.code != "not_running": # not_running: the task had already stopped
119
+ raise
120
+ ```
121
+
122
+ | Attribute | |
123
+ | --- | --- |
124
+ | `status` | The HTTP status. |
125
+ | `code` | The API's `error` code, such as `capacity_full`. `http_error` when the response carried none, such as an HTML error page from a proxy. |
126
+ | `reason` | A sentence for a person. Don't branch on it. |
127
+ | `request_id` | The request's id. Include it when you contact support. |
128
+ | `body` | The response body: the parsed JSON, or the text. Refusals carry extra fields here, such as `billingUrl`. |
129
+ | `headers` | The response headers. |
130
+
131
+ Network failures and timeouts are not wrapped: they raise the `httpx.HTTPError` (for example `httpx.TimeoutException`) that httpx raised.
132
+
133
+ The [API reference](https://docs.sinatra.dev/docs/api-reference) lists every operation's error codes.
134
+
135
+ ## Versions
136
+
137
+ Releases follow semver. While the version is `0.x`, a breaking API change bumps the minor version and anything else bumps the patch. From `1.0`, breaking changes bump the major version and additions bump the minor.
138
+
139
+ ## Development
140
+
141
+ Needs [uv](https://docs.astral.sh/uv/), and Node 24 for the contract tests.
142
+
143
+ ```sh
144
+ uv sync
145
+ bash scripts/install-oasdiff.sh # installs the pinned oasdiff into .tools/bin
146
+ bash scripts/generate.sh # regenerates src/sinatra_dev/_generated from openapi.json
147
+ uv run ruff check . && uv run ruff format --check . && uv run mypy
148
+ uv run pytest # wrapper tests against a local stub server
149
+ bash scripts/contract.sh # every operation against a Prism mock of openapi.json
150
+ PATH="$PWD/.tools/bin:$PATH" bash scripts/next-version.test.sh
151
+ bash scripts/build.sh # sdist and wheel in dist/, build backend pinned by hash
152
+ ```
153
+
154
+ `src/sinatra_dev/_generated` is written by `openapi-python-client` and never edited by hand; CI fails if it differs from what `openapi.json` generates (`bash scripts/check-generated.sh`). `openapi.json` is updated by the release workflow. See [RELEASING.md](RELEASING.md).
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,78 @@
1
+ [project]
2
+ name = "sinatra-dev"
3
+ version = "0.1.0"
4
+ description = "Python client for the Sinatra API: start, follow and stop Sinatra tasks, and delegate Linear and GitHub issues."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ authors = [{ name = "Sinatra", email = "contact@sinatra.dev" }]
10
+ keywords = ["sinatra", "sinatra.dev", "sdk", "api-client", "coding-agent"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Typing :: Typed",
21
+ ]
22
+ # What the generated code in src/sinatra_dev/_generated needs (the generator's
23
+ # own requirements for it), and nothing else.
24
+ dependencies = [
25
+ "httpx>=0.23.1,<0.29.0",
26
+ "attrs>=22.2.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://sinatra.dev"
31
+ Documentation = "https://docs.sinatra.dev/docs/api-reference"
32
+ Repository = "https://github.com/sinatra-dev/sinatra-python"
33
+ Issues = "https://github.com/sinatra-dev/sinatra-python/issues"
34
+
35
+ [dependency-groups]
36
+ # Exact pins: the generator (and the ruff its post-hooks run) decide what
37
+ # src/sinatra_dev/_generated contains, so CI's drift check depends on them.
38
+ dev = [
39
+ "mypy==2.4.0",
40
+ "openapi-python-client==0.29.1",
41
+ "pytest==9.1.1",
42
+ "ruff==0.17.1",
43
+ ]
44
+
45
+ [build-system]
46
+ requires = ["hatchling==1.32.4"]
47
+ build-backend = "hatchling.build"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/sinatra_dev"]
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ include = ["src/sinatra_dev", "README.md", "LICENSE"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests/unit"]
57
+ addopts = ["-ra"]
58
+ filterwarnings = ["error"]
59
+
60
+ [tool.ruff]
61
+ line-length = 120
62
+ target-version = "py311"
63
+ extend-exclude = ["src/sinatra_dev/_generated"]
64
+
65
+ [tool.ruff.lint]
66
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
67
+
68
+ [tool.mypy]
69
+ python_version = "3.11"
70
+ strict = true
71
+ files = ["src/sinatra_dev", "tests"]
72
+
73
+ # The generated httpx Client/AuthenticatedClient wrappers don't pass strict
74
+ # mypy, and the wrapper never uses them (it sends requests itself). The rest
75
+ # of the generated code, which the wrapper does use, is checked strictly.
76
+ [[tool.mypy.overrides]]
77
+ module = "sinatra_dev._generated.client"
78
+ ignore_errors = true
@@ -0,0 +1,71 @@
1
+ """A Python client for the Sinatra API.
2
+
3
+ from sinatra_dev import Sinatra
4
+
5
+ with Sinatra() as sinatra: # reads SINATRA_TOKEN
6
+ me = sinatra.me.get()
7
+
8
+ Reference: https://docs.sinatra.dev/docs/api-reference
9
+ """
10
+
11
+ from ._client import (
12
+ DEFAULT,
13
+ DEFAULT_BASE_URL,
14
+ DEFAULT_TIMEOUT,
15
+ MeApi,
16
+ MessagesApi,
17
+ Sinatra,
18
+ TasksApi,
19
+ TicketsApi,
20
+ Timeout,
21
+ )
22
+ from ._errors import SinatraError
23
+ from ._generated.models import (
24
+ CreateMessageResponse,
25
+ CreateTaskResponse,
26
+ DelegateTicketResponse,
27
+ Me,
28
+ Message,
29
+ MessageList,
30
+ MeUser,
31
+ PullRequest,
32
+ StopTaskResponse,
33
+ Task,
34
+ TaskList,
35
+ Ticket,
36
+ TicketSearchResult,
37
+ TicketSearchResultGithub,
38
+ TicketSearchResultLinear,
39
+ Workspace,
40
+ )
41
+ from ._generated.models import Error as ErrorBody
42
+
43
+ __all__ = [
44
+ "DEFAULT",
45
+ "DEFAULT_BASE_URL",
46
+ "DEFAULT_TIMEOUT",
47
+ "CreateMessageResponse",
48
+ "CreateTaskResponse",
49
+ "DelegateTicketResponse",
50
+ "ErrorBody",
51
+ "Me",
52
+ "MeApi",
53
+ "MeUser",
54
+ "Message",
55
+ "MessageList",
56
+ "MessagesApi",
57
+ "PullRequest",
58
+ "Sinatra",
59
+ "SinatraError",
60
+ "StopTaskResponse",
61
+ "Task",
62
+ "TaskList",
63
+ "TasksApi",
64
+ "Ticket",
65
+ "TicketSearchResult",
66
+ "TicketSearchResultGithub",
67
+ "TicketSearchResultLinear",
68
+ "TicketsApi",
69
+ "Timeout",
70
+ "Workspace",
71
+ ]