Skip to content

map

map #

Map — a key-scalar-keyed collection that broadcasts over its entries when loaded.

Map[Date, Entry] is storable: it knows its key scalar and element, so it reads/writes entries under folder names from the key scalar's key_encode. A loaded map is a Map — index it by a decoded key to select an entry (Missing if absent), iterate or call values() for the entries as an Array, and attribute access broadcasts over the entry values (sessions.rating is every entry's rating). It is mutable: assign, delete, and test membership by key, so a runtime keyed bag (an interceptor's persistence dict) is also a Map.

The key is a keyable scalar (Time/Date, String, UserID, Bool) — a real CGType, so .keys() has an element type.

Map #

Bases: CGType

A keyed collection.

Map[K, V] is storable when V is (so a keyed fan-out Map[K, Path[V]] is non-storable); a bare Map is a schemaless keyed view. The key is always a keyable — hence storable — scalar. The type parameters type the entry surface (keys()/values(), values by key); broadcasts carry object elements, narrowed by the caller. Key parameters accept object: a schema map's runtime keys are the decoded values (datetime for a Date key), not the scalar class the subscription names.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
class Map[K, V](CGType):
    """A keyed collection.

    ``Map[K, V]`` is storable when ``V`` is (so a keyed fan-out
    ``Map[K, Path[V]]`` is non-storable); a bare ``Map`` is a schemaless keyed
    view. The key is always a keyable — hence storable — scalar. The type
    parameters type the entry surface (``keys()``/``values()``, values by
    key); broadcasts carry ``object`` elements, narrowed by the caller. Key
    *parameters* accept ``object``: a schema map's runtime keys are the
    decoded values (``datetime`` for a ``Date`` key), not the scalar class
    the subscription names.
    """

    __slots__ = ("_entries",)
    _entries: dict[K, V]
    _key: ClassVar[type[Keyable] | None] = None
    _element: ClassVar[type[CGType] | None] = None

    def __init__(self, entries: Iterable[tuple[K, V]] | dict[K, V] = ()) -> None:
        """Wrap ``entries`` (any ``(key, value)`` iterable) as a Map."""
        object.__setattr__(self, "_entries", dict(entries))

    def __class_getitem__(cls, params: tuple[type[CGType], type[CGType]]) -> type[Map]:
        """Build ``Map[key, element]``, a storable subclass keyed by ``key``."""
        if not (isinstance(params, tuple) and len(params) == 2):
            raise TypeError("Map takes two parameters: Map[key_scalar, element]")
        key, element = params
        if not is_keyable(key):
            raise TypeError(
                f"Map key must be a keyable scalar (Time, String, UserID, Bool), got {key!r}"
            )
        check_cgtype(element, "Map element")
        return type(
            f"Map[{key.__name__}, {element.__name__}]",
            (cls,),
            {
                "__slots__": (),
                "_key": key,
                "_element": element,
                "storable": element.storable,
            },
        )

    # --- storage (a schema'd Map[K, V] only) --------------------------------

    @classmethod
    def schema(cls) -> dict[str, JSONValue]:
        """This map's wire form: its key and element schemas, tagged ``"map"``."""
        if cls._key is None or cls._element is None:
            raise TypeError("a schemaless Map has no wire form")
        return {
            "type": "map",
            "key": cls._key.schema(),
            "element": cls._element.schema(),
        }

    @classmethod
    def load(cls, base: Path) -> Self:
        """Read the key-encoded entries under ``base`` as this map type.

        Typed by the annotation naming this class — ``Map[Date, Session]``
        loads as itself. The schema supplies the runtime ``_key``/``_element``;
        the cast below is the one boundary where the type-erased registry meets
        the static entry types the schema declares.
        """
        if cls._key is None or cls._element is None:
            raise TypeError(f"{cls.__name__} cannot load without a key and element")
        if not base.exists():
            return cls()
        loaded = (
            (cls._key.key_decode(stem), cls._element.load(base / stem))
            for stem in entry_stems(base)
        )
        return cls(cast("Iterable[tuple[K, V]]", loaded))

    @classmethod
    def store(cls, base: Path, value: object) -> None:
        """Write ``value`` (a ``dict``) under ``base`` as key-encoded entries."""
        if cls._key is None or cls._element is None:
            raise TypeError(f"{cls.__name__} cannot store without a key and element")
        if isinstance(value, Map):
            entries = value.to_dict()
        elif isinstance(value, dict):
            entries = value
        else:
            raise TypeError(f"{cls.__name__}.store needs a dict or Map, got {value!r}")
        base.mkdir(parents=True, exist_ok=True)
        for key, item in entries.items():
            cls._element.store(base / cls._key.key_encode(key), item)

    # --- keyed access (any Map instance) ------------------------------------

    def __iter__(self) -> Iterator[V]:
        """Iterate the entry values, in insertion order."""
        return iter(self._entries.values())

    def __len__(self) -> int:
        """The number of entries."""
        return len(self._entries)

    def __bool__(self) -> bool:
        """Falsy when empty, like a plain dict."""
        return bool(self._entries)

    def __contains__(self, key: object) -> bool:
        """Whether ``key`` has an entry."""
        return key in self._entries

    def __eq__(self, other: object) -> bool:
        """Equal to another ``Map`` or a plain ``dict`` with the same entries."""
        if isinstance(other, Map):
            return self._entries == other._entries
        if isinstance(other, dict):
            return self._entries == other
        return NotImplemented

    __hash__ = None  # type: ignore[assignment]

    def __repr__(self) -> str:
        """The wrapped entries' own ``repr``."""
        return f"Map({self._entries!r})"

    def keys(self) -> Array[K]:
        """The decoded keys, as an ``Array``."""
        return Array(self._entries.keys())

    def values(self) -> Array[V]:
        """The entry values, as an ``Array``."""
        return Array(self._entries.values())

    def items(self) -> Array[tuple[K, V]]:
        """The ``(key, value)`` pairs, as an ``Array``."""
        return Array(self._entries.items())

    def __getitem__(self, key: object) -> V:
        """The value at ``key``; an absent key chains as a reasoned ``Missing``."""
        try:
            return _keyed(self)[key]
        except KeyError as error:
            return _Missing(error)

    def __setitem__(self, key: K, value: V) -> None:
        """Add or replace the entry at ``key``, in place."""
        self._entries[key] = value

    def __delitem__(self, key: object) -> None:
        """Remove the entry at ``key``, in place."""
        del _keyed(self)[key]

    # --- broadcast over entry values ----------------------------------------

    def __getattr__(self, name: str) -> Array[object]:
        """Broadcast attribute access over the entry values.

        A name no value has raises (a typo), a name some lack chains as
        ``Missing``.
        """
        if name.startswith("_"):
            raise AttributeError(name)
        return Array(broadcast_attr(list(self._entries.values()), name))

    def __dir__(self) -> list[str]:
        """Standard members plus the broadcastable entry-value field names.

        Surfaces ``sessions.rating`` in REPL/notebook autocomplete; the
        broadcast itself stays dynamically typed.
        """
        return [*super().__dir__(), *self.values().dkeys]

    # --- runtime helpers ----------------------------------------------------

    def get_or_insert(self, key: object, value: V) -> V:
        """The value at ``key``, inserting ``value`` first if it is absent."""
        entries = _keyed(self)
        if key not in entries:
            entries[key] = value
        return entries[key]

    def with_key(self, key: K, value: V) -> Map[K, V]:
        """A copy with ``key`` added or replaced."""
        return Map({**self._entries, key: value})

    def map(self, function: Callable[[V], object]) -> Map[K, object]:
        """Apply ``function`` to each value, keeping keys; failures become ``Missing``.

        Raises if the call fails on every entry for a non-data reason (a broken
        lambda fails loudly); a sparse data-absence failure chains as ``Missing``.
        """
        keys = list(self._entries)
        results: list[object] = []
        for value in self._entries.values():
            try:
                results.append(function(value))
            except Exception as error:  # noqa: BLE001 — per-entry isolation
                results.append(_Missing(error))
        guard_broadcast(results, "map")
        return Map(zip(keys, results, strict=True))

    def to_dict(self) -> dict[K, V]:
        """A plain ``dict`` of the entries (keys preserved)."""
        return dict(self._entries)

__bool__() #

Falsy when empty, like a plain dict.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __bool__(self) -> bool:
    """Falsy when empty, like a plain dict."""
    return bool(self._entries)

__class_getitem__(params) #

Build Map[key, element], a storable subclass keyed by key.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __class_getitem__(cls, params: tuple[type[CGType], type[CGType]]) -> type[Map]:
    """Build ``Map[key, element]``, a storable subclass keyed by ``key``."""
    if not (isinstance(params, tuple) and len(params) == 2):
        raise TypeError("Map takes two parameters: Map[key_scalar, element]")
    key, element = params
    if not is_keyable(key):
        raise TypeError(
            f"Map key must be a keyable scalar (Time, String, UserID, Bool), got {key!r}"
        )
    check_cgtype(element, "Map element")
    return type(
        f"Map[{key.__name__}, {element.__name__}]",
        (cls,),
        {
            "__slots__": (),
            "_key": key,
            "_element": element,
            "storable": element.storable,
        },
    )

__contains__(key) #

Whether key has an entry.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __contains__(self, key: object) -> bool:
    """Whether ``key`` has an entry."""
    return key in self._entries

__delitem__(key) #

Remove the entry at key, in place.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __delitem__(self, key: object) -> None:
    """Remove the entry at ``key``, in place."""
    del _keyed(self)[key]

__dir__() #

Standard members plus the broadcastable entry-value field names.

Surfaces sessions.rating in REPL/notebook autocomplete; the broadcast itself stays dynamically typed.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __dir__(self) -> list[str]:
    """Standard members plus the broadcastable entry-value field names.

    Surfaces ``sessions.rating`` in REPL/notebook autocomplete; the
    broadcast itself stays dynamically typed.
    """
    return [*super().__dir__(), *self.values().dkeys]

__eq__(other) #

Equal to another Map or a plain dict with the same entries.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __eq__(self, other: object) -> bool:
    """Equal to another ``Map`` or a plain ``dict`` with the same entries."""
    if isinstance(other, Map):
        return self._entries == other._entries
    if isinstance(other, dict):
        return self._entries == other
    return NotImplemented

__getattr__(name) #

Broadcast attribute access over the entry values.

A name no value has raises (a typo), a name some lack chains as Missing.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __getattr__(self, name: str) -> Array[object]:
    """Broadcast attribute access over the entry values.

    A name no value has raises (a typo), a name some lack chains as
    ``Missing``.
    """
    if name.startswith("_"):
        raise AttributeError(name)
    return Array(broadcast_attr(list(self._entries.values()), name))

__getitem__(key) #

The value at key; an absent key chains as a reasoned Missing.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __getitem__(self, key: object) -> V:
    """The value at ``key``; an absent key chains as a reasoned ``Missing``."""
    try:
        return _keyed(self)[key]
    except KeyError as error:
        return _Missing(error)

__init__(entries=()) #

Wrap entries (any (key, value) iterable) as a Map.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __init__(self, entries: Iterable[tuple[K, V]] | dict[K, V] = ()) -> None:
    """Wrap ``entries`` (any ``(key, value)`` iterable) as a Map."""
    object.__setattr__(self, "_entries", dict(entries))

__iter__() #

Iterate the entry values, in insertion order.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __iter__(self) -> Iterator[V]:
    """Iterate the entry values, in insertion order."""
    return iter(self._entries.values())

__len__() #

The number of entries.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __len__(self) -> int:
    """The number of entries."""
    return len(self._entries)

__repr__() #

The wrapped entries' own repr.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __repr__(self) -> str:
    """The wrapped entries' own ``repr``."""
    return f"Map({self._entries!r})"

__setitem__(key, value) #

Add or replace the entry at key, in place.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def __setitem__(self, key: K, value: V) -> None:
    """Add or replace the entry at ``key``, in place."""
    self._entries[key] = value

get_or_insert(key, value) #

The value at key, inserting value first if it is absent.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def get_or_insert(self, key: object, value: V) -> V:
    """The value at ``key``, inserting ``value`` first if it is absent."""
    entries = _keyed(self)
    if key not in entries:
        entries[key] = value
    return entries[key]

items() #

The (key, value) pairs, as an Array.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def items(self) -> Array[tuple[K, V]]:
    """The ``(key, value)`` pairs, as an ``Array``."""
    return Array(self._entries.items())

keys() #

The decoded keys, as an Array.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def keys(self) -> Array[K]:
    """The decoded keys, as an ``Array``."""
    return Array(self._entries.keys())

load(base) classmethod #

Read the key-encoded entries under base as this map type.

Typed by the annotation naming this class — Map[Date, Session] loads as itself. The schema supplies the runtime _key/_element; the cast below is the one boundary where the type-erased registry meets the static entry types the schema declares.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
@classmethod
def load(cls, base: Path) -> Self:
    """Read the key-encoded entries under ``base`` as this map type.

    Typed by the annotation naming this class — ``Map[Date, Session]``
    loads as itself. The schema supplies the runtime ``_key``/``_element``;
    the cast below is the one boundary where the type-erased registry meets
    the static entry types the schema declares.
    """
    if cls._key is None or cls._element is None:
        raise TypeError(f"{cls.__name__} cannot load without a key and element")
    if not base.exists():
        return cls()
    loaded = (
        (cls._key.key_decode(stem), cls._element.load(base / stem))
        for stem in entry_stems(base)
    )
    return cls(cast("Iterable[tuple[K, V]]", loaded))

map(function) #

Apply function to each value, keeping keys; failures become Missing.

Raises if the call fails on every entry for a non-data reason (a broken lambda fails loudly); a sparse data-absence failure chains as Missing.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def map(self, function: Callable[[V], object]) -> Map[K, object]:
    """Apply ``function`` to each value, keeping keys; failures become ``Missing``.

    Raises if the call fails on every entry for a non-data reason (a broken
    lambda fails loudly); a sparse data-absence failure chains as ``Missing``.
    """
    keys = list(self._entries)
    results: list[object] = []
    for value in self._entries.values():
        try:
            results.append(function(value))
        except Exception as error:  # noqa: BLE001 — per-entry isolation
            results.append(_Missing(error))
    guard_broadcast(results, "map")
    return Map(zip(keys, results, strict=True))

schema() classmethod #

This map's wire form: its key and element schemas, tagged "map".

Source code in capturegraph-lib/capturegraph/types/containers/map.py
@classmethod
def schema(cls) -> dict[str, JSONValue]:
    """This map's wire form: its key and element schemas, tagged ``"map"``."""
    if cls._key is None or cls._element is None:
        raise TypeError("a schemaless Map has no wire form")
    return {
        "type": "map",
        "key": cls._key.schema(),
        "element": cls._element.schema(),
    }

store(base, value) classmethod #

Write value (a dict) under base as key-encoded entries.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
@classmethod
def store(cls, base: Path, value: object) -> None:
    """Write ``value`` (a ``dict``) under ``base`` as key-encoded entries."""
    if cls._key is None or cls._element is None:
        raise TypeError(f"{cls.__name__} cannot store without a key and element")
    if isinstance(value, Map):
        entries = value.to_dict()
    elif isinstance(value, dict):
        entries = value
    else:
        raise TypeError(f"{cls.__name__}.store needs a dict or Map, got {value!r}")
    base.mkdir(parents=True, exist_ok=True)
    for key, item in entries.items():
        cls._element.store(base / cls._key.key_encode(key), item)

to_dict() #

A plain dict of the entries (keys preserved).

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def to_dict(self) -> dict[K, V]:
    """A plain ``dict`` of the entries (keys preserved)."""
    return dict(self._entries)

values() #

The entry values, as an Array.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def values(self) -> Array[V]:
    """The entry values, as an ``Array``."""
    return Array(self._entries.values())

with_key(key, value) #

A copy with key added or replaced.

Source code in capturegraph-lib/capturegraph/types/containers/map.py
def with_key(self, key: K, value: V) -> Map[K, V]:
    """A copy with ``key`` added or replaced."""
    return Map({**self._entries, key: value})