abysshub 0.0.1.dev8__tar.gz → 0.0.1.dev10__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.0.1.dev10/PKG-INFO +167 -0
- abysshub-0.0.1.dev10/README.md +150 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/pyproject.toml +1 -1
- abysshub-0.0.1.dev10/src/abysshub/__init__.py +28 -0
- abysshub-0.0.1.dev10/src/abysshub/_client.py +503 -0
- abysshub-0.0.1.dev10/src/abysshub/_files.py +94 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/src/abysshub/_http.py +18 -4
- abysshub-0.0.1.dev10/src/abysshub/_run.py +130 -0
- abysshub-0.0.1.dev10/src/abysshub/_types.py +117 -0
- abysshub-0.0.1.dev10/tests/caller.py +55 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/tests/fake_server.py +54 -6
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/tests/test_client.py +18 -0
- abysshub-0.0.1.dev10/tests/test_files.py +117 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/tests/test_replay.py +29 -27
- abysshub-0.0.1.dev10/tests/test_timeout.py +51 -0
- abysshub-0.0.1.dev8/PKG-INFO +0 -74
- abysshub-0.0.1.dev8/README.md +0 -57
- abysshub-0.0.1.dev8/src/abysshub/__init__.py +0 -6
- abysshub-0.0.1.dev8/src/abysshub/_client.py +0 -196
- abysshub-0.0.1.dev8/src/abysshub/_run.py +0 -61
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/.gitignore +0 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/LICENSE +0 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/src/abysshub/_error.py +0 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/src/abysshub/py.typed +0 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/tests/test_http.py +0 -0
- {abysshub-0.0.1.dev8 → abysshub-0.0.1.dev10}/tests/test_package.py +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: abysshub
|
|
3
|
+
Version: 0.0.1.dev10
|
|
4
|
+
Summary: The official Python library for the Abyss API.
|
|
5
|
+
Project-URL: Homepage, https://abysshub.com
|
|
6
|
+
Project-URL: Source, https://github.com/Abysshub/abysshub-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/Abysshub/abysshub-sdk/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: httpx<1,>=0.27
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# abysshub
|
|
19
|
+
|
|
20
|
+
The official Python library for the [Abyss](https://abysshub.com) API.
|
|
21
|
+
|
|
22
|
+
Pre-release: the Abyss API opens soon.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from abysshub import Abyss, AbyssError
|
|
26
|
+
|
|
27
|
+
abyss = Abyss() # reads ABYSS_API_KEY
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
run = abyss.run("widget_131", {"amount": 250000})
|
|
31
|
+
print(run.result)
|
|
32
|
+
except AbyssError as error:
|
|
33
|
+
print(error.code, error.param, error.request_id, error.run)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`run()` presses the Widget and waits for the run to end, however long it takes, then returns the succeeded run. A refusal or a failed run raises `AbyssError`.
|
|
37
|
+
|
|
38
|
+
It runs on Python 3.10+, with one dependency, `httpx`. The client is synchronous; from async code, run it in a thread:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
run = await asyncio.to_thread(abyss.run, "widget_131", {"amount": 250000})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## The client
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
abyss = Abyss(api_key=None, base_url=None, max_retries=2, timeout=None)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **`api_key`:** defaults to `ABYSS_API_KEY`. Without a key, `Abyss()` raises `AbyssError` with `code == "invalid_api_key"`.
|
|
51
|
+
- **`base_url`:** defaults to `ABYSS_BASE_URL`, else `https://api.abysshub.com`. A key starting `abyss_sk_dev_` goes to `https://api.dev.abysshub.com`.
|
|
52
|
+
- **`max_retries`:** how many times a request is sent again (see [Retries](#retries)). Defaults to 2.
|
|
53
|
+
- **`timeout`:** the seconds every call waits at most, unless it sets its own (see [Timeouts](#timeouts)). None by default.
|
|
54
|
+
|
|
55
|
+
Every request sends `Authorization: Bearer <key>` and `User-Agent: abysshub-python/<version> (python <version>)`.
|
|
56
|
+
|
|
57
|
+
`abyss.close()` closes its connections; `with Abyss() as abyss:` does it for you.
|
|
58
|
+
|
|
59
|
+
## Running a Widget
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
run = abyss.run(widget, input, max_price=None, idempotency_key=None, timeout=None)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- **`widget`** is `widget_<id>`, or an unlisted Widget's private token.
|
|
66
|
+
- **`input`** is the Widget's Fields by name, as a `dict` (`abyss.widgets.get(widget)` lists them as `input_schema`). A file Field takes a file (see [Files](#files)).
|
|
67
|
+
- **`max_price`:** the most the run may cost, in Byssium. A higher price is refused with `price_above_max`, and nothing is charged.
|
|
68
|
+
- **`idempotency_key`:** each call presses with a fresh UUID `Idempotency-Key` unless you give one. Retries send the same one, so a run is never made or charged twice.
|
|
69
|
+
- **`timeout`:** the seconds to wait at most (see [Timeouts](#timeouts)).
|
|
70
|
+
|
|
71
|
+
The press holds until the run ends. When the hold ends early (a deploy cuts it, the API's holds are all taken, or an hour passes), `run()` re-attaches with `GET <Location>?wait=true` until the run ends. Re-attaching has no limit and is not a retry. Connecting has 10 s, and 30 s of silence counts as a cut.
|
|
72
|
+
|
|
73
|
+
A **run** is a dataclass with `/v1`'s fields: `id`, `widget`, `status` (`queued`, `running`, `succeeded` or `failed`), `price`, `result` (the Widget's JSON, a `dict` when it wrote an object), `output_files`, `error`, `created_at`, `started_at` and `ended_at`.
|
|
74
|
+
|
|
75
|
+
## The routes
|
|
76
|
+
|
|
77
|
+
Under `run()` there is one method per route. Each also takes `timeout=` as a keyword.
|
|
78
|
+
|
|
79
|
+
| Method | Route | Returns |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `abyss.runs.create(widget, input, max_price=None, idempotency_key=None)` | `POST /v1/widgets/{widget}/runs` with `"wait": false` | the `Run`, `queued`, at once |
|
|
82
|
+
| `abyss.runs.get(id, wait=False)` | `GET /v1/runs/{id}`, with `?wait=true` when `wait` | the `Run` |
|
|
83
|
+
| `abyss.runs.list(limit=None, starting_after=None)` | `GET /v1/runs?limit=&starting_after=` | a `RunList`: `data`, `has_more`, newest first |
|
|
84
|
+
| `abyss.widgets.get(widget)` | `GET /v1/widgets/{widget}` | a `Widget`: `id`, `name`, `description`, `url`, `price`, `free_runs`, `input_schema` |
|
|
85
|
+
| `abyss.uploads.create(widget, field, filename, size=None)` | `POST /v1/widgets/{widget}/uploads` | an `Upload`: `id`, `upload.url`, `upload.fields` |
|
|
86
|
+
| `abyss.key()` | `GET /v1/key` | a `Key`: `name`, `key`, `user`, `spend_cap`, `spent_this_month`, `created_at` |
|
|
87
|
+
|
|
88
|
+
Each is a dataclass with `/v1`'s fields; `free_runs` is a `FreeRuns` (`limit`, `remaining`) or `None`, and `input_schema` stays a `dict`.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
queued = abyss.runs.create("widget_131", {"amount": 250000})
|
|
92
|
+
# … later, or in another process:
|
|
93
|
+
run = abyss.runs.get(queued.id, wait=True)
|
|
94
|
+
|
|
95
|
+
page = abyss.runs.list(limit=100)
|
|
96
|
+
while page.has_more:
|
|
97
|
+
page = abyss.runs.list(limit=100, starting_after=page.data[-1].id)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **`runs.create()`** uploads the input's files like `run()`, then returns without waiting.
|
|
101
|
+
- **`runs.get()`** returns the run as it is, failed or not: only `run()` raises a failed run. With `wait=True`, it holds until the run ends, reading again when the hold is cut. A hold answers after an hour, or at once when the API's holds are all taken, so read again while `status` is `queued` or `running`.
|
|
102
|
+
- **`runs.list()`** returns each of `data` as a `Run`, with `save()` and `output_files`.
|
|
103
|
+
- **`uploads.create()`** only asks for the grant. To upload by hand, POST every `upload.fields` entry, then the file as `file`, to `upload.url`, and put `id` in the input.
|
|
104
|
+
|
|
105
|
+
## Files
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from abysshub import Abyss, file
|
|
109
|
+
|
|
110
|
+
run = abyss.run("widget_1660", {
|
|
111
|
+
"report": file("report.pdf"),
|
|
112
|
+
"data": [file("jan.csv"), file(csv_bytes, filename="feb.csv"), "https://example.com/sales/mar.csv"],
|
|
113
|
+
})
|
|
114
|
+
paths = run.save("output") # [Path("output/charts/a.png"), Path("output/output.json")]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- **`file(path)`** is a file on disk, read when it is uploaded. A file Field also takes a `Path`, an open file (`open("report.pdf", "rb")`) or `bytes`. `file(data, filename=...)` names data. A file without a name uploads under its Field's name.
|
|
118
|
+
- **A string passes through** as an `https` URL or an upload id. A multi-file Field takes a list, which may mix all three kinds.
|
|
119
|
+
- **Before the press,** each file is uploaded with `POST /v1/widgets/{widget}/uploads`, then sent to storage, in parallel, and its id goes in the input. An upload refusal (`held_input_*`) raises before any press. A press that drops before any headers is sent again with the same ids; it never uploads again.
|
|
120
|
+
- **`run.save(dir)`** writes every output file under `dir` at its `path`, making folders, and returns the paths it wrote.
|
|
121
|
+
- **Each of `run.output_files`** has `f.save(path)` and `f.read()`, which returns `bytes`. An expired URL is refreshed once, by reading the run again with `GET /v1/runs/{id}`.
|
|
122
|
+
|
|
123
|
+
## Errors
|
|
124
|
+
|
|
125
|
+
A refusal, a failed run, a timeout and a lost connection all raise one class, `AbyssError`. There is no class per status: branch on `code`.
|
|
126
|
+
|
|
127
|
+
| Field | |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `code` | `/v1`'s error code, a failed run's `error.code`, or a library-only code below |
|
|
130
|
+
| `message` | what to do, for a person |
|
|
131
|
+
| `status` | the HTTP status of a refusal, else `None` |
|
|
132
|
+
| `param` | the Field, body key, query key or header at fault, else `None` |
|
|
133
|
+
| `doc_url` | the code on the API access page, else `None` |
|
|
134
|
+
| `request_id` | quote it to support, else `None` |
|
|
135
|
+
| `run` | the run, for a failed run or a timeout, else `None` |
|
|
136
|
+
|
|
137
|
+
- **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).
|
|
138
|
+
- **A failed run:** `widget_fault`, `platform_fault` or `budget_exceeded`, with the run as `run`.
|
|
139
|
+
- **Library-only codes:** `timeout` (see [Timeouts](#timeouts)); `connection_error`, when the connection still drops before any answer after the retries, with the cause as `__cause__`; and `unexpected_response`, when an answer is not what the route sends.
|
|
140
|
+
|
|
141
|
+
## Retries
|
|
142
|
+
|
|
143
|
+
A request is sent again, at most `max_retries` times (2 by default), only after:
|
|
144
|
+
|
|
145
|
+
- **a drop before any headers arrived:** a short backoff, then the same request, with the same `Idempotency-Key` for a press;
|
|
146
|
+
- **`429 rate_limited` and `409 idempotency_key_in_use`:** after the `Retry-After` seconds;
|
|
147
|
+
- **a `5xx`:** a short backoff (0.5 s, then 1 s).
|
|
148
|
+
|
|
149
|
+
When the last try fails too, it raises. Every other refusal raises at once, and re-attaching never counts as a retry.
|
|
150
|
+
|
|
151
|
+
## Timeouts
|
|
152
|
+
|
|
153
|
+
There is no timeout by default: `run()` waits as long as the run takes. `timeout` (seconds), on the client or on a call, stops the waiting, never the run.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
try:
|
|
157
|
+
run = abyss.run("widget_131", input, timeout=60)
|
|
158
|
+
except AbyssError as error:
|
|
159
|
+
if error.code == "timeout":
|
|
160
|
+
... # The run goes on. error.run is the run as last seen, or None when none was seen yet.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
It raises `AbyssError` with `code == "timeout"` and `run`, the run as last seen. During the press's first hold the run has not been seen yet, so `run` is `None`; to keep its `id` whatever happens, press with `runs.create()` and wait with `runs.get(id, wait=True, timeout=...)`.
|
|
164
|
+
|
|
165
|
+
## Names
|
|
166
|
+
|
|
167
|
+
Returned data keeps `/v1`'s names: `output_files`, `has_more`, `request_id`. The library's own options are snake_case: `max_price`, `starting_after`. A Field's name is never converted: `input` is sent as you write it.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# abysshub
|
|
2
|
+
|
|
3
|
+
The official Python library for the [Abyss](https://abysshub.com) API.
|
|
4
|
+
|
|
5
|
+
Pre-release: the Abyss API opens soon.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from abysshub import Abyss, AbyssError
|
|
9
|
+
|
|
10
|
+
abyss = Abyss() # reads ABYSS_API_KEY
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
run = abyss.run("widget_131", {"amount": 250000})
|
|
14
|
+
print(run.result)
|
|
15
|
+
except AbyssError as error:
|
|
16
|
+
print(error.code, error.param, error.request_id, error.run)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`run()` presses the Widget and waits for the run to end, however long it takes, then returns the succeeded run. A refusal or a failed run raises `AbyssError`.
|
|
20
|
+
|
|
21
|
+
It runs on Python 3.10+, with one dependency, `httpx`. The client is synchronous; from async code, run it in a thread:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
run = await asyncio.to_thread(abyss.run, "widget_131", {"amount": 250000})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## The client
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
abyss = Abyss(api_key=None, base_url=None, max_retries=2, timeout=None)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **`api_key`:** defaults to `ABYSS_API_KEY`. Without a key, `Abyss()` raises `AbyssError` with `code == "invalid_api_key"`.
|
|
34
|
+
- **`base_url`:** defaults to `ABYSS_BASE_URL`, else `https://api.abysshub.com`. A key starting `abyss_sk_dev_` goes to `https://api.dev.abysshub.com`.
|
|
35
|
+
- **`max_retries`:** how many times a request is sent again (see [Retries](#retries)). Defaults to 2.
|
|
36
|
+
- **`timeout`:** the seconds every call waits at most, unless it sets its own (see [Timeouts](#timeouts)). None by default.
|
|
37
|
+
|
|
38
|
+
Every request sends `Authorization: Bearer <key>` and `User-Agent: abysshub-python/<version> (python <version>)`.
|
|
39
|
+
|
|
40
|
+
`abyss.close()` closes its connections; `with Abyss() as abyss:` does it for you.
|
|
41
|
+
|
|
42
|
+
## Running a Widget
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
run = abyss.run(widget, input, max_price=None, idempotency_key=None, timeout=None)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **`widget`** is `widget_<id>`, or an unlisted Widget's private token.
|
|
49
|
+
- **`input`** is the Widget's Fields by name, as a `dict` (`abyss.widgets.get(widget)` lists them as `input_schema`). A file Field takes a file (see [Files](#files)).
|
|
50
|
+
- **`max_price`:** the most the run may cost, in Byssium. A higher price is refused with `price_above_max`, and nothing is charged.
|
|
51
|
+
- **`idempotency_key`:** each call presses with a fresh UUID `Idempotency-Key` unless you give one. Retries send the same one, so a run is never made or charged twice.
|
|
52
|
+
- **`timeout`:** the seconds to wait at most (see [Timeouts](#timeouts)).
|
|
53
|
+
|
|
54
|
+
The press holds until the run ends. When the hold ends early (a deploy cuts it, the API's holds are all taken, or an hour passes), `run()` re-attaches with `GET <Location>?wait=true` until the run ends. Re-attaching has no limit and is not a retry. Connecting has 10 s, and 30 s of silence counts as a cut.
|
|
55
|
+
|
|
56
|
+
A **run** is a dataclass with `/v1`'s fields: `id`, `widget`, `status` (`queued`, `running`, `succeeded` or `failed`), `price`, `result` (the Widget's JSON, a `dict` when it wrote an object), `output_files`, `error`, `created_at`, `started_at` and `ended_at`.
|
|
57
|
+
|
|
58
|
+
## The routes
|
|
59
|
+
|
|
60
|
+
Under `run()` there is one method per route. Each also takes `timeout=` as a keyword.
|
|
61
|
+
|
|
62
|
+
| Method | Route | Returns |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `abyss.runs.create(widget, input, max_price=None, idempotency_key=None)` | `POST /v1/widgets/{widget}/runs` with `"wait": false` | the `Run`, `queued`, at once |
|
|
65
|
+
| `abyss.runs.get(id, wait=False)` | `GET /v1/runs/{id}`, with `?wait=true` when `wait` | the `Run` |
|
|
66
|
+
| `abyss.runs.list(limit=None, starting_after=None)` | `GET /v1/runs?limit=&starting_after=` | a `RunList`: `data`, `has_more`, newest first |
|
|
67
|
+
| `abyss.widgets.get(widget)` | `GET /v1/widgets/{widget}` | a `Widget`: `id`, `name`, `description`, `url`, `price`, `free_runs`, `input_schema` |
|
|
68
|
+
| `abyss.uploads.create(widget, field, filename, size=None)` | `POST /v1/widgets/{widget}/uploads` | an `Upload`: `id`, `upload.url`, `upload.fields` |
|
|
69
|
+
| `abyss.key()` | `GET /v1/key` | a `Key`: `name`, `key`, `user`, `spend_cap`, `spent_this_month`, `created_at` |
|
|
70
|
+
|
|
71
|
+
Each is a dataclass with `/v1`'s fields; `free_runs` is a `FreeRuns` (`limit`, `remaining`) or `None`, and `input_schema` stays a `dict`.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
queued = abyss.runs.create("widget_131", {"amount": 250000})
|
|
75
|
+
# … later, or in another process:
|
|
76
|
+
run = abyss.runs.get(queued.id, wait=True)
|
|
77
|
+
|
|
78
|
+
page = abyss.runs.list(limit=100)
|
|
79
|
+
while page.has_more:
|
|
80
|
+
page = abyss.runs.list(limit=100, starting_after=page.data[-1].id)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- **`runs.create()`** uploads the input's files like `run()`, then returns without waiting.
|
|
84
|
+
- **`runs.get()`** returns the run as it is, failed or not: only `run()` raises a failed run. With `wait=True`, it holds until the run ends, reading again when the hold is cut. A hold answers after an hour, or at once when the API's holds are all taken, so read again while `status` is `queued` or `running`.
|
|
85
|
+
- **`runs.list()`** returns each of `data` as a `Run`, with `save()` and `output_files`.
|
|
86
|
+
- **`uploads.create()`** only asks for the grant. To upload by hand, POST every `upload.fields` entry, then the file as `file`, to `upload.url`, and put `id` in the input.
|
|
87
|
+
|
|
88
|
+
## Files
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from abysshub import Abyss, file
|
|
92
|
+
|
|
93
|
+
run = abyss.run("widget_1660", {
|
|
94
|
+
"report": file("report.pdf"),
|
|
95
|
+
"data": [file("jan.csv"), file(csv_bytes, filename="feb.csv"), "https://example.com/sales/mar.csv"],
|
|
96
|
+
})
|
|
97
|
+
paths = run.save("output") # [Path("output/charts/a.png"), Path("output/output.json")]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **`file(path)`** is a file on disk, read when it is uploaded. A file Field also takes a `Path`, an open file (`open("report.pdf", "rb")`) or `bytes`. `file(data, filename=...)` names data. A file without a name uploads under its Field's name.
|
|
101
|
+
- **A string passes through** as an `https` URL or an upload id. A multi-file Field takes a list, which may mix all three kinds.
|
|
102
|
+
- **Before the press,** each file is uploaded with `POST /v1/widgets/{widget}/uploads`, then sent to storage, in parallel, and its id goes in the input. An upload refusal (`held_input_*`) raises before any press. A press that drops before any headers is sent again with the same ids; it never uploads again.
|
|
103
|
+
- **`run.save(dir)`** writes every output file under `dir` at its `path`, making folders, and returns the paths it wrote.
|
|
104
|
+
- **Each of `run.output_files`** has `f.save(path)` and `f.read()`, which returns `bytes`. An expired URL is refreshed once, by reading the run again with `GET /v1/runs/{id}`.
|
|
105
|
+
|
|
106
|
+
## Errors
|
|
107
|
+
|
|
108
|
+
A refusal, a failed run, a timeout and a lost connection all raise one class, `AbyssError`. There is no class per status: branch on `code`.
|
|
109
|
+
|
|
110
|
+
| Field | |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `code` | `/v1`'s error code, a failed run's `error.code`, or a library-only code below |
|
|
113
|
+
| `message` | what to do, for a person |
|
|
114
|
+
| `status` | the HTTP status of a refusal, else `None` |
|
|
115
|
+
| `param` | the Field, body key, query key or header at fault, else `None` |
|
|
116
|
+
| `doc_url` | the code on the API access page, else `None` |
|
|
117
|
+
| `request_id` | quote it to support, else `None` |
|
|
118
|
+
| `run` | the run, for a failed run or a timeout, else `None` |
|
|
119
|
+
|
|
120
|
+
- **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).
|
|
121
|
+
- **A failed run:** `widget_fault`, `platform_fault` or `budget_exceeded`, with the run as `run`.
|
|
122
|
+
- **Library-only codes:** `timeout` (see [Timeouts](#timeouts)); `connection_error`, when the connection still drops before any answer after the retries, with the cause as `__cause__`; and `unexpected_response`, when an answer is not what the route sends.
|
|
123
|
+
|
|
124
|
+
## Retries
|
|
125
|
+
|
|
126
|
+
A request is sent again, at most `max_retries` times (2 by default), only after:
|
|
127
|
+
|
|
128
|
+
- **a drop before any headers arrived:** a short backoff, then the same request, with the same `Idempotency-Key` for a press;
|
|
129
|
+
- **`429 rate_limited` and `409 idempotency_key_in_use`:** after the `Retry-After` seconds;
|
|
130
|
+
- **a `5xx`:** a short backoff (0.5 s, then 1 s).
|
|
131
|
+
|
|
132
|
+
When the last try fails too, it raises. Every other refusal raises at once, and re-attaching never counts as a retry.
|
|
133
|
+
|
|
134
|
+
## Timeouts
|
|
135
|
+
|
|
136
|
+
There is no timeout by default: `run()` waits as long as the run takes. `timeout` (seconds), on the client or on a call, stops the waiting, never the run.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
try:
|
|
140
|
+
run = abyss.run("widget_131", input, timeout=60)
|
|
141
|
+
except AbyssError as error:
|
|
142
|
+
if error.code == "timeout":
|
|
143
|
+
... # The run goes on. error.run is the run as last seen, or None when none was seen yet.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It raises `AbyssError` with `code == "timeout"` and `run`, the run as last seen. During the press's first hold the run has not been seen yet, so `run` is `None`; to keep its `id` whatever happens, press with `runs.create()` and wait with `runs.get(id, wait=True, timeout=...)`.
|
|
147
|
+
|
|
148
|
+
## Names
|
|
149
|
+
|
|
150
|
+
Returned data keeps `/v1`'s names: `output_files`, `has_more`, `request_id`. The library's own options are snake_case: `max_price`, `starting_after`. A Field's name is never converted: `input` is sent as you write it.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The official Python library for the Abyss API."""
|
|
2
|
+
from ._client import Abyss, Runs, Uploads, Widgets
|
|
3
|
+
from ._error import AbyssError
|
|
4
|
+
from ._files import FileData, InputFile, file
|
|
5
|
+
from ._run import Run, RunError, RunFile, RunStatus
|
|
6
|
+
from ._types import FreeRuns, Key, KeyUser, RunList, Upload, UploadForm, Widget
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Abyss",
|
|
10
|
+
"AbyssError",
|
|
11
|
+
"FileData",
|
|
12
|
+
"FreeRuns",
|
|
13
|
+
"InputFile",
|
|
14
|
+
"Key",
|
|
15
|
+
"KeyUser",
|
|
16
|
+
"Run",
|
|
17
|
+
"RunError",
|
|
18
|
+
"RunFile",
|
|
19
|
+
"RunList",
|
|
20
|
+
"RunStatus",
|
|
21
|
+
"Runs",
|
|
22
|
+
"Upload",
|
|
23
|
+
"UploadForm",
|
|
24
|
+
"Uploads",
|
|
25
|
+
"Widget",
|
|
26
|
+
"Widgets",
|
|
27
|
+
"file",
|
|
28
|
+
]
|