Skip to content

stream.router

stream.router

Declarative schema selection and project routing.

Both are config-driven and orthogonal:

  • :class:DeclarativeSchemaSelector maps a document to schema name(s) via match rules (feed-bound or metadata matchers), unioned, with an optional custom classifier entry point as a fallback.
  • :class:DeclarativeRouter maps a document to project name(s) via axes (e.g. one project per ticker).

Custom selectors/routers register via angelo.stream.selectors / angelo.stream.routers entry points and are used in place of these.

DeclarativeSchemaSelector

Union schema selection from match rules + optional classifier fallback.

Source code in stream/router.py
class DeclarativeSchemaSelector:
    """Union schema selection from match rules + optional classifier fallback."""

    def __init__(self, cfg: SchemaSelection) -> None:
        self.cfg = cfg
        self._classifier = None
        if cfg.classifier:
            from . import contrib

            self._classifier = contrib.discover_selectors().get(cfg.classifier)

    def select(self, document: Document) -> list[str]:
        chosen: list[str] = []
        for rule in self.cfg.rules:
            if _matches(document, rule.get("match", {})):
                for s in rule.get("schemas", []):
                    if s not in chosen:
                        chosen.append(s)
        if self._classifier is not None:
            try:
                for s in self._classifier(document) or []:
                    if s not in chosen:
                        chosen.append(s)
            except Exception:  # pragma: no cover - custom code is best-effort
                pass
        if not chosen:
            chosen = list(self.cfg.default)
        return chosen

DeclarativeRouter

Map a document to project(s) from axis definitions.

Source code in stream/router.py
class DeclarativeRouter:
    """Map a document to project(s) from axis definitions."""

    def __init__(self, cfg: RouterConfig) -> None:
        self.cfg = cfg

    def route(self, document: Document) -> list[str]:
        projects: list[str] = []
        for axis in self.cfg.axes:
            for p in self._axis_projects(document, axis):
                if p and p not in projects:
                    projects.append(p)
        if not projects:
            projects = list(self.cfg.default)
        return projects

    def _axis_projects(self, document: Document, axis: dict[str, Any]) -> list[str]:
        atype = axis.get("type", "metadata")
        if atype != "metadata":
            return []
        key = axis.get("key", "")
        value = _field(document, key)
        if value is None:
            return []
        values = value if isinstance(value, list) else [value]
        template = axis.get("template", "{value}")
        mapping = axis.get("map", {}) or {}
        out: list[str] = []
        for v in values:
            if v in mapping:
                out.append(str(mapping[v]))
            else:
                out.append(template.format(value=v, key=key))
        return out

DeclarativeSpineSelector

Union spine selection from match rules + metadata axes, default fallback.

Mirrors :class:DeclarativeSchemaSelector (rules {match, spines}) and :class:DeclarativeRouter (metadata axes), unioned, falling back to default when nothing matches. The result is the per-document spine set, which the pipeline merges with the run-global StreamConfig.spines.

Source code in stream/router.py
class DeclarativeSpineSelector:
    """Union spine selection from match rules + metadata axes, default fallback.

    Mirrors :class:`DeclarativeSchemaSelector` (rules ``{match, spines}``) and
    :class:`DeclarativeRouter` (metadata axes), unioned, falling back to
    ``default`` when nothing matches. The result is the per-document spine set,
    which the pipeline merges with the run-global ``StreamConfig.spines``.
    """

    def __init__(self, cfg: SpineSelection) -> None:
        self.cfg = cfg

    def select(self, document: Document) -> list[str]:
        chosen: list[str] = []
        for rule in self.cfg.rules:
            if _matches(document, rule.get("match", {})):
                for s in rule.get("spines", []):
                    if s and s not in chosen:
                        chosen.append(s)
        for axis in self.cfg.axes:
            for s in self._axis_spines(document, axis):
                if s and s not in chosen:
                    chosen.append(s)
        if not chosen:
            chosen = list(self.cfg.default)
        return chosen

    def _axis_spines(self, document: Document, axis: dict[str, Any]) -> list[str]:
        atype = axis.get("type", "metadata")
        if atype != "metadata":
            return []
        key = axis.get("key", "")
        value = _field(document, key)
        if value is None:
            return []
        values = value if isinstance(value, list) else [value]
        template = axis.get("template", "{value}")
        mapping = axis.get("map", {}) or {}
        out: list[str] = []
        for v in values:
            if v in mapping:
                out.append(str(mapping[v]))
            else:
                out.append(template.format(value=v, key=key))
        return out