schedule-bit 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.
- schedule_bit/__init__.py +57 -0
- schedule_bit/_client.py +444 -0
- schedule_bit/_generated/__init__.py +2 -0
- schedule_bit/_generated/models.py +2218 -0
- schedule_bit/_generated/operations.py +588 -0
- schedule_bit/_options.py +26 -0
- schedule_bit/_redact.py +40 -0
- schedule_bit/_version.py +1 -0
- schedule_bit/_wire.py +46 -0
- schedule_bit/errors.py +110 -0
- schedule_bit/local_time.py +84 -0
- schedule_bit/masks.py +125 -0
- schedule_bit/py.typed +0 -0
- schedule_bit/rate_limit.py +87 -0
- schedule_bit/retry.py +87 -0
- schedule_bit/watch.py +296 -0
- schedule_bit-0.1.0.dist-info/METADATA +119 -0
- schedule_bit-0.1.0.dist-info/RECORD +20 -0
- schedule_bit-0.1.0.dist-info/WHEEL +4 -0
- schedule_bit-0.1.0.dist-info/licenses/LICENSE +21 -0
schedule_bit/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""The schedule-bit API from Python.
|
|
2
|
+
|
|
3
|
+
from schedule_bit import Client, Slots
|
|
4
|
+
|
|
5
|
+
with Client() as api: # key from SCHEDULE_BIT_API_KEY
|
|
6
|
+
api.create_world(name="room-1", preset="temporal-hour")
|
|
7
|
+
api.declare_occupier("room-1", name="omar")
|
|
8
|
+
receipt = api.claim("room-1", who="omar", date="2026-10-15", slots=Slots.range(9, 11))
|
|
9
|
+
|
|
10
|
+
See https://api.schedule-bit.io/v1/docs for every operation; each one is a method
|
|
11
|
+
here, in ``snake_case`` (``explainClaim`` is ``explain_claim``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ._client import DEFAULT_BASE_URL, AsyncClient, Client
|
|
15
|
+
from ._generated import models
|
|
16
|
+
from ._options import RequestOptions
|
|
17
|
+
from ._redact import redact
|
|
18
|
+
from ._version import __version__
|
|
19
|
+
from .errors import (
|
|
20
|
+
ApiError,
|
|
21
|
+
Cancelled,
|
|
22
|
+
LocalTimeError,
|
|
23
|
+
NetworkError,
|
|
24
|
+
RateLimited,
|
|
25
|
+
Refusal,
|
|
26
|
+
ScheduleBitError,
|
|
27
|
+
TimeoutError,
|
|
28
|
+
)
|
|
29
|
+
from .local_time import local_day, local_hour, local_slot
|
|
30
|
+
from .masks import Slots
|
|
31
|
+
from .rate_limit import RateLimit, TokenBucket
|
|
32
|
+
from .retry import RetryPolicy
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"DEFAULT_BASE_URL",
|
|
36
|
+
"ApiError",
|
|
37
|
+
"AsyncClient",
|
|
38
|
+
"Cancelled",
|
|
39
|
+
"Client",
|
|
40
|
+
"LocalTimeError",
|
|
41
|
+
"NetworkError",
|
|
42
|
+
"RateLimit",
|
|
43
|
+
"RateLimited",
|
|
44
|
+
"Refusal",
|
|
45
|
+
"RequestOptions",
|
|
46
|
+
"RetryPolicy",
|
|
47
|
+
"ScheduleBitError",
|
|
48
|
+
"Slots",
|
|
49
|
+
"TimeoutError",
|
|
50
|
+
"TokenBucket",
|
|
51
|
+
"__version__",
|
|
52
|
+
"local_day",
|
|
53
|
+
"local_hour",
|
|
54
|
+
"local_slot",
|
|
55
|
+
"models",
|
|
56
|
+
"redact",
|
|
57
|
+
]
|
schedule_bit/_client.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""`Client` and `AsyncClient`: the generated operations over one careful transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import datetime
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
|
|
13
|
+
from typing import Any, TypeVar
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from ._generated import models
|
|
20
|
+
from ._generated.operations import OPS, AsyncOperations, Operation, SyncOperations
|
|
21
|
+
from ._options import RequestOptions
|
|
22
|
+
from ._redact import redact
|
|
23
|
+
from ._version import __version__
|
|
24
|
+
from .errors import ApiError, Cancelled, NetworkError, Refusal, TimeoutError, error_for
|
|
25
|
+
from .rate_limit import RateLimit, TokenBucket
|
|
26
|
+
from .retry import RetryPolicy, should_retry, wait_for
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE_URL = "https://api.schedule-bit.io"
|
|
29
|
+
DEFAULT_TIMEOUT = 30.0
|
|
30
|
+
USER_AGENT = f"schedule-bit-python/{__version__}"
|
|
31
|
+
|
|
32
|
+
T = TypeVar("T")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _query_value(value: Any) -> str:
|
|
36
|
+
if isinstance(value, bool):
|
|
37
|
+
return "true" if value else "false"
|
|
38
|
+
if isinstance(value, (datetime.date, datetime.datetime)):
|
|
39
|
+
return value.isoformat()
|
|
40
|
+
if isinstance(value, (list, tuple)):
|
|
41
|
+
return ",".join(_query_value(v) for v in value) # form, explode: false
|
|
42
|
+
return str(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _ServerSentEvents:
|
|
46
|
+
"""``text/event-stream`` lines in, one `AssistEvent` per blank-line-ended event out."""
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
self._data: list[str] = []
|
|
50
|
+
|
|
51
|
+
def feed(self, line: str) -> list[models.AssistEvent]:
|
|
52
|
+
if line == "":
|
|
53
|
+
return self.end()
|
|
54
|
+
if line.startswith("data:"):
|
|
55
|
+
self._data.append(line[5:].lstrip(" "))
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
def end(self) -> list[models.AssistEvent]:
|
|
59
|
+
if not self._data:
|
|
60
|
+
return []
|
|
61
|
+
event = models.AssistEvent.model_validate(json.loads("\n".join(self._data)))
|
|
62
|
+
self._data = []
|
|
63
|
+
return [event]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _Base:
|
|
67
|
+
"""Configuration and request building shared by both clients."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
api_key: str | None = None,
|
|
72
|
+
*,
|
|
73
|
+
base_url: str | None = None,
|
|
74
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
75
|
+
retry: RetryPolicy | None = None,
|
|
76
|
+
max_retries: int | None = None,
|
|
77
|
+
rate_limit_per_minute: float | None = None,
|
|
78
|
+
on_rate_limit: Callable[[RateLimit], None] | None = None,
|
|
79
|
+
headers: Mapping[str, str] | None = None,
|
|
80
|
+
clock: Callable[[], float] = time.time,
|
|
81
|
+
rng: random.Random | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
key = api_key if api_key is not None else os.environ.get("SCHEDULE_BIT_API_KEY")
|
|
84
|
+
self._api_key = key or None
|
|
85
|
+
self.base_url = (base_url or os.environ.get("SCHEDULE_BIT_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
86
|
+
self.timeout = timeout
|
|
87
|
+
policy = retry or RetryPolicy()
|
|
88
|
+
if max_retries is not None:
|
|
89
|
+
policy = RetryPolicy(max_retries=max_retries, base=policy.base, cap=policy.cap)
|
|
90
|
+
self.retry = policy
|
|
91
|
+
self.bucket = TokenBucket(rate_limit_per_minute) if rate_limit_per_minute else None
|
|
92
|
+
self.on_rate_limit = on_rate_limit
|
|
93
|
+
self.rate_limit: RateLimit | None = None
|
|
94
|
+
self._extra_headers = dict(headers or {})
|
|
95
|
+
self._clock = clock
|
|
96
|
+
self._rng = rng or random.Random()
|
|
97
|
+
|
|
98
|
+
def __repr__(self) -> str:
|
|
99
|
+
key = "set" if self._api_key else "none"
|
|
100
|
+
return f"{type(self).__name__}(base_url={self.base_url!r}, api_key=<{key}>)"
|
|
101
|
+
|
|
102
|
+
# -- building -----------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def _url(self, op: Operation, path: Mapping[str, str]) -> str:
|
|
105
|
+
filled = op.path
|
|
106
|
+
for name, value in path.items():
|
|
107
|
+
filled = filled.replace("{" + name + "}", quote(str(value), safe=""))
|
|
108
|
+
return self.base_url + filled
|
|
109
|
+
|
|
110
|
+
def _headers(self, op: Operation, opts: RequestOptions, has_body: bool, key: str | None) -> dict[str, str]:
|
|
111
|
+
headers = {"Accept": "application/json, application/problem+json", "User-Agent": USER_AGENT}
|
|
112
|
+
if self._api_key:
|
|
113
|
+
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
114
|
+
if has_body:
|
|
115
|
+
headers["Content-Type"] = "application/json"
|
|
116
|
+
if key:
|
|
117
|
+
headers["Idempotency-Key"] = key
|
|
118
|
+
headers.update(self._extra_headers)
|
|
119
|
+
headers.update(opts.headers)
|
|
120
|
+
return headers
|
|
121
|
+
|
|
122
|
+
def _request(
|
|
123
|
+
self, op: Operation, path: Mapping[str, str], query: Mapping[str, Any], body: Any, opts: RequestOptions
|
|
124
|
+
) -> httpx.Request:
|
|
125
|
+
# One key per call, reused across its retries.
|
|
126
|
+
key = opts.idempotency_key or (str(uuid.uuid4()) if op.keyed else None)
|
|
127
|
+
params = {k: _query_value(v) for k, v in query.items() if v is not None}
|
|
128
|
+
content = json.dumps(body).encode() if body is not None else None
|
|
129
|
+
timeout = opts.timeout if opts.timeout is not None else self.timeout
|
|
130
|
+
return httpx.Request(
|
|
131
|
+
op.method,
|
|
132
|
+
self._url(op, path),
|
|
133
|
+
params=params,
|
|
134
|
+
content=content,
|
|
135
|
+
headers=self._headers(op, opts, body is not None, key),
|
|
136
|
+
extensions={"timeout": httpx.Timeout(timeout).as_dict()},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _observe(self, response: httpx.Response) -> None:
|
|
140
|
+
limit = RateLimit.from_headers(response.headers)
|
|
141
|
+
if limit is not None:
|
|
142
|
+
self.rate_limit = limit
|
|
143
|
+
if self.on_rate_limit:
|
|
144
|
+
self.on_rate_limit(limit)
|
|
145
|
+
|
|
146
|
+
def _retries(self, opts: RequestOptions) -> int:
|
|
147
|
+
return self.retry.max_retries if opts.max_retries is None else opts.max_retries
|
|
148
|
+
|
|
149
|
+
# -- answers ------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _decode(response: httpx.Response) -> Any:
|
|
153
|
+
if not response.content:
|
|
154
|
+
return None
|
|
155
|
+
ctype = response.headers.get("content-type", "")
|
|
156
|
+
if "json" in ctype:
|
|
157
|
+
try:
|
|
158
|
+
return response.json()
|
|
159
|
+
except ValueError:
|
|
160
|
+
return response.text
|
|
161
|
+
return response.text
|
|
162
|
+
|
|
163
|
+
def _error(self, response: httpx.Response, request: httpx.Request) -> ApiError:
|
|
164
|
+
return error_for(
|
|
165
|
+
response.status_code,
|
|
166
|
+
self._decode(response),
|
|
167
|
+
method=request.method,
|
|
168
|
+
url=str(request.url),
|
|
169
|
+
rate_limit=RateLimit.from_headers(response.headers),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _parse(op: Operation, response: httpx.Response, parse: type[BaseModel] | None) -> Any:
|
|
174
|
+
if op.answer == "empty" or not response.content:
|
|
175
|
+
return None
|
|
176
|
+
if op.answer == "text":
|
|
177
|
+
return response.text
|
|
178
|
+
data = response.json()
|
|
179
|
+
return parse.model_validate(data) if parse is not None else data
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _network(exc: Exception, request: httpx.Request, attempts: int) -> NetworkError:
|
|
183
|
+
kind = TimeoutError if isinstance(exc, httpx.TimeoutException) else NetworkError
|
|
184
|
+
what = type(exc).__name__
|
|
185
|
+
return kind(
|
|
186
|
+
f"{request.method} {redact(str(request.url))}: no answer after {attempts} attempt(s) ({what}: {exc})"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _sse_events(lines: Iterator[str]) -> Iterator[models.AssistEvent]:
|
|
191
|
+
parser = _ServerSentEvents()
|
|
192
|
+
for line in lines:
|
|
193
|
+
yield from parser.feed(line)
|
|
194
|
+
yield from parser.end()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Client(_Base, SyncOperations):
|
|
198
|
+
"""The schedule-bit API, synchronously.
|
|
199
|
+
|
|
200
|
+
>>> from schedule_bit import Client
|
|
201
|
+
>>> api = Client("sbk_…") # or set SCHEDULE_BIT_API_KEY
|
|
202
|
+
>>> api.claim("room-1", who="omar", date="2026-10-15", slots=[9, 10])
|
|
203
|
+
|
|
204
|
+
Every call retries 429/503 (and, when it is safe, network failures and other
|
|
205
|
+
5xx) with jittered back-off, honours ``Retry-After``, sends an
|
|
206
|
+
``Idempotency-Key`` on keyed writes, and raises a typed error otherwise.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(
|
|
210
|
+
self,
|
|
211
|
+
api_key: str | None = None,
|
|
212
|
+
*,
|
|
213
|
+
http: httpx.Client | None = None,
|
|
214
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
215
|
+
**kwargs: Any,
|
|
216
|
+
) -> None:
|
|
217
|
+
super().__init__(api_key, **kwargs)
|
|
218
|
+
self._http = http or httpx.Client()
|
|
219
|
+
self._sleep = sleep
|
|
220
|
+
|
|
221
|
+
def __enter__(self) -> Client:
|
|
222
|
+
return self
|
|
223
|
+
|
|
224
|
+
def __exit__(self, *exc: object) -> None:
|
|
225
|
+
self.close()
|
|
226
|
+
|
|
227
|
+
def close(self) -> None:
|
|
228
|
+
self._http.close()
|
|
229
|
+
|
|
230
|
+
def _wait(self, seconds: float, opts: RequestOptions) -> None:
|
|
231
|
+
if opts.cancel is not None:
|
|
232
|
+
if opts.cancel.wait(seconds):
|
|
233
|
+
raise Cancelled("cancelled while waiting to retry")
|
|
234
|
+
else:
|
|
235
|
+
self._sleep(seconds)
|
|
236
|
+
|
|
237
|
+
def _send(
|
|
238
|
+
self, op: Operation, request: httpx.Request, opts: RequestOptions, *, stream: bool = False
|
|
239
|
+
) -> httpx.Response:
|
|
240
|
+
retries = self._retries(opts)
|
|
241
|
+
attempt = 0
|
|
242
|
+
while True:
|
|
243
|
+
if opts.cancel is not None and opts.cancel.is_set():
|
|
244
|
+
raise Cancelled(f"{op.id} cancelled")
|
|
245
|
+
if self.bucket:
|
|
246
|
+
self.bucket.acquire()
|
|
247
|
+
try:
|
|
248
|
+
response = self._http.send(request, stream=stream)
|
|
249
|
+
except httpx.TransportError as exc:
|
|
250
|
+
if attempt >= retries or not should_retry(op, None):
|
|
251
|
+
raise self._network(exc, request, attempt + 1) from exc
|
|
252
|
+
self._wait(wait_for(self.retry, attempt, None, self._clock(), self._rng), opts)
|
|
253
|
+
attempt += 1
|
|
254
|
+
continue
|
|
255
|
+
self._observe(response)
|
|
256
|
+
if response.status_code < 400:
|
|
257
|
+
return response
|
|
258
|
+
if attempt >= retries or not should_retry(op, response.status_code):
|
|
259
|
+
if stream:
|
|
260
|
+
response.read()
|
|
261
|
+
raise self._error(response, request)
|
|
262
|
+
delay = wait_for(self.retry, attempt, response.headers, self._clock(), self._rng)
|
|
263
|
+
response.close()
|
|
264
|
+
self._wait(delay, opts)
|
|
265
|
+
attempt += 1
|
|
266
|
+
|
|
267
|
+
def _call(
|
|
268
|
+
self,
|
|
269
|
+
op: Operation,
|
|
270
|
+
path: dict[str, str],
|
|
271
|
+
query: dict[str, Any],
|
|
272
|
+
body: Any,
|
|
273
|
+
options: RequestOptions | None,
|
|
274
|
+
parse: Any,
|
|
275
|
+
) -> Any:
|
|
276
|
+
opts = options or RequestOptions()
|
|
277
|
+
request = self._request(op, path, query, body, opts)
|
|
278
|
+
return self._parse(op, self._send(op, request, opts), parse)
|
|
279
|
+
|
|
280
|
+
def _stream(
|
|
281
|
+
self, op: Operation, path: dict[str, str], query: dict[str, Any], body: Any, options: RequestOptions | None
|
|
282
|
+
) -> Iterator[models.AssistEvent]:
|
|
283
|
+
opts = options or RequestOptions()
|
|
284
|
+
request = self._request(op, path, query, body, opts)
|
|
285
|
+
request.headers["Accept"] = "text/event-stream"
|
|
286
|
+
|
|
287
|
+
def events() -> Iterator[models.AssistEvent]:
|
|
288
|
+
response = self._send(op, request, opts, stream=True)
|
|
289
|
+
try:
|
|
290
|
+
yield from self._sse_events(response.iter_lines())
|
|
291
|
+
finally:
|
|
292
|
+
response.close()
|
|
293
|
+
|
|
294
|
+
return events()
|
|
295
|
+
|
|
296
|
+
# -- the expected outcomes, as values -----------------------------------------
|
|
297
|
+
|
|
298
|
+
def try_claim(
|
|
299
|
+
self, w: str, body: Any = None, /, *, options: RequestOptions | None = None, **fields: Any
|
|
300
|
+
) -> models.Receipt | Refusal:
|
|
301
|
+
"""`claim`, but a refusal is returned rather than raised."""
|
|
302
|
+
try:
|
|
303
|
+
return self.claim(w, body, options=options, **fields)
|
|
304
|
+
except Refusal as refusal:
|
|
305
|
+
return refusal
|
|
306
|
+
|
|
307
|
+
def request(
|
|
308
|
+
self,
|
|
309
|
+
method: str,
|
|
310
|
+
path: str,
|
|
311
|
+
*,
|
|
312
|
+
query: Mapping[str, Any] | None = None,
|
|
313
|
+
body: Any = None,
|
|
314
|
+
options: RequestOptions | None = None,
|
|
315
|
+
idempotent: bool = False,
|
|
316
|
+
) -> Any:
|
|
317
|
+
"""Any path, with the same retries and errors: for an operation newer than this SDK."""
|
|
318
|
+
op = Operation(f"{method} {path}", method.upper(), path, "json", idempotent)
|
|
319
|
+
return self._call(op, {}, dict(query or {}), body, options, None)
|
|
320
|
+
|
|
321
|
+
def watch(self, w: str, **kwargs: Any) -> Any:
|
|
322
|
+
"""A live, resuming iterator of this World's frames; needs ``schedule-bit[watch]``."""
|
|
323
|
+
from .watch import Watch
|
|
324
|
+
|
|
325
|
+
return Watch(self, w, **kwargs)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class AsyncClient(_Base, AsyncOperations):
|
|
329
|
+
"""The schedule-bit API, with asyncio. Same behaviour as `Client`; cancel a call by cancelling its task."""
|
|
330
|
+
|
|
331
|
+
def __init__(
|
|
332
|
+
self,
|
|
333
|
+
api_key: str | None = None,
|
|
334
|
+
*,
|
|
335
|
+
http: httpx.AsyncClient | None = None,
|
|
336
|
+
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
|
|
337
|
+
**kwargs: Any,
|
|
338
|
+
) -> None:
|
|
339
|
+
super().__init__(api_key, **kwargs)
|
|
340
|
+
self._http = http or httpx.AsyncClient()
|
|
341
|
+
self._sleep = sleep
|
|
342
|
+
|
|
343
|
+
async def __aenter__(self) -> AsyncClient:
|
|
344
|
+
return self
|
|
345
|
+
|
|
346
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
347
|
+
await self.aclose()
|
|
348
|
+
|
|
349
|
+
async def aclose(self) -> None:
|
|
350
|
+
await self._http.aclose()
|
|
351
|
+
|
|
352
|
+
async def _send(
|
|
353
|
+
self, op: Operation, request: httpx.Request, opts: RequestOptions, *, stream: bool = False
|
|
354
|
+
) -> httpx.Response:
|
|
355
|
+
retries = self._retries(opts)
|
|
356
|
+
attempt = 0
|
|
357
|
+
while True:
|
|
358
|
+
if self.bucket:
|
|
359
|
+
await self.bucket.acquire_async()
|
|
360
|
+
try:
|
|
361
|
+
response = await self._http.send(request, stream=stream)
|
|
362
|
+
except httpx.TransportError as exc:
|
|
363
|
+
if attempt >= retries or not should_retry(op, None):
|
|
364
|
+
raise self._network(exc, request, attempt + 1) from exc
|
|
365
|
+
await self._sleep(wait_for(self.retry, attempt, None, self._clock(), self._rng))
|
|
366
|
+
attempt += 1
|
|
367
|
+
continue
|
|
368
|
+
self._observe(response)
|
|
369
|
+
if response.status_code < 400:
|
|
370
|
+
return response
|
|
371
|
+
if attempt >= retries or not should_retry(op, response.status_code):
|
|
372
|
+
if stream:
|
|
373
|
+
await response.aread()
|
|
374
|
+
raise self._error(response, request)
|
|
375
|
+
delay = wait_for(self.retry, attempt, response.headers, self._clock(), self._rng)
|
|
376
|
+
await response.aclose()
|
|
377
|
+
await self._sleep(delay)
|
|
378
|
+
attempt += 1
|
|
379
|
+
|
|
380
|
+
async def _call(
|
|
381
|
+
self,
|
|
382
|
+
op: Operation,
|
|
383
|
+
path: dict[str, str],
|
|
384
|
+
query: dict[str, Any],
|
|
385
|
+
body: Any,
|
|
386
|
+
options: RequestOptions | None,
|
|
387
|
+
parse: Any,
|
|
388
|
+
) -> Any:
|
|
389
|
+
opts = options or RequestOptions()
|
|
390
|
+
request = self._request(op, path, query, body, opts)
|
|
391
|
+
return self._parse(op, await self._send(op, request, opts), parse)
|
|
392
|
+
|
|
393
|
+
def _stream(
|
|
394
|
+
self, op: Operation, path: dict[str, str], query: dict[str, Any], body: Any, options: RequestOptions | None
|
|
395
|
+
) -> AsyncIterator[models.AssistEvent]:
|
|
396
|
+
opts = options or RequestOptions()
|
|
397
|
+
request = self._request(op, path, query, body, opts)
|
|
398
|
+
request.headers["Accept"] = "text/event-stream"
|
|
399
|
+
|
|
400
|
+
async def events() -> AsyncIterator[models.AssistEvent]:
|
|
401
|
+
response = await self._send(op, request, opts, stream=True)
|
|
402
|
+
parser = _ServerSentEvents()
|
|
403
|
+
try:
|
|
404
|
+
async for line in response.aiter_lines():
|
|
405
|
+
for event in parser.feed(line):
|
|
406
|
+
yield event
|
|
407
|
+
for event in parser.end():
|
|
408
|
+
yield event
|
|
409
|
+
finally:
|
|
410
|
+
await response.aclose()
|
|
411
|
+
|
|
412
|
+
return events()
|
|
413
|
+
|
|
414
|
+
async def try_claim(
|
|
415
|
+
self, w: str, body: Any = None, /, *, options: RequestOptions | None = None, **fields: Any
|
|
416
|
+
) -> models.Receipt | Refusal:
|
|
417
|
+
"""`claim`, but a refusal is returned rather than raised."""
|
|
418
|
+
try:
|
|
419
|
+
return await self.claim(w, body, options=options, **fields)
|
|
420
|
+
except Refusal as refusal:
|
|
421
|
+
return refusal
|
|
422
|
+
|
|
423
|
+
async def request(
|
|
424
|
+
self,
|
|
425
|
+
method: str,
|
|
426
|
+
path: str,
|
|
427
|
+
*,
|
|
428
|
+
query: Mapping[str, Any] | None = None,
|
|
429
|
+
body: Any = None,
|
|
430
|
+
options: RequestOptions | None = None,
|
|
431
|
+
idempotent: bool = False,
|
|
432
|
+
) -> Any:
|
|
433
|
+
"""Any path, with the same retries and errors: for an operation newer than this SDK."""
|
|
434
|
+
op = Operation(f"{method} {path}", method.upper(), path, "json", idempotent)
|
|
435
|
+
return await self._call(op, {}, dict(query or {}), body, options, None)
|
|
436
|
+
|
|
437
|
+
def watch(self, w: str, **kwargs: Any) -> Any:
|
|
438
|
+
"""A live, resuming async iterator of this World's frames; needs ``schedule-bit[watch]``."""
|
|
439
|
+
from .watch import AsyncWatch
|
|
440
|
+
|
|
441
|
+
return AsyncWatch(self, w, **kwargs)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
__all__ = ["DEFAULT_BASE_URL", "OPS", "AsyncClient", "Client"]
|