abysshub 0.0.1.dev16__tar.gz → 0.1.1.dev1__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.dev16 → abysshub-0.1.1.dev1}/PKG-INFO +23 -4
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/README.md +22 -3
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/pyproject.toml +1 -1
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/src/abysshub/__init__.py +10 -2
- abysshub-0.1.1.dev1/src/abysshub/_async_client.py +275 -0
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/src/abysshub/_client.py +199 -127
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/src/abysshub/_error.py +4 -3
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/src/abysshub/_files.py +38 -10
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/src/abysshub/_http.py +24 -0
- abysshub-0.1.1.dev1/src/abysshub/_run.py +214 -0
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/src/abysshub/_types.py +10 -1
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/tests/caller.py +22 -1
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/tests/fake_server.py +5 -1
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/tests/test_client.py +11 -1
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/tests/test_files.py +36 -4
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/tests/test_replay.py +57 -12
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/tests/test_timeout.py +16 -1
- abysshub-0.0.1.dev16/src/abysshub/_run.py +0 -130
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/.gitignore +0 -0
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/LICENSE +0 -0
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/src/abysshub/py.typed +0 -0
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/tests/test_http.py +0 -0
- {abysshub-0.0.1.dev16 → abysshub-0.1.1.dev1}/tests/test_package.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: abysshub
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1.dev1
|
|
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
|
|
@@ -35,10 +35,11 @@ except AbyssError as error:
|
|
|
35
35
|
|
|
36
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
37
|
|
|
38
|
-
It runs on Python 3.10+, with one dependency, `httpx`.
|
|
38
|
+
It runs on Python 3.10+, with one dependency, `httpx`. From async code, use `AsyncAbyss` (see [Async](#async)):
|
|
39
39
|
|
|
40
40
|
```python
|
|
41
|
-
|
|
41
|
+
async with AsyncAbyss() as abyss:
|
|
42
|
+
run = await abyss.run("widget_131", {"amount": 250000})
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
## The client
|
|
@@ -132,7 +133,7 @@ A refusal, a failed run, a timeout and a lost connection all raise one class, `A
|
|
|
132
133
|
| `param` | the Field, body key, query key or header at fault, else `None` |
|
|
133
134
|
| `doc_url` | the code on the API access page, else `None` |
|
|
134
135
|
| `request_id` | quote it to support, else `None` |
|
|
135
|
-
| `run` | the run, for a failed run or a timeout, else `None` |
|
|
136
|
+
| `run` | the run, for a failed run or a timeout, else `None`; an `AsyncRun` from `AsyncAbyss` |
|
|
136
137
|
|
|
137
138
|
- **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
139
|
- **A failed run:** `widget_fault`, `platform_fault` or `budget_exceeded`, with the run as `run`.
|
|
@@ -164,6 +165,24 @@ except AbyssError as error:
|
|
|
164
165
|
|
|
165
166
|
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=...)`.
|
|
166
167
|
|
|
168
|
+
## Async
|
|
169
|
+
|
|
170
|
+
`AsyncAbyss` is `Abyss` on httpx's async client. It takes the same options and has the same methods, each awaited; its retries, re-attaching and `timeout` behave the same.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from abysshub import AsyncAbyss, file
|
|
174
|
+
|
|
175
|
+
async with AsyncAbyss() as abyss: # or abyss = AsyncAbyss(), then await abyss.close()
|
|
176
|
+
run = await abyss.run("widget_1660", {"report": file("report.pdf")})
|
|
177
|
+
paths = await run.save("output")
|
|
178
|
+
queued = await abyss.runs.create("widget_131", {"amount": 250000})
|
|
179
|
+
run = await abyss.runs.get(queued.id, wait=True)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- **Runs:** it returns an `AsyncRun`, with the same fields as `Run`. Its `save(dir)` and each of its `output_files`' `save(path)` and `read()` are awaited. `runs.list()` returns an `AsyncRunList`, and an `AbyssError` raised by `AsyncAbyss` carries an `AsyncRun` as `run`.
|
|
183
|
+
- **Files** are uploaded as concurrent tasks, each read from disk in a thread.
|
|
184
|
+
- **`timeout=`** stops the waiting, as with `Abyss`. Cancelling the task stops it too, and raises `CancelledError`, not `AbyssError`.
|
|
185
|
+
|
|
167
186
|
## Names
|
|
168
187
|
|
|
169
188
|
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.
|
|
@@ -18,10 +18,11 @@ except AbyssError as error:
|
|
|
18
18
|
|
|
19
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
20
|
|
|
21
|
-
It runs on Python 3.10+, with one dependency, `httpx`.
|
|
21
|
+
It runs on Python 3.10+, with one dependency, `httpx`. From async code, use `AsyncAbyss` (see [Async](#async)):
|
|
22
22
|
|
|
23
23
|
```python
|
|
24
|
-
|
|
24
|
+
async with AsyncAbyss() as abyss:
|
|
25
|
+
run = await abyss.run("widget_131", {"amount": 250000})
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
## The client
|
|
@@ -115,7 +116,7 @@ A refusal, a failed run, a timeout and a lost connection all raise one class, `A
|
|
|
115
116
|
| `param` | the Field, body key, query key or header at fault, else `None` |
|
|
116
117
|
| `doc_url` | the code on the API access page, else `None` |
|
|
117
118
|
| `request_id` | quote it to support, else `None` |
|
|
118
|
-
| `run` | the run, for a failed run or a timeout, else `None` |
|
|
119
|
+
| `run` | the run, for a failed run or a timeout, else `None`; an `AsyncRun` from `AsyncAbyss` |
|
|
119
120
|
|
|
120
121
|
- **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
122
|
- **A failed run:** `widget_fault`, `platform_fault` or `budget_exceeded`, with the run as `run`.
|
|
@@ -147,6 +148,24 @@ except AbyssError as error:
|
|
|
147
148
|
|
|
148
149
|
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=...)`.
|
|
149
150
|
|
|
151
|
+
## Async
|
|
152
|
+
|
|
153
|
+
`AsyncAbyss` is `Abyss` on httpx's async client. It takes the same options and has the same methods, each awaited; its retries, re-attaching and `timeout` behave the same.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from abysshub import AsyncAbyss, file
|
|
157
|
+
|
|
158
|
+
async with AsyncAbyss() as abyss: # or abyss = AsyncAbyss(), then await abyss.close()
|
|
159
|
+
run = await abyss.run("widget_1660", {"report": file("report.pdf")})
|
|
160
|
+
paths = await run.save("output")
|
|
161
|
+
queued = await abyss.runs.create("widget_131", {"amount": 250000})
|
|
162
|
+
run = await abyss.runs.get(queued.id, wait=True)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- **Runs:** it returns an `AsyncRun`, with the same fields as `Run`. Its `save(dir)` and each of its `output_files`' `save(path)` and `read()` are awaited. `runs.list()` returns an `AsyncRunList`, and an `AbyssError` raised by `AsyncAbyss` carries an `AsyncRun` as `run`.
|
|
166
|
+
- **Files** are uploaded as concurrent tasks, each read from disk in a thread.
|
|
167
|
+
- **`timeout=`** stops the waiting, as with `Abyss`. Cancelling the task stops it too, and raises `CancelledError`, not `AbyssError`.
|
|
168
|
+
|
|
150
169
|
## Names
|
|
151
170
|
|
|
152
171
|
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.
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
"""The official Python library for the Abyss API."""
|
|
2
|
+
from ._async_client import AsyncAbyss, AsyncRuns, AsyncUploads, AsyncWidgets
|
|
2
3
|
from ._client import Abyss, Runs, Uploads, Widgets
|
|
3
4
|
from ._error import AbyssError
|
|
4
5
|
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
|
|
6
|
+
from ._run import AsyncRun, AsyncRunFile, Run, RunError, RunFile, RunStatus
|
|
7
|
+
from ._types import AsyncRunList, FreeRuns, Key, KeyUser, RunList, Upload, UploadForm, Widget
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"Abyss",
|
|
10
11
|
"AbyssError",
|
|
12
|
+
"AsyncAbyss",
|
|
13
|
+
"AsyncRun",
|
|
14
|
+
"AsyncRunFile",
|
|
15
|
+
"AsyncRunList",
|
|
16
|
+
"AsyncRuns",
|
|
17
|
+
"AsyncUploads",
|
|
18
|
+
"AsyncWidgets",
|
|
11
19
|
"FileData",
|
|
12
20
|
"FreeRuns",
|
|
13
21
|
"InputFile",
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import math
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._client import (
|
|
12
|
+
REATTACH_PAUSE,
|
|
13
|
+
_Base,
|
|
14
|
+
_Call,
|
|
15
|
+
_expect,
|
|
16
|
+
_has_ended,
|
|
17
|
+
_is_key,
|
|
18
|
+
_is_run,
|
|
19
|
+
_is_run_list,
|
|
20
|
+
_is_upload,
|
|
21
|
+
_is_widget,
|
|
22
|
+
_Reply,
|
|
23
|
+
)
|
|
24
|
+
from ._error import AbyssError
|
|
25
|
+
from ._files import upload_files_async
|
|
26
|
+
from ._http import Stopped, arequest, timeouts
|
|
27
|
+
from ._run import AsyncRun, parse_async_run
|
|
28
|
+
from ._types import AsyncRunList, Key, Upload, Widget, parse_key, parse_upload, parse_widget
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncRuns:
|
|
32
|
+
"""`/v1`'s run routes, one method each, awaited."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, client: AsyncAbyss) -> None:
|
|
35
|
+
self._client = client
|
|
36
|
+
|
|
37
|
+
async def create(
|
|
38
|
+
self,
|
|
39
|
+
widget: str,
|
|
40
|
+
input: Mapping[str, Any],
|
|
41
|
+
*,
|
|
42
|
+
max_price: float | None = None,
|
|
43
|
+
idempotency_key: str | None = None,
|
|
44
|
+
timeout: float | None = None,
|
|
45
|
+
) -> AsyncRun:
|
|
46
|
+
"""Uploads the input's files and presses with ``"wait": false``: returns the run at once, ``queued``."""
|
|
47
|
+
client = self._client
|
|
48
|
+
call = client._start(timeout)
|
|
49
|
+
press = await client._press(call, widget, input, max_price, idempotency_key, hold=False)
|
|
50
|
+
return client._run(_expect(press, _is_run, "The press answered without a run."))
|
|
51
|
+
|
|
52
|
+
async def get(self, id: str, wait: bool = False, *, timeout: float | None = None) -> AsyncRun:
|
|
53
|
+
"""Reads a run as it is, or with ``wait`` holds until it ends. A failed run is returned, not raised."""
|
|
54
|
+
client = self._client
|
|
55
|
+
return client._run(await client._get_run(client._start(timeout), id, wait))
|
|
56
|
+
|
|
57
|
+
async def list(
|
|
58
|
+
self,
|
|
59
|
+
limit: int | None = None,
|
|
60
|
+
starting_after: str | None = None,
|
|
61
|
+
*,
|
|
62
|
+
timeout: float | None = None,
|
|
63
|
+
) -> AsyncRunList:
|
|
64
|
+
"""Returns a page of runs, newest first."""
|
|
65
|
+
client = self._client
|
|
66
|
+
url = client._list_url(limit, starting_after)
|
|
67
|
+
page = await client._get(client._start(timeout), url, _is_run_list, "The run list answered without data.")
|
|
68
|
+
return AsyncRunList(data=[client._run(data) for data in page["data"]], has_more=page["has_more"])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AsyncWidgets:
|
|
72
|
+
"""`/v1`'s Widget route, awaited."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, client: AsyncAbyss) -> None:
|
|
75
|
+
self._client = client
|
|
76
|
+
|
|
77
|
+
async def get(self, widget: str, *, timeout: float | None = None) -> Widget:
|
|
78
|
+
"""Reads a Widget: its price, Free Runs and ``input_schema``."""
|
|
79
|
+
client = self._client
|
|
80
|
+
data = await client._get(
|
|
81
|
+
client._start(timeout), client._widget_url(widget), _is_widget, "The Widget answered without an id."
|
|
82
|
+
)
|
|
83
|
+
return parse_widget(data)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AsyncUploads:
|
|
87
|
+
"""`/v1`'s upload route, awaited."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, client: AsyncAbyss) -> None:
|
|
90
|
+
self._client = client
|
|
91
|
+
|
|
92
|
+
async def create(
|
|
93
|
+
self,
|
|
94
|
+
widget: str,
|
|
95
|
+
field: str,
|
|
96
|
+
filename: str,
|
|
97
|
+
size: int | None = None,
|
|
98
|
+
*,
|
|
99
|
+
timeout: float | None = None,
|
|
100
|
+
) -> Upload:
|
|
101
|
+
"""Asks for an upload's grant. ``run()`` and ``runs.create()`` upload files by themselves."""
|
|
102
|
+
client = self._client
|
|
103
|
+
return await client._grant(client._start(timeout), client._widget_url(widget), field, filename, size)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AsyncAbyss(_Base[AsyncRun]):
|
|
107
|
+
"""``Abyss`` for async code, on httpx's async client: every method is awaited.
|
|
108
|
+
|
|
109
|
+
The key defaults to ``ABYSS_API_KEY``, and the address to ``ABYSS_BASE_URL``,
|
|
110
|
+
else the address the key belongs to.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
runs: AsyncRuns
|
|
114
|
+
widgets: AsyncWidgets
|
|
115
|
+
uploads: AsyncUploads
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
api_key: str | None = None,
|
|
120
|
+
base_url: str | None = None,
|
|
121
|
+
max_retries: int = 2,
|
|
122
|
+
timeout: float | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
super().__init__(api_key, base_url, max_retries, timeout)
|
|
125
|
+
self.runs = AsyncRuns(self)
|
|
126
|
+
self.widgets = AsyncWidgets(self)
|
|
127
|
+
self.uploads = AsyncUploads(self)
|
|
128
|
+
self._http = httpx.AsyncClient(timeout=timeouts())
|
|
129
|
+
|
|
130
|
+
async def close(self) -> None:
|
|
131
|
+
"""Closes the client's connections."""
|
|
132
|
+
await self._http.aclose()
|
|
133
|
+
|
|
134
|
+
async def __aenter__(self) -> AsyncAbyss:
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
async def __aexit__(
|
|
138
|
+
self,
|
|
139
|
+
exc_type: type[BaseException] | None,
|
|
140
|
+
exc: BaseException | None,
|
|
141
|
+
tb: TracebackType | None,
|
|
142
|
+
) -> None:
|
|
143
|
+
await self.close()
|
|
144
|
+
|
|
145
|
+
async def run(
|
|
146
|
+
self,
|
|
147
|
+
widget: str,
|
|
148
|
+
input: Mapping[str, Any],
|
|
149
|
+
*,
|
|
150
|
+
max_price: float | None = None,
|
|
151
|
+
idempotency_key: str | None = None,
|
|
152
|
+
timeout: float | None = None,
|
|
153
|
+
) -> AsyncRun:
|
|
154
|
+
"""Uploads the input's files, presses a Widget and waits for the run's ending.
|
|
155
|
+
|
|
156
|
+
The same as ``Abyss.run()``, awaited: returns the succeeded run; raises
|
|
157
|
+
``AbyssError`` on a refusal or a failed run, and with ``code == "timeout"``
|
|
158
|
+
once ``timeout`` seconds have passed.
|
|
159
|
+
"""
|
|
160
|
+
call = self._start(timeout)
|
|
161
|
+
press = await self._press(call, widget, input, max_price, idempotency_key, hold=True)
|
|
162
|
+
return self._ended(press if _has_ended(press) else await self._hold(call, self._wait_url(press), _has_ended))
|
|
163
|
+
|
|
164
|
+
async def key(self, *, timeout: float | None = None) -> Key:
|
|
165
|
+
"""Returns the API Key making the call, with ``GET /v1/key``."""
|
|
166
|
+
url = f"{self.base_url}/v1/key"
|
|
167
|
+
return parse_key(await self._get(self._start(timeout), url, _is_key, "The key answered without a key."))
|
|
168
|
+
|
|
169
|
+
async def _sleep(self, call: _Call, seconds: float) -> None:
|
|
170
|
+
"""Waits ``seconds``, or raises ``timeout`` at the call's deadline."""
|
|
171
|
+
pause, stops = self._pause(call, seconds)
|
|
172
|
+
await asyncio.sleep(pause)
|
|
173
|
+
if stops:
|
|
174
|
+
raise self._timed_out(call)
|
|
175
|
+
|
|
176
|
+
async def _press(
|
|
177
|
+
self,
|
|
178
|
+
call: _Call,
|
|
179
|
+
widget: str,
|
|
180
|
+
input: Mapping[str, Any],
|
|
181
|
+
max_price: float | None,
|
|
182
|
+
idempotency_key: str | None,
|
|
183
|
+
hold: bool,
|
|
184
|
+
) -> _Reply:
|
|
185
|
+
"""Uploads the input's files and sends the press. Its retries never redo the uploads."""
|
|
186
|
+
widget_url = self._widget_url(widget)
|
|
187
|
+
uploaded = await upload_files_async(
|
|
188
|
+
input, lambda field, filename, data: self._upload(call, widget_url, field, filename, data)
|
|
189
|
+
)
|
|
190
|
+
headers, body = self._press_request(uploaded, max_price, idempotency_key, hold)
|
|
191
|
+
return await self._send(call, "POST", f"{widget_url}/runs", headers, body)
|
|
192
|
+
|
|
193
|
+
async def _grant(self, call: _Call, widget_url: str, field: str, filename: str, size: int | None) -> Upload:
|
|
194
|
+
"""Asks for an upload with ``POST /v1/widgets/{widget}/uploads``."""
|
|
195
|
+
body = self._grant_body(field, filename, size)
|
|
196
|
+
grant = await self._send(call, "POST", f"{widget_url}/uploads", {"Content-Type": "application/json"}, body)
|
|
197
|
+
return parse_upload(_expect(grant, _is_upload, "The upload answered without an id and a form."))
|
|
198
|
+
|
|
199
|
+
async def _upload(self, call: _Call, widget_url: str, field: str, filename: str, data: bytes) -> str:
|
|
200
|
+
"""Asks for an upload, sends the file to storage and answers the upload's id."""
|
|
201
|
+
grant = await self._grant(call, widget_url, field, filename, len(data))
|
|
202
|
+
stored: httpx.Response | httpx.TransportError
|
|
203
|
+
try:
|
|
204
|
+
stored = await self._http.post(
|
|
205
|
+
grant.upload.url,
|
|
206
|
+
data=grant.upload.fields,
|
|
207
|
+
files={"file": (filename, data)},
|
|
208
|
+
timeout=self._timeouts(call),
|
|
209
|
+
)
|
|
210
|
+
except httpx.TransportError as error:
|
|
211
|
+
stored = error
|
|
212
|
+
self._stored(call, field, filename, stored)
|
|
213
|
+
return grant.id
|
|
214
|
+
|
|
215
|
+
async def _download(self, url: str) -> tuple[int, bytes]:
|
|
216
|
+
"""Downloads an output file from storage, without the API key."""
|
|
217
|
+
try:
|
|
218
|
+
response = await self._http.get(url)
|
|
219
|
+
except httpx.TransportError as error:
|
|
220
|
+
raise AbyssError("connection_error", "The connection to storage dropped during a download.") from error
|
|
221
|
+
return response.status_code, response.content
|
|
222
|
+
|
|
223
|
+
async def _get_run(self, call: _Call, id: str, wait: bool) -> dict[str, Any]:
|
|
224
|
+
"""Reads a run with ``GET /v1/runs/{id}``, holding with ``?wait=true``. A cut hold is read again."""
|
|
225
|
+
reply = await self._hold(call, self._run_url(id, wait), lambda reply: not reply.cut)
|
|
226
|
+
return _expect(reply, _is_run, "The read answered without a run.")
|
|
227
|
+
|
|
228
|
+
async def _reread(self, id: str) -> dict[str, Any]:
|
|
229
|
+
return await self._get_run(_Call(None, None), id, False)
|
|
230
|
+
|
|
231
|
+
def _run(self, data: dict[str, Any]) -> AsyncRun:
|
|
232
|
+
return parse_async_run(data, self._reread, self._download)
|
|
233
|
+
|
|
234
|
+
async def _get(self, call: _Call, url: str, is_: Callable[[Any], bool], message: str) -> Any:
|
|
235
|
+
"""Reads ``url`` once and returns its body, when it is what the route answers."""
|
|
236
|
+
return _expect(await self._send(call, "GET", url), is_, message)
|
|
237
|
+
|
|
238
|
+
async def _hold(self, call: _Call, url: str, done: Callable[[_Reply], bool]) -> _Reply:
|
|
239
|
+
"""Reads ``url`` until ``done``, re-attaching with no limit, as ``Abyss`` does."""
|
|
240
|
+
reply = await self._send(call, "GET", url, retries=math.inf)
|
|
241
|
+
while not done(reply):
|
|
242
|
+
await self._sleep(call, REATTACH_PAUSE)
|
|
243
|
+
reply = await self._send(call, "GET", url, retries=math.inf)
|
|
244
|
+
return reply
|
|
245
|
+
|
|
246
|
+
async def _send(
|
|
247
|
+
self,
|
|
248
|
+
call: _Call,
|
|
249
|
+
method: str,
|
|
250
|
+
url: str,
|
|
251
|
+
headers: dict[str, str] | None = None,
|
|
252
|
+
content: bytes | None = None,
|
|
253
|
+
retries: float | None = None,
|
|
254
|
+
) -> _Reply:
|
|
255
|
+
"""Sends one request to `/v1`, with ``Abyss``'s retries."""
|
|
256
|
+
limit = self._limit(retries)
|
|
257
|
+
sent_headers = self._headers(headers)
|
|
258
|
+
attempt = 0
|
|
259
|
+
while True:
|
|
260
|
+
self._check(call)
|
|
261
|
+
try:
|
|
262
|
+
answer = await arequest(
|
|
263
|
+
self._http, method, url, sent_headers, content, self._timeouts(call), call.deadline
|
|
264
|
+
)
|
|
265
|
+
except Stopped:
|
|
266
|
+
raise self._timed_out(call) from None
|
|
267
|
+
except httpx.TransportError as error:
|
|
268
|
+
pause = self._dropped(call, error, attempt, limit)
|
|
269
|
+
else:
|
|
270
|
+
reply = self._answered(call, answer, attempt, limit)
|
|
271
|
+
if isinstance(reply, _Reply):
|
|
272
|
+
return reply
|
|
273
|
+
pause = reply
|
|
274
|
+
await self._sleep(call, pause)
|
|
275
|
+
attempt += 1
|