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
3 changes: 2 additions & 1 deletion AUDIT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Cycles Protocol v0.1.25 — Client (Python) Audit

**Date:** 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.),
**Date:** 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
**Spec:** `cycles-protocol-v0.yaml` (OpenAPI 3.1.0, v0.1.25)
**Client:** `runcycles` (Python 3.10+ / httpx / Pydantic v2)
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.3] - 2026-05-22

Wire-passthrough verification for `expires_from`/`expires_to` and `finalized_from`/`finalized_to` query params on `list_reservations`. Implements `cycles-protocol-v0.yaml` revision 2026-05-22 ([runcycles/cycles-protocol#98](https://github.com/runcycles/cycles-protocol/pull/98)) on the client side; runcycles/cycles-server#163 ships the server impl.

### Added

- Sync + async regression tests confirming `list_reservations` forwards the four new ISO-8601 window params to the URL query string byte-exactly. Unlike `from` (a Python keyword), the new param names are plain kwargs:
```python
client.list_reservations(
expires_from="2026-05-22T00:00:00Z",
expires_to="2026-05-23T00:00:00Z",
finalized_from="2026-05-15T00:00:00Z",
finalized_to="2026-05-22T00:00:00Z",
)
```

### Notes

- No protocol or wire-format change. Servers older than v0.1.25.21 will silently ignore the params per the additive-parameter guarantee in `cycles-protocol-v0.yaml`.
- 393 tests pass at 100% coverage (gate ≥95%).

## [0.4.2] - 2026-05-21

Wire-passthrough verification for the new `from` / `to` query params on `list_reservations`. Implements `cycles-protocol-v0.yaml` revision 2026-05-21 ([runcycles/cycles-protocol#97](https://github.com/runcycles/cycles-protocol/pull/97)) on the client side; runcycles/cycles-server#160 ships the server impl.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "runcycles"
version = "0.4.2"
version = "0.4.3"
description = "Python AI agent budget control — enforce LLM cost limits, tool permissions, and multi-tenant policies before agent actions execute."
readme = "README.md"
license = "Apache-2.0"
Expand Down
58 changes: 58 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,35 @@ def test_list_reservations_with_from_to_window(self, config: CyclesConfig, httpx

assert response.is_success

def test_list_reservations_with_expires_and_finalized_windows(self, config: CyclesConfig, httpx_mock) -> None: # type: ignore[no-untyped-def]
"""`expires_*`/`finalized_*` ISO-8601 window-filter passthrough (cycles-protocol v0.1.25 revision 2026-05-22).

Unlike `from`, these param names aren't Python reserved keywords, so callers
can use direct kwargs. Test pins all four params on the wire."""
httpx_mock.add_response(
method="GET",
url=(
"http://localhost:7878/v1/reservations?tenant=acme"
"&expires_from=2026-05-22T00%3A00%3A00Z"
"&expires_to=2026-05-23T00%3A00%3A00Z"
"&finalized_from=2026-05-15T00%3A00%3A00Z"
"&finalized_to=2026-05-22T00%3A00%3A00Z"
),
json={"reservations": [], "has_more": False},
status_code=200,
)

with CyclesClient(config) as client:
response = client.list_reservations(
tenant="acme",
expires_from="2026-05-22T00:00:00Z",
expires_to="2026-05-23T00:00:00Z",
finalized_from="2026-05-15T00:00:00Z",
finalized_to="2026-05-22T00:00:00Z",
)

assert response.is_success

def test_get_reservation(self, config: CyclesConfig, httpx_mock) -> None: # type: ignore[no-untyped-def]
httpx_mock.add_response(
method="GET",
Expand Down Expand Up @@ -443,6 +472,35 @@ async def test_list_reservations_with_from_to_window(self, config: CyclesConfig,

assert response.is_success

async def test_list_reservations_with_expires_and_finalized_windows(self, config: CyclesConfig, httpx_mock) -> None: # type: ignore[no-untyped-def]
"""Async parity for the `expires_*`/`finalized_*` window-filter passthrough.

See sync sibling for the reserved-keyword note on `from`; the new
v0.1.25.22 params use plain kwargs."""
httpx_mock.add_response(
method="GET",
url=(
"http://localhost:7878/v1/reservations?tenant=acme"
"&expires_from=2026-05-22T00%3A00%3A00Z"
"&expires_to=2026-05-23T00%3A00%3A00Z"
"&finalized_from=2026-05-15T00%3A00%3A00Z"
"&finalized_to=2026-05-22T00%3A00%3A00Z"
),
json={"reservations": [], "has_more": False},
status_code=200,
)

async with AsyncCyclesClient(config) as client:
response = await client.list_reservations(
tenant="acme",
expires_from="2026-05-22T00:00:00Z",
expires_to="2026-05-23T00:00:00Z",
finalized_from="2026-05-15T00:00:00Z",
finalized_to="2026-05-22T00:00:00Z",
)

assert response.is_success

async def test_get_reservation(self, config: CyclesConfig, httpx_mock) -> None: # type: ignore[no-untyped-def]
httpx_mock.add_response(
method="GET",
Expand Down