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.
Files changed (28) hide show
  1. {confamnode-0.2.4 → confamnode-0.2.5}/PKG-INFO +1 -1
  2. {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/__init__.py +1 -1
  3. confamnode-0.2.5/confamnode/utils.py +48 -0
  4. {confamnode-0.2.4 → confamnode-0.2.5}/pyproject.toml +1 -1
  5. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_client_errors.py +11 -5
  6. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_utils.py +21 -0
  7. {confamnode-0.2.4 → confamnode-0.2.5}/uv.lock +1 -1
  8. confamnode-0.2.4/confamnode/utils.py +0 -26
  9. {confamnode-0.2.4 → confamnode-0.2.5}/.gitignore +0 -0
  10. {confamnode-0.2.4 → confamnode-0.2.5}/.python-version +0 -0
  11. {confamnode-0.2.4 → confamnode-0.2.5}/LICENSE +0 -0
  12. {confamnode-0.2.4 → confamnode-0.2.5}/README.md +0 -0
  13. {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/ansa.py +0 -0
  14. {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/builders.py +0 -0
  15. {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/client.py +0 -0
  16. {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/config.py +0 -0
  17. {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/exceptions.py +0 -0
  18. {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/models.py +0 -0
  19. {confamnode-0.2.4 → confamnode-0.2.5}/confamnode/registry.py +0 -0
  20. {confamnode-0.2.4 → confamnode-0.2.5}/tests/__init__.py +0 -0
  21. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_ansa.py +0 -0
  22. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_cache_flag.py +0 -0
  23. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_client.py +0 -0
  24. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_exceptions.py +0 -0
  25. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_gist.py +0 -0
  26. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_init.py +0 -0
  27. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_models.py +0 -0
  28. {confamnode-0.2.4 → confamnode-0.2.5}/tests/test_stream.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confamnode
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: The Nigerian AI inference gateway
5
5
  Project-URL: Repository, https://github.com/confamnodeai/confamnode
6
6
  Project-URL: Bug Tracker, https://github.com/confamnodeai/confamnode/issues
@@ -8,7 +8,7 @@ from confamnode.exceptions import (
8
8
  from confamnode.ansa import Ansa, Usage, Cost
9
9
  from confamnode import models
10
10
 
11
- __version__ = "0.2.4"
11
+ __version__ = "0.2.5"
12
12
 
13
13
  __all__ = [
14
14
  "ConfamNode",
@@ -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] + "..."
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "confamnode"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "The Nigerian AI inference gateway"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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 test_html_gateway_error_surfaces_text(monkeypatch):
62
- _patch_post(monkeypatch, FakeResponse(502, json_raises=True,
63
- text="<html>502 Bad Gateway</html>"))
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
- assert "502" in str(exc.value)
68
- assert "Bad Gateway" in str(exc.value)
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
 
@@ -40,7 +40,7 @@ wheels = [
40
40
 
41
41
  [[package]]
42
42
  name = "confamnode"
43
- version = "0.2.3"
43
+ version = "0.2.4"
44
44
  source = { editable = "." }
45
45
  dependencies = [
46
46
  { name = "httpx" },
@@ -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