bynn-decisionnode 0.3.1__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.
- bynn_decisionnode-0.3.1.dist-info/METADATA +939 -0
- bynn_decisionnode-0.3.1.dist-info/RECORD +19 -0
- bynn_decisionnode-0.3.1.dist-info/WHEEL +4 -0
- bynn_decisionnode-0.3.1.dist-info/licenses/LICENSE +21 -0
- decisionnode/__init__.py +195 -0
- decisionnode/_base.py +548 -0
- decisionnode/_batches.py +333 -0
- decisionnode/_boosters.py +553 -0
- decisionnode/_client.py +2269 -0
- decisionnode/_errors.py +218 -0
- decisionnode/_images.py +283 -0
- decisionnode/_questions.py +262 -0
- decisionnode/_sessions.py +711 -0
- decisionnode/_types.py +764 -0
- decisionnode/_uploads.py +265 -0
- decisionnode/_validate.py +934 -0
- decisionnode/_version.py +5 -0
- decisionnode/_videos.py +197 -0
- decisionnode/py.typed +0 -0
decisionnode/_batches.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""The Batch API: many decide requests answered within 24 hours at half the price.
|
|
2
|
+
|
|
3
|
+
``batches.create`` uploads a draft as JSON Lines, ``batches.add`` adds requests in calls
|
|
4
|
+
of at most 10 MiB, ``batches.finalize`` queues it, ``batches.get`` reads it,
|
|
5
|
+
``batches.results`` streams its result lines, ``batches.cancel`` stops it, and
|
|
6
|
+
``run_batch`` does the whole lifecycle.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, AsyncIterator, Dict, Iterator, List, Mapping, NamedTuple, Optional, Sequence, Union
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from . import _base
|
|
18
|
+
from ._errors import ConnectionError, DecisionNodeError, ServerError, TimeoutError
|
|
19
|
+
from ._types import Decision, parse_decision
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Batch",
|
|
23
|
+
"BatchPage",
|
|
24
|
+
"BatchResult",
|
|
25
|
+
"BatchResults",
|
|
26
|
+
"AsyncBatchResults",
|
|
27
|
+
"BatchRun",
|
|
28
|
+
"ADD_BYTES",
|
|
29
|
+
"MIN_POLL_INTERVAL",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
ADD_BYTES = 10 * 1024 * 1024
|
|
33
|
+
MIN_POLL_INTERVAL = 10.0
|
|
34
|
+
DEFAULT_POLL_INTERVAL = 60.0
|
|
35
|
+
DEFAULT_RUN_TIMEOUT = 26 * 3600.0
|
|
36
|
+
MAX_CUSTOM_ID_CHARS = 512
|
|
37
|
+
NDJSON = "application/x-ndjson"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Batch:
|
|
42
|
+
"""A batch: every field of the API's batch object.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
id: ``batch_`` and 10 characters.
|
|
46
|
+
state: ``draft``, ``queued``, ``running``, ``completed``, ``failed``,
|
|
47
|
+
``cancelled`` or ``expired``.
|
|
48
|
+
counters: ``requests``, ``queued``, ``in_progress``, ``completed``, ``failed``,
|
|
49
|
+
``expired`` and ``cancelled``.
|
|
50
|
+
created_at, finalized_at, expires_at, ended_at, results_expire_at: ISO 8601 UTC
|
|
51
|
+
times, or ``None``.
|
|
52
|
+
results_url: Where the results are, once the batch has ended.
|
|
53
|
+
price_factor: The share of the live price a line costs.
|
|
54
|
+
usage: ``input_tokens`` and ``output_tokens`` of the lines answered so far.
|
|
55
|
+
error: For a ``failed`` batch, ``error_type`` and ``message``.
|
|
56
|
+
raw: The batch object as received.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
id: str
|
|
60
|
+
state: Optional[str]
|
|
61
|
+
counters: Dict[str, int]
|
|
62
|
+
created_at: Optional[str]
|
|
63
|
+
finalized_at: Optional[str]
|
|
64
|
+
expires_at: Optional[str]
|
|
65
|
+
ended_at: Optional[str]
|
|
66
|
+
results_url: Optional[str]
|
|
67
|
+
results_expire_at: Optional[str]
|
|
68
|
+
price_factor: Optional[float]
|
|
69
|
+
usage: Dict[str, Any]
|
|
70
|
+
error: Optional[Dict[str, Any]]
|
|
71
|
+
raw: Dict[str, Any] = field(repr=False)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def ended(self) -> bool:
|
|
75
|
+
"""True once the batch is ``completed``, ``failed``, ``cancelled`` or ``expired``."""
|
|
76
|
+
return self.ended_at is not None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class BatchPage:
|
|
81
|
+
"""One page of ``batches.list``, newest first.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
batches: The batches of this page.
|
|
85
|
+
offset, limit, total: Where the page starts, its size, and how many batches there are.
|
|
86
|
+
next_offset: The ``offset`` of the next page, or ``None`` on the last.
|
|
87
|
+
raw: The page as received.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
batches: List[Batch]
|
|
91
|
+
offset: Optional[int]
|
|
92
|
+
limit: Optional[int]
|
|
93
|
+
total: Optional[int]
|
|
94
|
+
next_offset: Optional[int]
|
|
95
|
+
raw: Dict[str, Any] = field(repr=False)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class BatchResult:
|
|
100
|
+
"""One result line, in upload order.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
custom_id: The request's ``custom_id``.
|
|
104
|
+
status: The HTTP status the request got (an ``int``), or ``"expired"`` or
|
|
105
|
+
``"cancelled"`` for a request that never ran.
|
|
106
|
+
decision: For a ``200`` line, the :class:`Decision`, read as ``decide`` reads it.
|
|
107
|
+
error: For an error status, the error the live call would have raised, as a
|
|
108
|
+
value: its class, ``status``, ``error_type``, message and ``reason``.
|
|
109
|
+
headers: The line's response headers, with lower-case names.
|
|
110
|
+
raw: The line as received.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
custom_id: Optional[str]
|
|
114
|
+
status: Union[int, str, None]
|
|
115
|
+
decision: Optional[Decision]
|
|
116
|
+
error: Optional[DecisionNodeError]
|
|
117
|
+
headers: Dict[str, str]
|
|
118
|
+
raw: Dict[str, Any] = field(repr=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class BatchRun(NamedTuple):
|
|
122
|
+
"""What ``run_batch`` returns: the ended batch and its results."""
|
|
123
|
+
|
|
124
|
+
batch: Batch
|
|
125
|
+
results: Any
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def parse_batch(body: Any) -> Batch:
|
|
129
|
+
raw = dict(body) if isinstance(body, Mapping) else {}
|
|
130
|
+
counters = raw.get("counters")
|
|
131
|
+
usage = raw.get("usage")
|
|
132
|
+
error = raw.get("error")
|
|
133
|
+
return Batch(
|
|
134
|
+
id=raw.get("id", ""),
|
|
135
|
+
state=raw.get("state"),
|
|
136
|
+
counters=dict(counters) if isinstance(counters, Mapping) else {},
|
|
137
|
+
created_at=raw.get("created_at"),
|
|
138
|
+
finalized_at=raw.get("finalized_at"),
|
|
139
|
+
expires_at=raw.get("expires_at"),
|
|
140
|
+
ended_at=raw.get("ended_at"),
|
|
141
|
+
results_url=raw.get("results_url"),
|
|
142
|
+
results_expire_at=raw.get("results_expire_at"),
|
|
143
|
+
price_factor=raw.get("price_factor"),
|
|
144
|
+
usage=dict(usage) if isinstance(usage, Mapping) else {},
|
|
145
|
+
error=dict(error) if isinstance(error, Mapping) else None,
|
|
146
|
+
raw=raw,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def parse_page(body: Any) -> BatchPage:
|
|
151
|
+
raw = dict(body) if isinstance(body, Mapping) else {}
|
|
152
|
+
batches = raw.get("batches")
|
|
153
|
+
return BatchPage(
|
|
154
|
+
batches=[parse_batch(b) for b in batches] if isinstance(batches, list) else [],
|
|
155
|
+
offset=raw.get("offset"),
|
|
156
|
+
limit=raw.get("limit"),
|
|
157
|
+
total=raw.get("total"),
|
|
158
|
+
next_offset=raw.get("next_offset"),
|
|
159
|
+
raw=raw,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_result(raw: Any) -> BatchResult:
|
|
164
|
+
line = dict(raw) if isinstance(raw, Mapping) else {}
|
|
165
|
+
headers_in = line.get("headers")
|
|
166
|
+
headers = {str(k).lower(): str(v) for k, v in headers_in.items()} if isinstance(headers_in, Mapping) else {}
|
|
167
|
+
status = line.get("status")
|
|
168
|
+
body = line.get("body")
|
|
169
|
+
decision: Optional[Decision] = None
|
|
170
|
+
error: Optional[DecisionNodeError] = None
|
|
171
|
+
if status == 200 and isinstance(body, Mapping):
|
|
172
|
+
decision = parse_decision(body, headers)
|
|
173
|
+
elif isinstance(status, int) and not isinstance(status, bool) and status != 200:
|
|
174
|
+
error = _base.make_error(status, headers, json.dumps(body))
|
|
175
|
+
return BatchResult(
|
|
176
|
+
custom_id=line.get("custom_id"),
|
|
177
|
+
status=status,
|
|
178
|
+
decision=decision,
|
|
179
|
+
error=error,
|
|
180
|
+
headers=headers,
|
|
181
|
+
raw=line,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def parse_result_line(text: str) -> BatchResult:
|
|
186
|
+
try:
|
|
187
|
+
raw = json.loads(text)
|
|
188
|
+
except ValueError as exc:
|
|
189
|
+
raise ServerError("a result line is not JSON", status=200, body=text) from exc
|
|
190
|
+
return parse_result(raw)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# --------------------------------------------------------------------------- the lines
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def line_bytes(
|
|
197
|
+
req: Mapping[str, Any],
|
|
198
|
+
questions: Mapping[str, Any],
|
|
199
|
+
wires: Sequence[Mapping[str, Any]],
|
|
200
|
+
model: str,
|
|
201
|
+
videos: Sequence[Mapping[str, Any]] = (),
|
|
202
|
+
boosters: Any = None,
|
|
203
|
+
) -> bytes:
|
|
204
|
+
"""One JSON Lines line: ``custom_id``, ``model``, ``state`` (``""`` when absent), the
|
|
205
|
+
questions, the images, the videos, the boosters (``booster_wire``; ``None`` for none),
|
|
206
|
+
and any other field as given."""
|
|
207
|
+
out: Dict[str, Any] = {
|
|
208
|
+
"custom_id": req["custom_id"],
|
|
209
|
+
"model": req.get("model") or model,
|
|
210
|
+
"state": "" if req.get("state") is None else req["state"],
|
|
211
|
+
"questions": dict(questions),
|
|
212
|
+
}
|
|
213
|
+
if wires:
|
|
214
|
+
out["images"] = list(wires)
|
|
215
|
+
if videos:
|
|
216
|
+
out["videos"] = list(videos)
|
|
217
|
+
if boosters is not None:
|
|
218
|
+
out["boosters"] = boosters
|
|
219
|
+
for key, value in req.items():
|
|
220
|
+
if key not in out and key not in ("images", "videos", "boosters"):
|
|
221
|
+
out[key] = value
|
|
222
|
+
return _base.encode(out) + b"\n"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def split_lines(lines: Sequence[bytes], limit: int = ADD_BYTES) -> List[bytes]:
|
|
226
|
+
"""The lines in bodies of at most ``limit`` bytes, in order. A line over the limit
|
|
227
|
+
goes alone; the server judges it."""
|
|
228
|
+
bodies: List[bytes] = []
|
|
229
|
+
current: List[bytes] = []
|
|
230
|
+
size = 0
|
|
231
|
+
for line in lines:
|
|
232
|
+
if current and size + len(line) > limit:
|
|
233
|
+
bodies.append(b"".join(current))
|
|
234
|
+
current, size = [], 0
|
|
235
|
+
current.append(line)
|
|
236
|
+
size += len(line)
|
|
237
|
+
if current:
|
|
238
|
+
bodies.append(b"".join(current))
|
|
239
|
+
return bodies
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def check_poll(poll_interval: Any, timeout: Any) -> None:
|
|
243
|
+
if (
|
|
244
|
+
isinstance(poll_interval, bool)
|
|
245
|
+
or not isinstance(poll_interval, (int, float))
|
|
246
|
+
or not poll_interval >= MIN_POLL_INTERVAL
|
|
247
|
+
):
|
|
248
|
+
raise ValueError("poll_interval must be at least %g seconds, got %r" % (MIN_POLL_INTERVAL, poll_interval))
|
|
249
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not timeout > 0:
|
|
250
|
+
raise ValueError("timeout must be a number of seconds above 0, got %r" % (timeout,))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def poll_wait(poll_interval: float, headers: Optional[Mapping[str, str]]) -> float:
|
|
254
|
+
"""The fixed interval, or the server's ``Retry-After`` when that is longer."""
|
|
255
|
+
asked = _base.requested_wait(headers) if headers is not None else None
|
|
256
|
+
return max(poll_interval, asked) if asked is not None else poll_interval
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# --------------------------------------------------------------------------- the results
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _stream_error(exc: httpx.TransportError, timeout: float) -> DecisionNodeError:
|
|
263
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
264
|
+
return TimeoutError("the results stopped arriving for %gs" % timeout)
|
|
265
|
+
return ConnectionError("connection error while reading the results: %s" % (exc or type(exc).__name__))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class BatchResults:
|
|
269
|
+
"""The result lines of a batch, read from the network one line at a time.
|
|
270
|
+
|
|
271
|
+
Iterate it once; each item is a :class:`BatchResult`. The connection closes when the
|
|
272
|
+
last line is read, on :meth:`close`, or at the end of a ``with`` block.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(self, response: httpx.Response, timeout: float) -> None:
|
|
276
|
+
self._response = response
|
|
277
|
+
self._lines = response.iter_lines()
|
|
278
|
+
self._timeout = timeout
|
|
279
|
+
|
|
280
|
+
def __iter__(self) -> Iterator[BatchResult]:
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def __next__(self) -> BatchResult:
|
|
284
|
+
try:
|
|
285
|
+
for line in self._lines:
|
|
286
|
+
if line.strip():
|
|
287
|
+
return parse_result_line(line)
|
|
288
|
+
except httpx.TransportError as exc:
|
|
289
|
+
self.close()
|
|
290
|
+
raise _stream_error(exc, self._timeout) from exc
|
|
291
|
+
self.close()
|
|
292
|
+
raise StopIteration
|
|
293
|
+
|
|
294
|
+
def close(self) -> None:
|
|
295
|
+
self._response.close()
|
|
296
|
+
|
|
297
|
+
def __enter__(self) -> BatchResults:
|
|
298
|
+
return self
|
|
299
|
+
|
|
300
|
+
def __exit__(self, *exc: Any) -> None:
|
|
301
|
+
self.close()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class AsyncBatchResults:
|
|
305
|
+
"""The result lines of a batch for the asyncio client: ``async for result in results``."""
|
|
306
|
+
|
|
307
|
+
def __init__(self, response: httpx.Response, timeout: float) -> None:
|
|
308
|
+
self._response = response
|
|
309
|
+
self._lines = response.aiter_lines()
|
|
310
|
+
self._timeout = timeout
|
|
311
|
+
|
|
312
|
+
def __aiter__(self) -> AsyncIterator[BatchResult]:
|
|
313
|
+
return self
|
|
314
|
+
|
|
315
|
+
async def __anext__(self) -> BatchResult:
|
|
316
|
+
try:
|
|
317
|
+
async for line in self._lines:
|
|
318
|
+
if line.strip():
|
|
319
|
+
return parse_result_line(line)
|
|
320
|
+
except httpx.TransportError as exc:
|
|
321
|
+
await self.aclose()
|
|
322
|
+
raise _stream_error(exc, self._timeout) from exc
|
|
323
|
+
await self.aclose()
|
|
324
|
+
raise StopAsyncIteration
|
|
325
|
+
|
|
326
|
+
async def aclose(self) -> None:
|
|
327
|
+
await self._response.aclose()
|
|
328
|
+
|
|
329
|
+
async def __aenter__(self) -> AsyncBatchResults:
|
|
330
|
+
return self
|
|
331
|
+
|
|
332
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
333
|
+
await self.aclose()
|