Source code for noob.node.tube

import warnings
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, Union

from pydantic import ConfigDict

from noob.edge import Signal, Slot
from noob.event import MetaSignal
from noob.exceptions import ExtraInputWarning
from noob.node.base import Node
from noob.node.spec import NodeSpecification
from noob.types import ConfigSource, Epoch, RunnerContext

if TYPE_CHECKING:
    from noob.runner import TubeRunner
    from noob.tube import Tube, TubeSpecification


[docs] class TubeNode(Node): """ A node that contains another tube within it. .. note:: A nested tube may not return a scalar literal ``None`` from its return node, that is interpreted as a ``NoEvent``, as the return value of ``process`` is ``None`` when no events are emitted. Wrap ``None`` s in a dictionary return to disambiguate them. i.e. rather than:: depends: node.value use:: depends: - something: node.value If you have a need for returning scalar ``None`` s from a nested tube, please raise an issue! """ spec: NodeSpecification # not optional for tube nodes tube: ConfigSource _tube: Union["Tube", None] = None _tube_spec: Union["TubeSpecification", None] = None _runner: Union["TubeRunner", None] = None model_config = ConfigDict(extra="allow") @property def tube_spec(self) -> "TubeSpecification": from noob.tube import TubeSpecification if self._tube_spec is None: self._tube_spec = TubeSpecification.from_any(self.tube) return self._tube_spec # TODO: support dependency injected inits in plugin
[docs] def init(self, context: RunnerContext) -> None: # type: ignore[override] from noob import SynchronousRunner, Tube spec_params = self.spec.params if self.spec.params else {} input_collection = context["input_collection"] input_params = input_collection.get_node_params( {k: v for k, v in spec_params.items() if k != "tube"} ) if self.__pydantic_extra__: extra_params = { k: v for k, v in self.__pydantic_extra__.items() if k not in input_params } else: extra_params = {} with warnings.catch_warnings(action="ignore", category=ExtraInputWarning): self._tube = Tube.from_specification( self.tube, input={**input_collection.chain, **input_params, **extra_params} ) self._runner = SynchronousRunner(tube=self._tube) self._runner.init()
[docs] def deinit(self) -> None: if self._runner is not None: self._runner.deinit()
[docs] def process(self, epoch: Epoch, **kwargs: Any) -> Any: if self._runner is None: raise RuntimeError( "TubeNode must be initialized within a Runner " "to receive the outer runner's context. " "It doesn't make sense to run a TubeNode on its own." ) res = self._runner.process(**kwargs) if isinstance(res, dict): now = datetime.now(UTC) return [ self._event_maker.new_event( signal=key, epoch=epoch, value=value, timestamp=now, ) for key, value in res.items() ] elif res is None: now = datetime.now(UTC) return [ self._event_maker.new_event( signal=key, epoch=epoch, value=MetaSignal.NoEvent, timestamp=now ) for key in self.signals ] else: return res
[docs] @classmethod def get_slots(cls, spec: NodeSpecification | None = None) -> dict[str, Slot]: if spec is None: raise ValueError("Must pass a spec to get slots for a tube node") from noob.input import InputScope from noob.tube import TubeSpecification if not spec.params or "tube" not in spec.params: raise ValueError("Tube node specifications must have a `tube` in their params") tube_spec = TubeSpecification.from_any(spec.params["tube"]) slots = {} for in_key, in_val in tube_spec.input.items(): if in_val.scope == InputScope.process: slots[in_key] = Slot(name=in_key, annotation=Any) return slots
[docs] @classmethod def get_signals(cls, spec: NodeSpecification | None = None) -> dict[str, Signal]: """Forward signals from the return node""" if spec is None: raise ValueError("Must pass a spec to get slots for a tube node") from noob.tube import TubeSpecification if not spec.params or "tube" not in spec.params: raise ValueError("Tube node specifications must have a `tube` in their params") tube_spec = TubeSpecification.from_any(spec.params["tube"]) signals = {} return_nodes = [n for n in tube_spec.nodes.values() if n.type_ == "return"] if not return_nodes: return {"value": Signal(name="value", annotation=Any)} else: return_node = return_nodes[0] if not return_node.depends or isinstance(return_node.depends, str): return {"value": Signal(name="value", annotation=Any)} for dep in return_node.depends: if isinstance(dep, str): continue name = list(dep.keys())[0] signals[name] = Signal(name=name, annotation=Any) return signals