Skip to content

Python(feat): expose multi-value metadata via a Metadata mapping with getall - #699

Draft
dengjonathan wants to merge 3 commits into
python-metadata-proto-to-dict-multivaluefrom
python-metadata-getall
Draft

Python(feat): expose multi-value metadata via a Metadata mapping with getall#699
dengjonathan wants to merge 3 commits into
python-metadata-proto-to-dict-multivaluefrom
python-metadata-getall

Conversation

@dengjonathan

Copy link
Copy Markdown
Contributor

What

Stacked on #698 (crash fix). That PR makes multi-value metadata safe to fetch; this one makes it usable: values beyond the first are no longer invisible to client code.

metadata_proto_to_dict now returns Metadata, a dict subclass:

run = client.runs.get(run_id=...)
run.metadata["associated_parts"]           # "ABC" — first value; all existing dict behavior unchanged
run.metadata.getall("associated_parts")    # ["ABC", "XYZ"] — full list, canonical order
run.metadata.getall("missing")             # []
  • Scalar view = first value per key, so every existing caller (dict access, .get(), iteration, ==, isinstance(md, dict), json.dumps) behaves exactly as before. Invariant: md[k] == md.getall(k)[0].
  • Run / Asset / Report / Channel .metadata fields adopt the type. A __get_pydantic_core_schema__ hook passes Metadata instances through validation untouched — without it, pydantic rebuilds the field as a plain dict and silently drops the extra values. Serialization (model_dump / model_dump_json) still emits a plain first-value dict, so dumped output is unchanged.
  • Follows the MultiDict pattern (werkzeug getlist, aiohttp getall, stdlib email.get_all); getall matches aiohttp's spelling. Same contract as the rule-payload Metadata shipping in azimuth (ENG-13104), so rule code, canvas code, and client scripts share one metadata API.

Scope

Read side only. The write path (accepting list values in metadata_dict_to_proto / update models) is Phase 3 of ENG-13281.

Testing

_tests/util/test_metadata.py grows from 4 to 13 tests: getall ordering/copy/fallback semantics, first-value invariant, JSON serializability, and pydantic field behavior (instance pass-through preserving lists, plain-dict wrapping, non-mapping rejection, model_dump output). Also verified end-to-end that Run._from_proto with duplicate-key metadata yields metadata.getall(...) with the full list. ruff check / ruff format --check pass at the pinned version (0.12.12); generated sync stubs don't reference the changed annotations.

Linear: ENG-13281 (Phase 2)

🤖 Generated with Claude Code

… getall

metadata_proto_to_dict now returns Metadata, a dict subclass whose
scalar view keeps the first value per key (unchanged for existing
callers) and whose getall(key) returns the full ordered value list.
Run/Asset/Report/Channel .metadata fields adopt the type, with a
pydantic core-schema hook so validation passes instances through
instead of rebuilding them as plain dicts (which would drop the extra
values).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Python docs preview: https://sift-stack.github.io/sift/python/pr-699/

Deployed from 6d222d8. The link may take up to a minute to become live as GitHub Pages propagates.

dengjonathan and others added 2 commits July 29, 2026 16:15
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seeds duplicate-key metadata through the raw UpdateRun proto (the public
write path is scalar-only until ENG-13281 Phase 3) and asserts the full
read-side contract: Metadata type, scalar first-value view, getall order,
and the first-value invariant. Skips cleanly against backends without the
multi-value-metadata flag so CI stays green until flag GA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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