railtracks.prebuilt.middleware.pre_verifier

  1from __future__ import annotations
  2
  3import asyncio
  4import functools
  5from typing import Any, Awaitable, Callable, ParamSpec, TypeVar, overload
  6
  7from typing_extensions import Never
  8
  9from railtracks.events.middleware import (
 10    MiddlewareVerifierPreFailureEvent,
 11    MiddlewareVerifierPreInvocationEvent,
 12    MiddlewareVerifierPreResponseEvent,
 13)
 14from railtracks.events.send import emit
 15from railtracks.middleware.core import Middleware, _MiddlewareSignature, wrap_node
 16from railtracks.middleware.verdict import (
 17    Verdict,
 18    VerifierDecision,
 19    VerifierRejectedError,
 20)
 21from railtracks.utils.logging.create import get_rt_logger
 22from railtracks.utils.unpack import unpack_async_sync
 23
 24logger = get_rt_logger(__name__)
 25
 26_P = ParamSpec("_P")
 27_R = TypeVar("_R")
 28
 29_ApproveFn = Callable[_P, Verdict | Awaitable[Verdict]]
 30
 31
 32# A pre-verifier constrains the call arguments but leaves the node output free.
 33@overload
 34def pre_verifier(
 35    approve_fn: _ApproveFn[_P],
 36    /,
 37    *,
 38    timeout: float | None = None,
 39    name: str | None = None,
 40) -> Middleware[_P, Any, _MiddlewareSignature[_P, Never]]: ...
 41@overload
 42def pre_verifier(
 43    *, timeout: float | None = None, name: str | None = None
 44) -> Callable[
 45    [_ApproveFn[_P]], Middleware[_P, Any, _MiddlewareSignature[_P, Never]]
 46]: ...
 47
 48
 49def pre_verifier(
 50    approve_fn: _ApproveFn[_P] | None = None,
 51    /,
 52    *,
 53    timeout: float | None = None,
 54    name: str | None = None,
 55) -> (
 56    Middleware[_P, Any, _MiddlewareSignature[_P, Never]]
 57    | Callable[[_ApproveFn[_P]], Middleware[_P, Any, _MiddlewareSignature[_P, Never]]]
 58):
 59    """Build a node-verification middleware around ``approve_fn`` that gates a call
 60    BEFORE it runs.
 61
 62    ``approve_fn`` is called with the exact ``*args, **kwargs`` the wrapped
 63    node was called with — sync or async, both supported — and must return a
 64    `Verdict`. On decline, `VerifierRejectedError` is raised and the node's own
 65    body never runs. On accept, the call is forwarded onward, using the
 66    verdict's ``args``/``kwargs`` if it supplied overrides, otherwise the
 67    original ones unchanged.
 68
 69    If ``timeout`` is set and ``approve_fn`` doesn't respond in time, the call
 70    is treated as declined with reason ``"timeout"``.
 71
 72    See also :func:`~railtracks.prebuilt.middleware.post_verifier.post_verifier`,
 73    which gates a call's output AFTER it has already run. For the full
 74    picture (composing with other middleware, custom approval backends,
 75    guided walkthroughs), see the Verifiers docs.
 76    """
 77
 78    if approve_fn is None:
 79        return lambda fn: pre_verifier(fn, timeout=timeout, name=name)
 80
 81    return wrap_node(_wrapper(approve_fn, timeout), name=name)
 82
 83
 84def _wrapper(approve_fn: _ApproveFn[_P], timeout: float | None):
 85    @functools.wraps(approve_fn)
 86    async def wrapped(
 87        call: Callable[_P, Awaitable[_R]], *args: _P.args, **kwargs: _P.kwargs
 88    ) -> _R:
 89        await emit(MiddlewareVerifierPreInvocationEvent(args=args, kwargs=kwargs))
 90
 91        is_timeout = False
 92        try:
 93            review = unpack_async_sync(approve_fn(*args, **kwargs))
 94            if timeout is None:
 95                verdict = await review
 96            else:
 97                verdict = await asyncio.wait_for(review, timeout=timeout)
 98        except asyncio.TimeoutError:
 99            verdict = Verdict(accepted=False, comment="timeout")
