Repository navigation
Conversation
brainhops.datamodel.metadata: Metadata, the format-agnostic metadata of an image or a transformation, declared as six vocabulary groups (provenance, acquisition, diffusion, display, microscopy, storage) with their BIDS keys, scopes and per-axis fields; the UNSUPPORTED sentinel and supports= capabilities; the raw record and its read-time snapshot; conversion between formats (Metadata.to) with ConversionReport and the loss policy; derive, _select and _reslice for derived images; the value classes (EncodingDirection, GeneratedBy, Channel); preferred_dtype and preferred_storage; Metadata.load, from_bids and to_bids (which import brainhops.io lazily); and MetadataField, the annotation that the metadata field of an image or a transformation will use. bagof-magic >= 0.3.dev2 is required. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…ta (#233) - io/metadata: FileBasedMetadata, the dispatcher of the formats whose files hold metadata (Metadata.load), OpaqueMetadata, the JSON codec of key/value stores, the BIDS sidecar, and sync_metadata, the helper a parser will call to keep its metadata in step with its raw record. - io/base/_metadata_parser.py: MetadataParser and Hdf5MetadataParser. - The metadata of each format, registered with FileBasedMetadata and exported next to its image or transformation classes: NiftiMetadata, MghMetadata (with the MGH tags codec), ZarrMetadata and OmeZarrMetadata, X5Metadata, ItkMetadata and ItkH5Metadata, and FlirtMetadata. - The readers they share with the image and transformation parsers: _load_nifti_header reads a stream too (and the header as stored), is_nifti_stream, _geometry_time_step; read_mgh_raw, is_mgh_stream; the Zarr node_attributes and write_attributes; read_h5_header, with H5Header moved to itk/_metadata.py. No image or transformation has a metadata field yet: the tests cover the metadata classes, their codecs, Metadata.load and the conversion matrix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
docs/dev/metadata-formats.md explains how to write the metadata of a format; the API pages document brainhops.datamodel.metadata (and the names for format authors), brainhops.io.metadata and the ITK metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
| # `UNSUPPORTED`, `None` and an empty `extra` are hidden, or a | ||
| # format that stores three fields would print forty. `format` and | ||
| # a format's own fields come first, then `extra`, then the | ||
| # vocabulary in its declared order (not `bagof`'s reverse MRO). |
There was a problem hiding this comment.
Look at the latest dev release of bagof magic, which allows specifying more complex repr policies.
There was a problem hiding this comment.
Done in 3c4238e. The custom __repr__ is gone. bagof-magic 0.3.dev2 has HideIf, so:
- a class-level
repr=HideIf(_is_absent)onMetadataandVocabularyhidesNoneandUNSUPPORTED; extrahasHideIf(operator.not_), so it is hidden when empty.
With reverse=True, generic metadata prints format, then extra, then the vocabulary.
One gap remains: a format class prints format last, e.g. NiftiMetadata(description='x', format='nifti'). The repr follows field order, and the on= pin leaves format where the parent declared it. Fixing that would need a per-field repr position option upstream. Is format-last acceptable, or should I ask bagof for one?
Generated by Claude Code
| # --- files ------------------------------------------------------ | ||
|
|
||
| @classmethod | ||
| def load(cls, file: tx.Any, **kwargs: tx.Any) -> "Metadata": |
There was a problem hiding this comment.
This should only be in the io version of metadata (filebased). No io ops on the base class.
There was a problem hiding this comment.
Removed in 510a2dd. A file's metadata is read with brainhops.io.metadata.FileBasedMetadata.load, which is the dispatcher; its docstring now lists the formats and gives examples. Tests, the guides and the memo are updated, and so is #287.
Generated by Claude Code
| # --- BIDS --------------------------------------------------------- | ||
|
|
||
| @classmethod | ||
| def from_bids(cls, sidecar: tx.Any) -> "Metadata": |
There was a problem hiding this comment.
Removed in 510a2dd. Sidecars are read with brainhops.io.metadata.bids.from_bids, or the BidsSidecar format.
Generated by Claude Code
|
|
||
| return from_bids(sidecar) | ||
|
|
||
| def to_bids( |
There was a problem hiding this comment.
Removed in 510a2dd. Sidecars are written with brainhops.io.metadata.bids.to_bids(meta).
Generated by Claude Code
| # ---------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _is_absent(value: tx.Any) -> bool: |
There was a problem hiding this comment.
In general, and when possible, move private helpers below public helpers which are below public classes or main functions.
There was a problem hiding this comment.
Done in 170af7d, for every module the PR adds: public code first, then public helpers, then private helpers. Seven private definitions must exist first at import time (_rgba, _vector, _space, _History, _T, _NEW, and the _ZarrMetadataParser base), and each has a one-line comment saying why.
Generated by Claude Code
| """ | ||
| return cls.from_fileobj(BytesIO(content), **kwargs) | ||
|
|
||
| def to_file(self, file: path.FileLike, **kwargs: tx.Any) -> None: |
There was a problem hiding this comment.
There is no to_ on (non writeable) FileParsers anyway.
There was a problem hiding this comment.
Removed in 2f9493d. Only plain Zarr metadata defines to_file. The test now asserts that NIfTI, MGH and OME-Zarr metadata have none.
Generated by Claude Code
| _READ_MODE = "rb" | ||
|
|
||
| @classmethod | ||
| def from_bytes(cls, content: bytes, **kwargs: tx.Any) -> tx.Any: |
There was a problem hiding this comment.
Why not have this on the FileParser? Is it better than the current one?
There was a problem hiding this comment.
Kept on purpose, with a docstring in 2f9493d. The reason is in how FileParser reads:
FileParser.from_bytesraisesParserNotImplementedError;FileParser.from_fileobjreads the whole stream and hands it tofrom_bytes.
Metadata formats implement only from_fileobj, since they read just a header. Without the override, FileBasedMetadata.load(raw_bytes) would fail; test_io_metadata_load covers it.
As a FileParser default it isn't strictly better. The two defaults would then call each other, so a parser that overrides neither would recurse forever. That needs a guard, e.g. only when the class overrides from_fileobj. I've left it for #297.
Generated by Claude Code
| ) | ||
|
|
||
|
|
||
| class Hdf5MetadataParser(MetadataParser): |
There was a problem hiding this comment.
Why do we have format specific sniffers in the general io metadata module?
There was a problem hiding this comment.
Moved in 2f9493d:
Hdf5MetadataParsernow lives next toHdf5Parserinio/base/hdf5.py, and derives from it. That drops about 150 lines of duplicated path/stream/bytes plumbing.- Since that module needs h5py,
ItkH5Metadataandread_h5_headermoved toitk/h5/_metadata.py.itkexportsItkH5Metadataonly when h5py is installed.
Generated by Claude Code
| float | ||
| The confidence, in `[0, 1]`. | ||
| """ | ||
| from brainhops.io.base.nifti import is_nifti_stream |
There was a problem hiding this comment.
Why not top import ?
There was a problem hiding this comment.
In 30fa2dc, imports moved to the top wherever no cycle exists. For example, the Zarr metadata now imports _ome and abczarr.open there; node_attributes/write_attributes moved into _metadata to break a cycle with _image.
This one has to stay lazy, now with a comment: nifti.py imports _nifti_metadata, which imports from nifti.py. MGH is in the same situation.
Generated by Claude Code
The groups (Vocabulary and its seven subclasses) are what a format names in supports=, so they are public names of brainhops.datamodel.metadata, and the docs that list the public names say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…icies (#233) The custom __repr__ goes: Metadata and the vocabulary groups hide a field that holds None or UNSUPPORTED (repr=HideIf(...), bagof-magic 0.3.dev2, the current floor), and extra is hidden when empty too. The fields are listed in reverse, with the groups in reverse order, so that generic metadata shows format, extra, then the vocabulary in its declared order. A format class declares the vocabulary again, and bagof cannot place format before it, so a format's repr shows format last. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
Metadata.load, Metadata.from_bids and Metadata.to_bids go: the data model reads and writes no file. The metadata of a file is read by FileBasedMetadata.load (the dispatcher, or the class of a format), and a BIDS sidecar by brainhops.io.metadata.bids (from_bids, to_bids, and the BidsSidecar reader). The tests, the format author's guide and the design memo follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…unctions (#233) No metadata class overrides Metadata._convert_from, so it is the private function _convert_from(cls, other, args, kwargs). The two _accepts_raw methods and FileBasedMetadata._raw_type give way to the class variable _raw_class, read by the function _accepts_raw(cls, raw): Metadata declares None (any record) and FileBasedMetadata type(None) (none) until a format declares its own type, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…rmation (#233) The space of an EncodingDirection may be a brainhops CoordinateSystem, kept as it is and equal only to an equal system. JSON names a space with a string, so the codec writes a system by its name, and encode_changes (which now takes the report) reports as lost a direction in a system without a name. EncodingDirection.transform also takes a Transformation, through the linear part of the affine it reduces to (_as_affine), and raises TypeError for one that does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
stored_values(data, dtype, slope, intercept) is what a writer stores with the type and the scaling preferred_storage chose: the unscaled values, rounded only when dtype is an integer type. The docstring of preferred_storage says so, and that a scaling is only returned with an integer type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…Parser (#233) MetadataParser loses to_file, which only refused: a FileParser does not write, and plain Zarr defines its own. It keeps from_bytes, documented: a format implements from_fileobj, and FileParser routes bytes the other way (a default for binary parsers is issue #297). Hdf5MetadataParser moves next to Hdf5Parser (io/base/hdf5.py) and derives from it, whose routing of paths, streams and bytes it used to duplicate; sniff_h5 takes error= as for Hdf5Parser. ItkH5Metadata and read_h5_header move next to the ITK .h5 parser (itk/h5/_metadata.py), since they need h5py, and itk exports ItkH5Metadata when h5py is installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
The Zarr metadata imports _ome and abczarr.open at the top, and the attribute helpers (node_attributes, write_attributes) move from _image to _metadata, which the image imports. The imports that stay in a function cross a real cycle, and say so: _nifti_metadata and _mgh_metadata import from nifti and mgh, which import them, EncodingDirection.transform imports the transformations, whose metadata field imports the metadata, and _with_brainhops reads brainhops.__version__. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
… helpers (#233) Every module the metadata adds lists its public classes and functions first, then its public helpers, then its private ones. A private definition that must exist first at import time (a converter a field annotation evaluates, a base class, a default, a TypeVar) stays above, with a comment that says why. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…tem) into the wiring Brings in the answers to the review of #310: the public vocabulary groups, the repr built by bagof, no input or output on Metadata, the conversion helpers as functions, directions in a CoordinateSystem, the rounding of scaled values, the read-only metadata parser and the HDF5 one next to Hdf5Parser, and the layout of the modules. The ITK .h5 parser takes ItkH5Metadata and read_h5_header from itk/h5/_metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…ages (#233) `Metadata.derive(operation)` replaces the private hooks `_select` and `_reslice`: an image operation describes what it did as an `Operation` (`Indexed` for an index, `Resampled` for a resampling), and each value propagates through it by the handler registered for its type, or else for the scope of its field (`propagates`, `propagate`, in the new module `_operations`). The encoding direction registers its handler in `_terms`, the scope defaults are registered under `Scope`, and the raw record of a format goes through `propagate_raw`, which NIfTI uses to scrub its header in place of its `_reslice` override. The index expansion and the axis types move from the images into `Indexed`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
#233) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…233) The class name already says the format. `FileBasedMetadata` declares `format` again with `HideIfDefault`, which bagof binds again on each class against the value the class pins, so `NiftiMetadata(...)` hides `format='nifti'`. Generic `Metadata` keeps showing its format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
…rser (#233) Since #311, `FileParser.from_bytes` wraps the bytes in a stream for a class that implements `from_fileobj`, which every metadata format does, so `FileBasedMetadata.load(raw_bytes)` needs no override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
A level of a pyramid is derived when the pyramid is opened, and its geometry needs its shape, so its data; the level passes none, and no handler reads it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
|
Operations-based
Also here
Generated by Claude Code |
Closes part of #233; split out of #287, which has the review history.
Base.
claude/chore/233-metadata-stack-baseis a merge of #306, #308 (with #307) and #309, so the diff here shows only the metadata system. Once those merge, retarget this PR tomain.What's in it. The metadata system, used on its own. It isn't yet wired into images or transformations; that's #287.
brainhops.datamodel.metadata:Metadata, the common vocabulary with BIDS names, grouped into*Vocabularyclasses;UNSUPPORTED;ScopeandAlong(AxisType);ConversionReportand the loss policy;MetadataField;Metadata.load.brainhops.io.metadata:FileBasedMetadata[RawT], the base of every format and the metadata dispatcher. It isn't aFileBasedObject, soio.loadnever returns metadata.OpaqueMetadata;sync_metadata.Formats: NIfTI, MGH, Zarr, OME-Zarr, x5, ITK
.h5/.tfm/.matand FLIRT. Each one is:MetadataParser(aFileParsermixin);_decode_rawand_encode_raw;This branch also gets the header readers it needs from the image and transformation parsers (moved here so this PR stands alone).
The format author's guide (
docs/dev/metadata-formats.md) and the API pages.Tests. Only the tests that don't need an image's
.metadatafield: 71 intest_datamodel_metadata.py, plusload,matrix, MGH, NIfTI and Zarr. The remaining tests come back in #287.Stack
FormatDispatchermixin and small fixes🤖 Generated with Claude Code
https://claude.ai/code/session_01VmK1jLTwd2hMkvYnMWEyYw
Generated by Claude Code