Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/hibira/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ._errors import (
AuthenticationError,
ConfigError,
ConnectionError,
HibiraError,
HTTPError,
InvalidWebhook,
Expand All @@ -27,6 +28,7 @@
"AuthenticationError",
"Client",
"ConfigError",
"ConnectionError",
"HTTPError",
"HibiraError",
"InvalidWebhook",
Expand Down
39 changes: 23 additions & 16 deletions src/hibira/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions src/hibira/_errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import builtins
import json
from typing import Any

Expand All @@ -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."""

Expand Down
17 changes: 17 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Loading