fastspec 0.2.2__tar.gz → 0.2.4__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.
- {fastspec-0.2.2/fastspec.egg-info → fastspec-0.2.4}/PKG-INFO +2 -2
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec/__init__.py +1 -1
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec/_modidx.py +4 -18
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec/oapi.py +19 -18
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec/spec.py +10 -6
- {fastspec-0.2.2 → fastspec-0.2.4/fastspec.egg-info}/PKG-INFO +2 -2
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec.egg-info/SOURCES.txt +0 -1
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec.egg-info/requires.txt +1 -1
- {fastspec-0.2.2 → fastspec-0.2.4}/pyproject.toml +1 -1
- fastspec-0.2.2/fastspec/errors.py +0 -178
- {fastspec-0.2.2 → fastspec-0.2.4}/LICENSE +0 -0
- {fastspec-0.2.2 → fastspec-0.2.4}/MANIFEST.in +0 -0
- {fastspec-0.2.2 → fastspec-0.2.4}/README.md +0 -0
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec/gql.py +0 -0
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec.egg-info/dependency_links.txt +0 -0
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec.egg-info/entry_points.txt +0 -0
- {fastspec-0.2.2 → fastspec-0.2.4}/fastspec.egg-info/top_level.txt +0 -0
- {fastspec-0.2.2 → fastspec-0.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastspec
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Dynamic OpenAPI, Discovery, and GraphQL spec client for Python — turn any API spec into a fully-typed async client with attribute chaining, streaming, and file uploads
|
|
5
5
|
Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -13,7 +13,7 @@ Requires-Python: >=3.10
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: fastcore>=2.2.1
|
|
16
|
-
Requires-Dist: fasttransport>=0.0.
|
|
16
|
+
Requires-Dist: fasttransport>=0.0.2
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: safepyrun>=0.2.3; extra == "dev"
|
|
19
19
|
Requires-Dist: pyskills>=0.0.17; extra == "dev"
|
|
@@ -5,23 +5,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
5
5
|
'doc_host': 'https://AnswerDotAI.github.io',
|
|
6
6
|
'git_url': 'https://github.com/AnswerDotAI/fastspec',
|
|
7
7
|
'lib_path': 'fastspec'},
|
|
8
|
-
'syms': { 'fastspec.
|
|
9
|
-
'fastspec.errors.APIError.__init__': ('errors.html#apierror.__init__', 'fastspec/errors.py'),
|
|
10
|
-
'fastspec.errors.APIError.__str__': ('errors.html#apierror.__str__', 'fastspec/errors.py'),
|
|
11
|
-
'fastspec.errors.APIError.with_context': ('errors.html#apierror.with_context', 'fastspec/errors.py'),
|
|
12
|
-
'fastspec.errors.FastSpecError': ('errors.html#fastspecerror', 'fastspec/errors.py'),
|
|
13
|
-
'fastspec.errors.SpecError': ('errors.html#specerror', 'fastspec/errors.py'),
|
|
14
|
-
'fastspec.errors._parse_sse_error_event': ('errors.html#_parse_sse_error_event', 'fastspec/errors.py'),
|
|
15
|
-
'fastspec.errors._req_id': ('errors.html#_req_id', 'fastspec/errors.py'),
|
|
16
|
-
'fastspec.errors._retryable': ('errors.html#_retryable', 'fastspec/errors.py'),
|
|
17
|
-
'fastspec.errors._to_text': ('errors.html#_to_text', 'fastspec/errors.py'),
|
|
18
|
-
'fastspec.errors.api_error_from_event': ('errors.html#api_error_from_event', 'fastspec/errors.py'),
|
|
19
|
-
'fastspec.errors.httpx2.HTTPStatusError.api_error': ( 'errors.html#httpx2.httpstatuserror.api_error',
|
|
20
|
-
'fastspec/errors.py'),
|
|
21
|
-
'fastspec.errors.httpx2.RequestError.api_error': ( 'errors.html#httpx2.requesterror.api_error',
|
|
22
|
-
'fastspec/errors.py'),
|
|
23
|
-
'fastspec.errors.httpx2.Response.error': ('errors.html#httpx2.response.error', 'fastspec/errors.py')},
|
|
24
|
-
'fastspec.gql': { 'fastspec.gql.GqlClient': ('gql.html#gqlclient', 'fastspec/gql.py'),
|
|
8
|
+
'syms': { 'fastspec.gql': { 'fastspec.gql.GqlClient': ('gql.html#gqlclient', 'fastspec/gql.py'),
|
|
25
9
|
'fastspec.gql.GqlClient.__call__': ('gql.html#gqlclient.__call__', 'fastspec/gql.py'),
|
|
26
10
|
'fastspec.gql.GqlClient.__dir__': ('gql.html#gqlclient.__dir__', 'fastspec/gql.py'),
|
|
27
11
|
'fastspec.gql.GqlClient.__getattr__': ('gql.html#gqlclient.__getattr__', 'fastspec/gql.py'),
|
|
@@ -87,8 +71,10 @@ d = { 'settings': { 'branch': 'main',
|
|
|
87
71
|
'fastspec.oapi.OpenAPIClient.full_docs': ('oapi.html#openapiclient.full_docs', 'fastspec/oapi.py'),
|
|
88
72
|
'fastspec.oapi.SyncOpFunc': ('oapi.html#syncopfunc', 'fastspec/oapi.py'),
|
|
89
73
|
'fastspec.oapi.SyncOpFunc.__call__': ('oapi.html#syncopfunc.__call__', 'fastspec/oapi.py'),
|
|
74
|
+
'fastspec.oapi._examples_doc': ('oapi.html#_examples_doc', 'fastspec/oapi.py'),
|
|
90
75
|
'fastspec.oapi._join_url': ('oapi.html#_join_url', 'fastspec/oapi.py'),
|
|
91
|
-
'fastspec.oapi._path': ('oapi.html#_path', 'fastspec/oapi.py')
|
|
76
|
+
'fastspec.oapi._path': ('oapi.html#_path', 'fastspec/oapi.py'),
|
|
77
|
+
'fastspec.oapi.op_func': ('oapi.html#op_func', 'fastspec/oapi.py')},
|
|
92
78
|
'fastspec.spec': { 'fastspec.spec.OpSpec': ('spec.html#opspec', 'fastspec/spec.py'),
|
|
93
79
|
'fastspec.spec.OpSpec.__post_init__': ('spec.html#opspec.__post_init__', 'fastspec/spec.py'),
|
|
94
80
|
'fastspec.spec.OpSpec._repr_markdown_': ('spec.html#opspec._repr_markdown_', 'fastspec/spec.py'),
|
|
@@ -5,7 +5,7 @@ Docs: https://AnswerDotAI.github.io/fastspec/oapi.html.md"""
|
|
|
5
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_oapi.ipynb.
|
|
6
6
|
|
|
7
7
|
# %% auto #0
|
|
8
|
-
__all__ = ['OpFunc', 'SyncOpFunc', 'OpenAPIClient']
|
|
8
|
+
__all__ = ['OpFunc', 'op_func', 'SyncOpFunc', 'OpenAPIClient']
|
|
9
9
|
|
|
10
10
|
# %% ../nbs/04_oapi.ipynb #632a2010
|
|
11
11
|
import httpx2,json
|
|
@@ -14,38 +14,32 @@ from fastcore.utils import *
|
|
|
14
14
|
from fastcore.meta import delegates
|
|
15
15
|
from fastcore.apisurface import snake, sanitized_params, mk_sig, mk_doc, OpGroup, mk_groups, full_docs as _full_docs
|
|
16
16
|
|
|
17
|
-
from .errors import APIError
|
|
17
|
+
from fasttransport.errors import APIError
|
|
18
18
|
from .spec import OpSpec, SpecParser
|
|
19
19
|
from fasttransport.core import AsyncTransport,SyncTransport
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
# %% ../nbs/04_oapi.ipynb #6b2f1057
|
|
23
|
+
def _examples_doc(exs):
|
|
24
|
+
"Render body examples as an `Examples:` doc section, one fenced JSON block per example"
|
|
25
|
+
if not exs: return ''
|
|
26
|
+
return '\n\nExamples:' + ''.join(f"\n- {v.get('summary') or k}:\n```json\n{json.dumps(v['value'], indent=2)}\n```" for k,v in exs.items())
|
|
27
|
+
|
|
23
28
|
class OpFunc:
|
|
24
29
|
def __init__(self, op_spec, client, base_url, form_encoder=None, defaults=None):
|
|
25
30
|
store_attr()
|
|
26
31
|
self.defaults = defaults or {}
|
|
27
32
|
self.sparams = sanitized_params(op_spec.params)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
32
|
-
self.verb = op_spec.verb
|
|
33
|
-
self.route_params = op_spec.route_params
|
|
34
|
-
self.query_params = op_spec.query_params
|
|
35
|
-
self.body_params = op_spec.body_params
|
|
36
|
-
self.request_content_type = op_spec.request_content_type
|
|
37
|
-
self.file_params = op_spec.file_params
|
|
38
|
-
self.summary = op_spec.summary or f"{op_spec.verb} {op_spec.path}"
|
|
39
|
-
self.docs_url = op_spec.docs_url
|
|
40
|
-
self.required_params = op_spec.required_params
|
|
41
|
-
self.param_docs = op_spec.param_docs
|
|
33
|
+
for o in ('name group path verb route_params query_params body_params request_content_type '
|
|
34
|
+
'file_params docs_url required_params param_docs').split(): setattr(self, o, getattr(op_spec, o))
|
|
35
|
+
self.__name__ = self.name
|
|
36
|
+
self.summary = op_spec.summary or f"{op_spec.verb} {op_spec.path}"
|
|
42
37
|
self.__signature__ = mk_sig(op_spec, self.sparams, self.defaults)
|
|
43
|
-
self.__doc__ = mk_doc(self, self.__signature__, self.sparams)
|
|
38
|
+
self.__doc__ = mk_doc(self, self.__signature__, self.sparams) + _examples_doc(op_spec.body_examples)
|
|
44
39
|
|
|
45
40
|
def _repr_markdown_(self): return self.__doc__
|
|
46
41
|
def __repr__(self): return f"{'.'.join(snake(g) for g in listify(self.group))}.{self.name}{self.__signature__}\n{self.docs_url}"
|
|
47
42
|
|
|
48
|
-
|
|
49
43
|
# %% ../nbs/04_oapi.ipynb #6e381df4
|
|
50
44
|
@patch
|
|
51
45
|
def _bind(self:OpFunc, args, kwargs):
|
|
@@ -147,6 +141,13 @@ async def __call__(self:OpFunc, *args, **kwargs):
|
|
|
147
141
|
if stream: return self._stream(url, headers=headers, query=query, route=route, **kw)
|
|
148
142
|
return await self._request(url, headers=headers, query=query, route=route, **kw)
|
|
149
143
|
|
|
144
|
+
# %% ../nbs/04_oapi.ipynb #75d6cd54
|
|
145
|
+
def op_func(spec, name, group=None):
|
|
146
|
+
"A detached `OpFunc` for spec op `name`: a signature and docs donor, e.g. a `delegates` target for wrappers"
|
|
147
|
+
o = first(o for o in spec.ops if o.name == name and (group is None or o.group == group))
|
|
148
|
+
if o is None: raise ValueError(f'no op named {name!r}' + (f' in group {group!r}' if group else ''))
|
|
149
|
+
return OpFunc(o, None, '')
|
|
150
|
+
|
|
150
151
|
# %% ../nbs/04_oapi.ipynb #b23f43ae
|
|
151
152
|
class SyncOpFunc(OpFunc):
|
|
152
153
|
"`OpFunc` over a `SyncTransport`: calls return results directly, with no `await`."
|
|
@@ -32,12 +32,13 @@ class OpSpec:
|
|
|
32
32
|
param_types: Dict = field(default_factory=dict)
|
|
33
33
|
param_defaults: Dict = field(default_factory=dict)
|
|
34
34
|
param_docs: Dict = field(default_factory=dict)
|
|
35
|
+
body_examples: Dict = field(default_factory=dict)
|
|
35
36
|
docs_url: str = ""
|
|
36
37
|
|
|
37
38
|
def mk_doc(self):
|
|
38
39
|
rows = []
|
|
39
40
|
for f,v in vars(self).items():
|
|
40
|
-
if f not in ('param_types','param_defaults','param_docs'): rows.append(f'| `{f}` | {v} |')
|
|
41
|
+
if f not in ('param_types','param_defaults','param_docs','body_examples'): rows.append(f'| `{f}` | {v} |')
|
|
41
42
|
md = f'| Field | Value |\n|---|---|\n' + '\n'.join(rows)
|
|
42
43
|
all_params = self.route_params + self.query_params + self.body_params
|
|
43
44
|
if all_params:
|
|
@@ -197,10 +198,13 @@ def _body_params(op, spec):
|
|
|
197
198
|
rb = _resolve_obj(op.get("requestBody", {}), spec)
|
|
198
199
|
content = rb.get("content", {})
|
|
199
200
|
ct = first((ct for ct in ctypes if ct in content), None)
|
|
200
|
-
|
|
201
|
+
mt = content.get(ct) or {}
|
|
202
|
+
schema = mt.get("schema")
|
|
203
|
+
exs = mt.get("examples") or ({"default": {"value": mt["example"]}} if "example" in mt else {})
|
|
204
|
+
bexs = {k: {"summary": r.get("summary",""), "value": r["value"]} for k,v in exs.items() if "value" in (r := _resolve_obj(v, spec))}
|
|
201
205
|
if not schema:
|
|
202
206
|
return AttrDict(body_params=[], file_params=[], required_params=set(), param_types={},
|
|
203
|
-
param_docs={}, param_defaults={}, request_content_type=ct)
|
|
207
|
+
param_docs={}, param_defaults={}, request_content_type=ct, body_examples=bexs)
|
|
204
208
|
props, req = _schema_props_required(schema, spec)
|
|
205
209
|
fparams = [k for k,v in props.items() if _resolve_obj(v, spec).get("format") == "binary"]
|
|
206
210
|
bparams = [k for k in props if k not in fparams]
|
|
@@ -210,7 +214,7 @@ def _body_params(op, spec):
|
|
|
210
214
|
# Params without a default or nullable type are required
|
|
211
215
|
# req |= {k for k in props if k not in defaults} # too aggressive doesn't match when spec is incomplete
|
|
212
216
|
return AttrDict(body_params=bparams, file_params=fparams, required_params=req, param_types=ptypes,
|
|
213
|
-
param_docs=pdocs, param_defaults=defaults, request_content_type=ct)
|
|
217
|
+
param_docs=pdocs, param_defaults=defaults, request_content_type=ct, body_examples=bexs)
|
|
214
218
|
|
|
215
219
|
# %% ../nbs/03_spec.ipynb #9ab326f7
|
|
216
220
|
_pat_md_url = re.compile(r"\[[^\]]+\]\((https?://[^)\s]+)\)")
|
|
@@ -232,7 +236,7 @@ def _op_docs_url(op):
|
|
|
232
236
|
if desc and (durl := _first_url(desc)): return durl
|
|
233
237
|
|
|
234
238
|
# %% ../nbs/03_spec.ipynb #f74f3391
|
|
235
|
-
_py_types = {t.__name__:t for t in (str,int,float,bool,list,dict)}
|
|
239
|
+
_py_types = {t.__name__:t for t in (str,int,float,bool,list,dict,bytes,type(None))}
|
|
236
240
|
|
|
237
241
|
def _plain(v):
|
|
238
242
|
"Normalize `v` to plain data: sets sorted, `L`/tuples to `list`, dicts recursed."
|
|
@@ -268,7 +272,7 @@ def openapi_to_ops(spec, group_func=None):
|
|
|
268
272
|
required_params=sorted(pdict.required_params | bpdict.required_params),
|
|
269
273
|
param_types={k:v for k,v in merge(pdict.param_types, bpdict.param_types).items() if v},
|
|
270
274
|
param_defaults=_plain(merge(pdict.param_defaults, bpdict.param_defaults)), param_docs=merge(pdict.param_docs, bpdict.param_docs),
|
|
271
|
-
docs_url=_op_docs_url(op) or ""))
|
|
275
|
+
docs_url=_op_docs_url(op) or "", body_examples=_plain(bpdict.body_examples)))
|
|
272
276
|
return res
|
|
273
277
|
|
|
274
278
|
# %% ../nbs/03_spec.ipynb #78ea6617
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastspec
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Dynamic OpenAPI, Discovery, and GraphQL spec client for Python — turn any API spec into a fully-typed async client with attribute chaining, streaming, and file uploads
|
|
5
5
|
Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -13,7 +13,7 @@ Requires-Python: >=3.10
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: fastcore>=2.2.1
|
|
16
|
-
Requires-Dist: fasttransport>=0.0.
|
|
16
|
+
Requires-Dist: fasttransport>=0.0.2
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: safepyrun>=0.2.3; extra == "dev"
|
|
19
19
|
Requires-Dist: pyskills>=0.0.17; extra == "dev"
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
16
|
"Programming Language :: Python :: 3 :: Only",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ['fastcore>=2.2.1', 'fasttransport>=0.0.
|
|
18
|
+
dependencies = ['fastcore>=2.2.1', 'fasttransport>=0.0.2']
|
|
19
19
|
|
|
20
20
|
[project.optional-dependencies]
|
|
21
21
|
dev = ['safepyrun>=0.2.3', 'pyskills>=0.0.17']
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
"""fastspec errors.
|
|
2
|
-
|
|
3
|
-
Docs: https://AnswerDotAI.github.io/fastspec/errors.html.md"""
|
|
4
|
-
|
|
5
|
-
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_errors.ipynb.
|
|
6
|
-
|
|
7
|
-
# %% auto #0
|
|
8
|
-
__all__ = ['FastSpecError', 'SpecError', 'APIError', 'api_error_from_event']
|
|
9
|
-
|
|
10
|
-
# %% ../nbs/00_errors.ipynb #b4e7c7ed
|
|
11
|
-
from fastcore.utils import *
|
|
12
|
-
import json, httpx2
|
|
13
|
-
|
|
14
|
-
# %% ../nbs/00_errors.ipynb #87c600ab
|
|
15
|
-
class FastSpecError(Exception):
|
|
16
|
-
"Base fastspec error."
|
|
17
|
-
|
|
18
|
-
# %% ../nbs/00_errors.ipynb #3832e5fa
|
|
19
|
-
class SpecError(FastSpecError):
|
|
20
|
-
"Raised when an OpenAPI spec cannot be parsed as expected."
|
|
21
|
-
|
|
22
|
-
# %% ../nbs/00_errors.ipynb #73881e99
|
|
23
|
-
def _to_text(v):
|
|
24
|
-
"Best-effort stringify helper."
|
|
25
|
-
if v is None: return ""
|
|
26
|
-
if isinstance(v, str): return v
|
|
27
|
-
try: return json.dumps(v, ensure_ascii=False)
|
|
28
|
-
except Exception: return str(v)
|
|
29
|
-
|
|
30
|
-
# %% ../nbs/00_errors.ipynb #2d51e006
|
|
31
|
-
def _retryable(status_code, error_type, code, message):
|
|
32
|
-
"Classify transient/retryable API failures."
|
|
33
|
-
if isinstance(status_code, int) and (status_code >= 500 or status_code in (408, 409, 425, 429)): return True
|
|
34
|
-
t = str(error_type or "").lower()
|
|
35
|
-
c = str(code or "").lower()
|
|
36
|
-
m = (message or "").lower()
|
|
37
|
-
hints = ("server_error", "internal", "overload", "rate_limit", "timeout", "unavailable", "temporar")
|
|
38
|
-
if any(h in t for h in hints): return True
|
|
39
|
-
if any(h in c for h in hints): return True
|
|
40
|
-
if any(h in m for h in ("try again", "server error", "temporar", "timeout", "overloaded", "unavailable", "rate limit")): return True
|
|
41
|
-
return False
|
|
42
|
-
|
|
43
|
-
# %% ../nbs/00_errors.ipynb #9c1cdf65
|
|
44
|
-
def _req_id(headers):
|
|
45
|
-
"Extract provider request id header when available."
|
|
46
|
-
if headers is None: return ""
|
|
47
|
-
for k in ("x-request-id", "request-id", "anthropic-request-id", "x-goog-request-id"):
|
|
48
|
-
v = headers.get(k)
|
|
49
|
-
if v: return str(v)
|
|
50
|
-
return ""
|
|
51
|
-
|
|
52
|
-
# %% ../nbs/00_errors.ipynb #fdf25eba
|
|
53
|
-
@patch
|
|
54
|
-
def error(self: httpx2.Response):
|
|
55
|
-
"Parse common HTTP API error shapes to (message, error_type, code, raw)."
|
|
56
|
-
try: raw = self.json()
|
|
57
|
-
except Exception: raw = self.text
|
|
58
|
-
|
|
59
|
-
msg, et, code, status = "", "", None, None
|
|
60
|
-
if isinstance(raw, dict):
|
|
61
|
-
err = raw.get("error")
|
|
62
|
-
if isinstance(err, dict):
|
|
63
|
-
msg = str(err.get("message") or raw.get("message") or raw.get("detail") or "")
|
|
64
|
-
status = err.get("status", raw.get("status"))
|
|
65
|
-
et = str(err.get("type") or status or raw.get("type") or "")
|
|
66
|
-
code = err.get("code", raw.get("code"))
|
|
67
|
-
elif err is not None:
|
|
68
|
-
msg = _to_text(err)
|
|
69
|
-
status = raw.get("status")
|
|
70
|
-
et = str(raw.get("type") or status or "")
|
|
71
|
-
code = raw.get("code")
|
|
72
|
-
else:
|
|
73
|
-
msg = str(raw.get("message") or raw.get("detail") or raw.get("error_description") or "")
|
|
74
|
-
status = raw.get("status")
|
|
75
|
-
et = str(raw.get("type") or status or "")
|
|
76
|
-
code = raw.get("code")
|
|
77
|
-
if not msg: msg = _to_text(raw)
|
|
78
|
-
else:
|
|
79
|
-
msg = _to_text(raw)
|
|
80
|
-
# Prefer semantic provider codes (e.g. Gemini `status`) over numeric HTTP-like codes.
|
|
81
|
-
if (code is None or isinstance(code, int)) and isinstance(status, str) and status:
|
|
82
|
-
code = status
|
|
83
|
-
if code in (None, "") and isinstance(et, str) and et:
|
|
84
|
-
code = et
|
|
85
|
-
return AttrDict(message=msg, type=et, code=code, raw=raw)
|
|
86
|
-
|
|
87
|
-
# %% ../nbs/00_errors.ipynb #24ed43ac
|
|
88
|
-
def _parse_sse_error_event(event):
|
|
89
|
-
"Parse common SSE `error` event shapes to (message, error_type, code, raw)."
|
|
90
|
-
raw = event
|
|
91
|
-
e = event.get("error") if isinstance(event, dict) and isinstance(event.get("error"), dict) else (
|
|
92
|
-
event if isinstance(event, dict) else {"message": _to_text(event)})
|
|
93
|
-
msg = str(e.get("message") or e.get("detail") or _to_text(e))
|
|
94
|
-
et = str(e.get("type") or e.get("status") or "")
|
|
95
|
-
code = e.get("code")
|
|
96
|
-
if (code is None or isinstance(code, int)) and isinstance(et, str) and et: code = et
|
|
97
|
-
return AttrDict(message=msg, type=et, code=code, raw=raw)
|
|
98
|
-
|
|
99
|
-
# %% ../nbs/00_errors.ipynb #0de145da
|
|
100
|
-
class APIError(FastSpecError):
|
|
101
|
-
"Structured API error with optional context."
|
|
102
|
-
def __init__(self, message: str, *, provider: str = "", model: str = "", endpoint: str = "",
|
|
103
|
-
status_code= None, error_type: str = "", code = None, request_id: str = "",
|
|
104
|
-
retryable = None, raw = None, response = None):
|
|
105
|
-
self.message = message or "API request failed"
|
|
106
|
-
self.provider = provider or ""
|
|
107
|
-
self.model = model or ""
|
|
108
|
-
self.endpoint = endpoint or ""
|
|
109
|
-
self.status_code = status_code
|
|
110
|
-
self.error_type = error_type or ""
|
|
111
|
-
self.code = code
|
|
112
|
-
self.request_id = request_id or ""
|
|
113
|
-
self.retryable = _retryable(status_code, self.error_type, self.code, self.message) if retryable is None else bool(retryable)
|
|
114
|
-
self.raw = raw
|
|
115
|
-
self.response = response
|
|
116
|
-
super().__init__(self.__str__())
|
|
117
|
-
|
|
118
|
-
def with_context(self, *, provider: str = "", model: str = "", endpoint: str = "") -> "APIError":
|
|
119
|
-
"Return a copy with missing context fields filled."
|
|
120
|
-
return APIError(
|
|
121
|
-
self.message,
|
|
122
|
-
provider=self.provider or provider,
|
|
123
|
-
model=self.model or model,
|
|
124
|
-
endpoint=self.endpoint or endpoint,
|
|
125
|
-
status_code=self.status_code,
|
|
126
|
-
error_type=self.error_type,
|
|
127
|
-
code=self.code,
|
|
128
|
-
request_id=self.request_id,
|
|
129
|
-
retryable=self.retryable,
|
|
130
|
-
raw=self.raw,
|
|
131
|
-
response=self.response,
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
def __str__(self):
|
|
135
|
-
fields = ['message', 'provider', 'model', 'endpoint', 'status_code', 'error_type', 'code', 'request_id', 'retryable', 'raw']
|
|
136
|
-
parts = [f"{f}={getattr(self, f)!r}" for f in fields if getattr(self, f) not in (None, "", False)]
|
|
137
|
-
return f"APIError({', '.join(parts)})"
|
|
138
|
-
|
|
139
|
-
__repr__ = __str__
|
|
140
|
-
|
|
141
|
-
# %% ../nbs/00_errors.ipynb #83d2dc0c
|
|
142
|
-
@patch
|
|
143
|
-
def api_error(self:httpx2.HTTPStatusError, *, provider: str = "", model: str = "", endpoint: str = ""):
|
|
144
|
-
"Build APIError from httpx2 HTTPStatusError."
|
|
145
|
-
resp = self.response
|
|
146
|
-
err = resp.error()
|
|
147
|
-
if not endpoint and resp.request is not None:
|
|
148
|
-
endpoint = f"{resp.request.method.upper()} {resp.request.url.path}"
|
|
149
|
-
return APIError(
|
|
150
|
-
err.message,
|
|
151
|
-
provider=provider,
|
|
152
|
-
model=model,
|
|
153
|
-
endpoint=endpoint,
|
|
154
|
-
status_code=resp.status_code,
|
|
155
|
-
error_type=err.type,
|
|
156
|
-
code=err.code,
|
|
157
|
-
request_id=_req_id(resp.headers),
|
|
158
|
-
raw=err.raw,
|
|
159
|
-
response=resp,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# %% ../nbs/00_errors.ipynb #859197d5
|
|
163
|
-
@patch
|
|
164
|
-
def api_error(self:httpx2.RequestError, *, provider:str="", model:str="", endpoint:str=""):
|
|
165
|
-
"Build APIError from httpx2 RequestError (transport-level failure)."
|
|
166
|
-
req = getattr(self, '_request', None)
|
|
167
|
-
if not endpoint and req is not None: endpoint = f"{req.method.upper()} {req.url.path}"
|
|
168
|
-
et = type(self).__name__
|
|
169
|
-
retry = isinstance(self, (httpx2.TimeoutException, httpx2.NetworkError, httpx2.ProtocolError, httpx2.ProxyError))
|
|
170
|
-
return APIError(str(self) or et, provider=provider, model=model, endpoint=endpoint,
|
|
171
|
-
error_type=et, code=et, retryable=retry, raw=self)
|
|
172
|
-
|
|
173
|
-
# %% ../nbs/00_errors.ipynb #e6e2cc84
|
|
174
|
-
def api_error_from_event(event, *, provider: str = "", model: str = "", endpoint: str = ""):
|
|
175
|
-
"Build APIError from provider SSE/event-level error payload."
|
|
176
|
-
parsed = _parse_sse_error_event(event)
|
|
177
|
-
return APIError(parsed.message, provider=provider, model=model, endpoint=endpoint, error_type=parsed.type, code=parsed.code, raw=parsed.raw)
|
|
178
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|