Skip to content

Dimension Parsing#

Dimension metadata extraction, time coordinate parsing, and variable-dimension relationship handling for NetCDF files.

pyramids.netcdf.dimensions #

DimMetaData dataclass #

Unified information for a single netCDF dimension.

This immutable dataclass captures both the structural information the GDAL netCDF driver exposes via NETCDF_DIM_* keys and, optionally, the per-dimension attribute mapping collected from keys of the form "<name>#<attr>" (e.g., time#units).

It subsumes the previous "Dimension" helper by adding an attrs field while still preserving the original raw bucket that stores the exact strings parsed from metadata.

Parameters:

Name Type Description Default
name str

Dimension name (e.g., "time", "level0").

required
size int | None

Dimension length (if known). Often derived from the first integer in *_DEF or the length of *_VALUES.

None
values list[str | Number] | None

Parsed scalar values from the *_VALUES entry, if provided by the GDAL netCDF driver.

None
def_fields tuple[int, ...] | None

Parsed integers from the *_DEF entry. The meaning is driver- specific; commonly the first value corresponds to the dimension size.

None
raw dict[str, str]

Raw strings captured from metadata for this dimension (e.g., the original DEF and VALUES content).

dict()
attrs dict[str, str]

Optional attribute dictionary associated with the same dimension name (e.g., {"axis": "T", "units": "days since ..."}).

dict()

Raises:

Type Description
ValueError

If size is negative.

Examples:

  • Construct manually for testing
    >>> from pyramids.netcdf.dimensions import DimMetaData
    >>> d = DimMetaData(name='time', size=2, values=[0, 31], def_fields=(2, 6))
    >>> d.name, d.size, d.values, d.def_fields
    ('time', 2, [0, 31], (2, 6))
    
  • With attributes merged
    >>> d = DimMetaData(name='time', size=2, values=[0, 31], def_fields=(2, 6), attrs={'axis': 'T'})
    >>> d.attrs['axis']
    'T'
    
See Also
  • :class:DimensionsIndex: Factory that populates structural entries.
  • :class:MetaData: Provides convenient construction with merged attrs.
Source code in src/pyramids/netcdf/dimensions.py
@dataclass(frozen=True)
class DimMetaData:
    """Unified information for a single netCDF dimension.

    This immutable dataclass captures both the structural information the GDAL
    netCDF driver exposes via ``NETCDF_DIM_*`` keys and, optionally, the
    per-dimension attribute mapping collected from keys of the form
    ``"<name>#<attr>"`` (e.g., ``time#units``).

    It subsumes the previous "Dimension" helper by adding an ``attrs`` field
    while still preserving the original ``raw`` bucket that stores the exact
    strings parsed from metadata.

    Args:
        name (str):
            Dimension name (e.g., "time", "level0").
        size (int | None):
            Dimension length (if known). Often derived from the first integer
            in ``*_DEF`` or the length of ``*_VALUES``.
        values (list[str | Number] | None):
            Parsed scalar values from the ``*_VALUES`` entry, if provided by the
            GDAL netCDF driver.
        def_fields (tuple[int, ...] | None):
            Parsed integers from the ``*_DEF`` entry. The meaning is driver-
            specific; commonly the first value corresponds to the dimension size.
        raw (dict[str, str]):
            Raw strings captured from metadata for this dimension (e.g., the
            original ``DEF`` and ``VALUES`` content).
        attrs (dict[str, str]):
            Optional attribute dictionary associated with the same dimension
            name (e.g., ``{"axis": "T", "units": "days since ..."}``).

    Raises:
        ValueError: If ``size`` is negative.

    Examples:
        - Construct manually for testing
            ```python
            >>> from pyramids.netcdf.dimensions import DimMetaData
            >>> d = DimMetaData(name='time', size=2, values=[0, 31], def_fields=(2, 6))
            >>> d.name, d.size, d.values, d.def_fields
            ('time', 2, [0, 31], (2, 6))

            ```
        - With attributes merged
            ```python

            >>> d = DimMetaData(name='time', size=2, values=[0, 31], def_fields=(2, 6), attrs={'axis': 'T'})
            >>> d.attrs['axis']
            'T'

            ```

    See Also:
        - :class:`DimensionsIndex`: Factory that populates structural entries.
        - :class:`MetaData`: Provides convenient construction with merged attrs.
    """

    name: str
    size: int | None = None
    values: list[str | Number] | None = None
    def_fields: tuple[int, ...] | None = None
    raw: dict[str, str] = field(default_factory=dict)
    attrs: dict[str, str] = field(default_factory=dict)

    def __post_init__(self):
        """Validate dimension fields for consistency."""
        if not self.name:
            raise ValueError("Dimension name cannot be empty.")
        if self.size is not None and self.size < 0:
            raise ValueError(
                f"Dimension '{self.name}': size cannot be negative, "
                f"got {self.size}."
            )
        if self.values is not None and self.size is not None:
            if len(self.values) != self.size:
                raise ValueError(
                    f"Dimension '{self.name}': values length "
                    f"({len(self.values)}) does not match size "
                    f"({self.size})."
                )
__post_init__() #

Validate dimension fields for consistency.

Source code in src/pyramids/netcdf/dimensions.py
def __post_init__(self):
    """Validate dimension fields for consistency."""
    if not self.name:
        raise ValueError("Dimension name cannot be empty.")
    if self.size is not None and self.size < 0:
        raise ValueError(
            f"Dimension '{self.name}': size cannot be negative, "
            f"got {self.size}."
        )
    if self.values is not None and self.size is not None:
        if len(self.values) != self.size:
            raise ValueError(
                f"Dimension '{self.name}': values length "
                f"({len(self.values)}) does not match size "
                f"({self.size})."
            )

DimensionsIndex dataclass #

Index of netCDF dimensions parsed from GDAL metadata.

A thin mapping-like container that stores :class:DimMetaData objects keyed by dimension name. Use :meth:from_metadata to construct an index from a GDAL metadata mapping (e.g., gdal.Dataset.GetMetadata()).

Behavior
  • Accepts dimensions listed under <prefix>EXTRA.
  • Also recognizes any <prefix><name>_DEF and <prefix><name>_VALUES keys, even when <name> is not listed in EXTRA.
  • Coerces numeric tokens to int/float where possible.
Notes

The default prefix is NETCDF_DIM_ but any prefix can be supplied to :meth:from_metadata and :meth:to_metadata.

See Also
  • :class:DimMetaData
  • :class:MetaData for a higher-level view that merges attributes like time#units with dimension structure.

Examples:

  • Build from typical NETCDF_DIM_* keys
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> md = {
    ...     'NETCDF_DIM_EXTRA': '{time,level0}',
    ...     'NETCDF_DIM_level0_DEF': '{3,6}',
    ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
    ...     'NETCDF_DIM_time_DEF': '{2,6}',
    ...     'NETCDF_DIM_time_VALUES': '{0,31}',
    ... }
    >>> idx = DimensionsIndex.from_metadata(md)
    >>> sorted(idx.names)
    ['level0', 'time']
    >>> idx['time'].size
    2
    >>> idx['level0'].values
    [1, 2, 3]
    
  • Using a custom prefix
    >>> md = {
    ...     'CUSTOM_DIM_time_DEF': '{2,6}',
    ...     'CUSTOM_DIM_time_VALUES': '{0,31}',
    ... }
    >>> DimensionsIndex.from_metadata(md).names
    []
    >>> DimensionsIndex.from_metadata(md, prefix='CUSTOM_DIM_').names
    ['time']
    
Source code in src/pyramids/netcdf/dimensions.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
@dataclass
class DimensionsIndex:
    """Index of netCDF dimensions parsed from GDAL metadata.

    A thin mapping-like container that stores :class:`DimMetaData` objects
    keyed by dimension name. Use :meth:`from_metadata` to construct an index
    from a GDAL metadata mapping (e.g., ``gdal.Dataset.GetMetadata()``).

    Behavior:
      - Accepts dimensions listed under ``<prefix>EXTRA``.
      - Also recognizes any ``<prefix><name>_DEF`` and ``<prefix><name>_VALUES``
        keys, even when ``<name>`` is not listed in ``EXTRA``.
      - Coerces numeric tokens to ``int``/``float`` where possible.

    Notes:
        The default prefix is ``NETCDF_DIM_`` but any prefix can be supplied to
        :meth:`from_metadata` and :meth:`to_metadata`.

    See Also:
        - :class:`DimMetaData`
        - :class:`MetaData` for a higher-level view that merges attributes
          like ``time#units`` with dimension structure.

    Examples:
        - Build from typical NETCDF_DIM_* keys
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> md = {
            ...     'NETCDF_DIM_EXTRA': '{time,level0}',
            ...     'NETCDF_DIM_level0_DEF': '{3,6}',
            ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
            ...     'NETCDF_DIM_time_DEF': '{2,6}',
            ...     'NETCDF_DIM_time_VALUES': '{0,31}',
            ... }
            >>> idx = DimensionsIndex.from_metadata(md)
            >>> sorted(idx.names)
            ['level0', 'time']
            >>> idx['time'].size
            2
            >>> idx['level0'].values
            [1, 2, 3]

            ```
        - Using a custom prefix
            ```python
            >>> md = {
            ...     'CUSTOM_DIM_time_DEF': '{2,6}',
            ...     'CUSTOM_DIM_time_VALUES': '{0,31}',
            ... }
            >>> DimensionsIndex.from_metadata(md).names
            []
            >>> DimensionsIndex.from_metadata(md, prefix='CUSTOM_DIM_').names
            ['time']

            ```
    """

    _dims: dict[str, DimMetaData] = field(default_factory=dict)

    @classmethod
    def from_metadata(
        cls,
        metadata: Mapping[str, str],
        *,
        prefix: str = "NETCDF_DIM_",
    ) -> DimensionsIndex:
        """Parse dimensions from a GDAL metadata dictionary.

        Args:
            metadata (Mapping[str, str]):
                GDAL metadata mapping (e.g., from ``Dataset.GetMetadata()``).
            prefix (str, optional):
                Key prefix to filter on (default: ``'NETCDF_DIM_'``). Custom
                prefixes are supported, e.g., ``'CUSTOM_DIM_'``.

        Returns:
            DimensionsIndex: Parsed index of dimensions.

        Raises:
            TypeError: If ``metadata`` is not a mapping or contains non-string
                keys/values.

        Examples:
            - Basic usage with standard prefix
                ```python
                >>> from pyramids.netcdf.dimensions import DimensionsIndex
                >>> md = {
                ...     'NETCDF_DIM_EXTRA': '{time,level0}',
                ...     'NETCDF_DIM_level0_DEF': '{3,6}',
                ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
                ...     'NETCDF_DIM_time_DEF': '{2,6}',
                ...     'NETCDF_DIM_time_VALUES': '{0,31}',
                ... }
                >>> idx = DimensionsIndex.from_metadata(md)
                >>> idx['time'].size
                2

                ```
            - Using a custom prefix
                ```python
                >>> md = {
                ...     'CUSTOM_DIM_time_DEF': '{2,6}',
                ...     'CUSTOM_DIM_time_VALUES': '{0,31}',
                ... }
                >>> DimensionsIndex.from_metadata(md).names
                []
                >>> DimensionsIndex.from_metadata(md, prefix='CUSTOM_DIM_').names
                ['time']

                ```
        """
        # Gather candidate keys first
        dim_names: set[str] = set()
        buckets: dict[str, dict[str, str]] = {}

        # Build a regex that respects the provided prefix
        # Example: ^NETCDF_DIM_(<name>)(?:_(DEF|VALUES))?$ when prefix is default
        _DIM_KEY_RE = re.compile(
            rf"^{re.escape(prefix)}([A-Za-z0-9_.-]+?)(?:_(DEF|VALUES))?$"
        )

        for key, value in metadata.items():
            if not key.startswith(prefix):
                continue
            m = _DIM_KEY_RE.match(key)
            if not m:
                # Keep unknown but prefixed keys under a synthetic "_misc" bucket
                buckets.setdefault("_misc", {})[key] = value
                continue
            name, suffix = m.groups()

            # Special case: EXTRA carries a comma-separated list of dim names
            if name.upper() == "EXTRA" and suffix is None:
                for nm in _smart_split_csv(value):
                    if nm:
                        dim_names.add(nm)
                continue

            # Normal dimension attributes (DEF/VALUES)
            buckets.setdefault(name, {})[suffix or "_root"] = value
            dim_names.add(name)

        dims: dict[str, DimMetaData] = {}
        for name in sorted(dim_names):
            raw_bucket = buckets.get(name, {})
            # DEF may contain integers, often first item is size
            def_fields: tuple[int, ...] | None = None
            size: int | None = None
            if "DEF" in raw_bucket:
                def_list = _parse_values_list(raw_bucket["DEF"])
                # Only keep integers in def_fields; ignore non-ints gracefully
                ints = tuple(int(x) for x in def_list if isinstance(x, int))
                def_fields = ints or None
                if ints:
                    size = ints[0]

            # VALUES: keep as numbers/strings
            values: list[str | Number] | None = None
            if "VALUES" in raw_bucket:
                values = _parse_values_list(raw_bucket["VALUES"]) or None
                # If size wasn't in DEF, infer from VALUES length
                if size is None and values is not None:
                    size = len(values)

            dims[name] = DimMetaData(
                name=name,
                size=size,
                values=values,
                def_fields=def_fields,
                raw=dict(raw_bucket),
            )

        return cls(dims)

    @property
    def names(self) -> list[str]:
        """Return the list of dimension names.

        Returns:
            list[str]: Names in insertion order (or sorted order, depending on
            construction) matching the keys of the index.

        Examples:
            - Simple index
                ```python
                >>> from pyramids.netcdf.dimensions import DimensionsIndex
                >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_x_DEF': '{2,0}'})
                >>> idx.names
                ['x']

                ```
        """
        return list(self._dims.keys())

    def __len__(self) -> int:  # pragma: no cover - trivial
        """Number of dimensions in the index.

        Returns:
            int: Count of stored dimensions.

        Examples:
            - Length of a simple index
                ```python
                >>> from pyramids.netcdf.dimensions import DimensionsIndex
                >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
                >>> len(idx)
                1

                ```
        """
        return len(self._dims)

    def __iter__(self) -> Iterable[DimMetaData]:  # pragma: no cover - trivial
        """Iterate over stored dimensions.

        Yields:
            DimMetaData: Each stored dimension in unspecified order.

        Examples:
          - Iterate names
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_a_DEF': '{1,0}', 'NETCDF_DIM_b_DEF': '{2,0}'})
            >>> [d.name for d in idx]
            ['a', 'b']

            ```
        """
        return iter(self._dims.values())

    def __contains__(self, name: str) -> bool:  # pragma: no cover - trivial
        """Check if a dimension name exists in the index.

        Args:
            name (str): Dimension name to check.

        Returns:
            bool: ``True`` if present, else ``False``.

        Examples:
            - Membership test
                ```python
                >>> from pyramids.netcdf.dimensions import DimensionsIndex
                >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
                >>> 'time' in idx, 'lat' in idx
                (True, False)

                ```
        """
        return name in self._dims

    def __getitem__(self, name: str) -> DimMetaData:
        """Get a dimension by name.

        Args:
            name (str): Dimension name.

        Returns:
            DimMetaData: The matching dimension.

        Raises:
            KeyError: If the name is not present in the index.

        Examples:
            - Access an existing dimension
                ```python
                >>> from pyramids.netcdf.dimensions import DimensionsIndex
                >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
                >>> idx['time'].size
                2

                ```
        """
        return self._dims[name]

    def __str__(self) -> str:
        """Return a compact, human-readable summary of the index.

        Returns:
            str: A multi-line string listing each dimension with size, values
            and DEF fields when available.

        Examples:
            - Pretty-print a small index
                ```python
                >>> from pyramids.netcdf.dimensions import DimensionsIndex
                >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'}
                >>> str(DimensionsIndex.from_metadata(md)).splitlines()[0]
                'DimensionsIndex(1 dims)'

                ```
        """
        lines: list[str] = [f"DimensionsIndex({len(self)} dims)"]
        for name in sorted(self._dims):
            d = self._dims[name]
            parts: list[str] = []
            if d.size is not None:
                parts.append(f"size={d.size}")
            if d.values is not None:
                vals = ", ".join(str(v) for v in d.values)
                parts.append(f"values=[{vals}]")
            if d.def_fields is not None:
                parts.append(f"def={tuple(d.def_fields)}")
            detail = ", ".join(parts) if parts else "(no details)"
            lines.append(f"- {name}: {detail}")
        return "\n".join(lines)

    def to_dict(self) -> dict[str, dict[str, object]]:
        """Serialize the index to a plain dictionary.

        Useful for logging, debugging, or JSON/YAML output.

        Returns:
            dict[str, dict[str, object]]: Mapping from dimension name to a
            structure with ``size``, ``values`` and ``def_fields`` fields.

        Examples:
            - Convert to a simple dict
                ```python
                >>> from pyramids.netcdf.dimensions import DimensionsIndex
                >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'})
                >>> d = idx.to_dict()
                >>> sorted(list(d['time'].keys()))
                ['def_fields', 'size', 'values']

                ```
        """
        out: dict[str, dict[str, object]] = {}
        for k, d in self._dims.items():
            out[k] = {
                "size": d.size,
                "values": d.values,
                "def_fields": d.def_fields,
            }
        return out

    def to_metadata(
        self,
        *,
        prefix: str = "NETCDF_DIM_",
        include_extra: bool = True,
        sort_names: bool = True,
    ) -> dict[str, str]:
        """Serialize the index back to GDAL netCDF metadata keys.

        This produces keys compatible with the netCDF GDAL driver such as
        ``<prefix>EXTRA``, ``<prefix><name>_DEF`` and ``<prefix><name>_VALUES``.

        Args:
            prefix (str): Metadata key prefix (defaults to ``"NETCDF_DIM_"``).
            include_extra (bool): Whether to include the ``<prefix>EXTRA`` key
                listing dimension names.
            sort_names (bool): Whether to sort names deterministically in
                outputs.

        Returns:
            dict[str, str]: A dictionary suitable for use with GDAL's metadata API.

        Examples:
            - Round-trip a simple index
                ```python
                >>> from pyramids.netcdf.dimensions import DimensionsIndex
                >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'}
                >>> idx = DimensionsIndex.from_metadata(md)
                >>> out = idx.to_metadata()
                >>> sorted(out.keys())
                ['NETCDF_DIM_EXTRA', 'NETCDF_DIM_time_DEF', 'NETCDF_DIM_time_VALUES']

                ```
        """
        md: dict[str, str] = {}
        names = list(self._dims.keys())
        if sort_names:
            names.sort()
        if include_extra and names:
            md[f"{prefix}EXTRA"] = _format_braced_list(names)
        for name in names:
            d = self._dims[name]
            if d.def_fields:
                md[f"{prefix}{name}_DEF"] = _format_braced_list(d.def_fields)
            if d.values is not None:
                md[f"{prefix}{name}_VALUES"] = _format_braced_list(d.values)
        return md
names property #

Return the list of dimension names.

Returns:

Type Description
list[str]

list[str]: Names in insertion order (or sorted order, depending on

list[str]

construction) matching the keys of the index.

Examples:

  • Simple index
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_x_DEF': '{2,0}'})
    >>> idx.names
    ['x']
    
from_metadata(metadata, *, prefix='NETCDF_DIM_') classmethod #

Parse dimensions from a GDAL metadata dictionary.

Parameters:

Name Type Description Default
metadata Mapping[str, str]

GDAL metadata mapping (e.g., from Dataset.GetMetadata()).

required
prefix str

Key prefix to filter on (default: 'NETCDF_DIM_'). Custom prefixes are supported, e.g., 'CUSTOM_DIM_'.

'NETCDF_DIM_'

Returns:

Name Type Description
DimensionsIndex DimensionsIndex

Parsed index of dimensions.

Raises:

Type Description
TypeError

If metadata is not a mapping or contains non-string keys/values.

Examples:

  • Basic usage with standard prefix
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> md = {
    ...     'NETCDF_DIM_EXTRA': '{time,level0}',
    ...     'NETCDF_DIM_level0_DEF': '{3,6}',
    ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
    ...     'NETCDF_DIM_time_DEF': '{2,6}',
    ...     'NETCDF_DIM_time_VALUES': '{0,31}',
    ... }
    >>> idx = DimensionsIndex.from_metadata(md)
    >>> idx['time'].size
    2
    
  • Using a custom prefix
    >>> md = {
    ...     'CUSTOM_DIM_time_DEF': '{2,6}',
    ...     'CUSTOM_DIM_time_VALUES': '{0,31}',
    ... }
    >>> DimensionsIndex.from_metadata(md).names
    []
    >>> DimensionsIndex.from_metadata(md, prefix='CUSTOM_DIM_').names
    ['time']
    
Source code in src/pyramids/netcdf/dimensions.py
@classmethod
def from_metadata(
    cls,
    metadata: Mapping[str, str],
    *,
    prefix: str = "NETCDF_DIM_",
) -> DimensionsIndex:
    """Parse dimensions from a GDAL metadata dictionary.

    Args:
        metadata (Mapping[str, str]):
            GDAL metadata mapping (e.g., from ``Dataset.GetMetadata()``).
        prefix (str, optional):
            Key prefix to filter on (default: ``'NETCDF_DIM_'``). Custom
            prefixes are supported, e.g., ``'CUSTOM_DIM_'``.

    Returns:
        DimensionsIndex: Parsed index of dimensions.

    Raises:
        TypeError: If ``metadata`` is not a mapping or contains non-string
            keys/values.

    Examples:
        - Basic usage with standard prefix
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> md = {
            ...     'NETCDF_DIM_EXTRA': '{time,level0}',
            ...     'NETCDF_DIM_level0_DEF': '{3,6}',
            ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
            ...     'NETCDF_DIM_time_DEF': '{2,6}',
            ...     'NETCDF_DIM_time_VALUES': '{0,31}',
            ... }
            >>> idx = DimensionsIndex.from_metadata(md)
            >>> idx['time'].size
            2

            ```
        - Using a custom prefix
            ```python
            >>> md = {
            ...     'CUSTOM_DIM_time_DEF': '{2,6}',
            ...     'CUSTOM_DIM_time_VALUES': '{0,31}',
            ... }
            >>> DimensionsIndex.from_metadata(md).names
            []
            >>> DimensionsIndex.from_metadata(md, prefix='CUSTOM_DIM_').names
            ['time']

            ```
    """
    # Gather candidate keys first
    dim_names: set[str] = set()
    buckets: dict[str, dict[str, str]] = {}

    # Build a regex that respects the provided prefix
    # Example: ^NETCDF_DIM_(<name>)(?:_(DEF|VALUES))?$ when prefix is default
    _DIM_KEY_RE = re.compile(
        rf"^{re.escape(prefix)}([A-Za-z0-9_.-]+?)(?:_(DEF|VALUES))?$"
    )

    for key, value in metadata.items():
        if not key.startswith(prefix):
            continue
        m = _DIM_KEY_RE.match(key)
        if not m:
            # Keep unknown but prefixed keys under a synthetic "_misc" bucket
            buckets.setdefault("_misc", {})[key] = value
            continue
        name, suffix = m.groups()

        # Special case: EXTRA carries a comma-separated list of dim names
        if name.upper() == "EXTRA" and suffix is None:
            for nm in _smart_split_csv(value):
                if nm:
                    dim_names.add(nm)
            continue

        # Normal dimension attributes (DEF/VALUES)
        buckets.setdefault(name, {})[suffix or "_root"] = value
        dim_names.add(name)

    dims: dict[str, DimMetaData] = {}
    for name in sorted(dim_names):
        raw_bucket = buckets.get(name, {})
        # DEF may contain integers, often first item is size
        def_fields: tuple[int, ...] | None = None
        size: int | None = None
        if "DEF" in raw_bucket:
            def_list = _parse_values_list(raw_bucket["DEF"])
            # Only keep integers in def_fields; ignore non-ints gracefully
            ints = tuple(int(x) for x in def_list if isinstance(x, int))
            def_fields = ints or None
            if ints:
                size = ints[0]

        # VALUES: keep as numbers/strings
        values: list[str | Number] | None = None
        if "VALUES" in raw_bucket:
            values = _parse_values_list(raw_bucket["VALUES"]) or None
            # If size wasn't in DEF, infer from VALUES length
            if size is None and values is not None:
                size = len(values)

        dims[name] = DimMetaData(
            name=name,
            size=size,
            values=values,
            def_fields=def_fields,
            raw=dict(raw_bucket),
        )

    return cls(dims)
__len__() #

Number of dimensions in the index.

Returns:

Name Type Description
int int

Count of stored dimensions.

Examples:

  • Length of a simple index
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
    >>> len(idx)
    1
    
Source code in src/pyramids/netcdf/dimensions.py
def __len__(self) -> int:  # pragma: no cover - trivial
    """Number of dimensions in the index.

    Returns:
        int: Count of stored dimensions.

    Examples:
        - Length of a simple index
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
            >>> len(idx)
            1

            ```
    """
    return len(self._dims)
__iter__() #

Iterate over stored dimensions.

Yields:

Name Type Description
DimMetaData Iterable[DimMetaData]

Each stored dimension in unspecified order.

Examples:

  • Iterate names
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_a_DEF': '{1,0}', 'NETCDF_DIM_b_DEF': '{2,0}'})
    >>> [d.name for d in idx]
    ['a', 'b']
    
Source code in src/pyramids/netcdf/dimensions.py
def __iter__(self) -> Iterable[DimMetaData]:  # pragma: no cover - trivial
    """Iterate over stored dimensions.

    Yields:
        DimMetaData: Each stored dimension in unspecified order.

    Examples:
      - Iterate names
        ```python
        >>> from pyramids.netcdf.dimensions import DimensionsIndex
        >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_a_DEF': '{1,0}', 'NETCDF_DIM_b_DEF': '{2,0}'})
        >>> [d.name for d in idx]
        ['a', 'b']

        ```
    """
    return iter(self._dims.values())
__contains__(name) #

Check if a dimension name exists in the index.

Parameters:

Name Type Description Default
name str

Dimension name to check.

required

Returns:

Name Type Description
bool bool

True if present, else False.

Examples:

  • Membership test
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
    >>> 'time' in idx, 'lat' in idx
    (True, False)
    
Source code in src/pyramids/netcdf/dimensions.py
def __contains__(self, name: str) -> bool:  # pragma: no cover - trivial
    """Check if a dimension name exists in the index.

    Args:
        name (str): Dimension name to check.

    Returns:
        bool: ``True`` if present, else ``False``.

    Examples:
        - Membership test
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
            >>> 'time' in idx, 'lat' in idx
            (True, False)

            ```
    """
    return name in self._dims
__getitem__(name) #

Get a dimension by name.

Parameters:

Name Type Description Default
name str

Dimension name.

required

Returns:

Name Type Description
DimMetaData DimMetaData

The matching dimension.

Raises:

Type Description
KeyError

If the name is not present in the index.

Examples:

  • Access an existing dimension
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
    >>> idx['time'].size
    2
    
Source code in src/pyramids/netcdf/dimensions.py
def __getitem__(self, name: str) -> DimMetaData:
    """Get a dimension by name.

    Args:
        name (str): Dimension name.

    Returns:
        DimMetaData: The matching dimension.

    Raises:
        KeyError: If the name is not present in the index.

    Examples:
        - Access an existing dimension
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}'})
            >>> idx['time'].size
            2

            ```
    """
    return self._dims[name]
__str__() #

Return a compact, human-readable summary of the index.

Returns:

Name Type Description
str str

A multi-line string listing each dimension with size, values

str

and DEF fields when available.

Examples:

  • Pretty-print a small index
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'}
    >>> str(DimensionsIndex.from_metadata(md)).splitlines()[0]
    'DimensionsIndex(1 dims)'
    
Source code in src/pyramids/netcdf/dimensions.py
def __str__(self) -> str:
    """Return a compact, human-readable summary of the index.

    Returns:
        str: A multi-line string listing each dimension with size, values
        and DEF fields when available.

    Examples:
        - Pretty-print a small index
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'}
            >>> str(DimensionsIndex.from_metadata(md)).splitlines()[0]
            'DimensionsIndex(1 dims)'

            ```
    """
    lines: list[str] = [f"DimensionsIndex({len(self)} dims)"]
    for name in sorted(self._dims):
        d = self._dims[name]
        parts: list[str] = []
        if d.size is not None:
            parts.append(f"size={d.size}")
        if d.values is not None:
            vals = ", ".join(str(v) for v in d.values)
            parts.append(f"values=[{vals}]")
        if d.def_fields is not None:
            parts.append(f"def={tuple(d.def_fields)}")
        detail = ", ".join(parts) if parts else "(no details)"
        lines.append(f"- {name}: {detail}")
    return "\n".join(lines)
to_dict() #

Serialize the index to a plain dictionary.

Useful for logging, debugging, or JSON/YAML output.

Returns:

Type Description
dict[str, dict[str, object]]

dict[str, dict[str, object]]: Mapping from dimension name to a

dict[str, dict[str, object]]

structure with size, values and def_fields fields.

Examples:

  • Convert to a simple dict
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'})
    >>> d = idx.to_dict()
    >>> sorted(list(d['time'].keys()))
    ['def_fields', 'size', 'values']
    
Source code in src/pyramids/netcdf/dimensions.py
def to_dict(self) -> dict[str, dict[str, object]]:
    """Serialize the index to a plain dictionary.

    Useful for logging, debugging, or JSON/YAML output.

    Returns:
        dict[str, dict[str, object]]: Mapping from dimension name to a
        structure with ``size``, ``values`` and ``def_fields`` fields.

    Examples:
        - Convert to a simple dict
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> idx = DimensionsIndex.from_metadata({'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'})
            >>> d = idx.to_dict()
            >>> sorted(list(d['time'].keys()))
            ['def_fields', 'size', 'values']

            ```
    """
    out: dict[str, dict[str, object]] = {}
    for k, d in self._dims.items():
        out[k] = {
            "size": d.size,
            "values": d.values,
            "def_fields": d.def_fields,
        }
    return out
to_metadata(*, prefix='NETCDF_DIM_', include_extra=True, sort_names=True) #

Serialize the index back to GDAL netCDF metadata keys.

This produces keys compatible with the netCDF GDAL driver such as <prefix>EXTRA, <prefix><name>_DEF and <prefix><name>_VALUES.

Parameters:

Name Type Description Default
prefix str

Metadata key prefix (defaults to "NETCDF_DIM_").

'NETCDF_DIM_'
include_extra bool

Whether to include the <prefix>EXTRA key listing dimension names.

True
sort_names bool

Whether to sort names deterministically in outputs.

True

Returns:

Type Description
dict[str, str]

dict[str, str]: A dictionary suitable for use with GDAL's metadata API.

Examples:

  • Round-trip a simple index
    >>> from pyramids.netcdf.dimensions import DimensionsIndex
    >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'}
    >>> idx = DimensionsIndex.from_metadata(md)
    >>> out = idx.to_metadata()
    >>> sorted(out.keys())
    ['NETCDF_DIM_EXTRA', 'NETCDF_DIM_time_DEF', 'NETCDF_DIM_time_VALUES']
    
Source code in src/pyramids/netcdf/dimensions.py
def to_metadata(
    self,
    *,
    prefix: str = "NETCDF_DIM_",
    include_extra: bool = True,
    sort_names: bool = True,
) -> dict[str, str]:
    """Serialize the index back to GDAL netCDF metadata keys.

    This produces keys compatible with the netCDF GDAL driver such as
    ``<prefix>EXTRA``, ``<prefix><name>_DEF`` and ``<prefix><name>_VALUES``.

    Args:
        prefix (str): Metadata key prefix (defaults to ``"NETCDF_DIM_"``).
        include_extra (bool): Whether to include the ``<prefix>EXTRA`` key
            listing dimension names.
        sort_names (bool): Whether to sort names deterministically in
            outputs.

    Returns:
        dict[str, str]: A dictionary suitable for use with GDAL's metadata API.

    Examples:
        - Round-trip a simple index
            ```python
            >>> from pyramids.netcdf.dimensions import DimensionsIndex
            >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'NETCDF_DIM_time_VALUES': '{0,31}'}
            >>> idx = DimensionsIndex.from_metadata(md)
            >>> out = idx.to_metadata()
            >>> sorted(out.keys())
            ['NETCDF_DIM_EXTRA', 'NETCDF_DIM_time_DEF', 'NETCDF_DIM_time_VALUES']

            ```
    """
    md: dict[str, str] = {}
    names = list(self._dims.keys())
    if sort_names:
        names.sort()
    if include_extra and names:
        md[f"{prefix}EXTRA"] = _format_braced_list(names)
    for name in names:
        d = self._dims[name]
        if d.def_fields:
            md[f"{prefix}{name}_DEF"] = _format_braced_list(d.def_fields)
        if d.values is not None:
            md[f"{prefix}{name}_VALUES"] = _format_braced_list(d.values)
    return md

MetaData dataclass #

Aggregate of dimension structure and per-dimension attributes.

This class ties together two complementary pieces of information commonly exposed by the GDAL netCDF driver: - A :class:DimensionsIndex parsed from NETCDF_DIM_* keys, describing dimension sizes, DEF fields, and VALUES. - Per-dimension attribute dictionaries collected from keys of the form "<name>#<attr>" (e.g., time#units, lat#axis).

Examples:

  • Build from a combined metadata mapping and inspect
    >>> from pyramids.netcdf.dimensions import MetaData
    >>> md = {
    ...     'NETCDF_DIM_EXTRA': '{time,level0}',
    ...     'NETCDF_DIM_time_DEF': '{2,6}',
    ...     'NETCDF_DIM_time_VALUES': '{0,31}',
    ...     'NETCDF_DIM_level0_DEF': '{3,6}',
    ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
    ...     'time#axis': 'T',
    ...     'time#units': 'days since 1-1-1 0:0:0',
    ...     'level0#axis': 'Z',
    ... }
    >>> meta = MetaData.from_metadata(md)
    >>> sorted(meta.names)
    ['level0', 'time']
    >>> meta.get_attrs('time')['axis']
    'T'
    
See Also
  • :class:DimensionsIndex
  • :func:parse_dimension_attributes
Source code in src/pyramids/netcdf/dimensions.py
@dataclass
class MetaData:
    """Aggregate of dimension structure and per-dimension attributes.

    This class ties together two complementary pieces of information commonly
    exposed by the GDAL netCDF driver:
      - A :class:`DimensionsIndex` parsed from ``NETCDF_DIM_*`` keys, describing
        dimension sizes, DEF fields, and VALUES.
      - Per-dimension attribute dictionaries collected from keys of the form
        ``"<name>#<attr>"`` (e.g., ``time#units``, ``lat#axis``).

    Examples:
        - Build from a combined metadata mapping and inspect
            ```python
            >>> from pyramids.netcdf.dimensions import MetaData
            >>> md = {
            ...     'NETCDF_DIM_EXTRA': '{time,level0}',
            ...     'NETCDF_DIM_time_DEF': '{2,6}',
            ...     'NETCDF_DIM_time_VALUES': '{0,31}',
            ...     'NETCDF_DIM_level0_DEF': '{3,6}',
            ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
            ...     'time#axis': 'T',
            ...     'time#units': 'days since 1-1-1 0:0:0',
            ...     'level0#axis': 'Z',
            ... }
            >>> meta = MetaData.from_metadata(md)
            >>> sorted(meta.names)
            ['level0', 'time']
            >>> meta.get_attrs('time')['axis']
            'T'

            ```

    See Also:
        - :class:`DimensionsIndex`
        - :func:`parse_dimension_attributes`
    """

    dims: DimensionsIndex
    attrs: dict[str, dict[str, str]] = field(default_factory=dict)

    @classmethod
    def from_metadata(
        cls,
        metadata: Mapping[str, str],
        *,
        prefix: str = "NETCDF_DIM_",
        normalize_attr_keys: bool = True,
        names: Iterable[str] | None = None,
    ) -> MetaData:
        """Build a MetaData object by parsing a GDAL metadata mapping.

        Args:
            metadata (Mapping[str, str]):
                GDAL metadata map (e.g., ``Dataset.GetMetadata()``).
            prefix (str):
                Prefix used for dimension entries (defaults to ``NETCDF_DIM_``).
            normalize_attr_keys (bool):
                Normalize attribute keys (the part after ``#``) to lowercase.
            names (Iterable[str] | None):
                If provided, limit attribute parsing to these names. By default
                uses the dimension names discovered under the prefix.

        Returns:
            MetaData: Combined structure and attributes parsed from metadata.

        Raises:
            TypeError: If the input mapping contains non-string keys/values.

        Examples:
            - Typical usage
                ```python
                >>> from pyramids.netcdf.dimensions import MetaData
                >>> md = {
                ...     'NETCDF_DIM_time_DEF': '{2,6}',
                ...     'NETCDF_DIM_time_VALUES': '{0,31}',
                ...     'time#axis': 'T',
                ... }
                >>> meta = MetaData.from_metadata(md)
                >>> meta.get_dimension('time').size
                2

                ```
        """
        dims = DimensionsIndex.from_metadata(metadata, prefix=prefix)
        # Decide which names we keep attributes for
        attr_names = list(names) if names is not None else dims.names
        attrs = parse_dimension_attributes(
            metadata, names=attr_names, normalize_attr_keys=normalize_attr_keys
        )
        return cls(dims=dims, attrs=attrs)

    @property
    def names(self) -> list[str]:
        """Return the list of dimension names represented in this metadata.

        Returns:
            list[str]: Names present in the underlying :class:`DimensionsIndex`.

        Examples:
          - Inspect names
            ```python
            >>> from pyramids.netcdf.dimensions import MetaData
            >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
            >>> MetaData.from_metadata(md).names
            ['time']

            ```
        """
        return self.dims.names

    def get_attrs(self, name: str) -> dict[str, str]:
        """Return attributes for a given dimension name.

        Args:
            name (str): Dimension name.

        Returns:
            dict[str, str]: Attribute dictionary; empty if the name is unknown
            or has no attributes.

        Examples:
            - Access attributes safely
                ```python
                >>> from pyramids.netcdf.dimensions import MetaData
                >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#units': 'days'}
                >>> meta = MetaData.from_metadata(md)
                >>> meta.get_attrs('time')
                {'units': 'days'}
                >>> meta.get_attrs('lat')
                {}

                ```
        """
        return self.attrs.get(name, {})

    def get_dimension(self, name: str) -> DimMetaData | None:
        """Return a DimMetaData with merged attributes for a given name, if present.

        Combines structural info from :class:`DimensionsIndex` with the
        attribute dictionary captured for the same name and returns a new
        :class:`DimMetaData` instance that includes both sets of information.

        Args:
            name (str): Dimension name.

        Returns:
            DimMetaData | None: The merged view if available, else ``None``.

        Examples:
            - Get a merged DimMetaData and inspect attributes
                ```python
                >>> from pyramids.netcdf.dimensions import MetaData
                >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
                >>> meta = MetaData.from_metadata(md)
                >>> dim = meta.get_dimension('time')
                >>> (dim.name, dim.size, dim.attrs['axis'])
                ('time', 2, 'T')

                ```
            - Unknown name returns None
                ```python
                >>> meta.get_dimension('lat') is None
                True

                ```
        """
        if name not in self.dims:
            return None
        d = self.dims[name]
        return DimMetaData(
            name=d.name,
            size=d.size,
            values=d.values,
            def_fields=d.def_fields,
            raw=dict(d.raw),
            attrs=self.get_attrs(name),
        )

    def iter_dimensions(self) -> Iterable[DimMetaData]:
        """Iterate over merged DimMetaData objects in name-sorted order.

        Yields:
            DimMetaData: Each dimension with merged structure and attributes.

        Examples:
            - Iterate and collect names
                ```python

                >>> from pyramids.netcdf.dimensions import MetaData
                >>> md = {'NETCDF_DIM_b_DEF': '{1,0}', 'NETCDF_DIM_a_DEF': '{2,0}'}
                >>> meta = MetaData.from_metadata(md)
                >>> [d.name for d in meta.iter_dimensions()]
                ['a', 'b']

                ```
        """
        for name in sorted(self.names):
            dim = self.get_dimension(name)
            if dim is not None:
                yield dim

    def to_metadata(
        self,
        *,
        prefix: str = "NETCDF_DIM_",
        include_extra: bool = True,
        sort_names: bool = True,
        include_attrs: bool = True,
    ) -> dict[str, str]:
        """Serialize back to a GDAL metadata mapping.

        Combines the dimension keys produced by :meth:`DimensionsIndex.to_metadata`
        with flattened attribute keys of the form ``"<name>#<attr>"``.

        Args:
            prefix (str): Metadata key prefix for dimension keys. Defaults to
                ``"NETCDF_DIM_"``.
            include_extra (bool): Include an ``<prefix>EXTRA`` key listing
                dimension names.
            sort_names (bool): Sort dimension names when serializing for
                determinism.
            include_attrs (bool): Whether to include ``"<name>#<attr>"`` keys
                for known attributes.

        Returns:
            dict[str, str]: A single flattened mapping suitable for GDAL.

        Examples:
            - Merge structure and attributes
                ```python
                >>> from pyramids.netcdf.dimensions import MetaData
                >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
                >>> meta = MetaData.from_metadata(md)
                >>> out = meta.to_metadata()
                >>> sorted(out.keys())
                ['NETCDF_DIM_EXTRA', 'NETCDF_DIM_time_DEF', 'time#axis']

                ```
        """
        md = self.dims.to_metadata(
            prefix=prefix, include_extra=include_extra, sort_names=sort_names
        )
        if include_attrs and self.attrs:
            names = list(self.names)
            if sort_names:
                names.sort()
            for name in names:
                a = self.attrs.get(name) or {}
                # Sort attributes to keep deterministic order
                for k in sorted(a.keys()):
                    md[f"{name}#{k}"] = a[k]
        return md

    def __str__(self) -> str:
        """Return a readable summary of dimensions and attributes.

        Returns:
            str: A multi-line summary listing each dimension name with basic
            statistics (size, number of values, attribute count).

        Examples:
            - Pretty-print a MetaData summary
                ```python

                >>> from pyramids.netcdf.dimensions import MetaData
                >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
                >>> s = str(MetaData.from_metadata(md))
                >>> s.splitlines()[0].startswith('MetaData(')
                True

                ```
        """
        lines: list[str] = [
            f"MetaData({len(self.dims)} dims, attrs for {len(self.attrs)} names)"
        ]
        # Show a compact, aligned summary for each dimension
        for name in sorted(self.dims.names):
            d = self.dims[name]
            parts: list[str] = []
            if d.size is not None:
                parts.append(f"size={d.size}")
            if d.values is not None:
                parts.append(f"values={len(d.values)} items")
            a = self.attrs.get(name)
            if a:
                parts.append(f"attrs={len(a)}")
            detail = ", ".join(parts) if parts else "(no details)"
            lines.append(f"- {name}: {detail}")
        return "\n".join(lines)
names property #

Return the list of dimension names represented in this metadata.

Returns:

Type Description
list[str]

list[str]: Names present in the underlying :class:DimensionsIndex.

Examples:

  • Inspect names
    >>> from pyramids.netcdf.dimensions import MetaData
    >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
    >>> MetaData.from_metadata(md).names
    ['time']
    
from_metadata(metadata, *, prefix='NETCDF_DIM_', normalize_attr_keys=True, names=None) classmethod #

Build a MetaData object by parsing a GDAL metadata mapping.

Parameters:

Name Type Description Default
metadata Mapping[str, str]

GDAL metadata map (e.g., Dataset.GetMetadata()).

required
prefix str

Prefix used for dimension entries (defaults to NETCDF_DIM_).

'NETCDF_DIM_'
normalize_attr_keys bool

Normalize attribute keys (the part after #) to lowercase.

True
names Iterable[str] | None

If provided, limit attribute parsing to these names. By default uses the dimension names discovered under the prefix.

None

Returns:

Name Type Description
MetaData MetaData

Combined structure and attributes parsed from metadata.

Raises:

Type Description
TypeError

If the input mapping contains non-string keys/values.

Examples:

  • Typical usage
    >>> from pyramids.netcdf.dimensions import MetaData
    >>> md = {
    ...     'NETCDF_DIM_time_DEF': '{2,6}',
    ...     'NETCDF_DIM_time_VALUES': '{0,31}',
    ...     'time#axis': 'T',
    ... }
    >>> meta = MetaData.from_metadata(md)
    >>> meta.get_dimension('time').size
    2
    
Source code in src/pyramids/netcdf/dimensions.py
@classmethod
def from_metadata(
    cls,
    metadata: Mapping[str, str],
    *,
    prefix: str = "NETCDF_DIM_",
    normalize_attr_keys: bool = True,
    names: Iterable[str] | None = None,
) -> MetaData:
    """Build a MetaData object by parsing a GDAL metadata mapping.

    Args:
        metadata (Mapping[str, str]):
            GDAL metadata map (e.g., ``Dataset.GetMetadata()``).
        prefix (str):
            Prefix used for dimension entries (defaults to ``NETCDF_DIM_``).
        normalize_attr_keys (bool):
            Normalize attribute keys (the part after ``#``) to lowercase.
        names (Iterable[str] | None):
            If provided, limit attribute parsing to these names. By default
            uses the dimension names discovered under the prefix.

    Returns:
        MetaData: Combined structure and attributes parsed from metadata.

    Raises:
        TypeError: If the input mapping contains non-string keys/values.

    Examples:
        - Typical usage
            ```python
            >>> from pyramids.netcdf.dimensions import MetaData
            >>> md = {
            ...     'NETCDF_DIM_time_DEF': '{2,6}',
            ...     'NETCDF_DIM_time_VALUES': '{0,31}',
            ...     'time#axis': 'T',
            ... }
            >>> meta = MetaData.from_metadata(md)
            >>> meta.get_dimension('time').size
            2

            ```
    """
    dims = DimensionsIndex.from_metadata(metadata, prefix=prefix)
    # Decide which names we keep attributes for
    attr_names = list(names) if names is not None else dims.names
    attrs = parse_dimension_attributes(
        metadata, names=attr_names, normalize_attr_keys=normalize_attr_keys
    )
    return cls(dims=dims, attrs=attrs)
get_attrs(name) #

Return attributes for a given dimension name.

Parameters:

Name Type Description Default
name str

Dimension name.

required

Returns:

Type Description
dict[str, str]

dict[str, str]: Attribute dictionary; empty if the name is unknown

dict[str, str]

or has no attributes.

Examples:

  • Access attributes safely
    >>> from pyramids.netcdf.dimensions import MetaData
    >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#units': 'days'}
    >>> meta = MetaData.from_metadata(md)
    >>> meta.get_attrs('time')
    {'units': 'days'}
    >>> meta.get_attrs('lat')
    {}
    
Source code in src/pyramids/netcdf/dimensions.py
def get_attrs(self, name: str) -> dict[str, str]:
    """Return attributes for a given dimension name.

    Args:
        name (str): Dimension name.

    Returns:
        dict[str, str]: Attribute dictionary; empty if the name is unknown
        or has no attributes.

    Examples:
        - Access attributes safely
            ```python
            >>> from pyramids.netcdf.dimensions import MetaData
            >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#units': 'days'}
            >>> meta = MetaData.from_metadata(md)
            >>> meta.get_attrs('time')
            {'units': 'days'}
            >>> meta.get_attrs('lat')
            {}

            ```
    """
    return self.attrs.get(name, {})
get_dimension(name) #

Return a DimMetaData with merged attributes for a given name, if present.

Combines structural info from :class:DimensionsIndex with the attribute dictionary captured for the same name and returns a new :class:DimMetaData instance that includes both sets of information.

Parameters:

Name Type Description Default
name str

Dimension name.

required

Returns:

Type Description
DimMetaData | None

DimMetaData | None: The merged view if available, else None.

Examples:

  • Get a merged DimMetaData and inspect attributes
    >>> from pyramids.netcdf.dimensions import MetaData
    >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
    >>> meta = MetaData.from_metadata(md)
    >>> dim = meta.get_dimension('time')
    >>> (dim.name, dim.size, dim.attrs['axis'])
    ('time', 2, 'T')
    
  • Unknown name returns None
    >>> meta.get_dimension('lat') is None
    True
    
Source code in src/pyramids/netcdf/dimensions.py
def get_dimension(self, name: str) -> DimMetaData | None:
    """Return a DimMetaData with merged attributes for a given name, if present.

    Combines structural info from :class:`DimensionsIndex` with the
    attribute dictionary captured for the same name and returns a new
    :class:`DimMetaData` instance that includes both sets of information.

    Args:
        name (str): Dimension name.

    Returns:
        DimMetaData | None: The merged view if available, else ``None``.

    Examples:
        - Get a merged DimMetaData and inspect attributes
            ```python
            >>> from pyramids.netcdf.dimensions import MetaData
            >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
            >>> meta = MetaData.from_metadata(md)
            >>> dim = meta.get_dimension('time')
            >>> (dim.name, dim.size, dim.attrs['axis'])
            ('time', 2, 'T')

            ```
        - Unknown name returns None
            ```python
            >>> meta.get_dimension('lat') is None
            True

            ```
    """
    if name not in self.dims:
        return None
    d = self.dims[name]
    return DimMetaData(
        name=d.name,
        size=d.size,
        values=d.values,
        def_fields=d.def_fields,
        raw=dict(d.raw),
        attrs=self.get_attrs(name),
    )
iter_dimensions() #

Iterate over merged DimMetaData objects in name-sorted order.

Yields:

Name Type Description
DimMetaData Iterable[DimMetaData]

Each dimension with merged structure and attributes.

Examples:

  • Iterate and collect names
    >>> from pyramids.netcdf.dimensions import MetaData
    >>> md = {'NETCDF_DIM_b_DEF': '{1,0}', 'NETCDF_DIM_a_DEF': '{2,0}'}
    >>> meta = MetaData.from_metadata(md)
    >>> [d.name for d in meta.iter_dimensions()]
    ['a', 'b']
    
Source code in src/pyramids/netcdf/dimensions.py
def iter_dimensions(self) -> Iterable[DimMetaData]:
    """Iterate over merged DimMetaData objects in name-sorted order.

    Yields:
        DimMetaData: Each dimension with merged structure and attributes.

    Examples:
        - Iterate and collect names
            ```python

            >>> from pyramids.netcdf.dimensions import MetaData
            >>> md = {'NETCDF_DIM_b_DEF': '{1,0}', 'NETCDF_DIM_a_DEF': '{2,0}'}
            >>> meta = MetaData.from_metadata(md)
            >>> [d.name for d in meta.iter_dimensions()]
            ['a', 'b']

            ```
    """
    for name in sorted(self.names):
        dim = self.get_dimension(name)
        if dim is not None:
            yield dim
to_metadata(*, prefix='NETCDF_DIM_', include_extra=True, sort_names=True, include_attrs=True) #

Serialize back to a GDAL metadata mapping.

Combines the dimension keys produced by :meth:DimensionsIndex.to_metadata with flattened attribute keys of the form "<name>#<attr>".

Parameters:

Name Type Description Default
prefix str

Metadata key prefix for dimension keys. Defaults to "NETCDF_DIM_".

'NETCDF_DIM_'
include_extra bool

Include an <prefix>EXTRA key listing dimension names.

True
sort_names bool

Sort dimension names when serializing for determinism.

True
include_attrs bool

Whether to include "<name>#<attr>" keys for known attributes.

True

Returns:

Type Description
dict[str, str]

dict[str, str]: A single flattened mapping suitable for GDAL.

Examples:

  • Merge structure and attributes
    >>> from pyramids.netcdf.dimensions import MetaData
    >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
    >>> meta = MetaData.from_metadata(md)
    >>> out = meta.to_metadata()
    >>> sorted(out.keys())
    ['NETCDF_DIM_EXTRA', 'NETCDF_DIM_time_DEF', 'time#axis']
    
Source code in src/pyramids/netcdf/dimensions.py
def to_metadata(
    self,
    *,
    prefix: str = "NETCDF_DIM_",
    include_extra: bool = True,
    sort_names: bool = True,
    include_attrs: bool = True,
) -> dict[str, str]:
    """Serialize back to a GDAL metadata mapping.

    Combines the dimension keys produced by :meth:`DimensionsIndex.to_metadata`
    with flattened attribute keys of the form ``"<name>#<attr>"``.

    Args:
        prefix (str): Metadata key prefix for dimension keys. Defaults to
            ``"NETCDF_DIM_"``.
        include_extra (bool): Include an ``<prefix>EXTRA`` key listing
            dimension names.
        sort_names (bool): Sort dimension names when serializing for
            determinism.
        include_attrs (bool): Whether to include ``"<name>#<attr>"`` keys
            for known attributes.

    Returns:
        dict[str, str]: A single flattened mapping suitable for GDAL.

    Examples:
        - Merge structure and attributes
            ```python
            >>> from pyramids.netcdf.dimensions import MetaData
            >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
            >>> meta = MetaData.from_metadata(md)
            >>> out = meta.to_metadata()
            >>> sorted(out.keys())
            ['NETCDF_DIM_EXTRA', 'NETCDF_DIM_time_DEF', 'time#axis']

            ```
    """
    md = self.dims.to_metadata(
        prefix=prefix, include_extra=include_extra, sort_names=sort_names
    )
    if include_attrs and self.attrs:
        names = list(self.names)
        if sort_names:
            names.sort()
        for name in names:
            a = self.attrs.get(name) or {}
            # Sort attributes to keep deterministic order
            for k in sorted(a.keys()):
                md[f"{name}#{k}"] = a[k]
    return md
__str__() #

Return a readable summary of dimensions and attributes.

Returns:

Name Type Description
str str

A multi-line summary listing each dimension name with basic

str

statistics (size, number of values, attribute count).

Examples:

  • Pretty-print a MetaData summary
    >>> from pyramids.netcdf.dimensions import MetaData
    >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
    >>> s = str(MetaData.from_metadata(md))
    >>> s.splitlines()[0].startswith('MetaData(')
    True
    
Source code in src/pyramids/netcdf/dimensions.py
def __str__(self) -> str:
    """Return a readable summary of dimensions and attributes.

    Returns:
        str: A multi-line summary listing each dimension name with basic
        statistics (size, number of values, attribute count).

    Examples:
        - Pretty-print a MetaData summary
            ```python

            >>> from pyramids.netcdf.dimensions import MetaData
            >>> md = {'NETCDF_DIM_time_DEF': '{2,6}', 'time#axis': 'T'}
            >>> s = str(MetaData.from_metadata(md))
            >>> s.splitlines()[0].startswith('MetaData(')
            True

            ```
    """
    lines: list[str] = [
        f"MetaData({len(self.dims)} dims, attrs for {len(self.attrs)} names)"
    ]
    # Show a compact, aligned summary for each dimension
    for name in sorted(self.dims.names):
        d = self.dims[name]
        parts: list[str] = []
        if d.size is not None:
            parts.append(f"size={d.size}")
        if d.values is not None:
            parts.append(f"values={len(d.values)} items")
        a = self.attrs.get(name)
        if a:
            parts.append(f"attrs={len(a)}")
        detail = ", ".join(parts) if parts else "(no details)"
        lines.append(f"- {name}: {detail}")
    return "\n".join(lines)

parse_gdal_netcdf_dimensions(metadata) #

Parse netCDF dimension info from GDAL metadata.

A convenience wrapper around :meth:DimensionsIndex.from_metadata that uses the default NETCDF_DIM_ prefix.

Parameters:

Name Type Description Default
metadata Mapping[str, str]

GDAL metadata mapping (e.g., from Dataset.GetMetadata()).

required

Returns:

Name Type Description
DimensionsIndex DimensionsIndex

Parsed index of dimensions.

Raises:

Type Description
TypeError

If metadata is not a mapping.

Examples:

  • Typical usage
    >>> md = {
    ...     'NETCDF_DIM_EXTRA': '{time,level0}',
    ...     'NETCDF_DIM_level0_DEF': '{3,6}',
    ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
    ...     'NETCDF_DIM_time_DEF': '{2,6}',
    ...     'NETCDF_DIM_time_VALUES': '{0,31}',
    ... }
    >>> idx = parse_gdal_netcdf_dimensions(md)
    >>> idx.to_dict()['time']['size']
    2
    >>> idx.to_dict()['level0']['values']
    [1, 2, 3]
    
See Also
  • :class:DimensionsIndex
Source code in src/pyramids/netcdf/dimensions.py
def parse_gdal_netcdf_dimensions(metadata: Mapping[str, str]) -> DimensionsIndex:
    """Parse netCDF dimension info from GDAL metadata.

    A convenience wrapper around :meth:`DimensionsIndex.from_metadata` that
    uses the default ``NETCDF_DIM_`` prefix.

    Args:
        metadata (Mapping[str, str]): GDAL metadata mapping (e.g., from
            ``Dataset.GetMetadata()``).

    Returns:
        DimensionsIndex: Parsed index of dimensions.

    Raises:
        TypeError: If ``metadata`` is not a mapping.

    Examples:
        - Typical usage
            ```python
            >>> md = {
            ...     'NETCDF_DIM_EXTRA': '{time,level0}',
            ...     'NETCDF_DIM_level0_DEF': '{3,6}',
            ...     'NETCDF_DIM_level0_VALUES': '{1,2,3}',
            ...     'NETCDF_DIM_time_DEF': '{2,6}',
            ...     'NETCDF_DIM_time_VALUES': '{0,31}',
            ... }
            >>> idx = parse_gdal_netcdf_dimensions(md)
            >>> idx.to_dict()['time']['size']
            2
            >>> idx.to_dict()['level0']['values']
            [1, 2, 3]

            ```

    See Also:
        - :class:`DimensionsIndex`
    """
    return DimensionsIndex.from_metadata(metadata)

parse_dimension_attributes(metadata, names=None, *, normalize_attr_keys=True) #

Extract per-dimension attributes from GDAL netCDF metadata.

This helper scans metadata entries whose keys look like "#" (e.g., "time#axis", "lat#units", "level0#positive") and groups them by dimension name.

Parameters:

Name Type Description Default
metadata Mapping[str, str]

Mapping of metadata keys to values (e.g., from GDAL).

required
names Iterable[str] | None

Optional iterable of dimension names to include. If provided, only attributes for these names are captured.

None
normalize_attr_keys bool

If True, attribute names after the "#" are converted to lowercase in the output. If False, original case is preserved.

True

Returns:

Type Description
dict[str, dict[str, str]]

dict[str, dict[str, str]]: A mapping from dimension name to a dictionary

dict[str, dict[str, str]]

of attributes for that dimension.

Raises:

Type Description
TypeError

If metadata is not a mapping or contains non-string keys.

Examples:

  • Parse all attributes for any name
    >>> md = {
    ...     'lat#bounds': 'bounds_lat',
    ...     'lat#long_name': 'latitude',
    ...     'lat#units': 'degrees_north',
    ...     'time#axis': 'T',
    ...     'time#long_name': 'time',
    ...     'time#units': 'days since 1-1-1 0:0:0',
    ... }
    >>> parse_dimension_attributes(md)
    {'lat': {'bounds': 'bounds_lat', 'long_name': 'latitude', 'units': 'degrees_north'}, 'time': {'axis': 'T', 'long_name': 'time', 'units': 'days since 1-1-1 0:0:0'}}
    
  • Restrict to provided names and preserve attribute case
    >>> parse_dimension_attributes(md, names=['time'], normalize_attr_keys=False)
    {'time': {'axis': 'T', 'long_name': 'time', 'units': 'days since 1-1-1 0:0:0'}}
    
See Also
  • :class:MetaData: Combines these attributes with dimension structure.
Source code in src/pyramids/netcdf/dimensions.py
def parse_dimension_attributes(
    metadata: Mapping[str, str],
    names: Iterable[str] | None = None,
    *,
    normalize_attr_keys: bool = True,
) -> dict[str, dict[str, str]]:
    """Extract per-dimension attributes from GDAL netCDF metadata.

    This helper scans metadata entries whose keys look like "<name>#<attr>"
    (e.g., "time#axis", "lat#units", "level0#positive") and groups them by
    dimension name.

    Args:
        metadata (Mapping[str, str]):
            Mapping of metadata keys to values (e.g., from GDAL).
        names (Iterable[str] | None):
            Optional iterable of dimension names to include. If provided, only
            attributes for these names are captured.
        normalize_attr_keys (bool):
            If True, attribute names after the "#" are converted to lowercase in
            the output. If False, original case is preserved.

    Returns:
        dict[str, dict[str, str]]: A mapping from dimension name to a dictionary
        of attributes for that dimension.

    Raises:
        TypeError: If ``metadata`` is not a mapping or contains non-string keys.

    Examples:
        - Parse all attributes for any name
            ```python
            >>> md = {
            ...     'lat#bounds': 'bounds_lat',
            ...     'lat#long_name': 'latitude',
            ...     'lat#units': 'degrees_north',
            ...     'time#axis': 'T',
            ...     'time#long_name': 'time',
            ...     'time#units': 'days since 1-1-1 0:0:0',
            ... }
            >>> parse_dimension_attributes(md)
            {'lat': {'bounds': 'bounds_lat', 'long_name': 'latitude', 'units': 'degrees_north'}, 'time': {'axis': 'T', 'long_name': 'time', 'units': 'days since 1-1-1 0:0:0'}}

            ```
        - Restrict to provided names and preserve attribute case
            ```python
            >>> parse_dimension_attributes(md, names=['time'], normalize_attr_keys=False)
            {'time': {'axis': 'T', 'long_name': 'time', 'units': 'days since 1-1-1 0:0:0'}}

            ```

    See Also:
        - :class:`MetaData`: Combines these attributes with dimension structure.
    """
    # Build a quick lookup for allowed names if provided
    allowed = set(names) if names is not None else None
    out: dict[str, dict[str, str]] = {}

    # Simple pattern: everything before first '#' is the dimension name; after is attribute
    # Keep it permissive but avoid empty parts.
    key_re = re.compile(r"^([^#\s]+)#([^#\s]+)$")

    for k, v in metadata.items():
        m = key_re.match(k.strip())
        if not m:
            continue
        name, attr = m.group(1), m.group(2)
        if allowed is not None and name not in allowed:
            continue
        if normalize_attr_keys:
            attr = attr.lower()
        bucket = out.setdefault(name, {})
        bucket[attr] = v

    return out