fix(sdk): make Author field non-empty + v3 capability probe#49
Merged
Conversation
Task #3 / Task #18 (2026-07-03): wire the SDK to the backend's v3 default. Per CLAUDE.md §24, every /check mints a server-side uuidv7 execution_id; the SDK receives it in the response and propagates it to /track. This is the SDK_MIN_VERSION for the v3 rollout per CLAUDE.md §0 pre-flip checklist. Changes: src/nullrun/uuid7.py (new): - RFC 9562 §5.7 time-ordered ID generator. 48-bit unix_ts_ms prefix + 12-bit rand_a + 62-bit rand_b. Same layout as the backend's mint_execution_id() so log scrapers can sort by ID alone. - Uses secrets.token_bytes(10) for cryptographically secure random component. - uuid7() returns stdlib UUID; uuid7_str() returns the canonical 36-char string. src/nullrun/capabilities.py (new): - ServerCapabilities dataclass mirrors /health payload. - is_v3_ready() returns True only when ALL three v3 caps (server_minted_execution_id, per_execution_reservations, heartbeat_time_based) are set. - probe_capabilities(api_url) — best-effort /health fetch with 2s timeout. Returns None on failure (not fatal). - validate_sdk_version(sdk_version, caps) — returns warnings for SDK_MIN_VERSION mismatch. - SDK_MIN_VERSION_FOR_V3 = '0.12.0' is the gate's coordinate for the v3 rollout. src/nullrun/__init__.py: - init() now probes /health after singleton registration and logs a startup warning for version mismatch (does NOT fail init() — the gate still rejects with PROTOCOL_TOO_OLD). - Probe is best-effort: timeout/5xx logs at INFO. src/nullrun/__version__.py: - Bumped 0.11.0 → 0.12.0 (the SDK_MIN_VERSION coordinate). CHANGELOG.md: - New 0.12.0 entry with Added/Changed sections. tests/test_uuid7.py (new): 8 tests pin the wire contract: - Returns stdlib UUID - 36-char string format - Version bits = 7 - Variant bits = 0b10 - Time-ordered (consecutive calls sort) - 1000 unique IDs under rapid calls - Round-trips through uuid.UUID() tests/test_capabilities.py (new): 9 tests pin: - v3-ready backend parses to is_v3_ready()=True - Missing keys default to False (fail-closed) - Partial v3 caps → not ready - Old SDK against v3 backend → warning - Current SDK → no warning - Legacy backend → 'not v3-ready' warning - Unparseable versions don't crash - as_dict() is wire-safe (no secrets) - SDK_MIN_VERSION_FOR_V3 = '0.12.0' Tests: 17 new SDK tests pass. Full backend test suite still green at 1443.
PEP 621 maps `authors` to PKG-INFO's `Author-email:` line but not to the legacy single `Author:` line that `pip show` renders, and pip does not display `Maintainer:` either. As a result every previous release shipped with an empty `Author:` and the maintainer's name never appeared in `pip show nullrun`. Hatchling compounds this: its authors parser only adds an entry to `authors_data["name"]` (which becomes `Author:`) when an inline-table has a `name` and NO `email`. When both are present the name is folded into `Author-email:`'s display_name and the legacy `Author:` line is suppressed entirely. Fix: declare `authors` and `maintainers` as dynamic fields and populate them from a custom hatchling metadata hook (`hatch_build.py`). The hook splits the primary author into a name-only + email-only inline-table pair so hatchling populates both `Author:` and `Author-email:`. Declaring at least one dynamic field is what actually wires `MetadataHookInterface.update()` — without it hatchling configures the hook but never invokes it.
239da4a to
467891f
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
CI on Python 3.11 failed with `NameError: name 'logger' is not defined` in 5 tests. The `feat(sdk)` commit (2a7886b) added new `logger.warning/info/debug` calls in `init()` after the existing `import logging` but never assigned the `logger` name. Master passed only because its pre-existing `logger.warning` calls sit inside an `if existing is not None:` branch that tests rarely exercise; the new ones run on every `init()` call. Also covers the 9 newly-uncovered lines Codecov flagged: `probe_capabilities` failure paths (non-2xx / ConnectError / malformed JSON) and the four new `init()` logging branches (`debug=True` sets DEBUG; probe unreachable → INFO; probe raises → DEBUG; existing runtime shutdown raises → WARNING). Local verification (.venv-ci, Python 3.14): - pytest: 1154 passed (was 1129; +25 new) - ruff: clean - mypy: clean - coverage: 82.02% (threshold 82.00%)
Ruff's isort rule flagged the import block in `init()` — the `from nullrun.__version__` line was placed after `from nullrun.capabilities` but `__version__` sorts before `capabilities` (underscore is 0x5F, letters are 0x61+), so the correct alphabetical order is reversed. CI `Run ruff` step was failing on this; the previous commit's ruff output was checked against an outdated working copy.
2 tasks
maltsev-dev
added a commit
that referenced
this pull request
Jul 3, 2026
…→ 0.12.0) (#50) The `feat(sdk)` commit (79a6e7e, PR #49) bumped `src/nullrun/__version__.py` to 0.12.0 but left `version` in `pyproject.toml` at 0.11.0. Hatchling uses `pyproject.toml`'s version, so `python -m build` was producing a wheel named `nullrun-0.11.0-py3-none-any.whl` — same name as the 0.11.0 artifact that `publish-test` had already uploaded to TestPyPI on its previous successful run (commit 18a91e2). TestPyPI rejects re-uploads of the same wheel hash with HTTP 400 "File already exists" (no overwrite semantics), so the `publish-test` workflow failed at the very last step. Verify locally: $ python -m build --wheel Successfully built nullrun-0.12.0-py3-none-any.whl Also adds `skip-existing: true` to the `pypa/gh-action-pypi-publish` step so a re-run of the same SHA becomes a no-op (matching twine's --skip-existing). Production PyPI cannot overwrite anyway, so this flag is harmless there too.
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.
What
Two related SDK release-prep fixes:
pip show nullrunnow showsAuthor: Anatolii MaltsevandAuthor-email: support@nullrun.io. Every previous release shipped with the legacyAuthor:line empty because PEP 621 mapsauthorsonly toAuthor-email:and hatchling suppressesAuthor:when an inline-table has both a name and an email. Fix: declareauthors/maintainersas dynamic fields and populate them from a custom hatchling metadata hook.v3 capability probe at
init()— wire the SDK to the backend's v3 default per CLAUDE.md §24.init()probes the backend's/healthendpoint and logs a startup warning if the SDK is older than the gate's required minimum (SDK_MIN_VERSION_FOR_V3 = "0.12.0"). Probe is best-effort: timeout / 5xx / exceptions log at INFO / DEBUG;init()itself never fails.Why
pip showonly renders the legacy singleAuthor:line. Adding a maintainer entry didn't help (pip doesn't renderMaintainer:at all). Without this fix the SDK maintainer's identity is invisible to anyone runningpip showagainst the published wheel./checkand rejects older SDKs with 400PROTOCOL_TOO_OLD. Probing atinit()lets the operator see the mismatch at startup instead of on the first failed/check.How
hatch_build.py(new) is a hatchling custom metadata hook that splits the primary author into two inline-tables ({name}+{email}) so hatchling populates bothAuthor:andAuthor-email:. Listingauthorsandmaintainersinproject.dynamicis what actually wiresMetadataHookInterface.update().src/nullrun/capabilities.py(new) definesServerCapabilities(mirror of/healthpayload),probe_capabilities()(best-effort fetch),validate_sdk_version()(returns warnings for mismatch), andSDK_MIN_VERSION_FOR_V3.src/nullrun/uuid7.py(new) is an RFC 9562 §5.7 generator matching the backend'smint_execution_id()layout.src/nullrun/__init__.pyinit()runs the probe in a try/except so transport failures degrade gracefully.Test plan
Local CI-equivalent run (
.venv-ciwith Python 3.14):pytest: 1154 passed, 7 skipped (was 1129; +25 new tests forprobe_capabilitiespaths and theinit()logging branches).ruff check src/: All checks passed.mypy src/: no issues found in 30 source files.coverage run -m pytest: 82.02% total (threshold is 82.00%,fail_underinpyproject.toml).New tests pin the previously-uncovered branches:
test_probe_capabilities_returns_none_on_non_2xx—/health5xx response →None.test_probe_capabilities_returns_none_on_network_error— ConnectError →None.test_probe_capabilities_returns_none_on_malformed_json— ValueError on.json()→None.test_init_with_debug_true_sets_log_level—debug=Trueflips logger to DEBUG.test_init_replaces_existing_runtime_logs_warning— secondinit()over a live runtime triggers the C3 fix warning; also exercises theexisting.shutdown()raises branch.test_init_logs_info_when_probe_unreachable—/health5xx atinit()time → INFO log,init()still returns.test_init_logs_debug_when_probe_raises—probe_capabilitiesraises → DEBUG log,init()still returns.Risk
hatch_build.pylives at the repo root (not undersrc/nullrun/) so it is NOT included in the published wheel. The wheel's METADATA only depends on what hatchling writes during the build, which is exactly what the hook injects. Verified locally:pip show nullrunafterpip install dist/nullrun-0.11.0-py3-none-any.whl --force-reinstallshowsAuthor: Anatolii Maltsev. No runtime surface change./health5xx / timeout / malformed-JSON does not failinit()— only logs. Backwards-compatible: SDKs that ignore the probe still work; the gate's 400PROTOCOL_TOO_OLDremains the source of truth for version mismatch.__version__moves 0.11.0 → 0.12.0 (per the v3 rollout plan in CLAUDE.md §0). The 0.12.0 bump is the gate's coordinate forSDK_MIN_VERSION_FOR_V3.Checklist
CONTRIBUTING.md(if present)feat(sdk)commit)