From 37b98ca876727ab40c5918640f3d6d298dce2917 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 14:43:11 +0000 Subject: [PATCH 1/2] CI: let setup-uv create and activate the test venv Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SHtsCCHwKcFDPRqugEWRfU --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7be6c30..81c6508 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,7 @@ jobs: - uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python }} + activate-environment: true - run: uv pip install -e . pytest fastapi - run: pytest -q From 16efbc0604bf0b2ef57de52731c20376d93769ce Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 14:43:40 +0000 Subject: [PATCH 2/2] Raise hibira.ConnectionError for network failures httpx transport errors (DNS, connect, TLS, proxy, timeout) escaped as raw httpx exceptions, so except hibira.HibiraError missed them. They now raise hibira.ConnectionError, a HibiraError and a builtin ConnectionError. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SHtsCCHwKcFDPRqugEWRfU --- CHANGELOG.md | 1 + README.md | 4 +++- src/hibira/__init__.py | 2 ++ src/hibira/_client.py | 39 +++++++++++++++++++++++---------------- src/hibira/_errors.py | 5 +++++ tests/test_client.py | 17 +++++++++++++++++ 6 files changed, 51 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de2758..1dc9410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,5 @@ First release. - Hibira Push: `push_to_interests`, `push_to_users`, `push_token`, `delete_push_user`. - Approvals: `request_approval`, `get_approval`, `wait_for_approval`. - `stream_to_channel` / `astream_to_channel`: stream LLM output to `useStream()` in `@hibira-io/react` and `@hibira-io/vue`, with `text_of` for OpenAI, Anthropic, OpenAI Agents SDK, LangChain / LangGraph and Gemini chunks. +- Errors: `HibiraError` base; `HTTPError` with `AuthenticationError`, `NotFound`, `PayloadTooLarge`, `RateLimited`; `ConnectionError` for network failures; `InvalidWebhook`, `ConfigError`. - `hibira.fastapi`: channel / user auth router and webhook dependency. diff --git a/README.md b/README.md index 71db114..a3b3193 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,9 @@ reads it once, waiting up to 25 seconds for an answer. Every error is a `hibira.HibiraError`. HTTP errors are `hibira.HTTPError` (`status`, `message`, `body`), with subclasses `AuthenticationError` (401, 403), `NotFound` (404), `PayloadTooLarge` (413, the event is over the plan's -message size) and `RateLimited` (429). Missing settings raise `ConfigError`. +message size) and `RateLimited` (429). Network failures (DNS, connect, +TLS, proxy, timeout) raise `hibira.ConnectionError`. Missing settings raise +`ConfigError`. ## Development diff --git a/src/hibira/__init__.py b/src/hibira/__init__.py index 248f55a..6fd087c 100644 --- a/src/hibira/__init__.py +++ b/src/hibira/__init__.py @@ -10,6 +10,7 @@ from ._errors import ( AuthenticationError, ConfigError, + ConnectionError, HibiraError, HTTPError, InvalidWebhook, @@ -27,6 +28,7 @@ "AuthenticationError", "Client", "ConfigError", + "ConnectionError", "HTTPError", "HibiraError", "InvalidWebhook", diff --git a/src/hibira/_client.py b/src/hibira/_client.py index 94cea22..bc7d1e6 100644 --- a/src/hibira/_client.py +++ b/src/hibira/_client.py @@ -10,6 +10,7 @@ from . import _signing from ._errors import ConfigError, error_for +from ._errors import ConnectionError as HibiraConnectionError from ._version import __version__ DEFAULT_HOST = "ws.hibira.io" @@ -291,14 +292,17 @@ def __init__( self._http = http_client or httpx.Client(timeout=self.timeout) def _send(self, req: _Request, timeout: float | None = None) -> Any: - r = self._http.request( - req.method, - req.url, - params=req.params or None, - content=req.content, - headers=req.headers, - timeout=timeout if timeout is not None else self.timeout, - ) + try: + r = self._http.request( + req.method, + req.url, + params=req.params or None, + content=req.content, + headers=req.headers, + timeout=timeout if timeout is not None else self.timeout, + ) + except httpx.TransportError as err: + raise HibiraConnectionError(f"{req.method} {req.url}: {err}") from err return _parse(r) def close(self) -> None: @@ -397,14 +401,17 @@ def __init__( self._http = http_client or httpx.AsyncClient(timeout=self.timeout) async def _send(self, req: _Request, timeout: float | None = None) -> Any: - r = await self._http.request( - req.method, - req.url, - params=req.params or None, - content=req.content, - headers=req.headers, - timeout=timeout if timeout is not None else self.timeout, - ) + try: + r = await self._http.request( + req.method, + req.url, + params=req.params or None, + content=req.content, + headers=req.headers, + timeout=timeout if timeout is not None else self.timeout, + ) + except httpx.TransportError as err: + raise HibiraConnectionError(f"{req.method} {req.url}: {err}") from err return _parse(r) async def aclose(self) -> None: diff --git a/src/hibira/_errors.py b/src/hibira/_errors.py index 2725c0c..89b76d5 100644 --- a/src/hibira/_errors.py +++ b/src/hibira/_errors.py @@ -1,5 +1,6 @@ from __future__ import annotations +import builtins import json from typing import Any @@ -12,6 +13,10 @@ class ConfigError(HibiraError): """A setting is missing or invalid (app id, key, secret, push instance...).""" +class ConnectionError(HibiraError, builtins.ConnectionError): # noqa: A001 + """The request did not get an answer: DNS, connect, TLS, proxy or timeout failure.""" + + class InvalidWebhook(HibiraError): """A webhook's key or signature does not match this app.""" diff --git a/tests/test_client.py b/tests/test_client.py index aa2af35..9f45075 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -182,3 +182,20 @@ async def main(): asyncio.run(main()) assert json.loads(seen[0].content)["data"] == '{"x":1}' check_signature(seen[0]) + + +def test_network_errors_are_hibira_errors(): + def handler(req): + raise httpx.ConnectError("connection refused", request=req) + + with pytest.raises(hibira.ConnectionError) as info: + make(handler).trigger("a", "e", 1) + assert isinstance(info.value, hibira.HibiraError) and isinstance(info.value, ConnectionError) + assert isinstance(info.value.__cause__, httpx.ConnectError) + + async def main(): + c = hibira.AsyncClient("1", "k", "s", http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler))) + with pytest.raises(hibira.ConnectionError): + await c.trigger("a", "e", 1) + + asyncio.run(main())