Skip to content

fix(sdk): make Author field non-empty + v3 capability probe#49

Merged
maltsev-dev merged 4 commits into
masterfrom
feat/sdk-author-metadata
Jul 3, 2026
Merged

fix(sdk): make Author field non-empty + v3 capability probe#49
maltsev-dev merged 4 commits into
masterfrom
feat/sdk-author-metadata

Conversation

@maltsev-dev

@maltsev-dev maltsev-dev commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

Two related SDK release-prep fixes:

  1. pip show nullrun now shows Author: Anatolii Maltsev and Author-email: support@nullrun.io. Every previous release shipped with the legacy Author: line empty because PEP 621 maps authors only to Author-email: and hatchling suppresses Author: when an inline-table has both a name and an email. Fix: declare authors / maintainers as dynamic fields and populate them from a custom hatchling metadata hook.

  2. v3 capability probe at init() — wire the SDK to the backend's v3 default per CLAUDE.md §24. init() probes the backend's /health endpoint 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

  • Author field: pip show only renders the legacy single Author: line. Adding a maintainer entry didn't help (pip doesn't render Maintainer: at all). Without this fix the SDK maintainer's identity is invisible to anyone running pip show against the published wheel.
  • Capability probe: required for the v3 wire-contract rollout. The backend now mints server-side uuidv7 execution_ids on every /check and rejects older SDKs with 400 PROTOCOL_TOO_OLD. Probing at init() lets the operator see the mismatch at startup instead of on the first failed /check.

How

  • Metadata: hatch_build.py (new) is a hatchling custom metadata hook that splits the primary author into two inline-tables ({name} + {email}) so hatchling populates both Author: and Author-email:. Listing authors and maintainers in project.dynamic is what actually wires MetadataHookInterface.update().
  • Capability probe: src/nullrun/capabilities.py (new) defines ServerCapabilities (mirror of /health payload), probe_capabilities() (best-effort fetch), validate_sdk_version() (returns warnings for mismatch), and SDK_MIN_VERSION_FOR_V3. src/nullrun/uuid7.py (new) is an RFC 9562 §5.7 generator matching the backend's mint_execution_id() layout. src/nullrun/__init__.py init() runs the probe in a try/except so transport failures degrade gracefully.

Test plan

Local CI-equivalent run (.venv-ci with Python 3.14):

  • pytest: 1154 passed, 7 skipped (was 1129; +25 new tests for probe_capabilities paths and the init() 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_under in pyproject.toml).

New tests pin the previously-uncovered branches:

  • test_probe_capabilities_returns_none_on_non_2xx/health 5xx 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_leveldebug=True flips logger to DEBUG.
  • test_init_replaces_existing_runtime_logs_warning — second init() over a live runtime triggers the C3 fix warning; also exercises the existing.shutdown() raises branch.
  • test_init_logs_info_when_probe_unreachable/health 5xx at init() time → INFO log, init() still returns.
  • test_init_logs_debug_when_probe_raisesprobe_capabilities raises → DEBUG log, init() still returns.

Risk

  • Metadata hook: hatch_build.py lives at the repo root (not under src/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 nullrun after pip install dist/nullrun-0.11.0-py3-none-any.whl --force-reinstall shows Author: Anatolii Maltsev. No runtime surface change.
  • Capability probe: best-effort by design. A /health 5xx / timeout / malformed-JSON does not fail init() — only logs. Backwards-compatible: SDKs that ignore the probe still work; the gate's 400 PROTOCOL_TOO_OLD remains the source of truth for version mismatch.
  • uuid7 bump: __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 for SDK_MIN_VERSION_FOR_V3.

Checklist

  • I have read the repo's CONTRIBUTING.md (if present)
  • My change does not introduce new lint warnings
  • I have updated the CHANGELOG (in the feat(sdk) commit)
  • I have considered backwards compatibility

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.
@maltsev-dev maltsev-dev force-pushed the feat/sdk-author-metadata branch from 239da4a to 467891f Compare July 3, 2026 11:57
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@maltsev-dev maltsev-dev changed the title Feat/sdk author metadata fix(sdk): make Author field non-empty + v3 capability probe Jul 3, 2026
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.
@maltsev-dev maltsev-dev merged commit 79a6e7e into master Jul 3, 2026
4 checks passed
@maltsev-dev maltsev-dev deleted the feat/sdk-author-metadata branch July 3, 2026 14:18
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant