railtracks.middleware

 1from railtracks.middleware.after import after_node
 2from railtracks.middleware.core import (
 3    Middleware,
 4    wrap_node,
 5)
 6from railtracks.middleware.post import post_node
 7from railtracks.middleware.verdict import Verdict, VerifierRejectedError
 8
 9__all__ = [
10    "post_node",
11    "after_node",
12    "Middleware",
13    "wrap_node",
14    "Verdict",
15    "VerifierRejectedError",
16]
def post_node( fn: Union[Callable[[~_R], Awaitable[~_R]], Callable[[~_R], ~_R], NoneType] = None, /, *, name: str | None = None) -> Union[Middleware[..., ~_R], Callable[[Union[Callable[[~_R], Awaitable[~_R]], Callable[[~_R], ~_R]]], Middleware[..., ~_R]]]:
31def post_node(
32    fn: Callable[[_R], Awaitable[_R]] | Callable[[_R], _R] | None = None,
33    /,
34    *,
35    name: str | None = None,
36) -> (
37    Middleware[..., _R]
38    | Callable[
39        [Callable[[_R], Awaitable[_R]] | Callable[[_R], _R]], Middleware[..., _R]
40    ]
41):
42    """
43    Special decorator to create a middleware that runs after the node completes. The wrapped function will run and then your post function will be called upon successful completion of the function.
44
45    NOTE: This middleware will not run if the node raises an exception.
46    """
47
48    if fn is None:
49        return lambda f: wrap_node(_wrapper(f), name=name)
50
51    return wrap_node(_wrapper(fn), name=name)

Special decorator to create a middleware that runs after the node completes. The wrapped function will run and then your post function will be called upon successful completion of the function.

NOTE: This middleware will not run if the node raises an exception.

def after_node( fn: Union[Callable[[~_R], Awaitable[~_R]], Callable[[~_R], ~_R], NoneType] = None, /, *, name: str | None = None) -> Any:
28def after_node(
29    fn: Callable[[_R], Awaitable[_R]] | Callable[[_R], _R] | None = None,
30    /,
31    *,
32    name: str | None = None,
33) -> Any:
34    """Deprecated: Use ``rt.post_node`` instead."""
35    warn_pending_change(
36        "rt.after_node",
37        change="is renamed",
38        instead="rt.post_node",
39        detail="The function itself is unchanged.",
40    )
41    if fn is None:
42        return post_node(name=name)
43    return post_node(fn, name=name)

Deprecated: Use rt.post_node instead.

class Middleware(typing.Generic[~_P, ~_R]):
41class Middleware(Generic[_P, _R]):
42    """Execution-control middleware: wraps a callable to control how it is invoked.
43
44    Built from an async call-style function ``fn(call, *args, **kwargs)`` where
45    ``call`` is the next callable in the chain::
46
47        @wrap_node
48        async def retry(call, *args, **kwargs):
49            for _ in range(3):
50                try:
51                    return await call(*args, **kwargs)
52                except Exception:
53                    pass
54            raise RuntimeError("All retries exhausted")
55
56    ``fn`` must be ``async``; passing a plain ``def`` raises ``TypeError``.
57    """
58
59    def __init__(
60        self,
61        fn: Callable[Concatenate[Callable[_P, Awaitable[_R]], _P], Awaitable[_R]],
62        name: str | None = None,
63    ) -> None:
64        _require_async(fn, "Middleware function")
65        self._fn = fn
66        self.name = name if name is not None else fn.__name__
67        self.type_id = str(
68            uuid.uuid4()
69        )  # identifies this middleware definition, shared by every invocation
70
71        self._has_registered = False
72
73    async def start_creation_task(self):
74        if self._has_registered:
75            return
76
77        self._has_registered = True
78        event = MiddlewareCreationEvent(
79            middleware_type_id=self.type_id,
80            middleware_name=self.name,
81        )
82        return await emit(event)
83
84    def wrap(self, inner: Callable[_P, Awaitable[_R]]) -> Callable[_P, Awaitable[_R]]:
85        """Compose this middleware onto ``inner``, returning a new callable with the same signature."""
86        fn = self._fn
87
88        async def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R:
89            return await fn(inner, *args, **kwargs)
90
91        return wrapped
92
93    def __repr__(self) -> str:
94        return f"Middleware({self._fn.__name__})"
95
96    def middleware_type(self) -> str:
97        return "General"

Execution-control middleware: wraps a callable to control how it is invoked.

Built from an async call-style function fn(call, *args, **kwargs) where call is the next callable in the chain::

@wrap_node
async def retry(call, *args, **kwargs):
    for _ in range(3):
        try:
            return await call(*args, **kwargs)
        except Exception:
            pass
    raise RuntimeError("All retries exhausted")

fn must be async; passing a plain def raises TypeError.

