diff --git a/AUDIT.md b/AUDIT.md index e6b67df..d95741a 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,6 +1,7 @@ # Cycles Protocol v0.1.25 — Client (Python) Audit -**Date:** 2026-07-03 (integration-test-only, no version bump — `test_health_check` now probes the public `/actuator/health/readiness` endpoint instead of aggregate `/actuator/health`, which requires `X-Admin-API-Key` since cycles-server v0.1.25.45 and fails closed with 500 when the server has no admin key configured. The old assertion had failed the org nightly Full-Stack Integration every night since 2026-06-28. No library code change.), +**Date:** 2026-07-09 (README + docstring transport-error documentation fix, no version bump — see the dated entry at the end of this file. `CyclesTransportError` is exported but never raised by the SDK; README and its docstring now describe the actual `status == -1` surfacing.), +2026-07-03 (integration-test-only, no version bump — `test_health_check` now probes the public `/actuator/health/readiness` endpoint instead of aggregate `/actuator/health`, which requires `X-Admin-API-Key` since cycles-server v0.1.25.45 and fails closed with 500 when the server has no admin key configured. The old assertion had failed the org nightly Full-Stack Integration every night since 2026-06-28. No library code change.), 2026-05-22 (v0.4.3 — `expires_from`/`expires_to` and `finalized_from`/`finalized_to` ISO-8601 window-filter passthrough on `list_reservations` per `cycles-protocol-v0.yaml` revision 2026-05-22; closes the Python-client side of runcycles/cycles-server#162. No code change — `**query_params` already forwards arbitrary kwargs. Added sync + async regression tests; unlike `from`/`to` the new param names are plain kwargs (no Python-reserved-word workaround needed). 393 tests pass at 100% coverage.), 2026-05-21 (v0.4.2 — `from` / `to` ISO-8601 window-filter passthrough on `list_reservations` per `cycles-protocol-v0.yaml` revision 2026-05-21; closes the client side of runcycles/cycles-server#159. No code change — the existing `**query_params` signature already forwards arbitrary kwargs to the URL query string. Added sync + async regression tests that lock the passthrough in (using the `**{"from": ..., "to": ...}` dict-unpack form because `from` is a Python reserved keyword). 391 tests pass at 100% coverage.), 2026-03-14 @@ -271,3 +272,17 @@ Cross-cutting hardening landed in response to org-wide tracking issues filed in Not included in this change: bumping the reusable-workflow ref `runcycles/.github/.github/workflows/ci-python.yml@main` to `@v1` (`runcycles/.github#60`). That bump is intentionally split into a separate follow-up PR — it depends on the `v1` tag existing in `runcycles/.github`, which is being cut after the canonical-script PR (`runcycles/.github#64`) merges. Protocol conformance: No protocol or wire-format changes. No SDK source touched. Test suite unaffected. + +## README Transport-Error Documentation Fix (added 2026-07-09) + +**Files:** `README.md` +**Version:** unreleased (docs-only, no version bump, no CHANGELOG entry per repo convention) + +Documentation-only correction. The README's exception-hierarchy table described `CyclesTransportError` as "Network-level failure (connection, DNS, timeout)", implying the SDK raises it — but nothing in the package ever raises it. Actual behavior: + +- **`@cycles` decorator / HOF paths:** a transport failure at reserve time raises `CyclesProtocolError` with `status == -1` and `error_code=None` (via `_build_protocol_exception` in `lifecycle.py`); commit-time transport failures are retried in the background by the commit retry engine, not raised. +- **Programmatic client:** never raises for transport failures — returns `CyclesResponse` with `is_transport_error == True` and `status == -1` (`CyclesResponse.transport_error` constructor in `response.py`). + +The table row now states the class is exported for user code but never raised by the SDK, and a new "Transport errors" subsection documents the `status == -1` behavior for both API surfaces with a detection example. Wording matches the docs site (`cycles-docs/how-to/error-handling-patterns-in-python.md`). `CyclesTransportError` remains exported from `runcycles/__init__.py` for use in user code — no API change. + +Protocol conformance: No protocol or wire-format changes. No SDK source touched. Test suite unaffected. diff --git a/README.md b/README.md index 2c290ec..63ce081 100644 --- a/README.md +++ b/README.md @@ -298,13 +298,32 @@ Exception hierarchy: | Exception | When | |---|---| | `CyclesError` | Base for all Cycles errors | -| `CyclesProtocolError` | Server returned a protocol-level error | +| `CyclesProtocolError` | Server returned a protocol-level error; also raised with `status == -1` when the SDK wraps a reserve-time transport failure | | `BudgetExceededError` | Budget insufficient for the reservation | | `OverdraftLimitExceededError` | Debt exceeds the overdraft limit | | `DebtOutstandingError` | Outstanding debt blocks new reservations | | `ReservationExpiredError` | Operating on an expired reservation | | `ReservationFinalizedError` | Operating on an already-committed/released reservation | -| `CyclesTransportError` | Network-level failure (connection, DNS, timeout) | +| `CyclesTransportError` | Exported for use in your own code; never raised by the SDK — transport failures surface as `status == -1` (see below) | + +### Transport errors + +When the HTTP request itself fails (DNS resolution, connection refused, timeout), the SDK never raises `CyclesTransportError` — the class is exported for use in your own code (e.g. wrapping transport-level failures in higher-level integrations). Instead: + +- **Lifecycle-managed surfaces (`@cycles` and `stream_reservation()`):** a transport failure at reserve time raises `CyclesProtocolError` with `status == -1` and `error_code=None`. Transport failures at commit time are retried in the background by the commit retry engine, not raised. +- **Programmatic client:** calls never raise for transport failures — they return a `CyclesResponse` with `is_transport_error == True` and `status == -1` (shown above). + +```python +from runcycles import CyclesProtocolError + +try: + guarded_func() +except CyclesProtocolError as e: + if e.status == -1: + print(f"Network error reaching Cycles: {e}") # retry or degrade + else: + raise +``` ## Preflight checks (decide) diff --git a/runcycles/exceptions.py b/runcycles/exceptions.py index 6d372e1..d434ed6 100644 --- a/runcycles/exceptions.py +++ b/runcycles/exceptions.py @@ -77,7 +77,13 @@ class ReservationFinalizedError(CyclesProtocolError): class CyclesTransportError(CyclesError): - """Raised when a transport-level error occurs (connection timeout, DNS failure, etc.).""" + """Transport-level error (connection timeout, DNS failure, etc.). + + Exported for use in application code; the SDK itself surfaces transport + failures as ``CyclesProtocolError`` with ``status == -1`` (decorator and + ``stream_reservation`` paths) or ``CyclesResponse.transport_error`` + (programmatic client) rather than raising this class. + """ def __init__(self, message: str, *, cause: Exception | None = None) -> None: super().__init__(message)