feat(decorator): support callables on subject and action fields (#45)#46
Merged
feat(decorator): support callables on subject and action fields (#45)#46
Conversation
Subject fields (tenant, workspace, app, workflow, agent, toolset), action fields (action_kind, action_name, action_tags), and dimensions on the @cycles decorator now accept callables in addition to constants. Callables are invoked with the decorated function's *args, **kwargs at reservation time, enabling per-call budget routing and dynamic action labeling. Mirrors the existing estimate / actual callable contract and re-aligns the Python client with the Java client's SpEL behavior shipped in cycles-spring-boot-starter 0.2.1 (java#50). Fallback semantics are preserved: - Subject callables returning None fall through to the client-config default (default_subject_fields). - action_kind / action_name returning None fall through to "unknown". - action_tags / dimensions returning None are omitted. - Exceptions in user callables propagate fail-fast without creating a reservation. Internal: - Adds _resolve_value(val, args, kwargs) helper in lifecycle.py. - Widens DecoratorConfig field types and the cycles() public signature. - Threads args/kwargs through _build_reservation_body and both sync and async execute() paths. No protocol or wire-format changes. Coverage holds at 99.38% with lifecycle.py and decorator.py both at 100%. Bumps version to 0.4.0; updates CHANGELOG.md, README.md, AUDIT.md.
Pre-merge cross-doc consistency pass: - AUDIT.md: add `**Version:** 0.4.0` to the new entry to match the format of the prior streaming entry. - CHANGELOG.md: add the `[0.4.0]` compare link at the bottom so the Keep-a-Changelog reference list stays complete. - runcycles/decorator.py: extend the docstring example block with a callable subject/action example so the rendered Sphinx/PyPI docs show the new feature, not just the existing estimate/actual pattern. - examples/decorator_usage.py: add a third decorator demonstrating per-call workspace + action_kind + action_name routing via lambdas. No code or test changes. Coverage 99.38% holds, mypy clean, ruff clean.
Adds 7 tests in tests/test_streaming.py to close the last 9 uncovered lines in runcycles/streaming.py: - ttl_ms=0 returns None from _start_heartbeat (sync + async) - heartbeat extend_reservation HTTP failure → warning branch (sync + async) - heartbeat extend_reservation raises → exception branch (sync + async) - AsyncStreamReservation.decision property getter Total coverage now 100.00% across all 13 modules (was 99.38%, with streaming.py at 97%). 389 passed / 5 skipped. No production code changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #45.
Widens the
@cyclesdecorator so every previously-static field — six subject fields (tenant,workspace,app,workflow,agent,toolset), three action fields (action_kind,action_name,action_tags), anddimensions— now accepts a callable in addition to a constant. Callables are invoked with the decorated function's*args, **kwargsat reservation time, enabling per-call budget routing and dynamic action labeling.Mirrors the existing
estimate/actualcallable contract and re-aligns the Python client with the Java client's SpEL behavior shipped incycles-spring-boot-starter0.2.1 (java#50).Fallback semantics (preserved)
Nonefall through to the client-config default.action_kind/action_namereturningNonefall through to"unknown".action_tags/dimensionsreturningNoneare omitted from the request.Implementation
_resolve_value(val, args, kwargs)helper inlifecycle.py(mirrors_evaluate_amount).DecoratorConfigfield types widened toT | Callable[..., T | None] | None._build_reservation_bodysignature now acceptsargs/kwargs; threaded through bothCyclesLifecycle.executeandAsyncCyclesLifecycle.execute.cycles(...)parameter types widened; docstring updated.No protocol or wire-format changes — only the source of each field's value is widened.
Files
runcycles/lifecycle.py,runcycles/decorator.py— implementationtests/test_lifecycle.py— three new test classes (TestCallableSubjectFields,TestCallableActionFields,TestCallableDimensions); existing_build_reservation_bodycalls updated for the new signaturetests/test_decorator.py— end-to-end test asserting the captured reservation bodyREADME.md— new "Dynamic subject and action fields" sectionAUDIT.md— dated entry per project policyCHANGELOG.md— 0.4.0 entrypyproject.toml— version bump 0.3.0 → 0.4.0Test plan
python -m ruff check runcycles tests— cleanpython -m mypy runcycles— no issuespython -m pytest --cov=runcycles— 382 passed / 5 skipped (live-server)lifecycle.pyanddecorator.pyboth 100% (project floor 95%)test_constant_subject_field_regression,test_constant_action_kind_regression,test_constant_dimensions_regression)Nonefallback covered for subject, action, and dimensions pathstest_callable_exception_propagates)workspacesubject fields