Errors and retries
The exception hierarchy
Every error the SDK raises for a platform response is a subclass of HoneyframeError:
HoneyframeError
├── HoneyframeConfigError missing or unsafe setting; raised before any request
└── HoneyframeAPIError any non-2xx response
├── HoneyframeAuthError 401, 403
├── HoneyframeNotFoundError 404
├── HoneyframeValidationError 400, 409, 422
├── HoneyframeRateLimitError 429 — retrying is correct (0.1.8+)
└── HoneyframeBudgetExceededError 429 budget_exceeded — not retryable (0.1.8+)
All of them are importable from honeyframeapi. A HoneyframeAPIError carries:
| Attribute | Content |
|---|---|
status_code | The HTTP status. |
detail | The server's message: the detail field of the JSON body when there is one, otherwise the body text. Capped at 4 KB. |
method, url | The request that failed. |
response | A copy of the httpx.Response for inspection. Its .request carries no credentials and no body. |
from honeyframeapi import (
HoneyframeAuthError, HoneyframeNotFoundError, HoneyframeValidationError,
)
try:
ds = project.get_dataset("fact_orders")
ds.get_metadata()
except HoneyframeNotFoundError:
print("no such dataset in this project")
except HoneyframeAuthError as exc:
print("token expired, revoked or not allowed:", exc.detail)
except HoneyframeValidationError as exc:
print("refused:", exc.status_code, exc.detail)
A few other exceptions are worth knowing:
ValueError— an argument the SDK can check locally is out of range (anexpires_daysoutside the allowed values, alimitabove 200, an unknown statistics card type, an inconsistentchrome(...)combination).TimeoutError— a wait gave up:JobsAPI.wait(raise_on_timeout=True),trigger_and_wait,build_code_environment(wait=True). The work continues on the server.httpxexceptions — a network failure (connection refused, DNS failure, read timeout) that persists after the automatic retries is raised as the underlyinghttpx.TransportErrorsubclass, not as aHoneyframeError. Catchhttpx.TransportError(orhttpx.HTTPError) if your script should survive network outages.
Rate limits and spent budgets
Both arrive as HTTP 429, and they need opposite handling. The SDK tells them apart (0.1.8+; 0.1.7 raises a plain HoneyframeAPIError for any 429):
HoneyframeRateLimitError— a rate or concurrency limit. Waiting and retrying is correct.exc.retry_afterholds the seconds from theRetry-Afterheader, orNonewhen the header is missing or uses the date form; fall back to your own backoff then.HoneyframeBudgetExceededError— the organization's metered budget is spent. It does not clear on a timer (the quota resets with the billing month), so retrying cannot succeed. Surface it to a person: raise the budget, or wait for the reset.
The SDK never retries a 429 itself, because only you know whether waiting inside your process is acceptable:
import time
from honeyframeapi import HoneyframeBudgetExceededError, HoneyframeRateLimitError
def with_rate_limit_retry(call, attempts=5):
for attempt in range(attempts):
try:
return call()
except HoneyframeBudgetExceededError:
raise # never retry a spent budget
except HoneyframeRateLimitError as exc:
if attempt == attempts - 1:
raise
time.sleep(exc.retry_after if exc.retry_after is not None else 2 ** attempt)
reply = with_rate_limit_retry(lambda: client.get_agent(42).ask("Orders today?"))
Automatic retries
The client retries a request automatically only when all of these hold:
- the method is
GET,HEADorOPTIONS(no side effects), and - the failure is a transport error (connection reset, read timeout, …) or a
502,503or504response — the kind of failure a restart or proxy hiccup causes.
Writes (POST, PUT, PATCH, DELETE) are never retried: a write that failed in transit may already have been applied, and repeating it could apply it twice. 429 responses are never retried (above).
The backoff doubles each time: retry_backoff, then 2 × retry_backoff, and so on.
client = HoneyframeClient.from_env(
max_retries=4, # default 2 — so up to 3 attempts in total
retry_backoff=1.0, # default 0.5 seconds
)
client = HoneyframeClient.from_env(max_retries=0) # no automatic retries
Expired sessions. Separately from the retries above, when a request returns 401 and the client signed in with a username and password, it signs in again and repeats the request once. A token passed as token= is not refreshed; the 401 is raised as HoneyframeAuthError.
Timeouts
The client's timeout (default 60 seconds; None disables it) applies to each request. Calls that can take longer take their own timeout= in seconds:
| Call | timeout= |
|---|---|
Dashboard.export_pdf, Dashboard.execute_all, Dataset.materialize | Overrides the client timeout for that request (0.1.8+). |
client.request(...) | Overrides the client timeout for that request (0.1.8+). |
MlRecipe.run, PluginRecipe.run | Bounds the synchronous run's HTTP call (0.1.8+). |
Scenario.run_and_wait, JobsAPI.wait, trigger_and_wait, Project.run_dbt(wait=True), Recipe.build(wait=True) | How long to keep polling; each poll is a normal request. |
Synchronous dbt calls through project.dbt (run, test, build, execute) use the client timeout; raise it for long builds, for example HoneyframeClient.from_env(timeout=1800).
When every call fails
If a script that worked suddenly fails on every call with HoneyframeAuthError, check the organization's license before the token: client.license_status() (0.1.8+) answers even when the platform is locked. Only valid and grace let other requests through; every status except valid comes with a message.
print(client.license_status()["status"])
Verified against honeyframeapi 0.1.8.