Skip to content
Closed
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- SDK/CLI: new accounts start on the free plan ($5.00 of usage a month at pay-as-you-go rates, no card), and a request with no credentials now gets a 401 whose `detail` is an object (`code: "UNAUTHENTICATED"`, `message`, `signup_url`, `plan`, `allowance_usd`, `cpc`, `cpm`) pointing at free signup. The SDK raises `AuthenticationError` with the server's `message`, as for any object `detail`; other 401s keep a string `detail`. No SDK code change; older versions show the JSON body as the message.
- SDK/CLI: the API now prices every billed request before it runs and refuses one the remaining balance cannot cover with a 403 whose `detail` is an object (`code: "INSUFFICIENT_BALANCE"`, `message`, `remaining_usd`, `max_affordable_records`, `requested_records`, `upgrade_url`). The SDK raises it as the new `InsufficientBalanceError`, a `PlanAccessError` subclass, so existing `except PlanAccessError` handlers still catch it; its message is the server's `message`, and `max_affordable_records`, `remaining_usd`, `requested_records` and `upgrade_url` are attributes, counted in the request's own size unit (records, or contacts, companies, domains, rows, queries or personas for requests sized by those; the message names it), so a caller can retry at `max_affordable_records`. Only a 403 carrying that code raises it. A request the API could not price answers 503 (`ServerError`), which the SDK retries automatically on GET calls. Any other object `detail` with a `message` now uses that message instead of the raw JSON. The CLI exits 3 with `"code": "insufficient_balance"` and adds `max_affordable_records` and `remaining_usd` to the error JSON. Older SDK versions raise a plain `PlanAccessError` whose message is the JSON body.

## 0.6.0 (2026-10-12)
Expand Down
18 changes: 18 additions & 0 deletions packages/discolike/tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,21 @@ def test_plain_403_stays_plan_access_error() -> None:
with pytest.raises(PlanAccessError) as exc_info:
raise_for_status(_response(403))
assert type(exc_info.value) is PlanAccessError


def test_no_credentials_401_object_detail_uses_its_message() -> None:
body = {
"detail": {
"code": "UNAUTHENTICATED",
"message": "No credentials. A human can create a free account (no card).",
"signup_url": "https://api.discolike.com/v1/public/signup",
"plan": "free",
"allowance_usd": 5.0,
"cpc": 0.7,
"cpm": 14.0,
}
}
with pytest.raises(AuthenticationError) as exc_info:
raise_for_status(_response(401, json_body=body))
assert str(exc_info.value) == "No credentials. A human can create a free account (no card)."
assert exc_info.value.payload["detail"]["signup_url"] == "https://api.discolike.com/v1/public/signup"
Loading