Source code for marivo.refs
"""Cross-layer semantic reference base.
This module sits below every other Marivo layer (datasource, semantic,
analysis) so a single ``SemanticRef`` base can be shared by all of them
without import cycles. It owns the ``SymbolKind`` enum for the same reason.
"""
from __future__ import annotations
from enum import StrEnum
from typing import Any
class SymbolKind(StrEnum):
"""The kind of a semantic object. One ref subclass exists per member."""
DOMAIN = "domain"
DATASOURCE = "datasource"
ENTITY = "entity"
DIMENSION = "dimension"
MEASURE = "measure"
TIME_DIMENSION = "time_dimension"
METRIC = "metric"
RELATIONSHIP = "relationship"
[docs]
class SemanticRef:
"""Stable identity for a semantic object, shared across all layers.
Identity (``id`` + ``kind``) is fixed at construction; ``__eq__`` /
``__hash__`` are stable. This is deliberately not a frozen dataclass:
field-kind subclasses attach a single late-bound resolver (see
``marivo.semantic.refs``). ``kind`` is normally encoded by the subclass.
"""
__slots__ = ("id", "kind")
id: str
kind: SymbolKind
def __init__(self, id: str, kind: SymbolKind) -> None:
normalized = id.strip()
if not normalized:
raise ValueError("ref id must be non-empty")
object.__setattr__(self, "id", normalized)
object.__setattr__(self, "kind", kind)
def __setattr__(self, name: str, value: Any) -> None:
if name == "_resolver":
# Allow field refs to attach a late-bound resolver.
object.__setattr__(self, name, value)
return
raise AttributeError("SemanticRef instances are immutable")
def __str__(self) -> str:
return self.id
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.id!r})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, SemanticRef):
return NotImplemented
return type(self) is type(other) and self.id == other.id
def __hash__(self) -> int:
return hash((type(self), self.id))
def __call__(self, *args: Any, **kwargs: Any) -> Any:
raise TypeError(
f"{self.id!r} is a declared semantic object, not a decorator. "
"Body-free constructors (ms.ratio / ms.weighted_average / ms.linear / "
"ms.aggregate / ms.relationship) return a ref — assign it, e.g. "
"`loss_rate = ms.ratio(name=..., numerator=..., denominator=...)`. "
"They have no function body."
)
@classmethod
def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any:
"""Allow ref subclasses to be used as Pydantic field types."""
from pydantic_core import core_schema
ref_cls: type[SemanticRef] = _source_type if _source_type is not SemanticRef else cls
def validate(value: Any) -> SemanticRef:
if isinstance(value, ref_cls):
return value
if isinstance(value, str):
return ref_cls(value) # type: ignore[call-arg]
raise ValueError(f"expected str or {ref_cls.__name__}, got {type(value).__name__}")
return core_schema.no_info_plain_validator_function(
validate,
serialization=core_schema.plain_serializer_function_ser_schema(
lambda v: v.id,
info_arg=False,
),
)