Python SDK for the Hawk daemon API. Provides an idiomatic Python client for chat, streaming, sessions, and stats.
- Thin wrapper — maps directly to the hawk daemon HTTP API
- Type-hinted — full type annotations for IDE support
- Zero dependencies — only
requestsandtyping_extensions
pip install -e ".[dev]" # Install with dev deps
pytest # Run tests
pytest --cov=hawk --cov-report=term-missing # Coverage
ruff check . # Lint
ruff format . # Format
mypy . # Type checkhawk/client.py— MainHawkClientclasshawk/models.py— Pydantic models for API responseshawk/errors.py— Typed error classes (HawkAPIError, etc.)hawk/discovery.py— Auto-discover running hawk daemonhawk/memory_tools.py— Memory graph operationstests/— Test suite with mocked HTTP responses
- Python 3.10+
rufffor linting and formatting (enforced in CI)- Type annotations required on all public APIs
- Conventional Commits:
feat:,fix:,docs:,refactor:,test: - No
Co-authored-by:trailers pytestfor testing,httprettyfor HTTP mocking
HawkClientuses context manager (withstatement) for cleanup- Discovery scans localhost ports — tests must mock this
Retry-After: 0is valid — respect it, don't retry immediately
- Modules: snake_case, one concept per file (
client.py,errors.py,retry.py,streaming.py) - Classes: PascalCase, noun-based (
HawkClient,AsyncHawkClient,StreamReader,RetryConfig) - Error classes: suffix with
Error(HawkAPIError,NotFoundError,RateLimitError,InternalServerError) - Private methods: leading underscore (
_request,_build_headers,_validate_base_url,_compute_backoff) - Type aliases: use
TypeVarfor generics (T = TypeVar("T")intypes.pyandretry.py) - Pydantic fields: use
Field(alias="snake_case")for API-mapped names (session_id,tokens_in,active_sessions) - Test classes:
Testprefix + subject (TestHawkClientSync,TestAsyncHawkClient,TestParseError,TestRetryConfig) - Test methods:
test_+ behavior (test_health,test_chat_stream_error,test_no_retry_on_404)
All API types live in src/hawk/types.py. Every model uses BaseModel with model_config = {"populate_by_name": True} to allow both alias and field name access. Fields use Field(alias="snake_case") to map to the daemon's JSON keys:
class ChatResponse(BaseModel):
session_id: str = Field(alias="session_id")
tokens_in: int = Field(alias="tokens_in")
model_config = {"populate_by_name": True}errors.py defines a status-code-based hierarchy: HawkAPIError is the base, with subclasses for each HTTP status (400, 401, 403, 404, 429, 500, 503). The parse_error() factory reads the JSON body (error, code, details fields) and returns the correct typed error. Always catch specific subclasses, not HawkAPIError broadly.
Every public client method wraps its logic in a _do() closure passed to with_retry_sync() (sync) or with_retry() (async). The retry function respects Retry-After headers on 429 responses and uses exponential backoff with jitter for other retryable statuses (429, 500, 502, 503, 504).
StreamReader and AsyncStreamReader wrap httpx.Response with SSE parsing. They implement context manager protocol (with/async with). The events() method yields StreamEvent objects; collect_text() and collect_tool_calls() are convenience collectors.
HawkClient (sync) and AsyncHawkClient (async) are structurally identical. Every method exists on both. Sync uses httpx.Client + with_retry_sync; async uses httpx.AsyncClient + with_retry. When adding a new method, add it to both classes with identical signatures (minus async/await).
Tests use respx (via pytest fixture respx_mock) to mock HTTP responses. Each test registers routes on BASE_URL = "http://127.0.0.1:4590":
def test_health(self, respx_mock: respx.MockRouter) -> None:
respx_mock.get(f"{BASE_URL}/v1/health").mock(
return_value=httpx.Response(200, json={...})
)Async test classes use @pytest.mark.asyncio decorator. The respx_mock fixture works for both sync and async.
test_errors.py uses a helper _make_response() to construct httpx.Response objects with specific status codes, JSON bodies, and headers. Tests verify both the error type (isinstance) and properties (status_code, message, retry_after).
SSE body strings follow the format "event: content\ndata: Hello\n\n". Tests verify both collect_text() and mid-stream break behavior.
test_retry.py tests retry logic by wrapping call counters in closures. Uses tiny backoff values (initial_backoff=0.01) to keep tests fast. Verifies both retry-on-retryable and no-retry-on-non-retryable paths.
| What | Where |
|---|---|
| Public API exports | src/hawk/__init__.py |
| Client (sync + async) | src/hawk/client.py |
| Pydantic models | src/hawk/types.py |
| Error types + parser | src/hawk/errors.py |
| Retry logic | src/hawk/retry.py |
| SSE streaming | src/hawk/streaming.py |
| Agent abstraction | src/hawk/agent.py |
| Tool decorator + loop | src/hawk/tools.py |
| Workflow engine | src/hawk/workflow.py |
| Agent discovery | src/hawk/discovery.py |
| Memory graph ops | src/hawk/memory_tools.py |
| Evaluation/benchmarks | src/hawk/evaluate.py |
| Tracing/observability | src/hawk/tracing.py |
| Client tests | tests/test_client.py |
| Error tests | tests/test_errors.py |
| Retry tests | tests/test_retry.py |
| Streaming tests | tests/test_streaming.py |
- Safe to refactor: internal helpers like
_build_headers,_validate_base_url,_compute_backoff— they are private and well-tested - Do not change:
parse_error()status-code mapping without updating all error subclasses and tests - Do not change: Pydantic
Field(alias=...)values — they match the daemon's JSON contract - Safe to extend: add new error subclasses by adding to
_STATUS_TO_ERRORdict and creating the class - Safe to extend: add new client methods by following the
_do()+with_retry_sync()pattern - When adding streaming endpoints: follow the
chat_streampattern — build request, send withstream=True, check status before returningStreamReader