100            is_timeout = True
101        except Exception as e:
102            await emit(MiddlewareVerifierPreFailureEvent.from_exception(e))
103            raise
104
105        new_args = verdict.args if verdict.args is not None else args
106        new_kwargs = verdict.kwargs if verdict.kwargs is not None else kwargs
107        overridden = verdict.args is not None or verdict.kwargs is not None
108
109        await emit(
110            MiddlewareVerifierPreResponseEvent(
111                decision=VerifierDecision.from_verdict(
112                    verdict, overridden=overridden, timeout=is_timeout
113                ),
114                args=new_args,
115                kwargs=new_kwargs,
116            )
117        )
118
119        if not verdict.accepted:
120            raise VerifierRejectedError(verdict.comment or "rejected")
121
122        if verdict.comment:
123            logger.info("pre_verifier accepted with comment: %s", verdict.comment)
124
125        return await call(*new_args, **new_kwargs)
126
127    return wrapped
logger = <RTContextLoggingAdapter RT.railtracks.prebuilt.middleware.pre_verifier (WARNING)>
def pre_verifier( approve_fn: Optional[Callable[~_P, Union[railtracks.middleware.Verdict, Awaitable[railtracks.middleware.Verdict]]]] = None, /, *, timeout: float | None = None, name: str | None = None) -> Union[railtracks.middleware.Middleware[~_P, Any, railtracks.middleware.core._MiddlewareSignature[~_P, typing_extensions.Never]], Callable[[Callable[~_P, Union[railtracks.middleware.Verdict, Awaitable[railtracks.middleware.Verdict]]]], railtracks.middleware.Middleware[~_P, Any, railtracks.middleware.core._MiddlewareSignature[~_P, typing_extensions.Never]]]]:
50def pre_verifier(
51    approve_fn: _ApproveFn[_P] | None = None,
52    /,
53    *,
54    timeout: float | None = None,
55    name: str | None = None,
56) -> (
57    Middleware[_P, Any, _MiddlewareSignature[_P, Never]]
58    | Callable[[_ApproveFn[_P]], Middleware[_P, Any, _MiddlewareSignature[_P, Never]]]
59):
60    """Build a node-verification middleware around ``approve_fn`` that gates a call
61    BEFORE it runs.
62
63    ``approve_fn`` is called with the exact ``*args, **kwargs`` the wrapped
64    node was called with — sync or async, both supported — and must return a
65    `Verdict`. On decline, `VerifierRejectedError` is raised and the node's own
66    body never runs. On accept, the call is forwarded onward, using the
67    verdict's ``args``/``kwargs`` if it supplied overrides, otherwise the
68    original ones unchanged.
69
70    If ``timeout`` is set and ``approve_fn`` doesn't respond in time, the call
71    is treated as declined with reason ``"timeout"``.
72
73    See also :func:`~railtracks.prebuilt.middleware.post_verifier.post_verifier`,
74    which gates a call's output AFTER it has already run. For the full
75    picture (composing with other middleware, custom approval backends,
76    guided walkthroughs), see the Verifiers docs.
77    """
78
79    if approve_fn is None:
80        return lambda fn: pre_verifier(fn, timeout=timeout, name=name)
81
82    return wrap_node(_wrapper(approve_fn, timeout), name=name)

Build a node-verification middleware around approve_fn that gates a call BEFORE it runs.

approve_fn is called with the exact *args, **kwargs the wrapped node was called with — sync or async, both supported — and must return a Verdict. On decline, VerifierRejectedError is raised and the node's own body never runs. On accept, the call is forwarded onward, using the verdict's args/kwargs if it supplied overrides, otherwise the original ones unchanged.

If timeout is set and approve_fn doesn't respond in time, the call is treated as declined with reason "timeout".

See also ~railtracks.prebuilt.middleware.post_verifier.post_verifier(), which gates a call's output AFTER it has already run. For the full picture (composing with other middleware, custom approval backends, guided walkthroughs), see the Verifiers docs.