confamnode 0.2.4__tar.gz → 0.2.5__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.
- {confamnode-0.2.4 → confamnode-0.2.5}/PKG-INFO +1 -1
- {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/__init__.py +1 -1
- confamnode-0.2.5/confamnode/utils.py +48 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/pyproject.toml +1 -1
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_client_errors.py +11 -5
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_utils.py +21 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/uv.lock +1 -1
- confamnode-0.2.4/confamnode/utils.py +0 -26
- {confamnode-0.2.4 → confamnode-0.2.5}/.gitignore +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/.python-version +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/LICENSE +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/README.md +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/ansa.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/builders.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/client.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/config.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/exceptions.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/models.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/registry.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/__init__.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_ansa.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_cache_flag.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_client.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_exceptions.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_gist.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_init.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_models.py +0 -0
- {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_stream.py +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Small internal helpers shared across the confamnode client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_error(response) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Best-effort error detail from a non-2xx response, WITHOUT raising.
|
|
12
|
+
|
|
13
|
+
The server's normal error shape is {"detail": "..."}, but gateway-level
|
|
14
|
+
errors (e.g. a 429 throttle, a 502/504 from a proxy, a 524 origin timeout)
|
|
15
|
+
often return an empty or non-JSON body. Calling response.json() directly on
|
|
16
|
+
those bodies raises JSONDecodeError and swallows the status code -- the one
|
|
17
|
+
thing the caller actually needs. So we parse defensively and always fall
|
|
18
|
+
back to something readable.
|
|
19
|
+
|
|
20
|
+
HTML error pages (Cloudflare, nginx, ...) are summarised to their <title>
|
|
21
|
+
rather than dumped in full, so error messages and logs stay readable.
|
|
22
|
+
"""
|
|
23
|
+
# 1. Normal JSON error shape.
|
|
24
|
+
try:
|
|
25
|
+
payload = response.json()
|
|
26
|
+
if isinstance(payload, dict):
|
|
27
|
+
detail = payload.get("detail") or payload.get("error") or payload.get("message")
|
|
28
|
+
if detail:
|
|
29
|
+
return str(detail)
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
text = (getattr(response, "text", "") or "").strip()
|
|
34
|
+
if not text:
|
|
35
|
+
return "no response body"
|
|
36
|
+
|
|
37
|
+
# 2. HTML error page: the full page is noise. Pull the <title>, which
|
|
38
|
+
# carries the human-readable status (e.g. "524: A timeout occurred").
|
|
39
|
+
if "<html" in text[:200].lower() or "<!doctype html" in text[:200].lower():
|
|
40
|
+
m = re.search(r"<title[^>]*>(.*?)</title>", text, re.IGNORECASE | re.DOTALL)
|
|
41
|
+
if m:
|
|
42
|
+
title = re.sub(r"\s+", " ", m.group(1)).strip()
|
|
43
|
+
if title:
|
|
44
|
+
return title
|
|
45
|
+
return "HTML error page (no title)"
|
|
46
|
+
|
|
47
|
+
# 3. Plain-text body: return it, truncated so we never dump a wall.
|
|
48
|
+
return text if len(text) <= 300 else text[:297] + "..."
|
|
@@ -58,14 +58,20 @@ def test_json_detail_error_surfaces_detail(monkeypatch):
|
|
|
58
58
|
assert "invalid model" in str(exc.value)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
def test_html_gateway_error_surfaces_title(monkeypatch):
|
|
62
|
+
html = (
|
|
63
|
+
"<!DOCTYPE html><html><head>"
|
|
64
|
+
"<title>confamnode.com | 524: A timeout occurred</title>"
|
|
65
|
+
"</head><body>...</body></html>"
|
|
66
|
+
)
|
|
67
|
+
_patch_post(monkeypatch, FakeResponse(524, json_raises=True, text=html))
|
|
64
68
|
client = ConfamNode(api_key=API_KEY)
|
|
65
69
|
with pytest.raises(Exception) as exc:
|
|
66
70
|
client.gist(model=MODEL, messages="hi")
|
|
67
|
-
|
|
68
|
-
assert "
|
|
71
|
+
msg = str(exc.value)
|
|
72
|
+
assert "524" in msg
|
|
73
|
+
assert "A timeout occurred" in msg
|
|
74
|
+
assert "<html" not in msg # the page itself must NOT be dumped into the error
|
|
69
75
|
|
|
70
76
|
|
|
71
77
|
def test_success_path_still_returns_ansa(monkeypatch):
|
|
@@ -29,6 +29,27 @@ def test_non_json_text_body_is_returned_stripped():
|
|
|
29
29
|
assert extract_error(_Resp(json_raises=True, text=" upstream timeout ")) == "upstream timeout"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def test_html_error_page_summarised_to_title():
|
|
33
|
+
# A Cloudflare 524 page: don't dump the whole page, surface the <title>.
|
|
34
|
+
html = (
|
|
35
|
+
"<!DOCTYPE html><html><head>"
|
|
36
|
+
"<title>confamnode.com | 524: A timeout occurred</title>"
|
|
37
|
+
"</head><body>...lots of markup...</body></html>"
|
|
38
|
+
)
|
|
39
|
+
assert extract_error(_Resp(json_raises=True, text=html)) == "confamnode.com | 524: A timeout occurred"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_html_without_title_falls_back():
|
|
43
|
+
html = "<html><body>502 Bad Gateway</body></html>"
|
|
44
|
+
assert extract_error(_Resp(json_raises=True, text=html)) == "HTML error page (no title)"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_long_plain_text_is_truncated():
|
|
48
|
+
body = "x" * 500
|
|
49
|
+
out = extract_error(_Resp(json_raises=True, text=body))
|
|
50
|
+
assert len(out) == 300 and out.endswith("...")
|
|
51
|
+
|
|
52
|
+
|
|
32
53
|
def test_detail_key_is_preferred():
|
|
33
54
|
assert extract_error(_Resp(payload={"detail": "invalid model"})) == "invalid model"
|
|
34
55
|
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Small internal helpers shared across the confamnode client.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def extract_error(response) -> str:
|
|
7
|
-
"""
|
|
8
|
-
Best-effort error detail from a non-2xx response, WITHOUT raising.
|
|
9
|
-
|
|
10
|
-
The server's normal error shape is {"detail": "..."}, but gateway-level
|
|
11
|
-
errors (e.g. a 429 throttle, a 502/504 from a proxy) often return an
|
|
12
|
-
empty or non-JSON body. Calling response.json() directly on those bodies
|
|
13
|
-
raises JSONDecodeError and swallows the status code -- the one thing the
|
|
14
|
-
caller actually needs. So we parse defensively and always fall back to
|
|
15
|
-
the raw text, then to a status-only message.
|
|
16
|
-
"""
|
|
17
|
-
try:
|
|
18
|
-
payload = response.json()
|
|
19
|
-
if isinstance(payload, dict):
|
|
20
|
-
detail = payload.get("detail") or payload.get("error") or payload.get("message")
|
|
21
|
-
if detail:
|
|
22
|
-
return str(detail)
|
|
23
|
-
except Exception:
|
|
24
|
-
pass
|
|
25
|
-
text = (getattr(response, "text", "") or "").strip()
|
|
26
|
-
return text or "no response body"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|