sinatra-dev 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.
- sinatra_dev/__init__.py +71 -0
- sinatra_dev/_client.py +391 -0
- sinatra_dev/_errors.py +53 -0
- sinatra_dev/_generated/__init__.py +8 -0
- sinatra_dev/_generated/api/__init__.py +1 -0
- sinatra_dev/_generated/api/me/__init__.py +1 -0
- sinatra_dev/_generated/api/me/me_get.py +156 -0
- sinatra_dev/_generated/api/me/me_revoke_token.py +173 -0
- sinatra_dev/_generated/api/messages/__init__.py +1 -0
- sinatra_dev/_generated/api/messages/messages_create.py +323 -0
- sinatra_dev/_generated/api/messages/messages_list.py +332 -0
- sinatra_dev/_generated/api/tasks/__init__.py +1 -0
- sinatra_dev/_generated/api/tasks/tasks_create.py +337 -0
- sinatra_dev/_generated/api/tasks/tasks_get.py +202 -0
- sinatra_dev/_generated/api/tasks/tasks_list.py +252 -0
- sinatra_dev/_generated/api/tasks/tasks_stop.py +231 -0
- sinatra_dev/_generated/api/tickets/__init__.py +1 -0
- sinatra_dev/_generated/api/tickets/tickets_delegate.py +273 -0
- sinatra_dev/_generated/api/tickets/tickets_search.py +265 -0
- sinatra_dev/_generated/client.py +280 -0
- sinatra_dev/_generated/errors.py +16 -0
- sinatra_dev/_generated/models/__init__.py +47 -0
- sinatra_dev/_generated/models/create_message_request.py +62 -0
- sinatra_dev/_generated/models/create_message_response.py +62 -0
- sinatra_dev/_generated/models/create_task_request.py +106 -0
- sinatra_dev/_generated/models/create_task_response.py +76 -0
- sinatra_dev/_generated/models/delegate_github_ticket.py +86 -0
- sinatra_dev/_generated/models/delegate_linear_ticket.py +78 -0
- sinatra_dev/_generated/models/delegate_ticket_response.py +62 -0
- sinatra_dev/_generated/models/error.py +168 -0
- sinatra_dev/_generated/models/me.py +86 -0
- sinatra_dev/_generated/models/me_user.py +98 -0
- sinatra_dev/_generated/models/message.py +113 -0
- sinatra_dev/_generated/models/message_list.py +91 -0
- sinatra_dev/_generated/models/pull_request.py +70 -0
- sinatra_dev/_generated/models/stop_task_response.py +62 -0
- sinatra_dev/_generated/models/task.py +201 -0
- sinatra_dev/_generated/models/task_list.py +91 -0
- sinatra_dev/_generated/models/ticket.py +147 -0
- sinatra_dev/_generated/models/ticket_search_result.py +97 -0
- sinatra_dev/_generated/models/ticket_search_result_github.py +77 -0
- sinatra_dev/_generated/models/ticket_search_result_linear.py +79 -0
- sinatra_dev/_generated/models/workspace.py +78 -0
- sinatra_dev/_generated/types.py +54 -0
- sinatra_dev/_retry.py +72 -0
- sinatra_dev/py.typed +0 -0
- sinatra_dev-0.1.0.dist-info/METADATA +184 -0
- sinatra_dev-0.1.0.dist-info/RECORD +50 -0
- sinatra_dev-0.1.0.dist-info/WHEEL +4 -0
- sinatra_dev-0.1.0.dist-info/licenses/LICENSE +21 -0
sinatra_dev/__init__.py
ADDED
|
@@ -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
|
+
]
|
sinatra_dev/_client.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""The Sinatra client: transport, retries and timeouts around the generated code.
|
|
2
|
+
|
|
3
|
+
The generated package (``_generated``) builds each request (method, encoded
|
|
4
|
+
path, query, headers, JSON body) and decodes each response into its attrs
|
|
5
|
+
models. This module owns the ``httpx.Client`` and sends every request itself,
|
|
6
|
+
so it can retry with the same request and apply a timeout per call, which the
|
|
7
|
+
generated ``sync_detailed`` functions can't.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import uuid
|
|
14
|
+
from collections.abc import Callable, Iterator
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
from types import TracebackType
|
|
17
|
+
from typing import Any, Final, Literal, TypeVar
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from . import _retry
|
|
22
|
+
from ._errors import SinatraError
|
|
23
|
+
from ._generated.api.me import me_get, me_revoke_token
|
|
24
|
+
from ._generated.api.messages import messages_create, messages_list
|
|
25
|
+
from ._generated.api.tasks import tasks_create, tasks_get, tasks_list, tasks_stop
|
|
26
|
+
from ._generated.api.tickets import tickets_delegate, tickets_search
|
|
27
|
+
from ._generated.models import (
|
|
28
|
+
CreateMessageRequest,
|
|
29
|
+
CreateMessageResponse,
|
|
30
|
+
CreateTaskRequest,
|
|
31
|
+
CreateTaskResponse,
|
|
32
|
+
DelegateGithubTicket,
|
|
33
|
+
DelegateLinearTicket,
|
|
34
|
+
DelegateTicketResponse,
|
|
35
|
+
Me,
|
|
36
|
+
Message,
|
|
37
|
+
MessageList,
|
|
38
|
+
StopTaskResponse,
|
|
39
|
+
Task,
|
|
40
|
+
TaskList,
|
|
41
|
+
TicketSearchResult,
|
|
42
|
+
)
|
|
43
|
+
from ._generated.types import UNSET, Unset
|
|
44
|
+
|
|
45
|
+
DEFAULT_BASE_URL: Final = "https://api.sinatra.dev"
|
|
46
|
+
DEFAULT_TIMEOUT: Final = 60.0
|
|
47
|
+
"""Seconds, as an httpx timeout: for connecting, and for each wait for data."""
|
|
48
|
+
|
|
49
|
+
Timeout = float | httpx.Timeout | None
|
|
50
|
+
"""Seconds (or an ``httpx.Timeout``); ``None`` for no timeout."""
|
|
51
|
+
|
|
52
|
+
T = TypeVar("T")
|
|
53
|
+
|
|
54
|
+
# Every key of a generated `_get_kwargs()` that `_Transport.send` passes on.
|
|
55
|
+
_REQUEST_KEYS: Final = frozenset({"method", "url", "params", "json", "headers"})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Default:
|
|
59
|
+
"""The type of :data:`DEFAULT`: "use the client's timeout"."""
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return "DEFAULT"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
DEFAULT: Final = _Default()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _user_agent() -> str:
|
|
69
|
+
try:
|
|
70
|
+
return f"sinatra-dev-python/{version('sinatra-dev')}"
|
|
71
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
72
|
+
return "sinatra-dev-python"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _unset(value: T | None) -> T | Unset:
|
|
76
|
+
return UNSET if value is None else value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Sinatra:
|
|
80
|
+
"""A client for the Sinatra API.
|
|
81
|
+
|
|
82
|
+
Its namespaces mirror the API's operations: ``me.get`` is ``GET /v1/me``,
|
|
83
|
+
``tasks.create`` is ``POST /v1/workspaces/{workspaceId}/tasks``.
|
|
84
|
+
|
|
85
|
+
Requests that are safe to repeat (GETs, the token DELETE, and starts and
|
|
86
|
+
messages, which always carry an ``Idempotency-Key``) are retried up to 3
|
|
87
|
+
times on ``429`` and ``503``. ``tasks.stop`` is retried on ``503`` only.
|
|
88
|
+
Error responses raise :class:`SinatraError`.
|
|
89
|
+
|
|
90
|
+
Use it as a context manager, or call :meth:`close`, to release its
|
|
91
|
+
connections.
|
|
92
|
+
|
|
93
|
+
Reference: https://docs.sinatra.dev/docs/api-reference
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
token: str | None = None,
|
|
100
|
+
base_url: str | None = None,
|
|
101
|
+
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
102
|
+
http_client: httpx.Client | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Args:
|
|
106
|
+
token: An API token (``sntr_...``). Defaults to the
|
|
107
|
+
``SINATRA_TOKEN`` environment variable.
|
|
108
|
+
base_url: Defaults to ``https://api.sinatra.dev``.
|
|
109
|
+
timeout: Seconds each attempt may wait to connect, and for each
|
|
110
|
+
chunk of the response, before httpx raises
|
|
111
|
+
``httpx.TimeoutException``. Defaults to 60; ``None`` for no
|
|
112
|
+
timeout. A call can override it with its ``timeout`` argument.
|
|
113
|
+
http_client: An ``httpx.Client`` to send requests with, for
|
|
114
|
+
example one with a proxy. The client doesn't close it.
|
|
115
|
+
"""
|
|
116
|
+
token = token or os.environ.get("SINATRA_TOKEN") or None
|
|
117
|
+
if not token:
|
|
118
|
+
raise ValueError("No Sinatra API token: pass token=... or set SINATRA_TOKEN.")
|
|
119
|
+
self._transport = _Transport(
|
|
120
|
+
http=http_client or httpx.Client(),
|
|
121
|
+
owns_http=http_client is None,
|
|
122
|
+
base_url=(base_url or DEFAULT_BASE_URL).rstrip("/"),
|
|
123
|
+
headers={"authorization": f"Bearer {token}", "user-agent": _user_agent()},
|
|
124
|
+
timeout=timeout,
|
|
125
|
+
)
|
|
126
|
+
self.me: Final = MeApi(self._transport)
|
|
127
|
+
self.tasks: Final = TasksApi(self._transport)
|
|
128
|
+
self.messages: Final = MessagesApi(self._transport)
|
|
129
|
+
self.tickets: Final = TicketsApi(self._transport)
|
|
130
|
+
|
|
131
|
+
def close(self) -> None:
|
|
132
|
+
"""Closes the connections, unless the ``httpx.Client`` was yours."""
|
|
133
|
+
self._transport.close()
|
|
134
|
+
|
|
135
|
+
def __enter__(self) -> Sinatra:
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def __exit__(
|
|
139
|
+
self,
|
|
140
|
+
exc_type: type[BaseException] | None,
|
|
141
|
+
exc: BaseException | None,
|
|
142
|
+
traceback: TracebackType | None,
|
|
143
|
+
) -> None:
|
|
144
|
+
self.close()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class _Transport:
|
|
148
|
+
"""Sends a generated request with the client's auth, retries and timeout."""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self, *, http: httpx.Client, owns_http: bool, base_url: str, headers: dict[str, str], timeout: Timeout
|
|
152
|
+
) -> None:
|
|
153
|
+
self.http = http
|
|
154
|
+
self.owns_http = owns_http
|
|
155
|
+
self.base_url = base_url
|
|
156
|
+
self.headers = headers
|
|
157
|
+
self.timeout = timeout
|
|
158
|
+
|
|
159
|
+
def close(self) -> None:
|
|
160
|
+
if self.owns_http:
|
|
161
|
+
self.http.close()
|
|
162
|
+
|
|
163
|
+
def send(self, request: dict[str, Any], retry_on: frozenset[int], timeout: Timeout | _Default) -> httpx.Response:
|
|
164
|
+
"""Sends ``request`` (a generated ``_get_kwargs()``) and returns the 2xx
|
|
165
|
+
response. Raises :class:`SinatraError` for any other status."""
|
|
166
|
+
unknown = set(request) - _REQUEST_KEYS
|
|
167
|
+
if unknown:
|
|
168
|
+
# A generator change built a request with parts this client doesn't
|
|
169
|
+
# send (a file upload, raw content, cookies...). Refuse rather than
|
|
170
|
+
# silently send something else.
|
|
171
|
+
raise RuntimeError(
|
|
172
|
+
f"sinatra-dev can't send {request.get('method', '?').upper()} {request.get('url', '?')}: "
|
|
173
|
+
f"the generated request has parts it doesn't handle: {', '.join(sorted(unknown))}"
|
|
174
|
+
)
|
|
175
|
+
method: str = request["method"]
|
|
176
|
+
url = self.base_url + request["url"]
|
|
177
|
+
headers = {**request.get("headers", {}), **self.headers}
|
|
178
|
+
per_call = self.timeout if isinstance(timeout, _Default) else timeout
|
|
179
|
+
|
|
180
|
+
def attempt() -> httpx.Response:
|
|
181
|
+
return self.http.request(
|
|
182
|
+
method.upper(),
|
|
183
|
+
url,
|
|
184
|
+
params=request.get("params"),
|
|
185
|
+
json=request.get("json"),
|
|
186
|
+
headers=headers,
|
|
187
|
+
timeout=per_call,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
response = _retry.with_retries(retry_on, attempt)
|
|
191
|
+
if not response.is_success:
|
|
192
|
+
raise SinatraError.from_response(response)
|
|
193
|
+
return response
|
|
194
|
+
|
|
195
|
+
def decode(
|
|
196
|
+
self,
|
|
197
|
+
request: dict[str, Any],
|
|
198
|
+
retry_on: frozenset[int],
|
|
199
|
+
timeout: Timeout | _Default,
|
|
200
|
+
model: Callable[[dict[str, Any]], T],
|
|
201
|
+
) -> T:
|
|
202
|
+
return model(self.send(request, retry_on, timeout).json())
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class MeApi:
|
|
206
|
+
"""``me.*``: the token's user."""
|
|
207
|
+
|
|
208
|
+
def __init__(self, transport: _Transport) -> None:
|
|
209
|
+
self._t = transport
|
|
210
|
+
|
|
211
|
+
def get(self, *, timeout: Timeout | _Default = DEFAULT) -> Me:
|
|
212
|
+
"""``me.get``: the user the token acts as, and their workspaces."""
|
|
213
|
+
return self._t.decode(me_get._get_kwargs(), _retry.RETRY_IDEMPOTENT, timeout, Me.from_dict)
|
|
214
|
+
|
|
215
|
+
def revoke_token(self, *, timeout: Timeout | _Default = DEFAULT) -> None:
|
|
216
|
+
"""``me.revokeToken``: revokes the token this client sends. Later requests get ``401``."""
|
|
217
|
+
self._t.send(me_revoke_token._get_kwargs(), _retry.RETRY_IDEMPOTENT, timeout)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TasksApi:
|
|
221
|
+
"""``tasks.*``: start, follow and stop tasks."""
|
|
222
|
+
|
|
223
|
+
def __init__(self, transport: _Transport) -> None:
|
|
224
|
+
self._t = transport
|
|
225
|
+
|
|
226
|
+
def list(
|
|
227
|
+
self,
|
|
228
|
+
workspace_id: str,
|
|
229
|
+
*,
|
|
230
|
+
status: str | None = None,
|
|
231
|
+
limit: int | None = None,
|
|
232
|
+
cursor: str | None = None,
|
|
233
|
+
timeout: Timeout | _Default = DEFAULT,
|
|
234
|
+
) -> TaskList:
|
|
235
|
+
"""``tasks.list``: one page of the workspace's tasks, most recently active first."""
|
|
236
|
+
request = tasks_list._get_kwargs(
|
|
237
|
+
workspace_id, status=_unset(status), limit=_unset(limit), cursor=_unset(cursor)
|
|
238
|
+
)
|
|
239
|
+
return self._t.decode(request, _retry.RETRY_IDEMPOTENT, timeout, TaskList.from_dict)
|
|
240
|
+
|
|
241
|
+
def list_all(
|
|
242
|
+
self,
|
|
243
|
+
workspace_id: str,
|
|
244
|
+
*,
|
|
245
|
+
status: str | None = None,
|
|
246
|
+
limit: int | None = None,
|
|
247
|
+
cursor: str | None = None,
|
|
248
|
+
timeout: Timeout | _Default = DEFAULT,
|
|
249
|
+
) -> Iterator[Task]:
|
|
250
|
+
"""Every task from ``cursor`` on (or from the start), following ``next_cursor``
|
|
251
|
+
page by page. ``limit`` is the page size."""
|
|
252
|
+
while True:
|
|
253
|
+
page = self.list(workspace_id, status=status, limit=limit, cursor=cursor, timeout=timeout)
|
|
254
|
+
yield from page.tasks
|
|
255
|
+
cursor = page.next_cursor
|
|
256
|
+
if not cursor:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
def get(self, workspace_id: str, task_id: str, *, timeout: Timeout | _Default = DEFAULT) -> Task:
|
|
260
|
+
"""``tasks.get``: one task, wherever it started."""
|
|
261
|
+
request = tasks_get._get_kwargs(workspace_id, task_id)
|
|
262
|
+
return self._t.decode(request, _retry.RETRY_IDEMPOTENT, timeout, Task.from_dict)
|
|
263
|
+
|
|
264
|
+
def create(
|
|
265
|
+
self,
|
|
266
|
+
workspace_id: str,
|
|
267
|
+
prompt: str,
|
|
268
|
+
*,
|
|
269
|
+
title: str | None = None,
|
|
270
|
+
repo: str | None = None,
|
|
271
|
+
idempotency_key: str | None = None,
|
|
272
|
+
timeout: Timeout | _Default = DEFAULT,
|
|
273
|
+
) -> CreateTaskResponse:
|
|
274
|
+
"""``tasks.create``: starts a task. Returns once the task is made, not when
|
|
275
|
+
it's done: poll ``tasks.get`` and ``messages.list`` to follow it.
|
|
276
|
+
|
|
277
|
+
``idempotency_key`` is 1 to 128 printable ASCII characters, with no
|
|
278
|
+
spaces. When you leave it out, the client makes a random UUID. Either
|
|
279
|
+
way, every retry of the call sends the same key, so a retried start
|
|
280
|
+
never starts a second task.
|
|
281
|
+
"""
|
|
282
|
+
body = CreateTaskRequest(prompt=prompt, title=_unset(title), repo=_unset(repo))
|
|
283
|
+
key = idempotency_key if idempotency_key is not None else str(uuid.uuid4())
|
|
284
|
+
request = tasks_create._get_kwargs(workspace_id, body=body, idempotency_key=key)
|
|
285
|
+
return self._t.decode(request, _retry.RETRY_IDEMPOTENT, timeout, CreateTaskResponse.from_dict)
|
|
286
|
+
|
|
287
|
+
def stop(self, workspace_id: str, task_id: str, *, timeout: Timeout | _Default = DEFAULT) -> StopTaskResponse:
|
|
288
|
+
"""``tasks.stop``: stops the task's running turn. A task that isn't running is ``409 not_running``."""
|
|
289
|
+
request = tasks_stop._get_kwargs(workspace_id, task_id)
|
|
290
|
+
return self._t.decode(request, _retry.RETRY_UNAVAILABLE, timeout, StopTaskResponse.from_dict)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class MessagesApi:
|
|
294
|
+
"""``messages.*``: a task's conversation."""
|
|
295
|
+
|
|
296
|
+
def __init__(self, transport: _Transport) -> None:
|
|
297
|
+
self._t = transport
|
|
298
|
+
|
|
299
|
+
def create(
|
|
300
|
+
self,
|
|
301
|
+
workspace_id: str,
|
|
302
|
+
task_id: str,
|
|
303
|
+
text: str,
|
|
304
|
+
*,
|
|
305
|
+
idempotency_key: str | None = None,
|
|
306
|
+
timeout: Timeout | _Default = DEFAULT,
|
|
307
|
+
) -> CreateMessageResponse:
|
|
308
|
+
"""``messages.create``: sends a follow-up on a task started from the API, CLI,
|
|
309
|
+
MCP or dashboard. The idempotency key works as in ``tasks.create``."""
|
|
310
|
+
key = idempotency_key if idempotency_key is not None else str(uuid.uuid4())
|
|
311
|
+
request = messages_create._get_kwargs(
|
|
312
|
+
workspace_id, task_id, body=CreateMessageRequest(text=text), idempotency_key=key
|
|
313
|
+
)
|
|
314
|
+
return self._t.decode(request, _retry.RETRY_IDEMPOTENT, timeout, CreateMessageResponse.from_dict)
|
|
315
|
+
|
|
316
|
+
def list(
|
|
317
|
+
self,
|
|
318
|
+
workspace_id: str,
|
|
319
|
+
task_id: str,
|
|
320
|
+
*,
|
|
321
|
+
after: str | None = None,
|
|
322
|
+
limit: int | None = None,
|
|
323
|
+
timeout: Timeout | _Default = DEFAULT,
|
|
324
|
+
) -> MessageList:
|
|
325
|
+
"""``messages.list``: one page of the task's conversation, oldest first. To wait
|
|
326
|
+
for a reply, poll with ``after`` set to the ``id`` of the last entry you read."""
|
|
327
|
+
request = messages_list._get_kwargs(workspace_id, task_id, after=_unset(after), limit=_unset(limit))
|
|
328
|
+
return self._t.decode(request, _retry.RETRY_IDEMPOTENT, timeout, MessageList.from_dict)
|
|
329
|
+
|
|
330
|
+
def list_all(
|
|
331
|
+
self,
|
|
332
|
+
workspace_id: str,
|
|
333
|
+
task_id: str,
|
|
334
|
+
*,
|
|
335
|
+
after: str | None = None,
|
|
336
|
+
limit: int | None = None,
|
|
337
|
+
timeout: Timeout | _Default = DEFAULT,
|
|
338
|
+
) -> Iterator[Message]:
|
|
339
|
+
"""Every entry after ``after`` (or from the start), following ``next`` page by page."""
|
|
340
|
+
while True:
|
|
341
|
+
page = self.list(workspace_id, task_id, after=after, limit=limit, timeout=timeout)
|
|
342
|
+
yield from page.messages
|
|
343
|
+
after = page.next_
|
|
344
|
+
if not after:
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class TicketsApi:
|
|
349
|
+
"""``tickets.*``: Linear and GitHub issues you can delegate."""
|
|
350
|
+
|
|
351
|
+
def __init__(self, transport: _Transport) -> None:
|
|
352
|
+
self._t = transport
|
|
353
|
+
|
|
354
|
+
def search(
|
|
355
|
+
self,
|
|
356
|
+
workspace_id: str,
|
|
357
|
+
*,
|
|
358
|
+
q: str | None = None,
|
|
359
|
+
repo: str | None = None,
|
|
360
|
+
timeout: Timeout | _Default = DEFAULT,
|
|
361
|
+
) -> TicketSearchResult:
|
|
362
|
+
"""``tickets.search``: open Linear and GitHub issues you could delegate."""
|
|
363
|
+
request = tickets_search._get_kwargs(workspace_id, q=_unset(q), repo=_unset(repo))
|
|
364
|
+
return self._t.decode(request, _retry.RETRY_IDEMPOTENT, timeout, TicketSearchResult.from_dict)
|
|
365
|
+
|
|
366
|
+
def delegate(
|
|
367
|
+
self,
|
|
368
|
+
workspace_id: str,
|
|
369
|
+
*,
|
|
370
|
+
source: Literal["linear", "github"],
|
|
371
|
+
id: str | None = None,
|
|
372
|
+
repo: str | None = None,
|
|
373
|
+
number: int | None = None,
|
|
374
|
+
timeout: Timeout | _Default = DEFAULT,
|
|
375
|
+
) -> DelegateTicketResponse:
|
|
376
|
+
"""``tickets.delegate``: hands Sinatra an existing issue as you: a Linear issue
|
|
377
|
+
by ``id`` (``source="linear"``), or a GitHub issue by ``repo`` and
|
|
378
|
+
``number`` (``source="github"``). Never retried: the operation takes no
|
|
379
|
+
``Idempotency-Key``."""
|
|
380
|
+
body: DelegateLinearTicket | DelegateGithubTicket
|
|
381
|
+
if source == "linear" and id is not None and repo is None and number is None:
|
|
382
|
+
body = DelegateLinearTicket(source="linear", id=id)
|
|
383
|
+
elif source == "github" and repo is not None and number is not None and id is None:
|
|
384
|
+
body = DelegateGithubTicket(source="github", repo=repo, number=number)
|
|
385
|
+
else:
|
|
386
|
+
raise ValueError(
|
|
387
|
+
'Delegate a Linear issue with source="linear", id=..., or a GitHub issue with '
|
|
388
|
+
'source="github", repo=..., number=...'
|
|
389
|
+
)
|
|
390
|
+
request = tickets_delegate._get_kwargs(workspace_id, body=body)
|
|
391
|
+
return self._t.decode(request, _retry.RETRY_NEVER, timeout, DelegateTicketResponse.from_dict)
|
sinatra_dev/_errors.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SinatraError(Exception):
|
|
9
|
+
"""The one error the client raises for an HTTP error response.
|
|
10
|
+
|
|
11
|
+
``code`` is the API's machine-readable ``error`` code, such as
|
|
12
|
+
``capacity_full``. New codes can appear at any time, so treat it as an open
|
|
13
|
+
string. Branch on ``code``, not on ``reason`` or the message.
|
|
14
|
+
|
|
15
|
+
Network failures and timeouts are not wrapped: they raise the
|
|
16
|
+
``httpx.HTTPError`` that httpx raised.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
status: int
|
|
20
|
+
"""The HTTP status, such as ``409``."""
|
|
21
|
+
code: str
|
|
22
|
+
"""The API's ``error`` code, such as ``not_running``. ``http_error`` when the
|
|
23
|
+
response had no code, for example an HTML page from a proxy."""
|
|
24
|
+
reason: str | None
|
|
25
|
+
"""A sentence saying what went wrong, written to show to a person."""
|
|
26
|
+
request_id: str | None
|
|
27
|
+
"""The request's id, from the body or the ``x-request-id`` header. Share it with support."""
|
|
28
|
+
body: Any
|
|
29
|
+
"""The response body: the parsed JSON, or the text when it wasn't JSON."""
|
|
30
|
+
headers: httpx.Headers
|
|
31
|
+
"""The response headers."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, status: int, body: Any, headers: httpx.Headers) -> None:
|
|
34
|
+
fields: dict[str, Any] = body if isinstance(body, dict) else {}
|
|
35
|
+
error = fields.get("error")
|
|
36
|
+
reason = fields.get("reason")
|
|
37
|
+
request_id = fields.get("requestId")
|
|
38
|
+
self.status = status
|
|
39
|
+
self.code = error if isinstance(error, str) else "http_error"
|
|
40
|
+
self.reason = reason if isinstance(reason, str) else None
|
|
41
|
+
self.request_id = request_id if isinstance(request_id, str) else headers.get("x-request-id")
|
|
42
|
+
self.body = body
|
|
43
|
+
self.headers = headers
|
|
44
|
+
super().__init__(f"{status} {self.code}: {self.reason}" if self.reason else f"{status} {self.code}")
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_response(cls, response: httpx.Response) -> SinatraError:
|
|
48
|
+
body: Any
|
|
49
|
+
try:
|
|
50
|
+
body = response.json()
|
|
51
|
+
except ValueError:
|
|
52
|
+
body = response.text
|
|
53
|
+
return cls(response.status_code, body, response.headers)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains methods for accessing the API"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains endpoint functions for accessing the API"""
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from ... import errors
|
|
7
|
+
from ...client import AuthenticatedClient, Client
|
|
8
|
+
from ...models.error import Error
|
|
9
|
+
from ...models.me import Me
|
|
10
|
+
from ...types import Response
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_kwargs() -> dict[str, Any]:
|
|
14
|
+
|
|
15
|
+
_kwargs: dict[str, Any] = {
|
|
16
|
+
"method": "get",
|
|
17
|
+
"url": "/v1/me",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return _kwargs
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_response(
|
|
24
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
25
|
+
) -> Error | Me | None:
|
|
26
|
+
if response.status_code == 200:
|
|
27
|
+
response_200 = Me.from_dict(response.json())
|
|
28
|
+
|
|
29
|
+
return response_200
|
|
30
|
+
|
|
31
|
+
if response.status_code == 401:
|
|
32
|
+
response_401 = Error.from_dict(response.json())
|
|
33
|
+
|
|
34
|
+
return response_401
|
|
35
|
+
|
|
36
|
+
if response.status_code == 429:
|
|
37
|
+
response_429 = Error.from_dict(response.json())
|
|
38
|
+
|
|
39
|
+
return response_429
|
|
40
|
+
|
|
41
|
+
if response.status_code == 500:
|
|
42
|
+
response_500 = Error.from_dict(response.json())
|
|
43
|
+
|
|
44
|
+
return response_500
|
|
45
|
+
|
|
46
|
+
if client.raise_on_unexpected_status:
|
|
47
|
+
raise errors.UnexpectedStatus(response.status_code, response.content)
|
|
48
|
+
else:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_response(
|
|
53
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
54
|
+
) -> Response[Error | Me]:
|
|
55
|
+
return Response(
|
|
56
|
+
status_code=HTTPStatus(response.status_code),
|
|
57
|
+
content=response.content,
|
|
58
|
+
headers=response.headers,
|
|
59
|
+
parsed=_parse_response(client=client, response=response),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sync_detailed(
|
|
64
|
+
*,
|
|
65
|
+
client: AuthenticatedClient | Client,
|
|
66
|
+
) -> Response[Error | Me]:
|
|
67
|
+
"""Get the token's user
|
|
68
|
+
|
|
69
|
+
Returns the user the token acts as and the workspaces they belong to, ordered by name. Use it to
|
|
70
|
+
find the workspace ids the other endpoints take.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
74
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Response[Error | Me]
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
kwargs = _get_kwargs()
|
|
81
|
+
|
|
82
|
+
response = client.get_httpx_client().request(
|
|
83
|
+
**kwargs,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return _build_response(client=client, response=response)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def sync(
|
|
90
|
+
*,
|
|
91
|
+
client: AuthenticatedClient | Client,
|
|
92
|
+
) -> Error | Me | None:
|
|
93
|
+
"""Get the token's user
|
|
94
|
+
|
|
95
|
+
Returns the user the token acts as and the workspaces they belong to, ordered by name. Use it to
|
|
96
|
+
find the workspace ids the other endpoints take.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
100
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Error | Me
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
return sync_detailed(
|
|
107
|
+
client=client,
|
|
108
|
+
).parsed
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def asyncio_detailed(
|
|
112
|
+
*,
|
|
113
|
+
client: AuthenticatedClient | Client,
|
|
114
|
+
) -> Response[Error | Me]:
|
|
115
|
+
"""Get the token's user
|
|
116
|
+
|
|
117
|
+
Returns the user the token acts as and the workspaces they belong to, ordered by name. Use it to
|
|
118
|
+
find the workspace ids the other endpoints take.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
122
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Response[Error | Me]
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
kwargs = _get_kwargs()
|
|
129
|
+
|
|
130
|
+
response = await client.get_async_httpx_client().request(**kwargs)
|
|
131
|
+
|
|
132
|
+
return _build_response(client=client, response=response)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def asyncio(
|
|
136
|
+
*,
|
|
137
|
+
client: AuthenticatedClient | Client,
|
|
138
|
+
) -> Error | Me | None:
|
|
139
|
+
"""Get the token's user
|
|
140
|
+
|
|
141
|
+
Returns the user the token acts as and the workspaces they belong to, ordered by name. Use it to
|
|
142
|
+
find the workspace ids the other endpoints take.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
146
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Error | Me
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
await asyncio_detailed(
|
|
154
|
+
client=client,
|
|
155
|
+
)
|
|
156
|
+
).parsed
|