Skip to content

graflo.architecture.schema.coerce

Coerce a raw value — typically text read off a document — to a field's declared type.

The inverse of :func:~graflo.architecture.onto_sample.infer_field_type: that one reads a type off values, this one makes a value conform to a declared type. Every refusal is a :class:CoercionError whose kind says what went wrong, so a caller can group failures and ask for a targeted correction.

Attributes

CoercionKind = Literal['type', 'unit_mismatch', 'list_item', 'empty'] module-attribute

Classes

CoercionError

Bases: ValueError

A value that does not conform to its field's declared type.

Attributes:

Name Type Description
field

Name of the field being coerced.

value

The raw value as received.

kind

type (wrong shape for the type), unit_mismatch (a unit other than the declared one), list_item (an item of a list failed; see index) or empty (nothing to coerce).

index

Position of the failing item when kind is list_item.

Source code in graflo/architecture/schema/coerce.py
class CoercionError(ValueError):
    """A value that does not conform to its field's declared type.

    Attributes:
        field: Name of the field being coerced.
        value: The raw value as received.
        kind: ``type`` (wrong shape for the type), ``unit_mismatch`` (a unit
            other than the declared one), ``list_item`` (an item of a list
            failed; see ``index``) or ``empty`` (nothing to coerce).
        index: Position of the failing item when ``kind`` is ``list_item``.
    """

    def __init__(
        self,
        message: str,
        *,
        field: str,
        value: Any,
        kind: CoercionKind,
        index: int | None = None,
    ) -> None:
        super().__init__(message)
        self.field = field
        self.value = value
        self.kind = kind
        self.index = index

Attributes

field = field instance-attribute
index = index instance-attribute
kind = kind instance-attribute
value = value instance-attribute

Methods:

__init__(message, *, field, value, kind, index=None)
Source code in graflo/architecture/schema/coerce.py
def __init__(
    self,
    message: str,
    *,
    field: str,
    value: Any,
    kind: CoercionKind,
    index: int | None = None,
) -> None:
    super().__init__(message)
    self.field = field
    self.value = value
    self.kind = kind
    self.index = index

Functions:

coerce_value(value, field)

Return value as the Python value field declares.

  • INT / UINT: an int; integral floats and digit strings are accepted, bool and grouped digits (1,000) are not. UINT refuses negatives.
  • FLOAT / DOUBLE: a finite float.
  • BOOL: a bool; true/false/yes/no/1/0 in any case.
  • STRING: a stripped str; numbers are stringified.
  • DATETIME: an ISO 8601 string, not a datetime. A space separator becomes T and z becomes Z. The partial dates YYYY and YYYY-MM are kept as written, never padded.
  • UUID: a lower-cased UUID string.
  • LIST: a list of item_type values; a bare scalar becomes a one-item list.
  • Untyped: the value unchanged.

For a numeric field (or a list of numerics) whose semantics.unit is set, a string carrying exactly that unit ("200 kV" for kV) has it stripped. Unit matching is case-sensitive; no conversion is attempted.

Raises:

Type Description
CoercionError

when value is empty or does not conform.

Source code in graflo/architecture/schema/coerce.py
def coerce_value(value: Any, field: Field) -> Any:
    """Return *value* as the Python value *field* declares.

    - ``INT`` / ``UINT``: an ``int``; integral floats and digit strings are
      accepted, ``bool`` and grouped digits (``1,000``) are not. ``UINT``
      refuses negatives.
    - ``FLOAT`` / ``DOUBLE``: a finite ``float``.
    - ``BOOL``: a ``bool``; ``true/false/yes/no/1/0`` in any case.
    - ``STRING``: a stripped ``str``; numbers are stringified.
    - ``DATETIME``: an ISO 8601 **string**, not a ``datetime``. A space
      separator becomes ``T`` and ``z`` becomes ``Z``. The partial dates
      ``YYYY`` and ``YYYY-MM`` are kept as written, never padded.
    - ``UUID``: a lower-cased UUID string.
    - ``LIST``: a list of ``item_type`` values; a bare scalar becomes a
      one-item list.
    - Untyped: the value unchanged.

    For a numeric field (or a list of numerics) whose ``semantics.unit`` is
    set, a string carrying exactly that unit (``"200 kV"`` for ``kV``) has it
    stripped. Unit matching is case-sensitive; no conversion is attempted.

    Raises:
        CoercionError: when *value* is empty or does not conform.
    """
    if _is_empty(value):
        raise CoercionError(
            f"{field.name}: no value", field=field.name, value=value, kind="empty"
        )
    if field.type is None:
        return value
    unit = field.semantics.unit if field.semantics is not None else None
    ftype = FieldType(field.type)
    if ftype == FieldType.LIST:
        item_type = FieldType(field.item_type or FieldType.STRING)
        items = list(value) if isinstance(value, (list, tuple)) else [value]
        out = []
        for index, item in enumerate(items):
            try:
                out.append(_coerce_scalar(item, item_type, unit, field.name))
            except CoercionError as error:
                raise CoercionError(
                    f"{field.name}[{index}]: {error}",
                    field=field.name,
                    value=value,
                    kind="list_item",
                    index=index,
                ) from error
        return out
    return _coerce_scalar(value, ftype, unit, field.name)