abysshub 0.1.1.dev2__tar.gz → 0.1.1.dev3__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.
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/PKG-INFO +6 -4
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/README.md +5 -3
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/pyproject.toml +1 -1
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/_async_client.py +9 -1
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/_client.py +41 -7
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/_error.py +6 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/_http.py +9 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/tests/test_timeout.py +2 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/.gitignore +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/LICENSE +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/__init__.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/_files.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/_run.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/_types.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/src/abysshub/py.typed +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/tests/caller.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/tests/fake_server.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/tests/test_client.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/tests/test_files.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/tests/test_http.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/tests/test_package.py +0 -0
- {abysshub-0.1.1.dev2 → abysshub-0.1.1.dev3}/tests/test_replay.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: abysshub
|
|
3
|
-
Version: 0.1.1.
|
|
3
|
+
Version: 0.1.1.dev3
|
|
4
4
|
Summary: The official Python library for the Abyss API.
|
|
5
5
|
Project-URL: Homepage, https://abysshub.com
|
|
6
6
|
Project-URL: Source, https://github.com/Abysshub/abysshub-sdk
|
|
@@ -135,6 +135,7 @@ A refusal, a failed run, a timeout and a lost connection all raise one class, `A
|
|
|
135
135
|
| `request_id` | quote it to support, else `None` |
|
|
136
136
|
| `shortfall` | with `insufficient_funds`: how much Byssium the wallet is short, else `None` |
|
|
137
137
|
| `price` | with `price_above_max`: what the run costs now, else `None` |
|
|
138
|
+
| `run_id` | the id of the run the call started or read, once the press's `Location` or a run body named it, else `None` |
|
|
138
139
|
| `run` | the run, for a failed run or a timeout, else `None`; an `AsyncRun` from `AsyncAbyss` |
|
|
139
140
|
|
|
140
141
|
- **Refusals:** `invalid_api_key` (401), `not_found` (404), `invalid_request`, `invalid_input`, `held_input_*`, `input_unreachable` and `idempotency_key_reused` (422), `insufficient_funds` (402), `spend_cap_reached` and `price_above_max` (400), `idempotency_key_in_use` (409), `rate_limited` (429), `server_error` (500).
|
|
@@ -161,11 +162,12 @@ There is no timeout by default: `run()` waits as long as the run takes. `timeout
|
|
|
161
162
|
try:
|
|
162
163
|
run = abyss.run("widget_131", input, timeout=60)
|
|
163
164
|
except AbyssError as error:
|
|
164
|
-
if error.code == "timeout":
|
|
165
|
-
|
|
165
|
+
if error.code == "timeout" and error.run_id:
|
|
166
|
+
# The run goes on: wait for it again, without pressing (and paying) twice.
|
|
167
|
+
run = abyss.runs.get(error.run_id, wait=True)
|
|
166
168
|
```
|
|
167
169
|
|
|
168
|
-
It raises `AbyssError` with `code == "timeout"
|
|
170
|
+
It raises `AbyssError` with `code == "timeout"`, `run`, the run as last seen or `None`, and `run_id`, the run's id. The press answers its `Location` at once, so `run_id` is set as soon as the press has answered, even while no run has been seen yet; it is `None` only when the waiting stopped before that. Nothing is sent after the waiting stops.
|
|
169
171
|
|
|
170
172
|
## Async
|
|
171
173
|
|
|
@@ -118,6 +118,7 @@ A refusal, a failed run, a timeout and a lost connection all raise one class, `A
|
|
|
118
118
|
| `request_id` | quote it to support, else `None` |
|
|
119
119
|
| `shortfall` | with `insufficient_funds`: how much Byssium the wallet is short, else `None` |
|
|
120
120
|
| `price` | with `price_above_max`: what the run costs now, else `None` |
|
|
121
|
+
| `run_id` | the id of the run the call started or read, once the press's `Location` or a run body named it, else `None` |
|
|
121
122
|
| `run` | the run, for a failed run or a timeout, else `None`; an `AsyncRun` from `AsyncAbyss` |
|
|
122
123
|
|
|
123
124
|
- **Refusals:** `invalid_api_key` (401), `not_found` (404), `invalid_request`, `invalid_input`, `held_input_*`, `input_unreachable` and `idempotency_key_reused` (422), `insufficient_funds` (402), `spend_cap_reached` and `price_above_max` (400), `idempotency_key_in_use` (409), `rate_limited` (429), `server_error` (500).
|
|
@@ -144,11 +145,12 @@ There is no timeout by default: `run()` waits as long as the run takes. `timeout
|
|
|
144
145
|
try:
|
|
145
146
|
run = abyss.run("widget_131", input, timeout=60)
|
|
146
147
|
except AbyssError as error:
|
|
147
|
-
if error.code == "timeout":
|
|
148
|
-
|
|
148
|
+
if error.code == "timeout" and error.run_id:
|
|
149
|
+
# The run goes on: wait for it again, without pressing (and paying) twice.
|
|
150
|
+
run = abyss.runs.get(error.run_id, wait=True)
|
|
149
151
|
```
|
|
150
152
|
|
|
151
|
-
It raises `AbyssError` with `code == "timeout"
|
|
153
|
+
It raises `AbyssError` with `code == "timeout"`, `run`, the run as last seen or `None`, and `run_id`, the run's id. The press answers its `Location` at once, so `run_id` is set as soon as the press has answered, even while no run has been seen yet; it is `None` only when the waiting stopped before that. Nothing is sent after the waiting stops.
|
|
152
154
|
|
|
153
155
|
## Async
|
|
154
156
|
|
|
@@ -14,6 +14,7 @@ from ._client import (
|
|
|
14
14
|
_Call,
|
|
15
15
|
_expect,
|
|
16
16
|
_has_ended,
|
|
17
|
+
_heard,
|
|
17
18
|
_is_key,
|
|
18
19
|
_is_run,
|
|
19
20
|
_is_run_list,
|
|
@@ -260,7 +261,14 @@ class AsyncAbyss(_Base[AsyncRun]):
|
|
|
260
261
|
self._check(call)
|
|
261
262
|
try:
|
|
262
263
|
answer = await arequest(
|
|
263
|
-
self._http,
|
|
264
|
+
self._http,
|
|
265
|
+
method,
|
|
266
|
+
url,
|
|
267
|
+
sent_headers,
|
|
268
|
+
content,
|
|
269
|
+
self._timeouts(call),
|
|
270
|
+
call.deadline,
|
|
271
|
+
lambda headers: _heard(call, headers),
|
|
264
272
|
)
|
|
265
273
|
except Stopped:
|
|
266
274
|
raise self._timed_out(call) from None
|
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import math
|
|
5
5
|
import os
|
|
6
6
|
import platform
|
|
7
|
+
import re
|
|
7
8
|
import time
|
|
8
9
|
import uuid
|
|
9
10
|
from collections.abc import Callable, Mapping
|
|
@@ -25,6 +26,9 @@ API = "https://api.abysshub.com"
|
|
|
25
26
|
DEV_API = "https://api.dev.abysshub.com"
|
|
26
27
|
DEV_KEY_PREFIX = "abyss_sk_dev_"
|
|
27
28
|
|
|
29
|
+
RUN_LOCATION = re.compile(r"/v1/runs/([^/?#]+)/?(?:[?#]|$)")
|
|
30
|
+
"""A ``Location`` naming a run, ``…/v1/runs/{id}``, with the id as its group."""
|
|
31
|
+
|
|
28
32
|
REATTACH_PAUSE = 1.0
|
|
29
33
|
"""Seconds between two re-attaches that both ended without the run's ending."""
|
|
30
34
|
|
|
@@ -55,16 +59,21 @@ class _Reply:
|
|
|
55
59
|
"""Whether the connection was cut, or went silent, after the headers."""
|
|
56
60
|
location: str | None
|
|
57
61
|
request_id: str | None
|
|
62
|
+
run_id: str | None
|
|
63
|
+
"""The id of the call's run, as far as the call knew it when this reply arrived."""
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
@dataclass
|
|
61
67
|
class _Call:
|
|
62
|
-
"""One call's waiting: when it stops, and the run as last
|
|
68
|
+
"""One call's waiting: when it stops, and what a timeout carries: the run as last
|
|
69
|
+
seen, the run's id once known, and the latest ``Request-Id``."""
|
|
63
70
|
|
|
64
71
|
timeout: float | None
|
|
65
72
|
deadline: float | None
|
|
66
73
|
"""A ``time.monotonic()`` reading, or None to wait as long as it takes."""
|
|
67
74
|
seen: dict[str, Any] | None = None
|
|
75
|
+
run_id: str | None = None
|
|
76
|
+
request_id: str | None = None
|
|
68
77
|
|
|
69
78
|
def left(self) -> float | None:
|
|
70
79
|
return None if self.deadline is None else self.deadline - time.monotonic()
|
|
@@ -182,6 +191,8 @@ class _Base(Generic[R]):
|
|
|
182
191
|
return AbyssError(
|
|
183
192
|
"timeout",
|
|
184
193
|
f"Stopped waiting after {call.timeout} s. The run goes on.",
|
|
194
|
+
request_id=call.request_id,
|
|
195
|
+
run_id=call.run_id,
|
|
185
196
|
run=self._run(call.seen) if call.seen is not None else None,
|
|
186
197
|
)
|
|
187
198
|
|
|
@@ -268,6 +279,7 @@ class _Base(Generic[R]):
|
|
|
268
279
|
"unexpected_response",
|
|
269
280
|
"The press answered without a Location to read the run at.",
|
|
270
281
|
request_id=reply.request_id,
|
|
282
|
+
run_id=reply.run_id,
|
|
271
283
|
)
|
|
272
284
|
return str(httpx.URL(f"{self.base_url}/").join(location).copy_set_param("wait", "true"))
|
|
273
285
|
|
|
@@ -290,6 +302,7 @@ class _Base(Generic[R]):
|
|
|
290
302
|
raise AbyssError(
|
|
291
303
|
"connection_error",
|
|
292
304
|
"The connection dropped, or timed out, before the API answered.",
|
|
305
|
+
run_id=call.run_id,
|
|
293
306
|
) from error
|
|
294
307
|
return _backoff(attempt)
|
|
295
308
|
|
|
@@ -302,10 +315,12 @@ class _Base(Generic[R]):
|
|
|
302
315
|
run = parsed if _is_run(parsed) else None
|
|
303
316
|
if run is not None:
|
|
304
317
|
call.seen = run
|
|
305
|
-
|
|
318
|
+
call.run_id = run["id"]
|
|
319
|
+
location = answer.headers.get("location")
|
|
320
|
+
return _Reply(parsed, run, answer.text is None, location, request_id, call.run_id)
|
|
306
321
|
pause = _retry_pause(answer, attempt)
|
|
307
322
|
if pause is None or attempt >= limit:
|
|
308
|
-
raise _refusal(answer.status, parsed, request_id)
|
|
323
|
+
raise _refusal(answer.status, parsed, request_id, call.run_id)
|
|
309
324
|
return pause
|
|
310
325
|
|
|
311
326
|
def _ended(self, reply: _Reply) -> R:
|
|
@@ -314,7 +329,7 @@ class _Base(Generic[R]):
|
|
|
314
329
|
run = self._run(reply.run)
|
|
315
330
|
if run.status == "failed":
|
|
316
331
|
error = run.error or RunError("platform_fault", "The run failed.")
|
|
317
|
-
raise AbyssError(error.code, error.message, request_id=reply.request_id, run=run)
|
|
332
|
+
raise AbyssError(error.code, error.message, request_id=reply.request_id, run_id=run.id, run=run)
|
|
318
333
|
return run
|
|
319
334
|
|
|
320
335
|
|
|
@@ -481,7 +496,16 @@ class Abyss(_Base[Run]):
|
|
|
481
496
|
while True:
|
|
482
497
|
self._check(call)
|
|
483
498
|
try:
|
|
484
|
-
answer = request(
|
|
499
|
+
answer = request(
|
|
500
|
+
self._http,
|
|
501
|
+
method,
|
|
502
|
+
url,
|
|
503
|
+
sent_headers,
|
|
504
|
+
content,
|
|
505
|
+
self._timeouts(call),
|
|
506
|
+
call.deadline,
|
|
507
|
+
lambda headers: _heard(call, headers),
|
|
508
|
+
)
|
|
485
509
|
except Stopped:
|
|
486
510
|
raise self._timed_out(call) from None
|
|
487
511
|
except httpx.TransportError as error:
|
|
@@ -495,6 +519,14 @@ class Abyss(_Base[Run]):
|
|
|
495
519
|
attempt += 1
|
|
496
520
|
|
|
497
521
|
|
|
522
|
+
def _heard(call: _Call, headers: httpx.Headers) -> None:
|
|
523
|
+
"""Notes a response's headers as they arrive: its ``Request-Id``, and the run its ``Location`` names."""
|
|
524
|
+
call.request_id = headers.get("request-id", call.request_id)
|
|
525
|
+
match = RUN_LOCATION.search(headers.get("location", ""))
|
|
526
|
+
if match is not None:
|
|
527
|
+
call.run_id = match.group(1)
|
|
528
|
+
|
|
529
|
+
|
|
498
530
|
def _retry_pause(answer: Answer, attempt: int) -> float | None:
|
|
499
531
|
"""How long to wait before sending a refused request again, or None when it is not retried."""
|
|
500
532
|
if answer.status in (409, 429):
|
|
@@ -516,10 +548,10 @@ def _expect(reply: _Reply, is_: Callable[[Any], bool], message: str) -> Any:
|
|
|
516
548
|
"""The reply's body when it is what the route answers, else ``unexpected_response``."""
|
|
517
549
|
if is_(reply.body):
|
|
518
550
|
return reply.body
|
|
519
|
-
raise AbyssError("unexpected_response", message, request_id=reply.request_id)
|
|
551
|
+
raise AbyssError("unexpected_response", message, request_id=reply.request_id, run_id=reply.run_id)
|
|
520
552
|
|
|
521
553
|
|
|
522
|
-
def _refusal(status: int, body: Any, request_id: str | None) -> AbyssError:
|
|
554
|
+
def _refusal(status: int, body: Any, request_id: str | None, run_id: str | None) -> AbyssError:
|
|
523
555
|
error = body.get("error") if isinstance(body, dict) else None
|
|
524
556
|
if not isinstance(error, dict) or not isinstance(error.get("code"), str):
|
|
525
557
|
return AbyssError(
|
|
@@ -527,6 +559,7 @@ def _refusal(status: int, body: Any, request_id: str | None) -> AbyssError:
|
|
|
527
559
|
f"The API answered {status} without an error body.",
|
|
528
560
|
status=status,
|
|
529
561
|
request_id=request_id,
|
|
562
|
+
run_id=run_id,
|
|
530
563
|
)
|
|
531
564
|
body_request_id = body.get("request_id")
|
|
532
565
|
return AbyssError(
|
|
@@ -538,6 +571,7 @@ def _refusal(status: int, body: Any, request_id: str | None) -> AbyssError:
|
|
|
538
571
|
shortfall=_number(error.get("shortfall")),
|
|
539
572
|
price=_number(error.get("price")),
|
|
540
573
|
request_id=body_request_id if isinstance(body_request_id, str) else request_id,
|
|
574
|
+
run_id=run_id,
|
|
541
575
|
)
|
|
542
576
|
|
|
543
577
|
|
|
@@ -19,6 +19,10 @@ class AbyssError(Exception):
|
|
|
19
19
|
param: str | None
|
|
20
20
|
doc_url: str | None
|
|
21
21
|
request_id: str | None
|
|
22
|
+
run_id: str | None
|
|
23
|
+
"""The id of the run the call started or read, once the call has learned it from a
|
|
24
|
+
``Location`` header or a run body; else None. A ``timeout`` carries it even when no
|
|
25
|
+
run body has arrived, so ``runs.get(run_id, wait=True)`` finds the run again."""
|
|
22
26
|
shortfall: float | None
|
|
23
27
|
"""With ``insufficient_funds``: how much Byssium the wallet is short; else None."""
|
|
24
28
|
price: float | None
|
|
@@ -35,6 +39,7 @@ class AbyssError(Exception):
|
|
|
35
39
|
param: str | None = None,
|
|
36
40
|
doc_url: str | None = None,
|
|
37
41
|
request_id: str | None = None,
|
|
42
|
+
run_id: str | None = None,
|
|
38
43
|
shortfall: float | None = None,
|
|
39
44
|
price: float | None = None,
|
|
40
45
|
run: Run | AsyncRun | None = None,
|
|
@@ -46,6 +51,7 @@ class AbyssError(Exception):
|
|
|
46
51
|
self.param = param
|
|
47
52
|
self.doc_url = doc_url
|
|
48
53
|
self.request_id = request_id
|
|
54
|
+
self.run_id = run_id
|
|
49
55
|
self.shortfall = shortfall
|
|
50
56
|
self.price = price
|
|
51
57
|
self.run = run
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
|
|
6
7
|
import httpx
|
|
@@ -37,16 +38,21 @@ def request(
|
|
|
37
38
|
content: bytes | None = None,
|
|
38
39
|
timeout: httpx.Timeout | None = None,
|
|
39
40
|
deadline: float | None = None,
|
|
41
|
+
heard: Callable[[httpx.Headers], None] | None = None,
|
|
40
42
|
) -> Answer:
|
|
41
43
|
"""Sends one request and reads its whole body.
|
|
42
44
|
|
|
43
45
|
A failure before the headers raises ``httpx.TransportError``; a cut after them
|
|
44
46
|
answers ``text=None``, so the caller can re-attach. Past ``deadline`` (a
|
|
45
47
|
``time.monotonic()`` reading), a hold that still sends its spaces raises ``Stopped``.
|
|
48
|
+
``heard`` is handed the headers as soon as they arrive, so a stop during the body
|
|
49
|
+
still leaves them known.
|
|
46
50
|
"""
|
|
47
51
|
with client.stream(
|
|
48
52
|
method, url, headers=headers, content=content, timeout=timeout or client.timeout
|
|
49
53
|
) as response:
|
|
54
|
+
if heard is not None:
|
|
55
|
+
heard(response.headers)
|
|
50
56
|
chunks = []
|
|
51
57
|
try:
|
|
52
58
|
for chunk in response.iter_bytes():
|
|
@@ -66,11 +72,14 @@ async def arequest(
|
|
|
66
72
|
content: bytes | None = None,
|
|
67
73
|
timeout: httpx.Timeout | None = None,
|
|
68
74
|
deadline: float | None = None,
|
|
75
|
+
heard: Callable[[httpx.Headers], None] | None = None,
|
|
69
76
|
) -> Answer:
|
|
70
77
|
"""``request()`` on httpx's async client."""
|
|
71
78
|
async with client.stream(
|
|
72
79
|
method, url, headers=headers, content=content, timeout=timeout or client.timeout
|
|
73
80
|
) as response:
|
|
81
|
+
if heard is not None:
|
|
82
|
+
heard(response.headers)
|
|
74
83
|
chunks = []
|
|
75
84
|
try:
|
|
76
85
|
async for chunk in response.aiter_bytes():
|
|
@@ -33,6 +33,7 @@ def test_the_clients_timeout_applies_to_every_call() -> None:
|
|
|
33
33
|
abyss.run(WIDGET, INPUT)
|
|
34
34
|
assert raised.value.code == "timeout"
|
|
35
35
|
assert raised.value.run is not None and raised.value.run.id == ID
|
|
36
|
+
assert raised.value.run_id == ID
|
|
36
37
|
|
|
37
38
|
against(work)
|
|
38
39
|
|
|
@@ -62,5 +63,6 @@ def test_async_the_clients_timeout_applies_and_a_calls_overrides_it(client: floa
|
|
|
62
63
|
assert raised.value.code == "timeout"
|
|
63
64
|
error_run = raised.value.run
|
|
64
65
|
assert isinstance(error_run, AsyncRun) and error_run.id == ID and error_run.status == "queued"
|
|
66
|
+
assert raised.value.run_id == ID
|
|
65
67
|
|
|
66
68
|
against(lambda base: asyncio.run(run(base)))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|