Middleware( fn: Callable[Concatenate[Callable[~_P, Awaitable[~_R]], ~_P], Awaitable[~_R]], name: str | None = None)
59    def __init__(
60        self,
61        fn: Callable[Concatenate[Callable[_P, Awaitable[_R]], _P], Awaitable[_R]],
62        name: str | None = None,
63    ) -> None:
64        _require_async(fn, "Middleware function")
65        self._fn = fn
66        self.name = name if name is not None else fn.__name__
67        self.type_id = str(
68            uuid.uuid4()
69        )  # identifies this middleware definition, shared by every invocation
70
71        self._has_registered = False
name
type_id
async def start_creation_task(self):
73    async def start_creation_task(self):
74        if self._has_registered:
75            return
76
77        self._has_registered = True
78        event = MiddlewareCreationEvent(
79            middleware_type_id=self.type_id,
80            middleware_name=self.name,
81        )
82        return await emit(event)
def wrap( self, inner: Callable[~_P, Awaitable[~_R]]) -> Callable[~_P, Awaitable[~_R]]:
84    def wrap(self, inner: Callable[_P, Awaitable[_R]]) -> Callable[_P, Awaitable[_R]]:
85        """Compose this middleware onto ``inner``, returning a new callable with the same signature."""
86        fn = self._fn
87
88        async def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R:
89            return await fn(inner, *args, **kwargs)
90
91        return wrapped

Compose this middleware onto inner, returning a new callable with the same signature.

def middleware_type(self) -> str:
96    def middleware_type(self) -> str:
97        return "General"
def wrap_node( fn: Optional[Callable[Concatenate[Callable[~_P, Awaitable[~_R]], ~_P], Awaitable[~_R]]] = None, /, *, name: str | None = None) -> Union[Middleware[~_P, ~_R], Callable[[Callable[Concatenate[Callable[~_P, Awaitable[~_R]], ~_P], Awaitable[~_R]]], Middleware[~_P, ~_R]]]:
115def wrap_node(
116    fn: _Wrapper[_P, _R] | None = None,
117    /,
118    *,
119    name: str | None = None,
120) -> Middleware[_P, _R] | Callable[[_Wrapper[_P, _R]], Middleware[_P, _R]]:
121    if fn is None:
122        return lambda f: Middleware(f, name=name)
123    return Middleware(fn, name=name)
@dataclass
class Verdict(typing.Generic[~_R]):
10@dataclass
11class Verdict(Generic[_R]):
12    """The result of an approve callable's review of a node call.
13
14    - accept: ``accepted=True``, ``comment=None``
15    - accept with comments: ``accepted=True``, ``comment=<str>``, optionally
16      ``args``/``kwargs`` (pre-call) or ``result`` (post-call) set to override
17      what gets forwarded.
18    - decline: ``accepted=False``, ``comment=None``
19    - decline with comments: ``accepted=False``, ``comment=<str>``
20
21    ``args``/``kwargs`` mean "forward these into the call instead" — used by
22    pre-call verifiers, which review before the node runs. ``result`` means
23    "propagate this instead of what the call produced" — used by post-call
24    verifiers, which review after the node has already run and can no longer
25    change what was passed in, only what continues onward.
26
27    ``Verdict`` is generic over ``result``'s type, matching the wrapped
28    node's return type, so a ``result=`` override of the wrong type is a
29    type-checker error rather than a silent runtime mismatch. There is no
30    runtime validation of ``args``/``kwargs`` overrides against the node's
31    signature — a bad override surfaces as a ``TypeError`` from the call
32    itself.
33    """
34
35    accepted: bool
36    comment: str | None = None
37    args: tuple | None = None
38    kwargs: dict | None = None
39    result: _R | None = None

The result of an approve callable's review of a node call.

  • accept: accepted=True, comment=None
  • accept with comments: accepted=True, comment=<str>, optionally args/kwargs (pre-call) or result (post-call) set to override what gets forwarded.
  • decline: accepted=False, comment=None
  • decline with comments: accepted=False, comment=<str>

args/kwargs mean "forward these into the call instead" — used by pre-call verifiers, which review before the node runs. result means "propagate this instead of what the call produced" — used by post-call verifiers, which review after the node has already run and can no longer change what was passed in, only what continues onward.

Verdict is generic over result's type, matching the wrapped node's return type, so a result= override of the wrong type is a type-checker error rather than a silent runtime mismatch. There is no runtime validation of args/kwargs overrides against the node's signature — a bad override surfaces as a TypeError from the call itself.

Verdict( accepted: bool, comment: str | None = None, args: tuple | None = None, kwargs: dict | None = None, result: Optional[~_R] = None)
accepted: bool
comment: str | None = None
args: tuple | None = None
kwargs: dict | None = None
result: Optional[~_R] = None
class VerifierRejectedError(builtins.Exception):
42class VerifierRejectedError(Exception):
43    """Raised when a verifier's approve callable declines a node call."""

Raised when a verifier's approve callable declines a node call.