- - - - -Dispatch Python SDK -This is the API reference for the Python SDK of Dispatch. - -Tutorials and guides: docs.stealthrocket.cloud. -Source: stealthrocket/dispatch-sdk-python. - -API Reference - - - - - - - - - - - The Dispatch SDK for Python. - - - - - - - - - - - - - - - - - DispatchID: TypeAlias = str - - - module-attribute - - - - - - - - Unique identifier in Dispatch. -It should be treated as an opaque value. - - - - - - - - - - - Call - - - - dataclass - - - - - - - - - Instruction to call a function. -Though this class can be built manually, it is recommended to use the -with_call method of a Function instead. - - - Source code in dispatch/proto.py - 178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198@dataclass -class Call: - """Instruction to call a function. - - Though this class can be built manually, it is recommended to use the - with_call method of a Function instead. - """ - - function: str - input: Any - endpoint: str | None = None - correlation_id: int | None = None - - def _as_proto(self) -> call_pb.Call: - input_bytes = _pb_any_pickle(self.input) - return call_pb.Call( - correlation_id=self.correlation_id, - endpoint=self.endpoint, - function=self.function, - input=input_bytes, - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Client - - - - - - - - - Client for the Dispatch API. - - - Source code in dispatch/client.py - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 39 - 40 - 41 - 42 - 43 - 44 - 45 - 46 - 47 - 48 - 49 - 50 - 51 - 52 - 53 - 54 - 55 - 56 - 57 - 58 - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103class Client: - """Client for the Dispatch API.""" - - def __init__(self, api_key: None | str = None, api_url: None | str = None): - """Create a new Dispatch client. - - Args: - api_key: Dispatch API key to use for authentication. Uses the value of - the DISPATCH_API_KEY environment variable by default. - - api_url: The URL of the Dispatch API to use. Uses the value of the - DISPATCH_API_URL environment variable if set, otherwise - defaults to the public Dispatch API (DEFAULT_API_URL). - - Raises: - ValueError: if the API key is missing. - """ - - if not api_key: - api_key = os.environ.get("DISPATCH_API_KEY") - if not api_key: - raise ValueError( - "missing API key: set it with the DISPATCH_API_KEY environment variable" - ) - - if not api_url: - api_url = os.environ.get("DISPATCH_API_URL", DEFAULT_API_URL) - if not api_url: - raise ValueError( - "missing API URL: set it with the DISPATCH_API_URL environment variable" - ) - - self.api_url = api_url - self.api_key = api_key - self._init_stub() - - def __getstate__(self): - return {"api_url": self.api_url, "api_key": self.api_key} - - def __setstate__(self, state): - self.api_url = state["api_url"] - self.api_key = state["api_key"] - self._init_stub() - - def _init_stub(self): - logger.debug("initializing client for Dispatch API at URL %s", self.api_url) - - result = urlparse(self.api_url) - match result.scheme: - case "http": - creds = grpc.local_channel_credentials() - case "https": - creds = grpc.ssl_channel_credentials() - case _: - raise ValueError(f"Invalid API scheme: '{result.scheme}'") - - call_creds = grpc.access_token_call_credentials(self.api_key) - creds = grpc.composite_channel_credentials(creds, call_creds) - channel = grpc.secure_channel(result.netloc, creds) - - self._stub = dispatch_grpc.DispatchServiceStub(channel) - - def dispatch(self, calls: Iterable[Call]) -> Iterable[DispatchID]: - """Dispatch function calls. - - Args: - calls: Calls to dispatch. - - Returns: - Identifiers for the function calls, in the same order as the inputs. - """ - calls_proto = [c._as_proto() for c in calls] - logger.debug("dispatching %d function call(s)", len(calls_proto)) - req = dispatch_pb.DispatchRequest(calls=calls_proto) - resp = self._stub.Dispatch(req) - dispatch_ids = [DispatchID(x) for x in resp.dispatch_ids] - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - "dispatched %d function call(s): %s", - len(calls_proto), - ", ".join(dispatch_ids), - ) - return dispatch_ids - - - - - - - - - - - - - - - - - - - - - - __init__(api_key=None, api_url=None) - - - - - - - Create a new Dispatch client. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - api_key - - None | str - - - - Dispatch API key to use for authentication. Uses the value of -the DISPATCH_API_KEY environment variable by default. - - - - None - - - - api_url - - None | str - - - - The URL of the Dispatch API to use. Uses the value of the -DISPATCH_API_URL environment variable if set, otherwise -defaults to the public Dispatch API (DEFAULT_API_URL). - - - - None - - - - - - - - Raises: - - - - Type - Description - - - - - - ValueError - - - - if the API key is missing. - - - - - - - - Source code in dispatch/client.py - 24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55def __init__(self, api_key: None | str = None, api_url: None | str = None): - """Create a new Dispatch client. - - Args: - api_key: Dispatch API key to use for authentication. Uses the value of - the DISPATCH_API_KEY environment variable by default. - - api_url: The URL of the Dispatch API to use. Uses the value of the - DISPATCH_API_URL environment variable if set, otherwise - defaults to the public Dispatch API (DEFAULT_API_URL). - - Raises: - ValueError: if the API key is missing. - """ - - if not api_key: - api_key = os.environ.get("DISPATCH_API_KEY") - if not api_key: - raise ValueError( - "missing API key: set it with the DISPATCH_API_KEY environment variable" - ) - - if not api_url: - api_url = os.environ.get("DISPATCH_API_URL", DEFAULT_API_URL) - if not api_url: - raise ValueError( - "missing API URL: set it with the DISPATCH_API_URL environment variable" - ) - - self.api_url = api_url - self.api_key = api_key - self._init_stub() - - - - - - - - - - - - - dispatch(calls) - - - - - - - Dispatch function calls. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - calls - - Iterable[Call] - - - - Calls to dispatch. - - - - required - - - - - - - - Returns: - - - - Type - Description - - - - - - Iterable[DispatchID] - - - - Identifiers for the function calls, in the same order as the inputs. - - - - - - - - Source code in dispatch/client.py - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103def dispatch(self, calls: Iterable[Call]) -> Iterable[DispatchID]: - """Dispatch function calls. - - Args: - calls: Calls to dispatch. - - Returns: - Identifiers for the function calls, in the same order as the inputs. - """ - calls_proto = [c._as_proto() for c in calls] - logger.debug("dispatching %d function call(s)", len(calls_proto)) - req = dispatch_pb.DispatchRequest(calls=calls_proto) - resp = self._stub.Dispatch(req) - dispatch_ids = [DispatchID(x) for x in resp.dispatch_ids] - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - "dispatched %d function call(s): %s", - len(calls_proto), - ", ".join(dispatch_ids), - ) - return dispatch_ids - - - - - - - - - - - - - - - - - - - - - Error - - - - - - - - - Error when running a function. -This is not a Python exception, but potentially part of a CallResult or -Output. - - - Source code in dispatch/proto.py - 243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290class Error: - """Error when running a function. - - This is not a Python exception, but potentially part of a CallResult or - Output. - """ - - def __init__(self, status: Status, type: str | None, message: str | None): - """Create a new Error. - - Args: - status: categorization of the error. - type: arbitrary string, used for humans. Optional. - message: arbitrary message. Optional. - - Raises: - ValueError: Neither type or message was provided or status is - invalid. - """ - if type is None and message is None: - raise ValueError("At least one of type or message is required") - if status is Status.OK: - raise ValueError("Status cannot be OK") - - self.type = type - self.message = message - self.status = status - - @classmethod - def from_exception(cls, ex: Exception, status: Status | None = None) -> Error: - """Create an Error from a Python exception, using its class qualified - named as type. - - The status tries to be inferred, but can be overridden. If it is not - provided or cannot be inferred, it defaults to TEMPORARY_ERROR. - """ - - if status is None: - status = status_for_error(ex) - - return Error(status, ex.__class__.__qualname__, str(ex)) - - @classmethod - def _from_proto(cls, proto: error_pb.Error) -> Error: - return cls(Status.UNSPECIFIED, proto.type, proto.message) - - def _as_proto(self) -> error_pb.Error: - return error_pb.Error(type=self.type, message=self.message) - - - - - - - - - - - - - - - - - - - - - - __init__(status, type, message) - - - - - - - Create a new Error. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - status - - Status - - - - categorization of the error. - - - - required - - - - type - - str | None - - - - arbitrary string, used for humans. Optional. - - - - required - - - - message - - str | None - - - - arbitrary message. Optional. - - - - required - - - - - - - - Raises: - - - - Type - Description - - - - - - ValueError - - - - Neither type or message was provided or status is -invalid. - - - - - - - - Source code in dispatch/proto.py - 250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269def __init__(self, status: Status, type: str | None, message: str | None): - """Create a new Error. - - Args: - status: categorization of the error. - type: arbitrary string, used for humans. Optional. - message: arbitrary message. Optional. - - Raises: - ValueError: Neither type or message was provided or status is - invalid. - """ - if type is None and message is None: - raise ValueError("At least one of type or message is required") - if status is Status.OK: - raise ValueError("Status cannot be OK") - - self.type = type - self.message = message - self.status = status - - - - - - - - - - - - - from_exception(ex, status=None) - - - classmethod - - - - - - - - Create an Error from a Python exception, using its class qualified -named as type. -The status tries to be inferred, but can be overridden. If it is not -provided or cannot be inferred, it defaults to TEMPORARY_ERROR. - - - Source code in dispatch/proto.py - 271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283@classmethod -def from_exception(cls, ex: Exception, status: Status | None = None) -> Error: - """Create an Error from a Python exception, using its class qualified - named as type. - - The status tries to be inferred, but can be overridden. If it is not - provided or cannot be inferred, it defaults to TEMPORARY_ERROR. - """ - - if status is None: - status = status_for_error(ex) - - return Error(status, ex.__class__.__qualname__, str(ex)) - - - - - - - - - - - - - - - - - - - - - Input - - - - - - - - - The input to a primitive function. -Functions always take a single argument of type Input. When the function is -run for the first time, it receives the input. When the function is a coroutine -that's resuming after a yield point, it receives the results of the yield -directive. Use the is_first_call and is_resume properties to differentiate -between the two cases. -This class is intended to be used as read-only. - - - Source code in dispatch/proto.py - 24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89class Input: - """The input to a primitive function. - - Functions always take a single argument of type Input. When the function is - run for the first time, it receives the input. When the function is a coroutine - that's resuming after a yield point, it receives the results of the yield - directive. Use the is_first_call and is_resume properties to differentiate - between the two cases. - - This class is intended to be used as read-only. - """ - - def __init__(self, req: function_pb.RunRequest): - self._has_input = req.HasField("input") - if self._has_input: - input_pb = google.protobuf.wrappers_pb2.BytesValue() - req.input.Unpack(input_pb) - input_bytes = input_pb.value - self._input = pickle.loads(input_bytes) - else: - state_bytes = req.poll_result.coroutine_state - if len(state_bytes) > 0: - self._coroutine_state = pickle.loads(state_bytes) - else: - self._coroutine_state = None - self._call_results = [ - CallResult._from_proto(r) for r in req.poll_result.results - ] - - @property - def is_first_call(self) -> bool: - return self._has_input - - @property - def is_resume(self) -> bool: - return not self.is_first_call - - @property - def input(self) -> Any: - self._assert_first_call() - return self._input - - def input_arguments(self) -> tuple[list[Any], dict[str, Any]]: - """Returns positional and keyword arguments carried by the input.""" - self._assert_first_call() - if not isinstance(self._input, _Arguments): - raise RuntimeError("input does not hold arguments") - return self._input.args, self._input.kwargs - - @property - def coroutine_state(self) -> Any: - self._assert_resume() - return self._coroutine_state - - @property - def call_results(self) -> list[CallResult]: - self._assert_resume() - return self._call_results - - def _assert_first_call(self): - if self.is_resume: - raise ValueError("This input is for a resumed coroutine") - - def _assert_resume(self): - if self.is_first_call: - raise ValueError("This input is for a first function call") - - - - - - - - - - - - - - - - - - - - - - input_arguments() - - - - - - - Returns positional and keyword arguments carried by the input. - - - Source code in dispatch/proto.py - 66 -67 -68 -69 -70 -71def input_arguments(self) -> tuple[list[Any], dict[str, Any]]: - """Returns positional and keyword arguments carried by the input.""" - self._assert_first_call() - if not isinstance(self._input, _Arguments): - raise RuntimeError("input does not hold arguments") - return self._input.args, self._input.kwargs - - - - - - - - - - - - - - - - - - - - - Output - - - - - - - - - The output of a primitive function. -This class is meant to be instantiated and returned by authors of functions -to indicate the follow up action they need to take. Use the various class -methods create an instance of this class. For example Output.value() or -Output.poll(). - - - Source code in dispatch/proto.py - 100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169class Output: - """The output of a primitive function. - - This class is meant to be instantiated and returned by authors of functions - to indicate the follow up action they need to take. Use the various class - methods create an instance of this class. For example Output.value() or - Output.poll(). - """ - - def __init__(self, proto: function_pb.RunResponse): - self._message = proto - - @classmethod - def value(cls, value: Any, status: Status | None = None) -> Output: - """Terminally exit the function with the provided return value.""" - if status is None: - status = status_for_output(value) - return cls.exit(result=CallResult.from_value(value), status=status) - - @classmethod - def error(cls, error: Error) -> Output: - """Terminally exit the function with the provided error.""" - return cls.exit(result=CallResult.from_error(error), status=error.status) - - @classmethod - def tail_call(cls, tail_call: Call) -> Output: - """Terminally exit the function, and instruct the orchestrator to - tail call the specified function.""" - return cls.exit(tail_call=tail_call) - - @classmethod - def exit( - cls, - result: CallResult | None = None, - tail_call: Call | None = None, - status: Status = Status.OK, - ) -> Output: - """Terminally exit the function.""" - result_proto = result._as_proto() if result else None - tail_call_proto = tail_call._as_proto() if tail_call else None - return Output( - function_pb.RunResponse( - status=status._proto, - exit=exit_pb.Exit(result=result_proto, tail_call=tail_call_proto), - ) - ) - - @classmethod - def poll(cls, state: Any, calls: None | list[Call] = None) -> Output: - """Suspend the function with a set of Calls, instructing the - orchestrator to resume the function with the provided state when - call results are ready.""" - state_bytes = pickle.dumps(state) - poll = poll_pb.Poll( - coroutine_state=state_bytes, - # FIXME: make this configurable - max_results=1, - max_wait=duration_pb2.Duration(seconds=5), - ) - - if calls is not None: - for c in calls: - poll.calls.append(c._as_proto()) - - return Output( - function_pb.RunResponse( - status=status_pb.STATUS_OK, - poll=poll, - ) - ) - - - - - - - - - - - - - - - - - - - - - - error(error) - - - classmethod - - - - - - - - Terminally exit the function with the provided error. - - - Source code in dispatch/proto.py - 119 -120 -121 -122@classmethod -def error(cls, error: Error) -> Output: - """Terminally exit the function with the provided error.""" - return cls.exit(result=CallResult.from_error(error), status=error.status) - - - - - - - - - - - - - exit(result=None, tail_call=None, status=Status.OK) - - - classmethod - - - - - - - - Terminally exit the function. - - - Source code in dispatch/proto.py - 130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145@classmethod -def exit( - cls, - result: CallResult | None = None, - tail_call: Call | None = None, - status: Status = Status.OK, -) -> Output: - """Terminally exit the function.""" - result_proto = result._as_proto() if result else None - tail_call_proto = tail_call._as_proto() if tail_call else None - return Output( - function_pb.RunResponse( - status=status._proto, - exit=exit_pb.Exit(result=result_proto, tail_call=tail_call_proto), - ) - ) - - - - - - - - - - - - - poll(state, calls=None) - - - classmethod - - - - - - - - Suspend the function with a set of Calls, instructing the -orchestrator to resume the function with the provided state when -call results are ready. - - - Source code in dispatch/proto.py - 147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169@classmethod -def poll(cls, state: Any, calls: None | list[Call] = None) -> Output: - """Suspend the function with a set of Calls, instructing the - orchestrator to resume the function with the provided state when - call results are ready.""" - state_bytes = pickle.dumps(state) - poll = poll_pb.Poll( - coroutine_state=state_bytes, - # FIXME: make this configurable - max_results=1, - max_wait=duration_pb2.Duration(seconds=5), - ) - - if calls is not None: - for c in calls: - poll.calls.append(c._as_proto()) - - return Output( - function_pb.RunResponse( - status=status_pb.STATUS_OK, - poll=poll, - ) - ) - - - - - - - - - - - - - tail_call(tail_call) - - - classmethod - - - - - - - - Terminally exit the function, and instruct the orchestrator to -tail call the specified function. - - - Source code in dispatch/proto.py - 124 -125 -126 -127 -128@classmethod -def tail_call(cls, tail_call: Call) -> Output: - """Terminally exit the function, and instruct the orchestrator to - tail call the specified function.""" - return cls.exit(tail_call=tail_call) - - - - - - - - - - - - - value(value, status=None) - - - classmethod - - - - - - - - Terminally exit the function with the provided return value. - - - Source code in dispatch/proto.py - 112 -113 -114 -115 -116 -117@classmethod -def value(cls, value: Any, status: Status | None = None) -> Output: - """Terminally exit the function with the provided return value.""" - if status is None: - status = status_for_output(value) - return cls.exit(result=CallResult.from_value(value), status=status) - - - - - - - - - - - - - - - - - - - - - Status - - - - - - - - Bases: int, Enum - - - Enumeration of the possible values that can be used in the return status -of functions. - - - Source code in dispatch/status.py - 7 - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23@enum.unique -class Status(int, enum.Enum): - """Enumeration of the possible values that can be used in the return status - of functions. - """ - - UNSPECIFIED = status_pb.STATUS_UNSPECIFIED - OK = status_pb.STATUS_OK - TIMEOUT = status_pb.STATUS_TIMEOUT - THROTTLED = status_pb.STATUS_THROTTLED - INVALID_ARGUMENT = status_pb.STATUS_INVALID_ARGUMENT - INVALID_RESPONSE = status_pb.STATUS_INVALID_RESPONSE - TEMPORARY_ERROR = status_pb.STATUS_TEMPORARY_ERROR - PERMANENT_ERROR = status_pb.STATUS_PERMANENT_ERROR - INCOMPATIBLE_STATE = status_pb.STATUS_INCOMPATIBLE_STATE - - _proto: status_pb.Status - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - poll(calls) - - - - - - - Suspend the function with a set of Calls, instructing the -orchestrator to resume the coroutine when call results are ready. - - - Source code in dispatch/coroutine.py - 16 -17 -18 -19 -20 -21@coroutine -@durable -def poll(calls: list[Call]) -> list[CallResult]: # type: ignore[misc] - """Suspend the function with a set of Calls, instructing the - orchestrator to resume the coroutine when call results are ready.""" - return (yield Poll(calls)) - - - - - - - - - - - - - client - - - - - - - - - - - - - - - - - - - - - - - Client - - - - - - - - - Client for the Dispatch API. - - - Source code in dispatch/client.py - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 39 - 40 - 41 - 42 - 43 - 44 - 45 - 46 - 47 - 48 - 49 - 50 - 51 - 52 - 53 - 54 - 55 - 56 - 57 - 58 - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103class Client: - """Client for the Dispatch API.""" - - def __init__(self, api_key: None | str = None, api_url: None | str = None): - """Create a new Dispatch client. - - Args: - api_key: Dispatch API key to use for authentication. Uses the value of - the DISPATCH_API_KEY environment variable by default. - - api_url: The URL of the Dispatch API to use. Uses the value of the - DISPATCH_API_URL environment variable if set, otherwise - defaults to the public Dispatch API (DEFAULT_API_URL). - - Raises: - ValueError: if the API key is missing. - """ - - if not api_key: - api_key = os.environ.get("DISPATCH_API_KEY") - if not api_key: - raise ValueError( - "missing API key: set it with the DISPATCH_API_KEY environment variable" - ) - - if not api_url: - api_url = os.environ.get("DISPATCH_API_URL", DEFAULT_API_URL) - if not api_url: - raise ValueError( - "missing API URL: set it with the DISPATCH_API_URL environment variable" - ) - - self.api_url = api_url - self.api_key = api_key - self._init_stub() - - def __getstate__(self): - return {"api_url": self.api_url, "api_key": self.api_key} - - def __setstate__(self, state): - self.api_url = state["api_url"] - self.api_key = state["api_key"] - self._init_stub() - - def _init_stub(self): - logger.debug("initializing client for Dispatch API at URL %s", self.api_url) - - result = urlparse(self.api_url) - match result.scheme: - case "http": - creds = grpc.local_channel_credentials() - case "https": - creds = grpc.ssl_channel_credentials() - case _: - raise ValueError(f"Invalid API scheme: '{result.scheme}'") - - call_creds = grpc.access_token_call_credentials(self.api_key) - creds = grpc.composite_channel_credentials(creds, call_creds) - channel = grpc.secure_channel(result.netloc, creds) - - self._stub = dispatch_grpc.DispatchServiceStub(channel) - - def dispatch(self, calls: Iterable[Call]) -> Iterable[DispatchID]: - """Dispatch function calls. - - Args: - calls: Calls to dispatch. - - Returns: - Identifiers for the function calls, in the same order as the inputs. - """ - calls_proto = [c._as_proto() for c in calls] - logger.debug("dispatching %d function call(s)", len(calls_proto)) - req = dispatch_pb.DispatchRequest(calls=calls_proto) - resp = self._stub.Dispatch(req) - dispatch_ids = [DispatchID(x) for x in resp.dispatch_ids] - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - "dispatched %d function call(s): %s", - len(calls_proto), - ", ".join(dispatch_ids), - ) - return dispatch_ids - - - - - - - - - - - - - - - - - - - - - - __init__(api_key=None, api_url=None) - - - - - - - Create a new Dispatch client. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - api_key - - None | str - - - - Dispatch API key to use for authentication. Uses the value of -the DISPATCH_API_KEY environment variable by default. - - - - None - - - - api_url - - None | str - - - - The URL of the Dispatch API to use. Uses the value of the -DISPATCH_API_URL environment variable if set, otherwise -defaults to the public Dispatch API (DEFAULT_API_URL). - - - - None - - - - - - - - Raises: - - - - Type - Description - - - - - - ValueError - - - - if the API key is missing. - - - - - - - - Source code in dispatch/client.py - 24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55def __init__(self, api_key: None | str = None, api_url: None | str = None): - """Create a new Dispatch client. - - Args: - api_key: Dispatch API key to use for authentication. Uses the value of - the DISPATCH_API_KEY environment variable by default. - - api_url: The URL of the Dispatch API to use. Uses the value of the - DISPATCH_API_URL environment variable if set, otherwise - defaults to the public Dispatch API (DEFAULT_API_URL). - - Raises: - ValueError: if the API key is missing. - """ - - if not api_key: - api_key = os.environ.get("DISPATCH_API_KEY") - if not api_key: - raise ValueError( - "missing API key: set it with the DISPATCH_API_KEY environment variable" - ) - - if not api_url: - api_url = os.environ.get("DISPATCH_API_URL", DEFAULT_API_URL) - if not api_url: - raise ValueError( - "missing API URL: set it with the DISPATCH_API_URL environment variable" - ) - - self.api_url = api_url - self.api_key = api_key - self._init_stub() - - - - - - - - - - - - - dispatch(calls) - - - - - - - Dispatch function calls. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - calls - - Iterable[Call] - - - - Calls to dispatch. - - - - required - - - - - - - - Returns: - - - - Type - Description - - - - - - Iterable[DispatchID] - - - - Identifiers for the function calls, in the same order as the inputs. - - - - - - - - Source code in dispatch/client.py - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103def dispatch(self, calls: Iterable[Call]) -> Iterable[DispatchID]: - """Dispatch function calls. - - Args: - calls: Calls to dispatch. - - Returns: - Identifiers for the function calls, in the same order as the inputs. - """ - calls_proto = [c._as_proto() for c in calls] - logger.debug("dispatching %d function call(s)", len(calls_proto)) - req = dispatch_pb.DispatchRequest(calls=calls_proto) - resp = self._stub.Dispatch(req) - dispatch_ids = [DispatchID(x) for x in resp.dispatch_ids] - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - "dispatched %d function call(s): %s", - len(calls_proto), - ", ".join(dispatch_ids), - ) - return dispatch_ids - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - coroutine - - - - - - - - - - - - - - - - - - - - - - - CoroutineState - - - - dataclass - - - - - - - - - Serialized representation of a coroutine. - - - Source code in dispatch/coroutine.py - 115 -116 -117 -118 -119 -120@dataclass -class CoroutineState: - """Serialized representation of a coroutine.""" - - coroutine: DurableCoroutine - version: str - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - exit(result=None, tail_call=None) - - - - - - - Exit exits a coroutine, with an optional result and -optional tail call. - - - Source code in dispatch/coroutine.py - 24 -25 -26 -27 -28 -29@coroutine -@durable -def exit(result: Any | None = None, tail_call: Call | None = None): - """Exit exits a coroutine, with an optional result and - optional tail call.""" - yield Exit(result, tail_call) - - - - - - - - - - - - - poll(calls) - - - - - - - Suspend the function with a set of Calls, instructing the -orchestrator to resume the coroutine when call results are ready. - - - Source code in dispatch/coroutine.py - 16 -17 -18 -19 -20 -21@coroutine -@durable -def poll(calls: list[Call]) -> list[CallResult]: # type: ignore[misc] - """Suspend the function with a set of Calls, instructing the - orchestrator to resume the coroutine when call results are ready.""" - return (yield Poll(calls)) - - - - - - - - - - - - - schedule(func, input) - - - - - - - Schedule schedules a coroutine with the provided input. - - - Source code in dispatch/coroutine.py - 43 - 44 - 45 - 46 - 47 - 48 - 49 - 50 - 51 - 52 - 53 - 54 - 55 - 56 - 57 - 58 - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112def schedule(func: DurableFunction, input: Input) -> Output: - """Schedule schedules a coroutine with the provided input.""" - try: - # (Re)hydrate the coroutine. - if input.is_first_call: - logger.debug("starting coroutine") - try: - args, kwargs = input.input_arguments() - except ValueError: - raise ValueError("incorrect input for function") - - coro = func(*args, **kwargs) - send = None - else: - logger.debug( - "resuming coroutine with %d bytes of state and %d call result(s)", - len(input.coroutine_state), - len(input.call_results), - ) - try: - coroutine_state = pickle.loads(input.coroutine_state) - if not isinstance(coroutine_state, CoroutineState): - raise ValueError("invalid coroutine state") - if coroutine_state.version != sys.version: - raise ValueError( - f"coroutine state version mismatch: '{coroutine_state.version}' vs. current '{sys.version}'" - ) - except (pickle.PickleError, ValueError) as e: - logger.warning("coroutine state is incompatible", exc_info=True) - return Output.error( - Error.from_exception(e, status=Status.INCOMPATIBLE_STATE) - ) - coro = coroutine_state.coroutine - send = input.call_results - - # Run the coroutine until its next yield or return. - try: - directive = coro.send(send) - except StopIteration as e: - logger.debug("coroutine returned") - return Output.value(e.value) - - # Handle directives that it yields. - logger.debug("handling coroutine directive: %s", directive) - match directive: - case Exit(): - return Output.exit( - result=CallResult.from_value(directive.result), - tail_call=directive.tail_call, - status=status_for_output(directive.result), - ) - - case Poll(): - try: - coroutine_state = pickle.dumps( - CoroutineState(coroutine=coro, version=sys.version) - ) - except pickle.PickleError as e: - logger.error("coroutine could not be serialized", exc_info=True) - return Output.error( - Error.from_exception(e, status=Status.PERMANENT_ERROR) - ) - return Output.poll(state=coroutine_state, calls=directive.calls) - - case _: - raise RuntimeError(f"coroutine unexpectedly yielded '{directive}'") - - except Exception as e: - logger.exception(f"@dispatch.coroutine: '{func.__name__}' raised an exception") - return Output.error(Error.from_exception(e)) - - - - - - - - - - - - - - - - - - - - experimental - - - - - - - - - - - - - - - - - - - - - - - - - durable - - - - - - - A decorator that makes generators and coroutines serializable. -This module defines a @durable decorator that can be applied to generator -functions and async functions. The generator and coroutine instances -they create can be pickled. -Example usage: -import pickle -from dispatch.experimental.durable import durable - -@durable -def my_generator(): - for i in range(3): - yield i - -# Run the generator to its first yield point: -g = my_generator() -print(next(g)) # 0 - -# Make a copy, and consume the remaining items: -b = pickle.dumps(g) -g2 = pickle.loads(b) -print(next(g2)) # 1 -print(next(g2)) # 2 - -# The original is not affected: -print(next(g)) # 1 -print(next(g)) # 2 - - - - - - - - - - - - - - - - - - - - - durable(fn) - - - - - - - Returns a "durable" function that creates serializable -generators or coroutines. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - fn - - - - - A generator function or async function. - - - - required - - - - - - - Source code in dispatch/experimental/durable/function.py - 44 -45 -46 -47 -48 -49 -50 -51def durable(fn) -> DurableFunction: - """Returns a "durable" function that creates serializable - generators or coroutines. - - Args: - fn: A generator function or async function. - """ - return DurableFunction(fn) - - - - - - - - - - - - - frame - - - - - - - - - - - - - - - - - - - - - - __file__ = '/opt/hostedtoolcache/Python/3.11.8/x64/lib/python3.11/site-packages/dispatch/experimental/durable/frame.cpython-311-x86_64-linux-gnu.so' - - - - - - - str(object='') -> str -str(bytes_or_buffer[, encoding[, errors]]) -> str -Create a new string object from the given object. If encoding or -errors is specified, then the object must expose a data buffer -that will be decoded using the given encoding and error handler. -Otherwise, returns the result of object.str() (if defined) -or repr(object). -encoding defaults to sys.getdefaultencoding(). -errors defaults to 'strict'. - - - - - - - - - - __name__ = 'dispatch.experimental.durable.frame' - - - - - - - str(object='') -> str -str(bytes_or_buffer[, encoding[, errors]]) -> str -Create a new string object from the given object. If encoding or -errors is specified, then the object must expose a data buffer -that will be decoded using the given encoding and error handler. -Otherwise, returns the result of object.str() (if defined) -or repr(object). -encoding defaults to sys.getdefaultencoding(). -errors defaults to 'strict'. - - - - - - - - - - __package__ = 'dispatch.experimental.durable' - - - - - - - str(object='') -> str -str(bytes_or_buffer[, encoding[, errors]]) -> str -Create a new string object from the given object. If encoding or -errors is specified, then the object must expose a data buffer -that will be decoded using the given encoding and error handler. -Otherwise, returns the result of object.str() (if defined) -or repr(object). -encoding defaults to sys.getdefaultencoding(). -errors defaults to 'strict'. - - - - - - - - - - - - - get_frame_ip() - - - builtin - - - - - - - - Get instruction pointer of a generator or coroutine. - - - - - - - - - - - - get_frame_sp() - - - builtin - - - - - - - - Get stack pointer of a generator or coroutine. - - - - - - - - - - - - get_frame_stack_at() - - - builtin - - - - - - - - Get an object from a generator or coroutine's stack, as an (is_null, obj) tuple. - - - - - - - - - - - - get_frame_state() - - - builtin - - - - - - - - Get frame state of a generator or coroutine. - - - - - - - - - - - - set_frame_ip() - - - builtin - - - - - - - - Set instruction pointer of a generator or coroutine. - - - - - - - - - - - - set_frame_sp() - - - builtin - - - - - - - - Set stack pointer of a generator or coroutine. - - - - - - - - - - - - set_frame_stack_at() - - - builtin - - - - - - - - Set or unset an object on the stack of a generator or coroutine. - - - - - - - - - - - - set_frame_state() - - - builtin - - - - - - - - Set frame state of a generator or coroutine. - - - - - - - - - - - - - - - - - - - function - - - - - - - - - - - - - - - - - - - - - - - DurableCoroutine - - - - - - - - Bases: Serializable, Coroutine[_YieldT, _SendT, _ReturnT] - - - A wrapper for a coroutine that makes it serializable (can be pickled). -Instances behave like the coroutines they wrap. - - - Source code in dispatch/experimental/durable/function.py - 116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173class DurableCoroutine(Serializable, Coroutine[_YieldT, _SendT, _ReturnT]): - """A wrapper for a coroutine that makes it serializable (can be pickled). - Instances behave like the coroutines they wrap.""" - - def __init__( - self, - coroutine: CoroutineType, - registered_fn: RegisteredFunction, - *args: Any, - **kwargs: Any, - ): - self.coroutine = coroutine - Serializable.__init__(self, coroutine, registered_fn, *args, **kwargs) - - def __await__(self) -> Generator[Any, None, _ReturnT]: - coroutine_wrapper = self.coroutine.__await__() - generator = cast(GeneratorType, coroutine_wrapper) - durable_coroutine_wrapper: Generator[Any, None, _ReturnT] = DurableGenerator( - generator, self.registered_fn, *self.args, coro_await=True, **self.kwargs - ) - return durable_coroutine_wrapper - - def send(self, send: _SendT) -> _YieldT: - return self.coroutine.send(send) - - def throw(self, typ, val=None, tb: TracebackType | None = None) -> _YieldT: - return self.coroutine.throw(typ, val, tb) - - def close(self) -> None: - self.coroutine.close() - - def __setstate__(self, state): - Serializable.__setstate__(self, state) - self.coroutine = cast(CoroutineType, self.g) - - @property - def cr_running(self) -> bool: - return self.coroutine.cr_running - - @property - def cr_suspended(self) -> bool: - return self.coroutine.cr_suspended - - @property - def cr_code(self) -> CodeType: - return self.coroutine.cr_code - - @property - def cr_frame(self) -> FrameType: - return self.coroutine.cr_frame - - @property - def cr_await(self) -> Any | None: - return self.coroutine.cr_await - - @property - def cr_origin(self) -> tuple[tuple[str, int, str], ...] | None: - return self.coroutine.cr_origin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DurableFunction - - - - - - - - - A wrapper for generator functions and async functions that make -their generator and coroutine instances serializable. - - - Source code in dispatch/experimental/durable/function.py - 16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41class DurableFunction: - """A wrapper for generator functions and async functions that make - their generator and coroutine instances serializable.""" - - def __init__(self, fn: FunctionType): - self.registered_fn = register_function(fn) - - def __call__(self, *args, **kwargs): - result = self.registered_fn.fn(*args, **kwargs) - - if isinstance(result, GeneratorType): - return DurableGenerator(result, self.registered_fn, *args, **kwargs) - elif isinstance(result, CoroutineType): - return DurableCoroutine(result, self.registered_fn, *args, **kwargs) - elif isinstance(result, AsyncGeneratorType): - raise NotImplementedError( - "only synchronous generator functions are supported at this time" - ) - else: - raise ValueError( - "@durable function did not return a generator or coroutine" - ) - - @property - def __name__(self): - return self.registered_fn.fn.__name__ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DurableGenerator - - - - - - - - Bases: Serializable, Generator[_YieldT, _SendT, _ReturnT] - - - A wrapper for a generator that makes it serializable (can be pickled). -Instances behave like the generators they wrap. - - - Source code in dispatch/experimental/durable/function.py - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113class DurableGenerator(Serializable, Generator[_YieldT, _SendT, _ReturnT]): - """A wrapper for a generator that makes it serializable (can be pickled). - Instances behave like the generators they wrap.""" - - def __init__( - self, - generator: GeneratorType, - registered_fn: RegisteredFunction, - *args: Any, - coro_await: bool = False, - **kwargs: Any, - ): - self.generator = generator - Serializable.__init__( - self, generator, registered_fn, *args, coro_await=coro_await, **kwargs - ) - - def __iter__(self) -> Generator[_YieldT, _SendT, _ReturnT]: - return self - - def __next__(self) -> _YieldT: - return next(self.generator) - - def send(self, send: _SendT) -> _YieldT: - return self.generator.send(send) - - def throw(self, typ, val=None, tb: TracebackType | None = None) -> _YieldT: - return self.generator.throw(typ, val, tb) - - def close(self) -> None: - self.generator.close() - - def __setstate__(self, state): - Serializable.__setstate__(self, state) - self.generator = cast(GeneratorType, self.g) - - @property - def gi_running(self) -> bool: - return self.generator.gi_running - - @property - def gi_suspended(self) -> bool: - return self.generator.gi_suspended - - @property - def gi_code(self) -> CodeType: - return self.generator.gi_code - - @property - def gi_frame(self) -> FrameType: - return self.generator.gi_frame - - @property - def gi_yieldfrom(self) -> GeneratorType | None: - return self.generator.gi_yieldfrom - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - durable(fn) - - - - - - - Returns a "durable" function that creates serializable -generators or coroutines. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - fn - - - - - A generator function or async function. - - - - required - - - - - - - Source code in dispatch/experimental/durable/function.py - 44 -45 -46 -47 -48 -49 -50 -51def durable(fn) -> DurableFunction: - """Returns a "durable" function that creates serializable - generators or coroutines. - - Args: - fn: A generator function or async function. - """ - return DurableFunction(fn) - - - - - - - - - - - - - - - - - - - - registry - - - - - - - - - - - - - - - - - - - - - - - RegisteredFunction - - - - dataclass - - - - - - - - - A function that can be referenced in durable state. - - - Source code in dispatch/experimental/durable/registry.py - 6 - 7 - 8 - 9 -10 -11 -12 -13 -14@dataclass -class RegisteredFunction: - """A function that can be referenced in durable state.""" - - key: str - fn: FunctionType - filename: str - lineno: int - hash: str - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - lookup_function(key) - - - - - - - Lookup a registered function by key. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - key - - str - - - - Unique identifier for the function. - - - - required - - - - - - - - Returns: - - - -Name Type - Description - - - - -RegisteredFunction - RegisteredFunction - - - - the function that was registered with the specified key. - - - - - - - - - Raises: - - - - Type - Description - - - - - - KeyError - - - - A function has not been registered with this key. - - - - - - - - Source code in dispatch/experimental/durable/registry.py - 55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67def lookup_function(key: str) -> RegisteredFunction: - """Lookup a registered function by key. - - Args: - key: Unique identifier for the function. - - Returns: - RegisteredFunction: the function that was registered with the specified key. - - Raises: - KeyError: A function has not been registered with this key. - """ - return _REGISTRY[key] - - - - - - - - - - - - - register_function(fn) - - - - - - - Register a function in the in-memory function registry. -When serializing a registered function, a reference to the function -is stored along with details about its location and contents. When -deserializing the function, the registry is consulted in order to -find the function associated with the reference (and in order to -check whether the function is the same). - - - - Parameters: - - - - Name - Type - Description - Default - - - - - fn - - FunctionType - - - - The function to register. - - - - required - - - - - - - - Returns: - - - -Name Type - Description - - - - -str - RegisteredFunction - - - - Unique identifier for the function. - - - - - - - - - Raises: - - - - Type - Description - - - - - - ValueError - - - - The function conflicts with another registered function. - - - - - - - - Source code in dispatch/experimental/durable/registry.py - 20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52def register_function(fn: FunctionType) -> RegisteredFunction: - """Register a function in the in-memory function registry. - - When serializing a registered function, a reference to the function - is stored along with details about its location and contents. When - deserializing the function, the registry is consulted in order to - find the function associated with the reference (and in order to - check whether the function is the same). - - Args: - fn: The function to register. - - Returns: - str: Unique identifier for the function. - - Raises: - ValueError: The function conflicts with another registered function. - """ - code = fn.__code__ - key = code.co_qualname - if key in _REGISTRY: - raise ValueError(f"durable function already registered with key {key}") - - filename = code.co_filename - lineno = code.co_firstlineno - code_hash = "sha256:" + hashlib.sha256(code.co_code).hexdigest() - - wrapper = RegisteredFunction( - key=key, fn=fn, filename=filename, lineno=lineno, hash=code_hash - ) - - _REGISTRY[key] = wrapper - return wrapper - - - - - - - - - - - - - - - - - - - - serializable - - - - - - - - - - - - - - - - - - - - - - - Serializable - - - - - - - - - A wrapper for a generator or coroutine that makes it serializable. - - - Source code in dispatch/experimental/durable/serializable.py - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 39 - 40 - 41 - 42 - 43 - 44 - 45 - 46 - 47 - 48 - 49 - 50 - 51 - 52 - 53 - 54 - 55 - 56 - 57 - 58 - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123class Serializable: - """A wrapper for a generator or coroutine that makes it serializable.""" - - g: GeneratorType | CoroutineType - registered_fn: RegisteredFunction - coro_await: bool - args: list[Any] - kwargs: dict[str, Any] - - def __init__( - self, - g: GeneratorType | CoroutineType, - registered_fn: RegisteredFunction, - *args: Any, - coro_await: bool = False, - **kwargs: Any, - ): - self.g = g - self.registered_fn = registered_fn - self.coro_await = coro_await - self.args = list(args) - self.kwargs = kwargs - - def __getstate__(self): - g = self.g - rfn = self.registered_fn - - # Capture the details necessary to recreate the generator. - ip = ext.get_frame_ip(g) - sp = ext.get_frame_sp(g) - frame_state = ext.get_frame_state(g) - stack = [ext.get_frame_stack_at(g, i) for i in range(ext.get_frame_sp(g))] - - if TRACE: - typ = "GENERATOR" if isinstance(g, GeneratorType) else "COROUTINE" - print(f"\n[DURABLE] {typ} STATE ({rfn.key}):") - print(f"function = {rfn.fn.__qualname__} ({rfn.filename}:{rfn.lineno})") - print(f"code hash = {rfn.hash}") - print(f"args = {self.args}") - print(f"kwargs = {self.kwargs}") - print(f"coro await = {self.coro_await}") - print(f"IP = {ip}") - print(f"SP = {sp}") - print(f"frame state = {frame_state}") - for i, (is_null, value) in enumerate(stack): - if is_null: - print(f"stack[{i}] = NULL") - else: - print(f"stack[{i}] = {value}") - print() - - state = { - "function": { - "key": rfn.key, - "filename": rfn.filename, - "lineno": rfn.lineno, - "hash": rfn.hash, - "coro_await": self.coro_await, - "args": self.args, - "kwargs": self.kwargs, - }, - "frame": { - "ip": ip, - "sp": sp, - "stack": stack, - "state": frame_state, - }, - } - return state - - def __setstate__(self, state): - function_state = state["function"] - frame_state = state["frame"] - - # Recreate the generator/coroutine by looking up the constructor - # and calling it with the same args/kwargs. - key, filename, lineno, code_hash, args, kwargs, coro_await = ( - function_state["key"], - function_state["filename"], - function_state["lineno"], - function_state["hash"], - function_state["args"], - function_state["kwargs"], - function_state["coro_await"], - ) - - rfn = lookup_function(key) - if filename != rfn.filename or lineno != rfn.lineno: - raise ValueError( - f"location mismatch for function {key}: {filename}:{lineno} vs. expected {rfn.filename}:{rfn.lineno}" - ) - elif code_hash != rfn.hash: - raise ValueError( - f"hash mismatch for function {key}: {code_hash} vs. expected {rfn.hash}" - ) - - g = rfn.fn(*args, **kwargs) - - if coro_await: - g = g.__await__() - - # Restore the frame state (stack + stack pointer + instruction pointer). - ext.set_frame_ip(g, frame_state["ip"]) - ext.set_frame_sp(g, frame_state["sp"]) - for i, (is_null, obj) in enumerate(frame_state["stack"]): - ext.set_frame_stack_at(g, i, is_null, obj) - ext.set_frame_state(g, frame_state["state"]) - - self.g = g - self.registered_fn = rfn - self.coro_await = coro_await - self.args = args - self.kwargs = kwargs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - multicolor - - - - - - - - - - - - - - - - - - - - - - - CustomYield - - - - dataclass - - - - - - - - Bases: YieldType - - - A yield from a function marked with @yields. - - - - Attributes: - - - - Name - Type - Description - - - - - type - - Any - - - - The type of yield that was specified in the @yields decorator. - - - - - args - - list[Any] - - - - Positional arguments to the function call. - - - - - kwargs - - dict[str, Any] | None - - - - Keyword arguments to the function call. - - - - - - - - Source code in dispatch/experimental/multicolor/yields.py - 31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51@dataclass -class CustomYield(YieldType): - """A yield from a function marked with @yields. - - Attributes: - type: The type of yield that was specified in the @yields decorator. - args: Positional arguments to the function call. - kwargs: Keyword arguments to the function call. - """ - - type: Any - args: list[Any] - kwargs: dict[str, Any] | None = None - - def kwarg(self, name, pos) -> Any: - if self.kwargs is None: - return self.args[pos] - try: - return self.kwargs[name] - except KeyError: - return self.args[pos] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - GeneratorYield - - - - dataclass - - - - - - - - Bases: YieldType - - - A yield from a generator. - - - - Attributes: - - - - Name - Type - Description - - - - - value - - Any - - - - The value that was yielded from the generator. - - - - - - - - Source code in dispatch/experimental/multicolor/yields.py - 54 -55 -56 -57 -58 -59 -60 -61 -62@dataclass -class GeneratorYield(YieldType): - """A yield from a generator. - - Attributes: - value: The value that was yielded from the generator. - """ - - value: Any = None - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NoSourceError - - - - - - - - Bases: RuntimeError - - - Function source code is not available. - - - Source code in dispatch/experimental/multicolor/parse.py - 35 -36class NoSourceError(RuntimeError): - """Function source code is not available.""" - - - - - - - - - - - - - - - - compile_function(fn, decorator=None, cache_key='default') - - - - - - - Compile a regular function into a generator that yields data passed -to functions marked with the @multicolor.yields decorator. Decorated yield -functions can be called from anywhere in the call stack, and functions -in between do not have to be generators or async functions (coroutines). -Example: -@multicolor.yields(type="sleep") -def sleep(seconds): ... - -def parent(): - sleep(3) # yield point - -def grandparent(): - parent() - -compiled_grandparent = multicolor.compile_function(grandparent) -generator = compiled_grandparent() -for item in generator: - print(item) # multicolor.CustomYield(type="sleep", args=[3]) - -Two-way data flow works as expected. At a yield point, generator.send(value) -can be used to send data back to the yield point and to resume execution. -The data sent back will be the return value of the function decorated with -@multicolor.yields: -@multicolor.yields(type="add") -def add(a: int, b: int) -> int: - return a + b # default/synchronous implementation - -def scheduler(generator): - try: - send = None - while True: - item = generator.send(send) - match item: - case multicolor.CustomYield(type="add"): - a, b = item.args - print(f"adding {a} + {b}") - send = a + b - except StopIteration as e: - return e.value # return value - -def adder(a: int, b: int) -> int: - return add(a, b) - -compiled_adder = multicolor.compile_function(adder) -generator = compiled_adder(1, 2) -result = scheduler(generator) -print(result) # 3 - -The @multicolor.yields decorator does not change the implementation of -the function it decorates. If the function is run without being -compiled, the default implementation will be used instead: -print(adder(1, 2)) # 3 - -The default implementation could also raise an error, to ensure that -the function is only ever called from a compiled function. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - fn - - FunctionType - - - - The function to compile. - - - - required - - - - decorator - - - - - An optional decorator to apply to the compiled function. - - - - None - - - - cache_key - - str - - - - Cache key to use when caching compiled functions. - - - - 'default' - - - - - - - - Returns: - - - -Name Type - Description - - - - -FunctionType - FunctionType | MethodType - - - - A compiled generator function. - - - - - - - - Source code in dispatch/experimental/multicolor/compile.py - 22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94def compile_function( - fn: FunctionType, decorator=None, cache_key: str = "default" -) -> FunctionType | MethodType: - """Compile a regular function into a generator that yields data passed - to functions marked with the @multicolor.yields decorator. Decorated yield - functions can be called from anywhere in the call stack, and functions - in between do not have to be generators or async functions (coroutines). - - Example: - - @multicolor.yields(type="sleep") - def sleep(seconds): ... - - def parent(): - sleep(3) # yield point - - def grandparent(): - parent() - - compiled_grandparent = multicolor.compile_function(grandparent) - generator = compiled_grandparent() - for item in generator: - print(item) # multicolor.CustomYield(type="sleep", args=[3]) - - Two-way data flow works as expected. At a yield point, generator.send(value) - can be used to send data back to the yield point and to resume execution. - The data sent back will be the return value of the function decorated with - @multicolor.yields: - - @multicolor.yields(type="add") - def add(a: int, b: int) -> int: - return a + b # default/synchronous implementation - - def scheduler(generator): - try: - send = None - while True: - item = generator.send(send) - match item: - case multicolor.CustomYield(type="add"): - a, b = item.args - print(f"adding {a} + {b}") - send = a + b - except StopIteration as e: - return e.value # return value - - def adder(a: int, b: int) -> int: - return add(a, b) - - compiled_adder = multicolor.compile_function(adder) - generator = compiled_adder(1, 2) - result = scheduler(generator) - print(result) # 3 - - The @multicolor.yields decorator does not change the implementation of - the function it decorates. If the function is run without being - compiled, the default implementation will be used instead: - - print(adder(1, 2)) # 3 - - The default implementation could also raise an error, to ensure that - the function is only ever called from a compiled function. - - Args: - fn: The function to compile. - decorator: An optional decorator to apply to the compiled function. - cache_key: Cache key to use when caching compiled functions. - - Returns: - FunctionType: A compiled generator function. - """ - compiled_fn, _ = _compile_internal(fn, decorator, cache_key) - return compiled_fn - - - - - - - - - - - - - no_yields(fn) - - - - - - - Decorator that hints that a function (and anything called -recursively) does not yield. - - - Source code in dispatch/experimental/multicolor/yields.py - 20 -21 -22 -23 -24def no_yields(fn): - """Decorator that hints that a function (and anything called - recursively) does not yield.""" - fn._multicolor_no_yields = True # type: ignore[attr-defined] - return fn - - - - - - - - - - - - - compile - - - - - - - - - - - - - - - - - - - - - - - CallTransformer - - - - - - - - Bases: NodeTransformer - - - Replace explicit function calls with a gadget that recursively compiles -functions into generators and then replaces the function call with a -yield from. -The transformations are only valid for ASTs that have passed through the -desugaring pass; only ast.Expr(value=ast.Call(...)) and -ast.Assign(targets=..., value=ast.Call(..)) nodes are transformed here. - - - Source code in dispatch/experimental/multicolor/compile.py - 249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336class CallTransformer(ast.NodeTransformer): - """Replace explicit function calls with a gadget that recursively compiles - functions into generators and then replaces the function call with a - yield from. - - The transformations are only valid for ASTs that have passed through the - desugaring pass; only ast.Expr(value=ast.Call(...)) and - ast.Assign(targets=..., value=ast.Call(..)) nodes are transformed here. - """ - - def visit_Assign(self, node: ast.Assign) -> ast.stmt: - if not isinstance(node.value, ast.Call): - return node - assign_stmt = ast.Assign(targets=node.targets) - return self._build_call_gadget(node.value, assign_stmt) - - def visit_Expr(self, node: ast.Expr) -> ast.stmt: - if not isinstance(node.value, ast.Call): - return node - return self._build_call_gadget(node.value) - - def _build_call_gadget( - self, fn_call: ast.Call, assign: ast.Assign | None = None - ) -> ast.stmt: - fn = fn_call.func - args = ast.List(elts=fn_call.args, ctx=ast.Load()) - if fn_call.keywords: - kwargs: ast.expr = ast.Call( - func=ast.Name(id="dict", ctx=ast.Load()), - args=[], - keywords=fn_call.keywords, - ) - else: - kwargs = ast.Constant(value=None) - - compiled_fn = ast.Name(id="_multicolor_compiled_fn", ctx=ast.Store()) - compiled_fn_call = ast.Call( - func=ast.Name(id="_multicolor_compiled_fn", ctx=ast.Load()), - args=fn_call.args, - keywords=fn_call.keywords, - ) - - if assign: - assign.value = ast.Name(id="_multicolor_result", ctx=ast.Load()) - assign_result: ast.stmt = assign - else: - assign_result = ast.Pass() - - result = rewrite_template( - """ - if hasattr(__fn__, "_multicolor_yield_type"): - _multicolor_result = yield _multicolor_custom_yield(type=__fn__._multicolor_yield_type, args=__args__, kwargs=__kwargs__) - __assign_result__ - elif hasattr(__fn__, "_multicolor_no_yields"): - _multicolor_result = __fn_call__ - __assign_result__ - else: - _multicolor_result = None - try: - if isinstance(__fn__, type): - raise _multicolor_no_source_error # FIXME: this bypasses compilation for calls that are actually class instantiations - __compiled_fn__, _multicolor_color = _multicolor_compile(__fn__, _multicolor_decorator, _multicolor_cache_key) - except _multicolor_no_source_error: - _multicolor_result = __fn_call__ - else: - _multicolor_generator = __compiled_fn_call__ - if _multicolor_color == _multicolor_generator_color: - _multicolor_result = [] - for _multicolor_yield in _multicolor_generator: - if isinstance(_multicolor_yield, _multicolor_generator_yield): - _multicolor_result.append(_multicolor_yield.value) - else: - yield _multicolor_yield - else: - _multicolor_result = yield from _multicolor_generator - finally: - __assign_result__ - """, - __fn__=fn, - __fn_call__=fn_call, - __args__=args, - __kwargs__=kwargs, - __compiled_fn__=compiled_fn, - __compiled_fn_call__=compiled_fn_call, - __assign_result__=assign_result, - ) - - return result[0] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FunctionColor - - - - - - - - Bases: Enum - - - Color (aka. type/flavor) of a function. -There are four colors of functions in Python: -* regular (e.g. def fn(): pass) -* generator (e.g. def fn(): yield) -* async (e.g. async def fn(): pass) -* async generator (e.g. async def fn(): yield) -Only the first two colors are supported at this time. - - - Source code in dispatch/experimental/multicolor/compile.py - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110class FunctionColor(Enum): - """Color (aka. type/flavor) of a function. - - There are four colors of functions in Python: - * regular (e.g. def fn(): pass) - * generator (e.g. def fn(): yield) - * async (e.g. async def fn(): pass) - * async generator (e.g. async def fn(): yield) - - Only the first two colors are supported at this time. - """ - - REGULAR_FUNCTION = 0 - GENERATOR_FUNCTION = 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - GeneratorTransformer - - - - - - - - Bases: NodeTransformer - - - Wrap ast.Yield values in a GeneratorYield container. - - - Source code in dispatch/experimental/multicolor/compile.py - 233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246class GeneratorTransformer(ast.NodeTransformer): - """Wrap ast.Yield values in a GeneratorYield container.""" - - def visit_Yield(self, node: ast.Yield) -> ast.Yield: - value = node.value - if node.value is None: - value = ast.Constant(value=None) - - wrapped_value = ast.Call( - func=ast.Name(id="_multicolor_generator_yield", ctx=ast.Load()), - args=[], - keywords=[ast.keyword(arg="value", value=value)], - ) - return ast.Yield(value=wrapped_value) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - compile_function(fn, decorator=None, cache_key='default') - - - - - - - Compile a regular function into a generator that yields data passed -to functions marked with the @multicolor.yields decorator. Decorated yield -functions can be called from anywhere in the call stack, and functions -in between do not have to be generators or async functions (coroutines). -Example: -@multicolor.yields(type="sleep") -def sleep(seconds): ... - -def parent(): - sleep(3) # yield point - -def grandparent(): - parent() - -compiled_grandparent = multicolor.compile_function(grandparent) -generator = compiled_grandparent() -for item in generator: - print(item) # multicolor.CustomYield(type="sleep", args=[3]) - -Two-way data flow works as expected. At a yield point, generator.send(value) -can be used to send data back to the yield point and to resume execution. -The data sent back will be the return value of the function decorated with -@multicolor.yields: -@multicolor.yields(type="add") -def add(a: int, b: int) -> int: - return a + b # default/synchronous implementation - -def scheduler(generator): - try: - send = None - while True: - item = generator.send(send) - match item: - case multicolor.CustomYield(type="add"): - a, b = item.args - print(f"adding {a} + {b}") - send = a + b - except StopIteration as e: - return e.value # return value - -def adder(a: int, b: int) -> int: - return add(a, b) - -compiled_adder = multicolor.compile_function(adder) -generator = compiled_adder(1, 2) -result = scheduler(generator) -print(result) # 3 - -The @multicolor.yields decorator does not change the implementation of -the function it decorates. If the function is run without being -compiled, the default implementation will be used instead: -print(adder(1, 2)) # 3 - -The default implementation could also raise an error, to ensure that -the function is only ever called from a compiled function. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - fn - - FunctionType - - - - The function to compile. - - - - required - - - - decorator - - - - - An optional decorator to apply to the compiled function. - - - - None - - - - cache_key - - str - - - - Cache key to use when caching compiled functions. - - - - 'default' - - - - - - - - Returns: - - - -Name Type - Description - - - - -FunctionType - FunctionType | MethodType - - - - A compiled generator function. - - - - - - - - Source code in dispatch/experimental/multicolor/compile.py - 22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94def compile_function( - fn: FunctionType, decorator=None, cache_key: str = "default" -) -> FunctionType | MethodType: - """Compile a regular function into a generator that yields data passed - to functions marked with the @multicolor.yields decorator. Decorated yield - functions can be called from anywhere in the call stack, and functions - in between do not have to be generators or async functions (coroutines). - - Example: - - @multicolor.yields(type="sleep") - def sleep(seconds): ... - - def parent(): - sleep(3) # yield point - - def grandparent(): - parent() - - compiled_grandparent = multicolor.compile_function(grandparent) - generator = compiled_grandparent() - for item in generator: - print(item) # multicolor.CustomYield(type="sleep", args=[3]) - - Two-way data flow works as expected. At a yield point, generator.send(value) - can be used to send data back to the yield point and to resume execution. - The data sent back will be the return value of the function decorated with - @multicolor.yields: - - @multicolor.yields(type="add") - def add(a: int, b: int) -> int: - return a + b # default/synchronous implementation - - def scheduler(generator): - try: - send = None - while True: - item = generator.send(send) - match item: - case multicolor.CustomYield(type="add"): - a, b = item.args - print(f"adding {a} + {b}") - send = a + b - except StopIteration as e: - return e.value # return value - - def adder(a: int, b: int) -> int: - return add(a, b) - - compiled_adder = multicolor.compile_function(adder) - generator = compiled_adder(1, 2) - result = scheduler(generator) - print(result) # 3 - - The @multicolor.yields decorator does not change the implementation of - the function it decorates. If the function is run without being - compiled, the default implementation will be used instead: - - print(adder(1, 2)) # 3 - - The default implementation could also raise an error, to ensure that - the function is only ever called from a compiled function. - - Args: - fn: The function to compile. - decorator: An optional decorator to apply to the compiled function. - cache_key: Cache key to use when caching compiled functions. - - Returns: - FunctionType: A compiled generator function. - """ - compiled_fn, _ = _compile_internal(fn, decorator, cache_key) - return compiled_fn - - - - - - - - - - - - - - - - - - - - desugar - - - - - - - - - - - - - - - - - - - - - - - Desugar - - - - - - - - - The desugar pass simplifies subsequent AST transformations that need -to replace an expression (e.g. a function call) with a statement (e.g. an -if branch) in a function definition. -The pass recursively simplifies control flow and compound expressions -in a function definition such that: -- expressions that are children of statements either have no children, or - only have children of type ast.Name and/or ast.Constant -- those parent expressions are either part of an ast.Expr(value=expr) - statement or an ast.Assign(value=expr) statement -The pass does not recurse into lambda expressions, or nested function or -class definitions. - - - Source code in dispatch/experimental/multicolor/desugar.py - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 39 - 40 - 41 - 42 - 43 - 44 - 45 - 46 - 47 - 48 - 49 - 50 - 51 - 52 - 53 - 54 - 55 - 56 - 57 - 58 - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626class Desugar: - """The desugar pass simplifies subsequent AST transformations that need - to replace an expression (e.g. a function call) with a statement (e.g. an - if branch) in a function definition. - - The pass recursively simplifies control flow and compound expressions - in a function definition such that: - - expressions that are children of statements either have no children, or - only have children of type ast.Name and/or ast.Constant - - those parent expressions are either part of an ast.Expr(value=expr) - statement or an ast.Assign(value=expr) statement - - The pass does not recurse into lambda expressions, or nested function or - class definitions. - """ - - def __init__(self): - self.name_count = 0 - - def desugar(self, stmts: list[ast.stmt]) -> list[ast.stmt]: - return self._desugar_stmts(stmts) - - def _desugar_stmt(self, stmt: ast.stmt) -> tuple[ast.stmt, list[ast.stmt]]: - deps: list[ast.stmt] = [] - match stmt: - # Pass - case ast.Pass(): - pass - - # Break - case ast.Break(): - pass - - # Continue - case ast.Continue(): - pass - - # Import(alias* names) - case ast.Import(): - pass - - # ImportFrom(identifier? module, alias* names, int? level) - case ast.ImportFrom(): - pass - - # Nonlocal(identifier* names) - case ast.Nonlocal(): - pass - - # Global(identifier* names) - case ast.Global(): - pass - - # Return(expr? value) - case ast.Return(): - if stmt.value is not None: - stmt.value, deps = self._desugar_expr(stmt.value) - - # Expr(expr value) - case ast.Expr(): - stmt.value, deps = self._desugar_expr(stmt.value, expr_stmt=True) - - # Assert(expr test, expr? msg) - case ast.Assert(): - stmt.test, deps = self._desugar_expr(stmt.test) - if stmt.msg is not None: - stmt.msg, msg_deps = self._desugar_expr(stmt.msg) - deps.extend(msg_deps) - - # Assign(expr* targets, expr value, string? type_comment) - case ast.Assign(): - stmt.targets, deps = self._desugar_exprs(stmt.targets) - stmt.value, value_deps = self._desugar_expr(stmt.value) - deps.extend(value_deps) - - # AugAssign(expr target, operator op, expr value) - case ast.AugAssign(): - target = cast( - ast.expr, stmt.target - ) # ast.Name | ast.Attribute | ast.Subscript - target, deps = self._desugar_expr(target) - stmt.target = cast(ast.Name | ast.Attribute | ast.Subscript, target) - stmt.value, value_deps = self._desugar_expr(stmt.value) - deps.extend(value_deps) - - # AnnAssign(expr target, expr annotation, expr? value, int simple) - case ast.AnnAssign(): - target = cast( - ast.expr, stmt.target - ) # ast.Name | ast.Attribute | ast.Subscript - target, deps = self._desugar_expr(target) - stmt.target = cast(ast.Name | ast.Attribute | ast.Subscript, target) - stmt.annotation, annotation_deps = self._desugar_expr(stmt.annotation) - deps.extend(annotation_deps) - if stmt.value is not None: - stmt.value, value_deps = self._desugar_expr(stmt.value) - deps.extend(value_deps) - - # Delete(expr* targets) - case ast.Delete(): - stmt.targets, deps = self._desugar_exprs(stmt.targets, del_stmt=True) - - # Raise(expr? exc, expr? cause) - case ast.Raise(): - if stmt.exc is not None: - stmt.exc, exc_deps = self._desugar_expr(stmt.exc) - deps.extend(exc_deps) - if stmt.cause is not None: - stmt.cause, cause_deps = self._desugar_expr(stmt.cause) - deps.extend(cause_deps) - - # If(expr test, stmt* body, stmt* orelse) - case ast.If(): - stmt.test, deps = self._desugar_expr(stmt.test) - stmt.body = self._desugar_stmts(stmt.body) - stmt.orelse = self._desugar_stmts(stmt.orelse) - - # While(expr test, stmt* body, stmt* orelse) - case ast.While(): - stmt.test, deps = self._desugar_expr(stmt.test) - stmt.body = self._desugar_stmts(stmt.body) - stmt.orelse = self._desugar_stmts(stmt.orelse) - - # For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) - case ast.For(): - stmt.target, deps = self._desugar_expr(stmt.target) - stmt.iter, iter_deps = self._desugar_expr(stmt.iter) - deps.extend(iter_deps) - stmt.body = self._desugar_stmts(stmt.body) - stmt.orelse = self._desugar_stmts(stmt.orelse) - - # AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) - case ast.AsyncFor(): - stmt.target, deps = self._desugar_expr(stmt.target) - stmt.iter, iter_deps = self._desugar_expr(stmt.iter) - deps.extend(iter_deps) - stmt.body = self._desugar_stmts(stmt.body) - stmt.orelse = self._desugar_stmts(stmt.orelse) - - # Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) - case ast.Try(): - stmt.body = self._desugar_stmts(stmt.body) - stmt.handlers, deps = self._desugar_except_handlers(stmt.handlers) - stmt.orelse = self._desugar_stmts(stmt.orelse) - stmt.finalbody = self._desugar_stmts(stmt.finalbody) - - # TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) - case ast.TryStar(): - stmt.body = self._desugar_stmts(stmt.body) - stmt.handlers, deps = self._desugar_except_handlers(stmt.handlers) - stmt.orelse = self._desugar_stmts(stmt.orelse) - stmt.finalbody = self._desugar_stmts(stmt.finalbody) - - # Match(expr subject, match_case* cases) - case ast.Match(): - stmt.subject, deps = self._desugar_expr(stmt.subject) - stmt.cases, match_case_deps = self._desugar_match_cases(stmt.cases) - deps.extend(match_case_deps) - - # With(withitem* items, stmt* body, string? type_comment) - case ast.With(): - while len(stmt.items) > 1: - last = stmt.items.pop() - stmt.body = [ast.With(items=[last], body=stmt.body)] - - stmt.items, deps = self._desugar_withitems(stmt.items) - stmt.body = self._desugar_stmts(stmt.body) - - # AsyncWith(withitem* items, stmt* body, string? type_comment) - case ast.AsyncWith(): - while len(stmt.items) > 1: - last = stmt.items.pop() - stmt.body = [ast.AsyncWith(items=[last], body=stmt.body)] - - stmt.items, deps = self._desugar_withitems(stmt.items) - stmt.body = self._desugar_stmts(stmt.body) - - # FunctionDef(identifier name, arguments args, stmt* body, expr* decorator_list, expr? returns, string? type_comment) - case ast.FunctionDef(): - pass # do not recurse - - # AsyncFunctionDef(identifier name, arguments args, stmt* body, expr* decorator_list, expr? returns, string? type_comment) - case ast.AsyncFunctionDef(): - pass # do not recurse - - # ClassDef(identifier name, expr* bases, keyword* keywords, stmt* body, expr* decorator_list) - case ast.ClassDef(): - pass # do not recurse - - case _: - raise NotImplementedError(f"desugar {stmt}") - - return stmt, deps - - def _desugar_expr( - self, expr: ast.expr, expr_stmt=False, del_stmt=False - ) -> tuple[ast.expr, list[ast.stmt]]: - # These cases have no nested expressions or statements. Return - # early so that no superfluous temporaries are generated. - if isinstance(expr, ast.Name): - # Name(identifier id, expr_context ctx) - return expr, [] - elif isinstance(expr, ast.Constant): - # Constant(constant value, string? kind) - return expr, [] - elif isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name): - # Attribute(expr value, identifier attr, expr_context ctx) - return expr, [] - - deps: list[ast.stmt] = [] - wrapper = None - create_temporary = not expr_stmt and not del_stmt - is_store = False - match expr: - # Call(expr func, expr* args, keyword* keywords) - case ast.Call(): - expr.func, deps = self._desugar_expr(expr.func) - expr.args, args_deps = self._desugar_exprs(expr.args) - deps.extend(args_deps) - expr.keywords, keywords_deps = self._desugar_keywords(expr.keywords) - deps.extend(keywords_deps) - - # BinOp(expr left, operator op, expr right) - case ast.BinOp(): - expr.left, deps = self._desugar_expr(expr.left) - expr.right, right_deps = self._desugar_expr(expr.right) - deps.extend(right_deps) - - # UnaryOp(unaryop op, expr operand) - case ast.UnaryOp(): - expr.operand, deps = self._desugar_expr(expr.operand) - - # BoolOp(boolop op, expr* values) - case ast.BoolOp(): - expr.values, deps = self._desugar_exprs(expr.values) - - # Tuple(expr* elts, expr_context ctx) - case ast.Tuple(): - expr.elts, deps = self._desugar_exprs(expr.elts) - is_store = isinstance(expr.ctx, ast.Store) - - # List(expr* elts, expr_context ctx) - case ast.List(): - expr.elts, deps = self._desugar_exprs(expr.elts) - is_store = isinstance(expr.ctx, ast.Store) - - # Set(expr* elts) - case ast.Set(): - expr.elts, deps = self._desugar_exprs(expr.elts) - - # Dict(expr* keys, expr* values) - case ast.Dict(): - for i, key in enumerate(expr.keys): - if key is not None: - key, key_deps = self._desugar_expr(key) - deps.extend(key_deps) - expr.keys[i] = key - expr.values, values_deps = self._desugar_exprs(expr.values) - deps.extend(values_deps) - - # Starred(expr value, expr_context ctx) - case ast.Starred(): - expr.value, deps = self._desugar_expr(expr.value) - is_store = isinstance(expr.ctx, ast.Store) - create_temporary = False - - # Compare(expr left, cmpop* ops, expr* comparators) - case ast.Compare(): - expr.left, deps = self._desugar_expr(expr.left) - expr.comparators, comparators_deps = self._desugar_exprs( - expr.comparators - ) - deps.extend(comparators_deps) - - # NamedExpr(expr target, expr value) - case ast.NamedExpr(): - target = cast(ast.expr, expr.target) # ast.Name - target, deps = self._desugar_expr(target) - expr.target = cast(ast.Name, target) - expr.value, value_deps = self._desugar_expr(expr.value) - deps.extend(value_deps) - - # We need to preserve the assignment so that the target is accessible - # from subsequent expressions/statements. ast.NamedExpr isn't valid as - # a standalone a statement, so we need to convert to ast.Assign. - deps.append(ast.Assign(targets=[expr.target], value=expr.value)) - expr = expr.target - - # Lambda(arguments args, expr body) - case ast.Lambda(): - pass # do not recurse - - # Await(expr value) - case ast.Await(): - expr.value, deps = self._desugar_expr(expr.value) - - # Yield(expr? value) - case ast.Yield(): - if expr.value is not None: - expr.value, deps = self._desugar_expr(expr.value) - - # YieldFrom(expr value) - case ast.YieldFrom(): - expr.value, deps = self._desugar_expr(expr.value) - - # JoinedStr(expr* values) - case ast.JoinedStr(): - expr.values, deps = self._desugar_exprs(expr.values) - - # FormattedValue(expr value, int conversion, expr? format_spec) - case ast.FormattedValue(): - expr.value, deps = self._desugar_expr(expr.value) - # Note: expr.format_spec is an expression, but we do not expect to - # find compound expressions there. - - conversion = expr.conversion - format_spec = expr.format_spec - expr = expr.value - create_temporary = False - - def wrapper(value): - return ast.FormattedValue( - value=value, conversion=conversion, format_spec=format_spec - ) - - # Attribute(expr value, identifier attr, expr_context ctx) - case ast.Attribute(): - expr.value, deps = self._desugar_expr(expr.value) - is_store = isinstance(expr.ctx, ast.Store) - - # Subscript(expr value, expr slice, expr_context ctx) - case ast.Subscript(): - expr.value, deps = self._desugar_expr(expr.value) - expr.slice, slice_deps = self._desugar_expr(expr.slice) - deps.extend(slice_deps) - is_store = isinstance(expr.ctx, ast.Store) - - # Slice(expr? lower, expr? upper, expr? step) - case ast.Slice(): - if expr.lower is not None: - expr.lower, lower_deps = self._desugar_expr(expr.lower) - deps.extend(lower_deps) - if expr.upper is not None: - expr.upper, upper_deps = self._desugar_expr(expr.upper) - deps.extend(upper_deps) - if expr.step is not None: - expr.step, step_deps = self._desugar_expr(expr.step) - deps.extend(step_deps) - is_store = True - - # IfExp(expr test, expr body, expr orelse) - case ast.IfExp(): - tmp = self._new_name() - if_stmt, deps = self._desugar_stmt( - ast.If( - test=expr.test, - body=[ - ast.Assign( - targets=[ast.Name(id=tmp, ctx=ast.Store())], - value=expr.body, - ) - ], - orelse=[ - ast.Assign( - targets=[ast.Name(id=tmp, ctx=ast.Store())], - value=expr.orelse, - ) - ], - ) - ) - deps.append(if_stmt) - expr = ast.Name(id=tmp, ctx=ast.Load()) - create_temporary = False - - # ListComp(expr elt, comprehension* generators) - case ast.ListComp(): - tmp = self._new_name() - - deps = [ - ast.Assign( - targets=[ast.Name(id=tmp, ctx=ast.Store())], - value=ast.List(elts=[], ctx=ast.Load()), - ) - ] - - inner_statement: ast.stmt = ast.Expr( - value=ast.Call( - func=ast.Attribute( - value=ast.Name(id=tmp, ctx=ast.Load()), - attr="append", - ctx=ast.Load(), - ), - args=[expr.elt], - keywords=[], - ) - ) - - deps += self._desugar_comprehensions(expr.generators, inner_statement) - expr = ast.Name(id=tmp, ctx=ast.Load()) - create_temporary = False - - # SetComp(expr elt, comprehension* generators) - case ast.SetComp(): - tmp = self._new_name() - - deps = [ - ast.Assign( - targets=[ast.Name(id=tmp, ctx=ast.Store())], - value=ast.Call( - func=ast.Name(id="set", ctx=ast.Load()), - args=[], - keywords=[], - ), - ) - ] - - inner_statement = ast.Expr( - value=ast.Call( - func=ast.Attribute( - value=ast.Name(id=tmp, ctx=ast.Load()), - attr="add", - ctx=ast.Load(), - ), - args=[expr.elt], - keywords=[], - ) - ) - - deps += self._desugar_comprehensions(expr.generators, inner_statement) - expr = ast.Name(id=tmp, ctx=ast.Load()) - create_temporary = False - - # DictComp(expr key, expr value, comprehension* generators) - case ast.DictComp(): - tmp = self._new_name() - - deps = [ - ast.Assign( - targets=[ast.Name(id=tmp, ctx=ast.Store())], - value=ast.Dict(keys=[], values=[]), - ) - ] - - inner_statement = ast.Assign( - targets=[ - ast.Subscript( - value=ast.Name(id=tmp, ctx=ast.Store()), - slice=expr.key, - ctx=ast.Store(), - ) - ], - value=expr.value, - ) - - deps += self._desugar_comprehensions(expr.generators, inner_statement) - expr = ast.Name(id=tmp, ctx=ast.Load()) - create_temporary = False - - # GeneratorExp(expr elt, comprehension* generators) - case ast.GeneratorExp(): - tmp = self._new_name() - inner_statement = ast.Expr(value=ast.Yield(value=expr.elt)) - body = self._desugar_comprehensions(expr.generators, inner_statement) - deps = [ - ast.FunctionDef( - name=tmp, - args=ast.arguments( - args=[], - posonlyargs=[], - kwonlyargs=[], - kw_defaults=[], - defaults=[], - ), - body=body, - decorator_list=[], - ) - ] - expr = ast.Call( - func=ast.Name(id=tmp, ctx=ast.Load()), args=[], keywords=[] - ) - - case _: - raise NotImplementedError(f"desugar {expr}") - - if create_temporary and not is_store: - tmp = self._new_name() - deps.append( - ast.Assign(targets=[ast.Name(id=tmp, ctx=ast.Store())], value=expr) - ) - expr = ast.Name(id=tmp, ctx=ast.Load()) - - if wrapper is not None: - expr = wrapper(expr) - - return expr, deps - - def _desugar_stmts(self, stmts: list[ast.stmt]) -> list[ast.stmt]: - desugared = [] - for stmt in stmts: - stmt, deps = self._desugar_stmt(stmt) - desugared.extend(deps) - desugared.append(stmt) - return desugared - - def _desugar_exprs( - self, exprs: list[ast.expr], del_stmt=False - ) -> tuple[list[ast.expr], list[ast.stmt]]: - desugared = [] - deps = [] - for expr in exprs: - expr, expr_deps = self._desugar_expr(expr, del_stmt=del_stmt) - deps.extend(expr_deps) - desugared.append(expr) - return desugared, deps - - def _desugar_keywords( - self, keywords: list[ast.keyword] - ) -> tuple[list[ast.keyword], list[ast.stmt]]: - # keyword(identifier? arg, expr value) - desugared = [] - deps = [] - for keyword in keywords: - keyword.value, keyword_deps = self._desugar_expr(keyword.value) - deps.extend(keyword_deps) - desugared.append(keyword) - return desugared, deps - - def _desugar_except_handlers( - self, handlers: list[ast.ExceptHandler] - ) -> tuple[list[ast.ExceptHandler], list[ast.stmt]]: - # excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) - desugared = [] - deps: list[ast.stmt] = [] - for handler in handlers: - if handler.type is not None: - # FIXME: exception type exprs need special handling. Each handler's - # type expr is evaluated one at a time until there's a match. The - # remaining handler's type exprs are not evaluated. - # handler.type, type_deps = self._desugar_expr(handler.type) - # deps.extend(type_deps) - pass - handler.body = self._desugar_stmts(handler.body) - desugared.append(handler) - return desugared, deps - - def _desugar_match_cases( - self, cases: list[ast.match_case] - ) -> tuple[list[ast.match_case], list[ast.stmt]]: - # match_case(pattern pattern, expr? guard, stmt* body) - desugared: list[ast.match_case] = [] - deps: list[ast.stmt] = [] - for case in cases: - if case.guard is not None: - # FIXME: match guards need special handling; they shouldn't be evaluated - # unless the pattern matches. - # case.guard, guard_deps = self._desugar_expr(case.guard) - # deps.extend(guard_deps) - pass - case.body = self._desugar_stmts(case.body) - desugared.append(case) - # You're supposed to be able to pass the AST root to this function - # to have it repair (fill in missing) line numbers and such. It - # seems there's a bug where it doesn't recurse into match cases. - # Work around the issue by manually fixing the match case here. - ast.fix_missing_locations(case) - return desugared, deps - - def _desugar_withitems( - self, withitems: list[ast.withitem] - ) -> tuple[list[ast.withitem], list[ast.stmt]]: - # withitem(expr context_expr, expr? optional_vars) - desugared = [] - deps = [] - for withitem in withitems: - withitem.context_expr, context_expr_deps = self._desugar_expr( - withitem.context_expr - ) - deps.extend(context_expr_deps) - if withitem.optional_vars is not None: - withitem.optional_vars, optional_vars_deps = self._desugar_expr( - withitem.optional_vars - ) - deps.extend(optional_vars_deps) - desugared.append(withitem) - return desugared, deps - - def _desugar_comprehensions( - self, comprehensions: list[ast.comprehension], inner_statement: ast.stmt - ) -> list[ast.stmt]: - # comprehension(expr target, expr iter, expr* ifs, int is_async) - stmt = inner_statement - while comprehensions: - last_for = comprehensions.pop() - while last_for.ifs: - test = last_for.ifs.pop() - stmt = ast.If(test=test, body=[stmt], orelse=[]) - cls = ast.AsyncFor if last_for.is_async else ast.For - stmt = cls( - target=last_for.target, iter=last_for.iter, body=[stmt], orelse=[] - ) - - stmt, deps = self._desugar_stmt(stmt) - return deps + [stmt] - - def _new_name(self) -> str: - name = f"_v{self.name_count}" - self.name_count += 1 - return name - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - desugar_function(fn_def) - - - - - - - Desugar a function to simplify subsequent AST transformations. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - fn_def - - FunctionDef - - - - A function definition. - - - - required - - - - - - - - Returns: - - - -Name Type - Description - - - - -FunctionDef - FunctionDef - - - - The desugared function definition. - - - - - - - - Source code in dispatch/experimental/multicolor/desugar.py - 5 - 6 - 7 - 8 - 9 -10 -11 -12 -13 -14 -15 -16def desugar_function(fn_def: ast.FunctionDef) -> ast.FunctionDef: - """Desugar a function to simplify subsequent AST transformations. - - Args: - fn_def: A function definition. - - Returns: - FunctionDef: The desugared function definition. - """ - fn_def.body = Desugar().desugar(fn_def.body) - ast.fix_missing_locations(fn_def) - return fn_def - - - - - - - - - - - - - - - - - - - - generator - - - - - - - - - - - - - - - - - - - - - - - YieldCounter - - - - - - - - Bases: NodeVisitor - - - AST visitor that walks an ast.FunctionDef to count yield and yield from -statements. -The resulting count can be used to determine if the input function is -a generator or not. -Yields from nested function/class definitions are not counted. - - - Source code in dispatch/experimental/multicolor/generator.py - 16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50class YieldCounter(ast.NodeVisitor): - """AST visitor that walks an ast.FunctionDef to count yield and yield from - statements. - - The resulting count can be used to determine if the input function is - a generator or not. - - Yields from nested function/class definitions are not counted. - """ - - def __init__(self): - self.count = 0 - self.depth = 0 - - def visit_Yield(self, node: ast.Yield): - self.count += 1 - - def visit_YieldFrom(self, node: ast.YieldFrom): - self.count += 1 - - def visit_FunctionDef(self, node: ast.FunctionDef): - self._visit_nested(node) - - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): - self._visit_nested(node) - - def visit_ClassDef(self, node: ast.ClassDef): - self._visit_nested(node) - - def _visit_nested(self, node: ast.stmt): - self.depth += 1 - if self.depth > 1: - return # do not recurse - self.generic_visit(node) - self.depth -= 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - empty_generator() - - - - - - - A generator that yields nothing. -A yield from this generator can be inserted into a function definition in -order to turn the function into a generator, without causing any visible -side effects. - - - Source code in dispatch/experimental/multicolor/generator.py - 53 -54 -55 -56 -57 -58 -59 -60 -61def empty_generator(): - """A generator that yields nothing. - - A `yield from` this generator can be inserted into a function definition in - order to turn the function into a generator, without causing any visible - side effects. - """ - if False: - yield - - - - - - - - - - - - - is_generator(fn_def) - - - - - - - Returns a boolean indicating whether a function is a -generator function. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - fn_def - - FunctionDef - - - - A function definition. - - - - required - - - - - - - Source code in dispatch/experimental/multicolor/generator.py - 4 - 5 - 6 - 7 - 8 - 9 -10 -11 -12 -13def is_generator(fn_def: ast.FunctionDef) -> bool: - """Returns a boolean indicating whether a function is a - generator function. - - Args: - fn_def: A function definition. - """ - yield_counter = YieldCounter() - yield_counter.visit(fn_def) - return yield_counter.count > 0 - - - - - - - - - - - - - - - - - - - - parse - - - - - - - - - - - - - - - - - - - - - - - NoSourceError - - - - - - - - Bases: RuntimeError - - - Function source code is not available. - - - Source code in dispatch/experimental/multicolor/parse.py - 35 -36class NoSourceError(RuntimeError): - """Function source code is not available.""" - - - - - - - - - - - - - - - - parse_function(fn) - - - - - - - Parse an AST from a function. The function source must be available. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - fn - - FunctionType - - - - The function to parse. - - - - required - - - - - - - - Raises: - - - - Type - Description - - - - - - NoSourceError - - - - If the function source cannot be retrieved. - - - - - - - - Source code in dispatch/experimental/multicolor/parse.py - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32def parse_function(fn: FunctionType) -> tuple[ast.Module, ast.FunctionDef]: - """Parse an AST from a function. The function source must be available. - - Args: - fn: The function to parse. - - Raises: - NoSourceError: If the function source cannot be retrieved. - """ - try: - src = inspect.getsource(fn) - except TypeError as e: - # The source is not always available. For example, the function - # may be defined in a C extension, or may be a builtin function. - raise NoSourceError from e - except OSError as e: - raise NoSourceError from e - - try: - module = ast.parse(src) - except IndentationError: - module = ast.parse(textwrap.dedent(src)) - - fn_def = cast(ast.FunctionDef, module.body[0]) - return module, fn_def - - - - - - - - - - - - - - - - - - - - template - - - - - - - - - - - - - - - - - - - - - - - NameTransformer - - - - - - - - Bases: NodeTransformer - - - Replace ast.Name nodes in an AST. - - - Source code in dispatch/experimental/multicolor/template.py - 23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50class NameTransformer(ast.NodeTransformer): - """Replace ast.Name nodes in an AST.""" - - exprs: dict[str, ast.expr] - stmts: dict[str, ast.stmt] - - def __init__(self, **replacements: ast.expr | ast.stmt): - self.exprs = {} - self.stmts = {} - for key, node in replacements.items(): - if isinstance(node, ast.expr): - self.exprs[key] = node - elif isinstance(node, ast.stmt): - self.stmts[key] = node - - def visit_Name(self, node: ast.Name) -> ast.expr: - try: - return self.exprs[node.id] - except KeyError: - return node - - def visit_Expr(self, node: ast.Expr) -> ast.stmt: - if not isinstance(node.value, ast.Name): - return node - try: - return self.stmts[node.value.id] - except KeyError: - return node - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - rewrite_template(template, **replacements) - - - - - - - Create an AST by parsing a template string and then replacing -embedded identifiers with the provided AST nodes. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - template - - str - - - - String containing source code (one or more statements). - - - - required - - - - **replacements - - expr | stmt - - - - Dictionary mapping identifiers to replacement nodes. - - - - {} - - - - - - - - Returns: - - - - Type - Description - - - - - - list[stmt] - - - - list[ast.stmt]: List of AST statements. - - - - - - - - Source code in dispatch/experimental/multicolor/template.py - 5 - 6 - 7 - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20def rewrite_template( - template: str, **replacements: ast.expr | ast.stmt -) -> list[ast.stmt]: - """Create an AST by parsing a template string and then replacing - embedded identifiers with the provided AST nodes. - - Args: - template: String containing source code (one or more statements). - **replacements: Dictionary mapping identifiers to replacement nodes. - - Returns: - list[ast.stmt]: List of AST statements. - """ - root = ast.parse(textwrap.dedent(template)) - root = NameTransformer(**replacements).visit(root) - return root.body - - - - - - - - - - - - - - - - - - - - yields - - - - - - - - - - - - - - - - - - - - - - - CustomYield - - - - dataclass - - - - - - - - Bases: YieldType - - - A yield from a function marked with @yields. - - - - Attributes: - - - - Name - Type - Description - - - - - type - - Any - - - - The type of yield that was specified in the @yields decorator. - - - - - args - - list[Any] - - - - Positional arguments to the function call. - - - - - kwargs - - dict[str, Any] | None - - - - Keyword arguments to the function call. - - - - - - - - Source code in dispatch/experimental/multicolor/yields.py - 31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51@dataclass -class CustomYield(YieldType): - """A yield from a function marked with @yields. - - Attributes: - type: The type of yield that was specified in the @yields decorator. - args: Positional arguments to the function call. - kwargs: Keyword arguments to the function call. - """ - - type: Any - args: list[Any] - kwargs: dict[str, Any] | None = None - - def kwarg(self, name, pos) -> Any: - if self.kwargs is None: - return self.args[pos] - try: - return self.kwargs[name] - except KeyError: - return self.args[pos] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - GeneratorYield - - - - dataclass - - - - - - - - Bases: YieldType - - - A yield from a generator. - - - - Attributes: - - - - Name - Type - Description - - - - - value - - Any - - - - The value that was yielded from the generator. - - - - - - - - Source code in dispatch/experimental/multicolor/yields.py - 54 -55 -56 -57 -58 -59 -60 -61 -62@dataclass -class GeneratorYield(YieldType): - """A yield from a generator. - - Attributes: - value: The value that was yielded from the generator. - """ - - value: Any = None - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - YieldType - - - - - - - - - Base class for yield types. - - - Source code in dispatch/experimental/multicolor/yields.py - 27 -28class YieldType: - """Base class for yield types.""" - - - - - - - - - - - - - - - - no_yields(fn) - - - - - - - Decorator that hints that a function (and anything called -recursively) does not yield. - - - Source code in dispatch/experimental/multicolor/yields.py - 20 -21 -22 -23 -24def no_yields(fn): - """Decorator that hints that a function (and anything called - recursively) does not yield.""" - fn._multicolor_no_yields = True # type: ignore[attr-defined] - return fn - - - - - - - - - - - - - yields(type) - - - - - - - Returns a decorator that marks functions as a type of yield. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - type - - Any - - - - Opaque type for this yield. - - - - required - - - - - - - Source code in dispatch/experimental/multicolor/yields.py - 6 - 7 - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17def yields(type: Any): - """Returns a decorator that marks functions as a type of yield. - - Args: - type: Opaque type for this yield. - """ - - def decorator(fn: FunctionType) -> FunctionType: - fn._multicolor_yield_type = type # type: ignore[attr-defined] - return fn - - return decorator - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - fastapi - - - - - - - Integration of Dispatch programmable endpoints for FastAPI. -Example: -import fastapi -from dispatch.fastapi import Dispatch - -app = fastapi.FastAPI() -dispatch = Dispatch(app, api_key="test-key") - -@dispatch.function() -def my_function(): - return "Hello World!" - -@app.get("/") -def read_root(): - dispatch.call(my_function) - - - - - - - - - - - - - - - - - - - Dispatch - - - - - - - - Bases: Registry - - - A Dispatch programmable endpoint, powered by FastAPI. - - - Source code in dispatch/fastapi.py - 47 - 48 - 49 - 50 - 51 - 52 - 53 - 54 - 55 - 56 - 57 - 58 - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135class Dispatch(Registry): - """A Dispatch programmable endpoint, powered by FastAPI.""" - - def __init__( - self, - app: fastapi.FastAPI, - endpoint: str | None = None, - verification_key: Ed25519PublicKey | None = None, - api_key: str | None = None, - api_url: str | None = None, - ): - """Initialize a Dispatch endpoint, and integrate it into a FastAPI app. - - It mounts a sub-app that implements the Dispatch gRPC interface. - - Args: - app: The FastAPI app to configure. - - endpoint: Full URL of the application the Dispatch programmable - endpoint will be running on. Uses the value of the - DISPATCH_ENDPOINT_URL environment variable by default. - - verification_key: Key to use when verifying signed requests. Uses - the value of the DISPATCH_VERIFICATION_KEY environment variable - by default. The environment variable is expected to carry an - Ed25519 public key in base64 or PEM format. - If not set, request signature verification is disabled (a warning - will be logged by the constructor). - - api_key: Dispatch API key to use for authentication. Uses the value of - the DISPATCH_API_KEY environment variable by default. - - api_url: The URL of the Dispatch API to use. Uses the value of the - DISPATCH_API_URL environment variable if set, otherwise - defaults to the public Dispatch API (DEFAULT_API_URL). - - Raises: - ValueError: If any of the required arguments are missing. - """ - if not app: - raise ValueError( - "missing FastAPI app as first argument of the Dispatch constructor" - ) - - endpoint_from = "endpoint argument" - if not endpoint: - endpoint = os.getenv("DISPATCH_ENDPOINT_URL") - endpoint_from = "DISPATCH_ENDPOINT_URL" - if not endpoint: - raise ValueError( - "missing application endpoint: set it with the DISPATCH_ENDPOINT_URL environment variable" - ) - - if not verification_key: - try: - verification_key_raw = os.environ["DISPATCH_VERIFICATION_KEY"] - except KeyError: - pass - else: - # Be forgiving when accepting keys in PEM format. - verification_key_raw = verification_key_raw.replace("\\n", "\n") - try: - verification_key = public_key_from_pem(verification_key_raw) - except ValueError: - verification_key = public_key_from_bytes( - base64.b64decode(verification_key_raw) - ) - - logger.info("configuring Dispatch endpoint %s", endpoint) - - parsed_url = _urlparse.urlparse(endpoint) - if not parsed_url.netloc or not parsed_url.scheme: - raise ValueError( - f"{endpoint_from} must be a full URL with protocol and domain (e.g., https://example.com)" - ) - - if verification_key: - base64_key = base64.b64encode(verification_key.public_bytes_raw()).decode() - logger.info("verifying request signatures using key %s", base64_key) - else: - logger.warning( - "request verification is disabled because DISPATCH_VERIFICATION_KEY is not set" - ) - - client = Client(api_key=api_key, api_url=api_url) - super().__init__(endpoint, client) - - function_service = _new_app(self, verification_key) - app.mount("/dispatch.sdk.v1.FunctionService", function_service) - - - - - - - - - - - - - - - - - - - - - - __init__(app, endpoint=None, verification_key=None, api_key=None, api_url=None) - - - - - - - Initialize a Dispatch endpoint, and integrate it into a FastAPI app. -It mounts a sub-app that implements the Dispatch gRPC interface. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - app - - FastAPI - - - - The FastAPI app to configure. - - - - required - - - - endpoint - - str | None - - - - Full URL of the application the Dispatch programmable -endpoint will be running on. Uses the value of the -DISPATCH_ENDPOINT_URL environment variable by default. - - - - None - - - - verification_key - - Ed25519PublicKey | None - - - - Key to use when verifying signed requests. Uses -the value of the DISPATCH_VERIFICATION_KEY environment variable -by default. The environment variable is expected to carry an -Ed25519 public key in base64 or PEM format. -If not set, request signature verification is disabled (a warning -will be logged by the constructor). - - - - None - - - - api_key - - str | None - - - - Dispatch API key to use for authentication. Uses the value of -the DISPATCH_API_KEY environment variable by default. - - - - None - - - - api_url - - str | None - - - - The URL of the Dispatch API to use. Uses the value of the -DISPATCH_API_URL environment variable if set, otherwise -defaults to the public Dispatch API (DEFAULT_API_URL). - - - - None - - - - - - - - Raises: - - - - Type - Description - - - - - - ValueError - - - - If any of the required arguments are missing. - - - - - - - - Source code in dispatch/fastapi.py - 50 - 51 - 52 - 53 - 54 - 55 - 56 - 57 - 58 - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135def __init__( - self, - app: fastapi.FastAPI, - endpoint: str | None = None, - verification_key: Ed25519PublicKey | None = None, - api_key: str | None = None, - api_url: str | None = None, -): - """Initialize a Dispatch endpoint, and integrate it into a FastAPI app. - - It mounts a sub-app that implements the Dispatch gRPC interface. - - Args: - app: The FastAPI app to configure. - - endpoint: Full URL of the application the Dispatch programmable - endpoint will be running on. Uses the value of the - DISPATCH_ENDPOINT_URL environment variable by default. - - verification_key: Key to use when verifying signed requests. Uses - the value of the DISPATCH_VERIFICATION_KEY environment variable - by default. The environment variable is expected to carry an - Ed25519 public key in base64 or PEM format. - If not set, request signature verification is disabled (a warning - will be logged by the constructor). - - api_key: Dispatch API key to use for authentication. Uses the value of - the DISPATCH_API_KEY environment variable by default. - - api_url: The URL of the Dispatch API to use. Uses the value of the - DISPATCH_API_URL environment variable if set, otherwise - defaults to the public Dispatch API (DEFAULT_API_URL). - - Raises: - ValueError: If any of the required arguments are missing. - """ - if not app: - raise ValueError( - "missing FastAPI app as first argument of the Dispatch constructor" - ) - - endpoint_from = "endpoint argument" - if not endpoint: - endpoint = os.getenv("DISPATCH_ENDPOINT_URL") - endpoint_from = "DISPATCH_ENDPOINT_URL" - if not endpoint: - raise ValueError( - "missing application endpoint: set it with the DISPATCH_ENDPOINT_URL environment variable" - ) - - if not verification_key: - try: - verification_key_raw = os.environ["DISPATCH_VERIFICATION_KEY"] - except KeyError: - pass - else: - # Be forgiving when accepting keys in PEM format. - verification_key_raw = verification_key_raw.replace("\\n", "\n") - try: - verification_key = public_key_from_pem(verification_key_raw) - except ValueError: - verification_key = public_key_from_bytes( - base64.b64decode(verification_key_raw) - ) - - logger.info("configuring Dispatch endpoint %s", endpoint) - - parsed_url = _urlparse.urlparse(endpoint) - if not parsed_url.netloc or not parsed_url.scheme: - raise ValueError( - f"{endpoint_from} must be a full URL with protocol and domain (e.g., https://example.com)" - ) - - if verification_key: - base64_key = base64.b64encode(verification_key.public_bytes_raw()).decode() - logger.info("verifying request signatures using key %s", base64_key) - else: - logger.warning( - "request verification is disabled because DISPATCH_VERIFICATION_KEY is not set" - ) - - client = Client(api_key=api_key, api_url=api_url) - super().__init__(endpoint, client) - - function_service = _new_app(self, verification_key) - app.mount("/dispatch.sdk.v1.FunctionService", function_service) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function - - - - - - - - - - - - - - - - - - - - - - PrimitiveFunctionType: TypeAlias = Callable[[Input], Output] - - - module-attribute - - - - - - - - A primitive function is a function that accepts a dispatch.function.Input -and unconditionally returns a dispatch.function.Output. It must not raise -exceptions. - - - - - - - - - - - Function - - - - - - - - - Callable wrapper around a function meant to be used throughout the -Dispatch Python SDK. - - - Source code in dispatch/function.py - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 39 - 40 - 41 - 42 - 43 - 44 - 45 - 46 - 47 - 48 - 49 - 50 - 51 - 52 - 53 - 54 - 55 - 56 - 57 - 58 - 59 - 60 - 61 - 62 - 63 - 64 - 65 - 66 - 67 - 68 - 69 - 70 - 71 - 72 - 73 - 74 - 75 - 76 - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124class Function: - """Callable wrapper around a function meant to be used throughout the - Dispatch Python SDK. - """ - - def __init__( - self, - endpoint: str, - client: Client | None, - name: str, - func: Callable[[Input], Output], - ): - self._endpoint = endpoint - self._client = client - self._name = name - self._func = func - - def __call__(self, *args, **kwargs): - return self._func(*args, **kwargs) - - @property - def endpoint(self) -> str: - return self._endpoint - - @property - def name(self) -> str: - return self._name - - def dispatch(self, *args, **kwargs) -> DispatchID: - """Dispatch a call to the function. - - The Registry this function was registered with must be initialized - with a Client / api_key for this call facility to be available. - - Args: - *args: Positional arguments for the function. - **kwargs: Keyword arguments for the function. - - Returns: - DispatchID: ID of the dispatched call. - - Raises: - RuntimeError: if a Dispatch client has not been configured. - """ - return self.primitive_dispatch(_Arguments(list(args), kwargs)) - - def primitive_dispatch(self, input: Any = None) -> DispatchID: - """Dispatch a primitive call. - - The Registry this function was registered with must be initialized - with a Client / api_key for this call facility to be available. - - Args: - input: Input to the function. - - Returns: - DispatchID: ID of the dispatched call. - - Raises: - RuntimeError: if a Dispatch client has not been configured. - """ - if self._client is None: - raise RuntimeError( - "Dispatch Client has not been configured (api_key not provided)" - ) - - [dispatch_id] = self._client.dispatch([self.primitive_call_with(input)]) - return dispatch_id - - def call_with(self, *args, correlation_id: int | None = None, **kwargs) -> Call: - """Create a Call for this function with the provided input. Useful to - generate calls when polling. - - Args: - *args: Positional arguments for the function. - correlation_id: optional arbitrary integer the caller can use to - match this call to a call result. - **kwargs: Keyword arguments for the function. - - Returns: - Call: can be passed to Output.poll(). - """ - return self.primitive_call_with( - _Arguments(list(args), kwargs), correlation_id=correlation_id - ) - - def primitive_call_with( - self, input: Any, correlation_id: int | None = None - ) -> Call: - """Create a Call for this function with the provided input. Useful to - generate calls when polling. - - Args: - input: any pickle-able Python value that will be passed as input to - this function. - correlation_id: optional arbitrary integer the caller can use to - match this call to a call result. - - Returns: - Call: can be passed to Output.poll(). - """ - return Call( - correlation_id=correlation_id, - endpoint=self.endpoint, - function=self.name, - input=input, - ) - - - - - - - - - - - - - - - - - - - - - - call_with(*args, correlation_id=None, **kwargs) - - - - - - - Create a Call for this function with the provided input. Useful to -generate calls when polling. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - *args - - - - - Positional arguments for the function. - - - - () - - - - correlation_id - - int | None - - - - optional arbitrary integer the caller can use to -match this call to a call result. - - - - None - - - - **kwargs - - - - - Keyword arguments for the function. - - - - {} - - - - - - - - Returns: - - - -Name Type - Description - - - - -Call - Call - - - - can be passed to Output.poll(). - - - - - - - - Source code in dispatch/function.py - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102def call_with(self, *args, correlation_id: int | None = None, **kwargs) -> Call: - """Create a Call for this function with the provided input. Useful to - generate calls when polling. - - Args: - *args: Positional arguments for the function. - correlation_id: optional arbitrary integer the caller can use to - match this call to a call result. - **kwargs: Keyword arguments for the function. - - Returns: - Call: can be passed to Output.poll(). - """ - return self.primitive_call_with( - _Arguments(list(args), kwargs), correlation_id=correlation_id - ) - - - - - - - - - - - - - dispatch(*args, **kwargs) - - - - - - - Dispatch a call to the function. -The Registry this function was registered with must be initialized -with a Client / api_key for this call facility to be available. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - *args - - - - - Positional arguments for the function. - - - - () - - - - **kwargs - - - - - Keyword arguments for the function. - - - - {} - - - - - - - - Returns: - - - -Name Type - Description - - - - -DispatchID - DispatchID - - - - ID of the dispatched call. - - - - - - - - - Raises: - - - - Type - Description - - - - - - RuntimeError - - - - if a Dispatch client has not been configured. - - - - - - - - Source code in dispatch/function.py - 46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62def dispatch(self, *args, **kwargs) -> DispatchID: - """Dispatch a call to the function. - - The Registry this function was registered with must be initialized - with a Client / api_key for this call facility to be available. - - Args: - *args: Positional arguments for the function. - **kwargs: Keyword arguments for the function. - - Returns: - DispatchID: ID of the dispatched call. - - Raises: - RuntimeError: if a Dispatch client has not been configured. - """ - return self.primitive_dispatch(_Arguments(list(args), kwargs)) - - - - - - - - - - - - - primitive_call_with(input, correlation_id=None) - - - - - - - Create a Call for this function with the provided input. Useful to -generate calls when polling. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - input - - Any - - - - any pickle-able Python value that will be passed as input to -this function. - - - - required - - - - correlation_id - - int | None - - - - optional arbitrary integer the caller can use to -match this call to a call result. - - - - None - - - - - - - - Returns: - - - -Name Type - Description - - - - -Call - Call - - - - can be passed to Output.poll(). - - - - - - - - Source code in dispatch/function.py - 104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124def primitive_call_with( - self, input: Any, correlation_id: int | None = None -) -> Call: - """Create a Call for this function with the provided input. Useful to - generate calls when polling. - - Args: - input: any pickle-able Python value that will be passed as input to - this function. - correlation_id: optional arbitrary integer the caller can use to - match this call to a call result. - - Returns: - Call: can be passed to Output.poll(). - """ - return Call( - correlation_id=correlation_id, - endpoint=self.endpoint, - function=self.name, - input=input, - ) - - - - - - - - - - - - - primitive_dispatch(input=None) - - - - - - - Dispatch a primitive call. -The Registry this function was registered with must be initialized -with a Client / api_key for this call facility to be available. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - input - - Any - - - - Input to the function. - - - - None - - - - - - - - Returns: - - - -Name Type - Description - - - - -DispatchID - DispatchID - - - - ID of the dispatched call. - - - - - - - - - Raises: - - - - Type - Description - - - - - - RuntimeError - - - - if a Dispatch client has not been configured. - - - - - - - - Source code in dispatch/function.py - 64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85def primitive_dispatch(self, input: Any = None) -> DispatchID: - """Dispatch a primitive call. - - The Registry this function was registered with must be initialized - with a Client / api_key for this call facility to be available. - - Args: - input: Input to the function. - - Returns: - DispatchID: ID of the dispatched call. - - Raises: - RuntimeError: if a Dispatch client has not been configured. - """ - if self._client is None: - raise RuntimeError( - "Dispatch Client has not been configured (api_key not provided)" - ) - - [dispatch_id] = self._client.dispatch([self.primitive_call_with(input)]) - return dispatch_id - - - - - - - - - - - - - - - - - - - - - Registry - - - - - - - - - Registry of local functions. - - - Source code in dispatch/function.py - 134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255class Registry: - """Registry of local functions.""" - - def __init__(self, endpoint: str, client: Client | None): - """Initialize a local function registry. - - Args: - endpoint: URL of the endpoint that the function is accessible from. - client: Optional client for the Dispatch API. If provided, calls - to local functions can be dispatched directly. - """ - self._functions: Dict[str, Function] = {} - self._endpoint = endpoint - self._client = client - - def function(self) -> Callable[[FunctionType], Function]: - """Returns a decorator that registers functions.""" - - # Note: the indirection here means that we can add parameters - # to the decorator later without breaking existing apps. - return self._register_function - - def coroutine(self) -> Callable[[FunctionType], Function | FunctionType]: - """Returns a decorator that registers coroutine functions.""" - - # Note: the indirection here means that we can add parameters - # to the decorator later without breaking existing apps. - return self._register_coroutine - - def primitive_function(self) -> Callable[[PrimitiveFunctionType], Function]: - """Returns a decorator that registers primitive functions.""" - - # Note: the indirection here means that we can add parameters - # to the decorator later without breaking existing apps. - return self._register_primitive_function - - def _register_function(self, func: FunctionType) -> Function: - """Register a function with the Dispatch programmable endpoints. - - Args: - func: The function to register. - - Returns: - Function: A registered Dispatch Function. - - Raises: - ValueError: If the function is already registered. - """ - if inspect.iscoroutinefunction(func): - raise TypeError( - "async functions must be registered via @dispatch.coroutine" - ) - - @functools.wraps(func) - def primitive_func(input: Input) -> Output: - try: - try: - args, kwargs = input.input_arguments() - except ValueError: - raise ValueError("incorrect input for function") - raw_output = func(*args, **kwargs) - except Exception as e: - logger.exception( - f"@dispatch.function: '{func.__name__}' raised an exception" - ) - return Output.error(Error.from_exception(e)) - else: - return Output.value(raw_output) - - return self._register_primitive_function(primitive_func) - - def _register_coroutine(self, func: FunctionType) -> Function: - """(EXPERIMENTAL) Register a coroutine function with the Dispatch - programmable endpoints. - - The function is compiled into a durable coroutine. - - The coroutine can use directives such as poll() partway through - execution. The coroutine will be suspended at these yield points, - and will resume execution from the same point when results are - available. The state of the coroutine is stored durably across - yield points. - - Args: - func: The coroutine to register. - - Returns: - Function: A registered Dispatch Function. - - Raises: - ValueError: If the function is already registered. - """ - if not inspect.iscoroutinefunction(func): - raise TypeError(f"{func.__qualname__} must be an async function") - - durable_func = durable(func) - - @functools.wraps(func) - def primitive_func(input: Input) -> Output: - return schedule(durable_func, input) - - return self._register_primitive_function(primitive_func) - - def _register_primitive_function(self, func: PrimitiveFunctionType) -> Function: - """Register a primitive function with the Dispatch programmable endpoints. - - Args: - func: The function to register. - - Returns: - Function: A registered Dispatch Function. - - Raises: - ValueError: If the function is already registered. - """ - name = func.__qualname__ - logger.info("registering function '%s'", name) - if name in self._functions: - raise ValueError(f"Function {name} already registered") - wrapped_func = Function(self._endpoint, self._client, name, func) - self._functions[name] = wrapped_func - return wrapped_func - - - - - - - - - - - - - - - - - - - - - - __init__(endpoint, client) - - - - - - - Initialize a local function registry. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - endpoint - - str - - - - URL of the endpoint that the function is accessible from. - - - - required - - - - client - - Client | None - - - - Optional client for the Dispatch API. If provided, calls -to local functions can be dispatched directly. - - - - required - - - - - - - Source code in dispatch/function.py - 137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147def __init__(self, endpoint: str, client: Client | None): - """Initialize a local function registry. - - Args: - endpoint: URL of the endpoint that the function is accessible from. - client: Optional client for the Dispatch API. If provided, calls - to local functions can be dispatched directly. - """ - self._functions: Dict[str, Function] = {} - self._endpoint = endpoint - self._client = client - - - - - - - - - - - - - coroutine() - - - - - - - Returns a decorator that registers coroutine functions. - - - Source code in dispatch/function.py - 156 -157 -158 -159 -160 -161def coroutine(self) -> Callable[[FunctionType], Function | FunctionType]: - """Returns a decorator that registers coroutine functions.""" - - # Note: the indirection here means that we can add parameters - # to the decorator later without breaking existing apps. - return self._register_coroutine - - - - - - - - - - - - - function() - - - - - - - Returns a decorator that registers functions. - - - Source code in dispatch/function.py - 149 -150 -151 -152 -153 -154def function(self) -> Callable[[FunctionType], Function]: - """Returns a decorator that registers functions.""" - - # Note: the indirection here means that we can add parameters - # to the decorator later without breaking existing apps. - return self._register_function - - - - - - - - - - - - - primitive_function() - - - - - - - Returns a decorator that registers primitive functions. - - - Source code in dispatch/function.py - 163 -164 -165 -166 -167 -168def primitive_function(self) -> Callable[[PrimitiveFunctionType], Function]: - """Returns a decorator that registers primitive functions.""" - - # Note: the indirection here means that we can add parameters - # to the decorator later without breaking existing apps. - return self._register_primitive_function - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - id - - - - - - - - - - - - - - - - - - - - - - DispatchID: TypeAlias = str - - - module-attribute - - - - - - - - Unique identifier in Dispatch. -It should be treated as an opaque value. - - - - - - - - - - - - - - - - - - - - integrations - - - - - - - - - - - - - - - - - - - - - - - - - http - - - - - - - - - - - - - - - - - - - - - - - - - http_response_code_status(code) - - - - - - - Returns a Status that's broadly equivalent to an HTTP response -status code. - - - Source code in dispatch/integrations/http.py - 4 - 5 - 6 - 7 - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26def http_response_code_status(code: int) -> Status: - """Returns a Status that's broadly equivalent to an HTTP response - status code.""" - match code: - case 429: # Too Many Requests - return Status.THROTTLED - case 501: # Not Implemented - return Status.PERMANENT_ERROR - - category = code // 100 - match category: - case 1: # 1xx informational - return Status.PERMANENT_ERROR - case 2: # 2xx success - return Status.OK - case 3: # 3xx redirection - return Status.PERMANENT_ERROR - case 4: # 4xx client error - return Status.PERMANENT_ERROR - case 5: # 5xx server error - return Status.TEMPORARY_ERROR - - return Status.UNSPECIFIED - - - - - - - - - - - - - - - - - - - - httpx - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - requests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - proto - - - - - - - - - - - - - - - - - - - - - - - Call - - - - dataclass - - - - - - - - - Instruction to call a function. -Though this class can be built manually, it is recommended to use the -with_call method of a Function instead. - - - Source code in dispatch/proto.py - 178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198@dataclass -class Call: - """Instruction to call a function. - - Though this class can be built manually, it is recommended to use the - with_call method of a Function instead. - """ - - function: str - input: Any - endpoint: str | None = None - correlation_id: int | None = None - - def _as_proto(self) -> call_pb.Call: - input_bytes = _pb_any_pickle(self.input) - return call_pb.Call( - correlation_id=self.correlation_id, - endpoint=self.endpoint, - function=self.function, - input=input_bytes, - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CallResult - - - - dataclass - - - - - - - - - Result of a Call. - - - Source code in dispatch/proto.py - 201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240@dataclass -class CallResult: - """Result of a Call.""" - - correlation_id: int | None = None - output: Any | None = None - error: Error | None = None - - def _as_proto(self) -> call_pb.CallResult: - output_any = None - error_proto = None - if self.output is not None: - output_any = _pb_any_pickle(self.output) - if self.error is not None: - error_proto = self.error._as_proto() - - return call_pb.CallResult( - correlation_id=self.correlation_id, output=output_any, error=error_proto - ) - - @classmethod - def _from_proto(cls, proto: call_pb.CallResult) -> CallResult: - output = None - error = None - if proto.HasField("output"): - output = _any_unpickle(proto.output) - if proto.HasField("error"): - error = Error._from_proto(proto.error) - - return CallResult( - correlation_id=proto.correlation_id, output=output, error=error - ) - - @classmethod - def from_value(cls, output: Any, correlation_id: int | None = None) -> CallResult: - return CallResult(correlation_id=correlation_id, output=output) - - @classmethod - def from_error(cls, error: Error, correlation_id: int | None = None) -> CallResult: - return CallResult(correlation_id=correlation_id, error=error) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Error - - - - - - - - - Error when running a function. -This is not a Python exception, but potentially part of a CallResult or -Output. - - - Source code in dispatch/proto.py - 243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290class Error: - """Error when running a function. - - This is not a Python exception, but potentially part of a CallResult or - Output. - """ - - def __init__(self, status: Status, type: str | None, message: str | None): - """Create a new Error. - - Args: - status: categorization of the error. - type: arbitrary string, used for humans. Optional. - message: arbitrary message. Optional. - - Raises: - ValueError: Neither type or message was provided or status is - invalid. - """ - if type is None and message is None: - raise ValueError("At least one of type or message is required") - if status is Status.OK: - raise ValueError("Status cannot be OK") - - self.type = type - self.message = message - self.status = status - - @classmethod - def from_exception(cls, ex: Exception, status: Status | None = None) -> Error: - """Create an Error from a Python exception, using its class qualified - named as type. - - The status tries to be inferred, but can be overridden. If it is not - provided or cannot be inferred, it defaults to TEMPORARY_ERROR. - """ - - if status is None: - status = status_for_error(ex) - - return Error(status, ex.__class__.__qualname__, str(ex)) - - @classmethod - def _from_proto(cls, proto: error_pb.Error) -> Error: - return cls(Status.UNSPECIFIED, proto.type, proto.message) - - def _as_proto(self) -> error_pb.Error: - return error_pb.Error(type=self.type, message=self.message) - - - - - - - - - - - - - - - - - - - - - - __init__(status, type, message) - - - - - - - Create a new Error. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - status - - Status - - - - categorization of the error. - - - - required - - - - type - - str | None - - - - arbitrary string, used for humans. Optional. - - - - required - - - - message - - str | None - - - - arbitrary message. Optional. - - - - required - - - - - - - - Raises: - - - - Type - Description - - - - - - ValueError - - - - Neither type or message was provided or status is -invalid. - - - - - - - - Source code in dispatch/proto.py - 250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269def __init__(self, status: Status, type: str | None, message: str | None): - """Create a new Error. - - Args: - status: categorization of the error. - type: arbitrary string, used for humans. Optional. - message: arbitrary message. Optional. - - Raises: - ValueError: Neither type or message was provided or status is - invalid. - """ - if type is None and message is None: - raise ValueError("At least one of type or message is required") - if status is Status.OK: - raise ValueError("Status cannot be OK") - - self.type = type - self.message = message - self.status = status - - - - - - - - - - - - - from_exception(ex, status=None) - - - classmethod - - - - - - - - Create an Error from a Python exception, using its class qualified -named as type. -The status tries to be inferred, but can be overridden. If it is not -provided or cannot be inferred, it defaults to TEMPORARY_ERROR. - - - Source code in dispatch/proto.py - 271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283@classmethod -def from_exception(cls, ex: Exception, status: Status | None = None) -> Error: - """Create an Error from a Python exception, using its class qualified - named as type. - - The status tries to be inferred, but can be overridden. If it is not - provided or cannot be inferred, it defaults to TEMPORARY_ERROR. - """ - - if status is None: - status = status_for_error(ex) - - return Error(status, ex.__class__.__qualname__, str(ex)) - - - - - - - - - - - - - - - - - - - - - Input - - - - - - - - - The input to a primitive function. -Functions always take a single argument of type Input. When the function is -run for the first time, it receives the input. When the function is a coroutine -that's resuming after a yield point, it receives the results of the yield -directive. Use the is_first_call and is_resume properties to differentiate -between the two cases. -This class is intended to be used as read-only. - - - Source code in dispatch/proto.py - 24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89class Input: - """The input to a primitive function. - - Functions always take a single argument of type Input. When the function is - run for the first time, it receives the input. When the function is a coroutine - that's resuming after a yield point, it receives the results of the yield - directive. Use the is_first_call and is_resume properties to differentiate - between the two cases. - - This class is intended to be used as read-only. - """ - - def __init__(self, req: function_pb.RunRequest): - self._has_input = req.HasField("input") - if self._has_input: - input_pb = google.protobuf.wrappers_pb2.BytesValue() - req.input.Unpack(input_pb) - input_bytes = input_pb.value - self._input = pickle.loads(input_bytes) - else: - state_bytes = req.poll_result.coroutine_state - if len(state_bytes) > 0: - self._coroutine_state = pickle.loads(state_bytes) - else: - self._coroutine_state = None - self._call_results = [ - CallResult._from_proto(r) for r in req.poll_result.results - ] - - @property - def is_first_call(self) -> bool: - return self._has_input - - @property - def is_resume(self) -> bool: - return not self.is_first_call - - @property - def input(self) -> Any: - self._assert_first_call() - return self._input - - def input_arguments(self) -> tuple[list[Any], dict[str, Any]]: - """Returns positional and keyword arguments carried by the input.""" - self._assert_first_call() - if not isinstance(self._input, _Arguments): - raise RuntimeError("input does not hold arguments") - return self._input.args, self._input.kwargs - - @property - def coroutine_state(self) -> Any: - self._assert_resume() - return self._coroutine_state - - @property - def call_results(self) -> list[CallResult]: - self._assert_resume() - return self._call_results - - def _assert_first_call(self): - if self.is_resume: - raise ValueError("This input is for a resumed coroutine") - - def _assert_resume(self): - if self.is_first_call: - raise ValueError("This input is for a first function call") - - - - - - - - - - - - - - - - - - - - - - input_arguments() - - - - - - - Returns positional and keyword arguments carried by the input. - - - Source code in dispatch/proto.py - 66 -67 -68 -69 -70 -71def input_arguments(self) -> tuple[list[Any], dict[str, Any]]: - """Returns positional and keyword arguments carried by the input.""" - self._assert_first_call() - if not isinstance(self._input, _Arguments): - raise RuntimeError("input does not hold arguments") - return self._input.args, self._input.kwargs - - - - - - - - - - - - - - - - - - - - - Output - - - - - - - - - The output of a primitive function. -This class is meant to be instantiated and returned by authors of functions -to indicate the follow up action they need to take. Use the various class -methods create an instance of this class. For example Output.value() or -Output.poll(). - - - Source code in dispatch/proto.py - 100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169class Output: - """The output of a primitive function. - - This class is meant to be instantiated and returned by authors of functions - to indicate the follow up action they need to take. Use the various class - methods create an instance of this class. For example Output.value() or - Output.poll(). - """ - - def __init__(self, proto: function_pb.RunResponse): - self._message = proto - - @classmethod - def value(cls, value: Any, status: Status | None = None) -> Output: - """Terminally exit the function with the provided return value.""" - if status is None: - status = status_for_output(value) - return cls.exit(result=CallResult.from_value(value), status=status) - - @classmethod - def error(cls, error: Error) -> Output: - """Terminally exit the function with the provided error.""" - return cls.exit(result=CallResult.from_error(error), status=error.status) - - @classmethod - def tail_call(cls, tail_call: Call) -> Output: - """Terminally exit the function, and instruct the orchestrator to - tail call the specified function.""" - return cls.exit(tail_call=tail_call) - - @classmethod - def exit( - cls, - result: CallResult | None = None, - tail_call: Call | None = None, - status: Status = Status.OK, - ) -> Output: - """Terminally exit the function.""" - result_proto = result._as_proto() if result else None - tail_call_proto = tail_call._as_proto() if tail_call else None - return Output( - function_pb.RunResponse( - status=status._proto, - exit=exit_pb.Exit(result=result_proto, tail_call=tail_call_proto), - ) - ) - - @classmethod - def poll(cls, state: Any, calls: None | list[Call] = None) -> Output: - """Suspend the function with a set of Calls, instructing the - orchestrator to resume the function with the provided state when - call results are ready.""" - state_bytes = pickle.dumps(state) - poll = poll_pb.Poll( - coroutine_state=state_bytes, - # FIXME: make this configurable - max_results=1, - max_wait=duration_pb2.Duration(seconds=5), - ) - - if calls is not None: - for c in calls: - poll.calls.append(c._as_proto()) - - return Output( - function_pb.RunResponse( - status=status_pb.STATUS_OK, - poll=poll, - ) - ) - - - - - - - - - - - - - - - - - - - - - - error(error) - - - classmethod - - - - - - - - Terminally exit the function with the provided error. - - - Source code in dispatch/proto.py - 119 -120 -121 -122@classmethod -def error(cls, error: Error) -> Output: - """Terminally exit the function with the provided error.""" - return cls.exit(result=CallResult.from_error(error), status=error.status) - - - - - - - - - - - - - exit(result=None, tail_call=None, status=Status.OK) - - - classmethod - - - - - - - - Terminally exit the function. - - - Source code in dispatch/proto.py - 130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145@classmethod -def exit( - cls, - result: CallResult | None = None, - tail_call: Call | None = None, - status: Status = Status.OK, -) -> Output: - """Terminally exit the function.""" - result_proto = result._as_proto() if result else None - tail_call_proto = tail_call._as_proto() if tail_call else None - return Output( - function_pb.RunResponse( - status=status._proto, - exit=exit_pb.Exit(result=result_proto, tail_call=tail_call_proto), - ) - ) - - - - - - - - - - - - - poll(state, calls=None) - - - classmethod - - - - - - - - Suspend the function with a set of Calls, instructing the -orchestrator to resume the function with the provided state when -call results are ready. - - - Source code in dispatch/proto.py - 147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169@classmethod -def poll(cls, state: Any, calls: None | list[Call] = None) -> Output: - """Suspend the function with a set of Calls, instructing the - orchestrator to resume the function with the provided state when - call results are ready.""" - state_bytes = pickle.dumps(state) - poll = poll_pb.Poll( - coroutine_state=state_bytes, - # FIXME: make this configurable - max_results=1, - max_wait=duration_pb2.Duration(seconds=5), - ) - - if calls is not None: - for c in calls: - poll.calls.append(c._as_proto()) - - return Output( - function_pb.RunResponse( - status=status_pb.STATUS_OK, - poll=poll, - ) - ) - - - - - - - - - - - - - tail_call(tail_call) - - - classmethod - - - - - - - - Terminally exit the function, and instruct the orchestrator to -tail call the specified function. - - - Source code in dispatch/proto.py - 124 -125 -126 -127 -128@classmethod -def tail_call(cls, tail_call: Call) -> Output: - """Terminally exit the function, and instruct the orchestrator to - tail call the specified function.""" - return cls.exit(tail_call=tail_call) - - - - - - - - - - - - - value(value, status=None) - - - classmethod - - - - - - - - Terminally exit the function with the provided return value. - - - Source code in dispatch/proto.py - 112 -113 -114 -115 -116 -117@classmethod -def value(cls, value: Any, status: Status | None = None) -> Output: - """Terminally exit the function with the provided return value.""" - if status is None: - status = status_for_output(value) - return cls.exit(result=CallResult.from_value(value), status=status) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sdk - - - - - - - - - - - - - - - - - - - - - - - - - v1 - - - - - - - - - - - - - - - - - - - - - - - - - call_pb2 - - - - - - - Generated protocol buffer code. - - - - - - - - - - - - - - - - - - - - - - - - - - - call_pb2_grpc - - - - - - - Client and server classes corresponding to protobuf-defined services. - - - - - - - - - - - - - - - - - - - - - - - - - - - dispatch_pb2 - - - - - - - Generated protocol buffer code. - - - - - - - - - - - - - - - - - - - - - - - - - - - dispatch_pb2_grpc - - - - - - - Client and server classes corresponding to protobuf-defined services. - - - - - - - - - - - - - - - - - - DispatchService - - - - - - - - Bases: object - - - DispatchService is a service allowing the trigger of programmable endpoints -from a dispatch SDK. - - - Source code in dispatch/sdk/v1/dispatch_pb2_grpc.py - 62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94class DispatchService(object): - """DispatchService is a service allowing the trigger of programmable endpoints - from a dispatch SDK. - """ - - @staticmethod - def Dispatch( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/dispatch.sdk.v1.DispatchService/Dispatch", - dispatch_dot_sdk_dot_v1_dot_dispatch__pb2.DispatchRequest.SerializeToString, - dispatch_dot_sdk_dot_v1_dot_dispatch__pb2.DispatchResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DispatchServiceServicer - - - - - - - - Bases: object - - - DispatchService is a service allowing the trigger of programmable endpoints -from a dispatch SDK. - - - Source code in dispatch/sdk/v1/dispatch_pb2_grpc.py - 26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44class DispatchServiceServicer(object): - """DispatchService is a service allowing the trigger of programmable endpoints - from a dispatch SDK. - """ - - def Dispatch(self, request, context): - """Dispatch submits a list of asynchronous function calls to the service. - - The method does not wait for executions to complete before returning, - it only ensures that the creation was persisted, and returns unique - identifiers to represent the executions. - - The request contains a list of executions to be triggered; the method is - atomic, either all executions are recorded, or none and an error is - returned to explain the reason for the failure. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - - - - - - - - - - - - - - - - - - - - - Dispatch(request, context) - - - - - - - Dispatch submits a list of asynchronous function calls to the service. -The method does not wait for executions to complete before returning, -it only ensures that the creation was persisted, and returns unique -identifiers to represent the executions. -The request contains a list of executions to be triggered; the method is -atomic, either all executions are recorded, or none and an error is -returned to explain the reason for the failure. - - - Source code in dispatch/sdk/v1/dispatch_pb2_grpc.py - 31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44def Dispatch(self, request, context): - """Dispatch submits a list of asynchronous function calls to the service. - - The method does not wait for executions to complete before returning, - it only ensures that the creation was persisted, and returns unique - identifiers to represent the executions. - - The request contains a list of executions to be triggered; the method is - atomic, either all executions are recorded, or none and an error is - returned to explain the reason for the failure. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - - - - - - - - - - - - - - - - - - - - DispatchServiceStub - - - - - - - - Bases: object - - - DispatchService is a service allowing the trigger of programmable endpoints -from a dispatch SDK. - - - Source code in dispatch/sdk/v1/dispatch_pb2_grpc.py - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23class DispatchServiceStub(object): - """DispatchService is a service allowing the trigger of programmable endpoints - from a dispatch SDK. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Dispatch = channel.unary_unary( - "/dispatch.sdk.v1.DispatchService/Dispatch", - request_serializer=dispatch_dot_sdk_dot_v1_dot_dispatch__pb2.DispatchRequest.SerializeToString, - response_deserializer=dispatch_dot_sdk_dot_v1_dot_dispatch__pb2.DispatchResponse.FromString, - ) - - - - - - - - - - - - - - - - - - - - - - __init__(channel) - - - - - - - Constructor. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - channel - - - - - A grpc.Channel. - - - - required - - - - - - - Source code in dispatch/sdk/v1/dispatch_pb2_grpc.py - 13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Dispatch = channel.unary_unary( - "/dispatch.sdk.v1.DispatchService/Dispatch", - request_serializer=dispatch_dot_sdk_dot_v1_dot_dispatch__pb2.DispatchRequest.SerializeToString, - response_deserializer=dispatch_dot_sdk_dot_v1_dot_dispatch__pb2.DispatchResponse.FromString, - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - error_pb2 - - - - - - - Generated protocol buffer code. - - - - - - - - - - - - - - - - - - - - - - - - - - - error_pb2_grpc - - - - - - - Client and server classes corresponding to protobuf-defined services. - - - - - - - - - - - - - - - - - - - - - - - - - - - exit_pb2 - - - - - - - Generated protocol buffer code. - - - - - - - - - - - - - - - - - - - - - - - - - - - exit_pb2_grpc - - - - - - - Client and server classes corresponding to protobuf-defined services. - - - - - - - - - - - - - - - - - - - - - - - - - - - function_pb2 - - - - - - - Generated protocol buffer code. - - - - - - - - - - - - - - - - - - - - - - - - - - - function_pb2_grpc - - - - - - - Client and server classes corresponding to protobuf-defined services. - - - - - - - - - - - - - - - - - - FunctionService - - - - - - - - Bases: object - - - The FunctionService service is used to interface with programmable endpoints -exposing remote functions. - - - Source code in dispatch/sdk/v1/function_pb2_grpc.py - 56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88class FunctionService(object): - """The FunctionService service is used to interface with programmable endpoints - exposing remote functions. - """ - - @staticmethod - def Run( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/dispatch.sdk.v1.FunctionService/Run", - dispatch_dot_sdk_dot_v1_dot_function__pb2.RunRequest.SerializeToString, - dispatch_dot_sdk_dot_v1_dot_function__pb2.RunResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - FunctionServiceServicer - - - - - - - - Bases: object - - - The FunctionService service is used to interface with programmable endpoints -exposing remote functions. - - - Source code in dispatch/sdk/v1/function_pb2_grpc.py - 26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38class FunctionServiceServicer(object): - """The FunctionService service is used to interface with programmable endpoints - exposing remote functions. - """ - - def Run(self, request, context): - """Run runs the function identified by the request, and returns a response - that either contains a result when the function completed, or a poll - directive and the associated coroutine state if the function was suspended. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - - - - - - - - - - - - - - - - - - - - - Run(request, context) - - - - - - - Run runs the function identified by the request, and returns a response -that either contains a result when the function completed, or a poll -directive and the associated coroutine state if the function was suspended. - - - Source code in dispatch/sdk/v1/function_pb2_grpc.py - 31 -32 -33 -34 -35 -36 -37 -38def Run(self, request, context): - """Run runs the function identified by the request, and returns a response - that either contains a result when the function completed, or a poll - directive and the associated coroutine state if the function was suspended. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - - - - - - - - - - - - - - - - - - - - FunctionServiceStub - - - - - - - - Bases: object - - - The FunctionService service is used to interface with programmable endpoints -exposing remote functions. - - - Source code in dispatch/sdk/v1/function_pb2_grpc.py - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23class FunctionServiceStub(object): - """The FunctionService service is used to interface with programmable endpoints - exposing remote functions. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Run = channel.unary_unary( - "/dispatch.sdk.v1.FunctionService/Run", - request_serializer=dispatch_dot_sdk_dot_v1_dot_function__pb2.RunRequest.SerializeToString, - response_deserializer=dispatch_dot_sdk_dot_v1_dot_function__pb2.RunResponse.FromString, - ) - - - - - - - - - - - - - - - - - - - - - - __init__(channel) - - - - - - - Constructor. - - - - Parameters: - - - - Name - Type - Description - Default - - - - - channel - - - - - A grpc.Channel. - - - - required - - - - - - - Source code in dispatch/sdk/v1/function_pb2_grpc.py - 13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Run = channel.unary_unary( - "/dispatch.sdk.v1.FunctionService/Run", - request_serializer=dispatch_dot_sdk_dot_v1_dot_function__pb2.RunRequest.SerializeToString, - response_deserializer=dispatch_dot_sdk_dot_v1_dot_function__pb2.RunResponse.FromString, - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - poll_pb2 - - - - - - - Generated protocol buffer code. - - - - - - - - - - - - - - - - - - - - - - - - - - - poll_pb2_grpc - - - - - - - Client and server classes corresponding to protobuf-defined services. - - - - - - - - - - - - - - - - - - - - - - - - - - - status_pb2 - - - - - - - Generated protocol buffer code. - - - - - - - - - - - - - - - - - - - - - - - - - - - status_pb2_grpc - - - - - - - Client and server classes corresponding to protobuf-defined services. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - signature - - - - - - - - - - - - - - - - - - - - - - - - - sign_request(request, key, created) - - - - - - - Sign a request using HTTP Message Signatures. -The function adds three additional headers: Content-Digest, -Signature-Input, and Signature. See the following spec for more details: -https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures -The signature covers the request method, the URL host and path, the -Content-Type header, and the request body. At this time, an ED25519 -signature is generated with a hard-coded key ID of "default". - - - - Parameters: - - - - Name - Type - Description - Default - - - - - request - - Request - - - - The request to sign. - - - - required - - - - key - - Ed25519PrivateKey - - - - The Ed25519 private key to use to generate the signature. - - - - required - - - - created - - datetime - - - - The times at which the signature is created. - - - - required - - - - - - - Source code in dispatch/signature/__init__.py - 43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74def sign_request(request: Request, key: Ed25519PrivateKey, created: datetime): - """Sign a request using HTTP Message Signatures. - - The function adds three additional headers: Content-Digest, - Signature-Input, and Signature. See the following spec for more details: - https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures - - The signature covers the request method, the URL host and path, the - Content-Type header, and the request body. At this time, an ED25519 - signature is generated with a hard-coded key ID of "default". - - Args: - request: The request to sign. - key: The Ed25519 private key to use to generate the signature. - created: The times at which the signature is created. - """ - logger.debug("signing request with %d byte body", len(request.body)) - request.headers["Content-Digest"] = generate_content_digest(request.body) - - signer = HTTPMessageSigner( - signature_algorithm=ALGORITHM, - key_resolver=KeyResolver(key_id=DEFAULT_KEY_ID, private_key=key), - ) - signer.sign( - request, - key_id=DEFAULT_KEY_ID, - covered_component_ids=cast(Sequence[str], COVERED_COMPONENT_IDS), - created=created, - label="dispatch", - include_alg=True, - ) - logger.debug("signed request successfully") - - - - - - - - - - - - - verify_request(request, key, max_age) - - - - - - - Verify a request containing an HTTP Message Signature. -The function checks three additional headers: Content-Digest, -Signature-Input, and Signature. See the following spec for more details: -https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures -The function checks signatures that cover at least the request method, the -URL host and path, the Content-Type header, and the request body (via the -Content-Digest header). At this time, signatures must use a hard-coded key -ID of "default". - - - - Parameters: - - - - Name - Type - Description - Default - - - - - request - - Request - - - - The request to verify. - - - - required - - - - key - - Ed25519PublicKey - - - - The Ed25519 public key to use to verify the signature. - - - - required - - - - max_age - - timedelta - - - - The maximum age of the signature. - - - - required - - - - - - - Source code in dispatch/signature/__init__.py - 77 - 78 - 79 - 80 - 81 - 82 - 83 - 84 - 85 - 86 - 87 - 88 - 89 - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 - 98 - 99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114def verify_request(request: Request, key: Ed25519PublicKey, max_age: timedelta): - """Verify a request containing an HTTP Message Signature. - - The function checks three additional headers: Content-Digest, - Signature-Input, and Signature. See the following spec for more details: - https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures - - The function checks signatures that cover at least the request method, the - URL host and path, the Content-Type header, and the request body (via the - Content-Digest header). At this time, signatures must use a hard-coded key - ID of "default". - - Args: - request: The request to verify. - key: The Ed25519 public key to use to verify the signature. - max_age: The maximum age of the signature. - """ - logger.debug("verifying request signature") - - # Verify embedded signatures. - key_resolver = KeyResolver(key_id=DEFAULT_KEY_ID, public_key=key) - verifier = HTTPMessageVerifier( - signature_algorithm=ALGORITHM, key_resolver=key_resolver - ) - results = verifier.verify(request, max_age=max_age) - - # Check that at least one signature covers the required components. - for result in results: - covered_components = extract_covered_components(result) - if covered_components.issuperset(COVERED_COMPONENT_IDS): - break - else: - raise ValueError( - f"no signatures found that covered all required components ({COVERED_COMPONENT_IDS})" - ) - - # Check that the Content-Digest header matches the body. - verify_content_digest(request.headers["Content-Digest"], request.body) - - - - - - - - - - - - - digest - - - - - - - - - - - - - - - - - - - - - - - - - generate_content_digest(body) - - - - - - - Returns a SHA-512 Content-Digest header, according to -https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-digest-headers-13 - - - Source code in dispatch/signature/digest.py - 7 - 8 - 9 -10 -11 -12 -13 -14 -15def generate_content_digest(body: str | bytes) -> str: - """Returns a SHA-512 Content-Digest header, according to - https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-digest-headers-13 - """ - if isinstance(body, str): - body = body.encode() - - digest = hashlib.sha512(body).digest() - return str(http_sfv.Dictionary({"sha-512": digest})) - - - - - - - - - - - - - verify_content_digest(digest_header, body) - - - - - - - Verify a SHA-256 or SHA-512 Content-Digest header matches a -request body. - - - Source code in dispatch/signature/digest.py - 18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40def verify_content_digest(digest_header: str | bytes, body: str | bytes): - """Verify a SHA-256 or SHA-512 Content-Digest header matches a - request body.""" - if isinstance(body, str): - body = body.encode() - if isinstance(digest_header, str): - digest_header = digest_header.encode() - - parsed_header = http_sfv.Dictionary() - parsed_header.parse(digest_header) - - # See https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-digest-headers-13#establish-hash-algorithm-registry - if "sha-512" in parsed_header: - digest = parsed_header["sha-512"].value - expect_digest = hashlib.sha512(body).digest() - elif "sha-256" in parsed_header: - digest = parsed_header["sha-256"].value - expect_digest = hashlib.sha256(body).digest() - else: - raise ValueError("missing content digest") - - if not hmac.compare_digest(digest, expect_digest): - raise ValueError("unexpected content digest") - - - - - - - - - - - - - - - - - - - - key - - - - - - - - - - - - - - - - - - - - - - - KeyResolver - - - - dataclass - - - - - - - - Bases: HTTPSignatureKeyResolver - - - KeyResolver provides public and private keys. -At this time, multiple keys and/or key types are not supported. -Keys must be Ed25519 keys and have an ID of DEFAULT_KEY_ID. - - - Source code in dispatch/signature/key.py - 51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73@dataclass -class KeyResolver(HTTPSignatureKeyResolver): - """KeyResolver provides public and private keys. - - At this time, multiple keys and/or key types are not supported. - Keys must be Ed25519 keys and have an ID of DEFAULT_KEY_ID. - """ - - key_id: str - public_key: Ed25519PublicKey | None = None - private_key: Ed25519PrivateKey | None = None - - def resolve_public_key(self, key_id: str): - if key_id != self.key_id or self.public_key is None: - raise ValueError(f"public key '{key_id}' not available") - - return self.public_key - - def resolve_private_key(self, key_id: str): - if key_id != self.key_id or self.private_key is None: - raise ValueError(f"private key '{key_id}' not available") - - return self.private_key - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - private_key_from_bytes(key) - - - - - - - Returns an Ed25519 private key from 32 raw bytes. - - - Source code in dispatch/signature/key.py - 46 -47 -48def private_key_from_bytes(key: bytes) -> Ed25519PrivateKey: - """Returns an Ed25519 private key from 32 raw bytes.""" - return Ed25519PrivateKey.from_private_bytes(key) - - - - - - - - - - - - - private_key_from_pem(pem, password=None) - - - - - - - Returns an Ed25519 private key given a PEM representation -and optional password. - - - Source code in dispatch/signature/key.py - 30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43def private_key_from_pem( - pem: str | bytes, password: bytes | None = None -) -> Ed25519PrivateKey: - """Returns an Ed25519 private key given a PEM representation - and optional password.""" - if isinstance(pem, str): - pem = pem.encode() - if isinstance(password, str): - password = password.encode() - - key = load_pem_private_key(pem, password=password) - if not isinstance(key, Ed25519PrivateKey): - raise ValueError(f"unexpected private key type: {type(key)}") - return key - - - - - - - - - - - - - public_key_from_bytes(key) - - - - - - - Returns an Ed25519 public key from 32 raw bytes. - - - Source code in dispatch/signature/key.py - 25 -26 -27def public_key_from_bytes(key: bytes) -> Ed25519PublicKey: - """Returns an Ed25519 public key from 32 raw bytes.""" - return Ed25519PublicKey.from_public_bytes(key) - - - - - - - - - - - - - public_key_from_pem(pem) - - - - - - - Returns an Ed25519 public key given a PEM representation. - - - Source code in dispatch/signature/key.py - 14 -15 -16 -17 -18 -19 -20 -21 -22def public_key_from_pem(pem: str | bytes) -> Ed25519PublicKey: - """Returns an Ed25519 public key given a PEM representation.""" - if isinstance(pem, str): - pem = pem.encode() - - key = load_pem_public_key(pem) - if not isinstance(key, Ed25519PublicKey): - raise ValueError(f"unexpected public key type: {type(key)}") - return key - - - - - - - - - - - - - - - - - - - - request - - - - - - - - - - - - - - - - - - - - - - - Request - - - - dataclass - - - - - - - - - A framework-agnostic representation of an HTTP request. - - - Source code in dispatch/signature/request.py - 6 - 7 - 8 - 9 -10 -11 -12 -13@dataclass -class Request: - """A framework-agnostic representation of an HTTP request.""" - - method: str - url: str - headers: CaseInsensitiveDict - body: str | bytes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - status - - - - - - - - - - - - - - - - - - - - - - - Status - - - - - - - - Bases: int, Enum - - - Enumeration of the possible values that can be used in the return status -of functions. - - - Source code in dispatch/status.py - 7 - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23@enum.unique -class Status(int, enum.Enum): - """Enumeration of the possible values that can be used in the return status - of functions. - """ - - UNSPECIFIED = status_pb.STATUS_UNSPECIFIED - OK = status_pb.STATUS_OK - TIMEOUT = status_pb.STATUS_TIMEOUT - THROTTLED = status_pb.STATUS_THROTTLED - INVALID_ARGUMENT = status_pb.STATUS_INVALID_ARGUMENT - INVALID_RESPONSE = status_pb.STATUS_INVALID_RESPONSE - TEMPORARY_ERROR = status_pb.STATUS_TEMPORARY_ERROR - PERMANENT_ERROR = status_pb.STATUS_PERMANENT_ERROR - INCOMPATIBLE_STATE = status_pb.STATUS_INCOMPATIBLE_STATE - - _proto: status_pb.Status - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - register_error_type(error_type, handler) - - - - - - - Register an error type, and a handler which derives a Status from -errors of this type. - - - Source code in dispatch/status.py - 100 -101 -102 -103 -104 -105def register_error_type( - error_type: Type[Exception], handler: Callable[[Exception], Status] -): - """Register an error type, and a handler which derives a Status from - errors of this type.""" - _ERROR_TYPES[error_type] = handler - - - - - - - - - - - - - register_output_type(output_type, handler) - - - - - - - Register an output type, and a handler which derives a Status from -outputs of this type. - - - Source code in dispatch/status.py - 108 -109 -110 -111def register_output_type(output_type: Type[Any], handler: Callable[[Any], Status]): - """Register an output type, and a handler which derives a Status from - outputs of this type.""" - _OUTPUT_TYPES[output_type] = handler - - - - - - - - - - - - - status_for_error(error) - - - - - - - Returns a Status that corresponds to the specified error. - - - Source code in dispatch/status.py - 59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87def status_for_error(error: Exception) -> Status: - """Returns a Status that corresponds to the specified error.""" - # See if the error matches one of the registered types. - handler = _find_handler(error, _ERROR_TYPES) - if handler is not None: - return handler(error) - # If not, resort to standard error categorization. - # - # See https://docs.python.org/3/library/exceptions.html - status = Status.PERMANENT_ERROR - try: - # Raise the exception and catch it so that the interpreter deals - # with exception groups and chaining for us. - raise error - except TimeoutError: - status = Status.TIMEOUT - except (TypeError, ValueError): - status = Status.INVALID_ARGUMENT - except (EOFError, InterruptedError, KeyboardInterrupt, OSError): - # For OSError, we might want to categorize the values of errno to - # determine whether the error is temporary or permanent. - # - # In general, permanent errors from the OS are rare because they tend to - # be caused by invalid use of syscalls, which are unlikely at higher - # abstraction levels. - status = Status.TEMPORARY_ERROR - except BaseException: - pass - return status - - - - - - - - - - - - - status_for_output(output) - - - - - - - Returns a Status that corresponds to the specified output value. - - - Source code in dispatch/status.py - 90 -91 -92 -93 -94 -95 -96 -97def status_for_output(output: Any) -> Status: - """Returns a Status that corresponds to the specified output value.""" - # See if the output value matches one of the registered types. - handler = _find_handler(output, _OUTPUT_TYPES) - if handler is not None: - return handler(output) - - return Status.OK - - - - - - - - - - - - - - - - - - - - + +
+ + +Dispatch Python SDK¤ +This is the API reference for the Python SDK of Dispatch. + +Tutorials and guides: docs.stealthrocket.cloud. +Source: stealthrocket/dispatch-sdk-python. + - - - @@ -17846,7 +1280,7 @@
+ + + + + SUMMARY + + + dispatch + client + coroutine + experimental + durable + function + registry + serializable + + + multicolor + compile + desugar + generator + parse + template + yields + + + + + fastapi + function + id + integrations + http + httpx + requests + + + proto + signature + digest + key + request + + + status + + + + + + + + + + + + + + + + +
+ + + + + + + + + + client + + +¤ + + + + + + + + + + + + + + + + + + + + Client + + +¤ +Client( + api_key: None | str = None, api_url: None | str = None +) + + + + + + Client for the Dispatch API. + + + + + Parameters: + + + + Name + Type + Description + Default + + + + + api_key + + None | str + + + + Dispatch API key to use for authentication. Uses the value of +the DISPATCH_API_KEY environment variable by default. + + + + None + + + + api_url + + None | str + + + + The URL of the Dispatch API to use. Uses the value of the +DISPATCH_API_URL environment variable if set, otherwise +defaults to the public Dispatch API (DEFAULT_API_URL). + + + + None + + + + + + + + Raises: + + + + Type + Description + + + + + + ValueError + + + + if the API key is missing. + + + + + + + + + + + + + + + + + + + + + + + + + + dispatch + + +¤ +dispatch(calls: Iterable[Call]) -> Iterable[DispatchID] + + + + + Dispatch function calls. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + calls + + Iterable[Call] + + + + Calls to dispatch. + + + + required + + + + + + + + Returns: + + + + Type + Description + + + + + + Iterable[DispatchID] + + + + Identifiers for the function calls, in the same order as the inputs. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + coroutine + + +¤ + + + + + + + + + + + + + + + + + + + + CoroutineState + + + + dataclass + + +¤ + + + + + + Serialized representation of a coroutine. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + poll + + +¤ +poll(calls: list[Call]) -> list[CallResult] + + + + + Suspend the function with a set of Calls, instructing the +orchestrator to resume the coroutine when call results are ready. + + + + + + + + + + + + exit + + +¤ +exit( + result: Any | None = None, tail_call: Call | None = None +) + + + + + Exit exits a coroutine, with an optional result and +optional tail call. + + + + + + + + + + + + schedule + + +¤ +schedule(func: DurableFunction, input: Input) -> Output + + + + + Schedule schedules a coroutine with the provided input. + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + function + + +¤ + + + + + + + + + + + + + + + + + + + + DurableFunction + + +¤ +DurableFunction(fn: FunctionType) + + + + + + A wrapper for generator functions and async functions that make +their generator and coroutine instances serializable. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DurableGenerator + + +¤ +DurableGenerator( + generator: GeneratorType, + registered_fn: RegisteredFunction, + *args: Any, + coro_await: bool = False, + **kwargs: Any +) + + + + + Bases: Serializable, Generator[_YieldT, _SendT, _ReturnT] + + + A wrapper for a generator that makes it serializable (can be pickled). +Instances behave like the generators they wrap. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DurableCoroutine + + +¤ +DurableCoroutine( + coroutine: CoroutineType, + registered_fn: RegisteredFunction, + *args: Any, + **kwargs: Any +) + + + + + Bases: Serializable, Coroutine[_YieldT, _SendT, _ReturnT] + + + A wrapper for a coroutine that makes it serializable (can be pickled). +Instances behave like the coroutines they wrap. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + durable + + +¤ +durable(fn) -> DurableFunction + + + + + Returns a "durable" function that creates serializable +generators or coroutines. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + fn + + + + + A generator function or async function. + + + + required + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + durable + + +¤ + + + + A decorator that makes generators and coroutines serializable. +This module defines a @durable decorator that can be applied to generator +functions and async functions. The generator and coroutine instances +they create can be pickled. +Example usage: +import pickle +from dispatch.experimental.durable import durable + +@durable +def my_generator(): + for i in range(3): + yield i + +# Run the generator to its first yield point: +g = my_generator() +print(next(g)) # 0 + +# Make a copy, and consume the remaining items: +b = pickle.dumps(g) +g2 = pickle.loads(b) +print(next(g2)) # 1 +print(next(g2)) # 2 + +# The original is not affected: +print(next(g)) # 1 +print(next(g)) # 2 + + + + + + + + + + + + + + + + + + + + + durable + + +¤ +durable(fn) -> DurableFunction + + + + + Returns a "durable" function that creates serializable +generators or coroutines. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + fn + + + + + A generator function or async function. + + + + required + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + registry + + +¤ + + + + + + + + + + + + + + + + + + + + RegisteredFunction + + + + dataclass + + +¤ + + + + + + A function that can be referenced in durable state. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + register_function + + +¤ +register_function(fn: FunctionType) -> RegisteredFunction + + + + + Register a function in the in-memory function registry. +When serializing a registered function, a reference to the function +is stored along with details about its location and contents. When +deserializing the function, the registry is consulted in order to +find the function associated with the reference (and in order to +check whether the function is the same). + + + + Parameters: + + + + Name + Type + Description + Default + + + + + fn + + FunctionType + + + + The function to register. + + + + required + + + + + + + + Returns: + + + +Name Type + Description + + + + +str + RegisteredFunction + + + + Unique identifier for the function. + + + + + + + + + Raises: + + + + Type + Description + + + + + + ValueError + + + + The function conflicts with another registered function. + + + + + + + + + + + + + + + + + lookup_function + + +¤ +lookup_function(key: str) -> RegisteredFunction + + + + + Lookup a registered function by key. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + key + + str + + + + Unique identifier for the function. + + + + required + + + + + + + + Returns: + + + +Name Type + Description + + + + +RegisteredFunction + RegisteredFunction + + + + the function that was registered with the specified key. + + + + + + + + + Raises: + + + + Type + Description + + + + + + KeyError + + + + A function has not been registered with this key. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + serializable + + +¤ + + + + + + + + + + + + + + + + + + + + Serializable + + +¤ +Serializable( + g: GeneratorType | CoroutineType, + registered_fn: RegisteredFunction, + *args: Any, + coro_await: bool = False, + **kwargs: Any +) + + + + + + A wrapper for a generator or coroutine that makes it serializable. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + experimental + + +¤ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + compile + + +¤ + + + + + + + + + + + + + + + + + + + + FunctionColor + + +¤ + + + + + Bases: Enum + + + Color (aka. type/flavor) of a function. +There are four colors of functions in Python: +* regular (e.g. def fn(): pass) +* generator (e.g. def fn(): yield) +* async (e.g. async def fn(): pass) +* async generator (e.g. async def fn(): yield) +Only the first two colors are supported at this time. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GeneratorTransformer + + +¤ + + + + + Bases: NodeTransformer + + + Wrap ast.Yield values in a GeneratorYield container. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CallTransformer + + +¤ + + + + + Bases: NodeTransformer + + + Replace explicit function calls with a gadget that recursively compiles +functions into generators and then replaces the function call with a +yield from. +The transformations are only valid for ASTs that have passed through the +desugaring pass; only ast.Expr(value=ast.Call(...)) and +ast.Assign(targets=..., value=ast.Call(..)) nodes are transformed here. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + compile_function + + +¤ +compile_function( + fn: FunctionType, + decorator=None, + cache_key: str = "default", +) -> FunctionType | MethodType + + + + + Compile a regular function into a generator that yields data passed +to functions marked with the @multicolor.yields decorator. Decorated yield +functions can be called from anywhere in the call stack, and functions +in between do not have to be generators or async functions (coroutines). +Example: +@multicolor.yields(type="sleep") +def sleep(seconds): ... + +def parent(): + sleep(3) # yield point + +def grandparent(): + parent() + +compiled_grandparent = multicolor.compile_function(grandparent) +generator = compiled_grandparent() +for item in generator: + print(item) # multicolor.CustomYield(type="sleep", args=[3]) + +Two-way data flow works as expected. At a yield point, generator.send(value) +can be used to send data back to the yield point and to resume execution. +The data sent back will be the return value of the function decorated with +@multicolor.yields: +@multicolor.yields(type="add") +def add(a: int, b: int) -> int: + return a + b # default/synchronous implementation + +def scheduler(generator): + try: + send = None + while True: + item = generator.send(send) + match item: + case multicolor.CustomYield(type="add"): + a, b = item.args + print(f"adding {a} + {b}") + send = a + b + except StopIteration as e: + return e.value # return value + +def adder(a: int, b: int) -> int: + return add(a, b) + +compiled_adder = multicolor.compile_function(adder) +generator = compiled_adder(1, 2) +result = scheduler(generator) +print(result) # 3 + +The @multicolor.yields decorator does not change the implementation of +the function it decorates. If the function is run without being +compiled, the default implementation will be used instead: +print(adder(1, 2)) # 3 + +The default implementation could also raise an error, to ensure that +the function is only ever called from a compiled function. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + fn + + FunctionType + + + + The function to compile. + + + + required + + + + decorator + + + + + An optional decorator to apply to the compiled function. + + + + None + + + + cache_key + + str + + + + Cache key to use when caching compiled functions. + + + + 'default' + + + + + + + + Returns: + + + +Name Type + Description + + + + +FunctionType + FunctionType | MethodType + + + + A compiled generator function. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + desugar + + +¤ + + + + + + + + + + + + + + + + + + + + Desugar + + +¤ +Desugar() + + + + + + The desugar pass simplifies subsequent AST transformations that need +to replace an expression (e.g. a function call) with a statement (e.g. an +if branch) in a function definition. +The pass recursively simplifies control flow and compound expressions +in a function definition such that: +- expressions that are children of statements either have no children, or + only have children of type ast.Name and/or ast.Constant +- those parent expressions are either part of an ast.Expr(value=expr) + statement or an ast.Assign(value=expr) statement +The pass does not recurse into lambda expressions, or nested function or +class definitions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + desugar_function + + +¤ +desugar_function(fn_def: FunctionDef) -> FunctionDef + + + + + Desugar a function to simplify subsequent AST transformations. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + fn_def + + FunctionDef + + + + A function definition. + + + + required + + + + + + + + Returns: + + + +Name Type + Description + + + + +FunctionDef + FunctionDef + + + + The desugared function definition. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + generator + + +¤ + + + + + + + + + + + + + + + + + + + + YieldCounter + + +¤ +YieldCounter() + + + + + Bases: NodeVisitor + + + AST visitor that walks an ast.FunctionDef to count yield and yield from +statements. +The resulting count can be used to determine if the input function is +a generator or not. +Yields from nested function/class definitions are not counted. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + is_generator + + +¤ +is_generator(fn_def: FunctionDef) -> bool + + + + + Returns a boolean indicating whether a function is a +generator function. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + fn_def + + FunctionDef + + + + A function definition. + + + + required + + + + + + + + + + + + + + + + empty_generator + + +¤ +empty_generator() + + + + + A generator that yields nothing. +A yield from this generator can be inserted into a function definition in +order to turn the function into a generator, without causing any visible +side effects. + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + multicolor + + +¤ + + + + + + + + + + + + + + + + + + + + NoSourceError + + +¤ + + + + + Bases: RuntimeError + + + Function source code is not available. + + + + + + + + + + + + + CustomYield + + + + dataclass + + +¤ + + + + + Bases: YieldType + + + A yield from a function marked with @yields. + + + + Attributes: + + + + Name + Type + Description + + + + + type + + Any + + + + The type of yield that was specified in the @yields decorator. + + + + + args + + list[Any] + + + + Positional arguments to the function call. + + + + + kwargs + + dict[str, Any] | None + + + + Keyword arguments to the function call. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GeneratorYield + + + + dataclass + + +¤ + + + + + Bases: YieldType + + + A yield from a generator. + + + + Attributes: + + + + Name + Type + Description + + + + + value + + Any + + + + The value that was yielded from the generator. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + compile_function + + +¤ +compile_function( + fn: FunctionType, + decorator=None, + cache_key: str = "default", +) -> FunctionType | MethodType + + + + + Compile a regular function into a generator that yields data passed +to functions marked with the @multicolor.yields decorator. Decorated yield +functions can be called from anywhere in the call stack, and functions +in between do not have to be generators or async functions (coroutines). +Example: +@multicolor.yields(type="sleep") +def sleep(seconds): ... + +def parent(): + sleep(3) # yield point + +def grandparent(): + parent() + +compiled_grandparent = multicolor.compile_function(grandparent) +generator = compiled_grandparent() +for item in generator: + print(item) # multicolor.CustomYield(type="sleep", args=[3]) + +Two-way data flow works as expected. At a yield point, generator.send(value) +can be used to send data back to the yield point and to resume execution. +The data sent back will be the return value of the function decorated with +@multicolor.yields: +@multicolor.yields(type="add") +def add(a: int, b: int) -> int: + return a + b # default/synchronous implementation + +def scheduler(generator): + try: + send = None + while True: + item = generator.send(send) + match item: + case multicolor.CustomYield(type="add"): + a, b = item.args + print(f"adding {a} + {b}") + send = a + b + except StopIteration as e: + return e.value # return value + +def adder(a: int, b: int) -> int: + return add(a, b) + +compiled_adder = multicolor.compile_function(adder) +generator = compiled_adder(1, 2) +result = scheduler(generator) +print(result) # 3 + +The @multicolor.yields decorator does not change the implementation of +the function it decorates. If the function is run without being +compiled, the default implementation will be used instead: +print(adder(1, 2)) # 3 + +The default implementation could also raise an error, to ensure that +the function is only ever called from a compiled function. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + fn + + FunctionType + + + + The function to compile. + + + + required + + + + decorator + + + + + An optional decorator to apply to the compiled function. + + + + None + + + + cache_key + + str + + + + Cache key to use when caching compiled functions. + + + + 'default' + + + + + + + + Returns: + + + +Name Type + Description + + + + +FunctionType + FunctionType | MethodType + + + + A compiled generator function. + + + + + + + + + + + + + + + + + no_yields + + +¤ +no_yields(fn) + + + + + Decorator that hints that a function (and anything called +recursively) does not yield. + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + parse + + +¤ + + + + + + + + + + + + + + + + + + + + NoSourceError + + +¤ + + + + + Bases: RuntimeError + + + Function source code is not available. + + + + + + + + + + + + + + + parse_function + + +¤ +parse_function( + fn: FunctionType, +) -> tuple[Module, FunctionDef] + + + + + Parse an AST from a function. The function source must be available. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + fn + + FunctionType + + + + The function to parse. + + + + required + + + + + + + + Raises: + + + + Type + Description + + + + + + NoSourceError + + + + If the function source cannot be retrieved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + template + + +¤ + + + + + + + + + + + + + + + + + + + + NameTransformer + + +¤ +NameTransformer(**replacements: expr | stmt) + + + + + Bases: NodeTransformer + + + Replace ast.Name nodes in an AST. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rewrite_template + + +¤ +rewrite_template( + template: str, **replacements: expr | stmt +) -> list[stmt] + + + + + Create an AST by parsing a template string and then replacing +embedded identifiers with the provided AST nodes. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + template + + str + + + + String containing source code (one or more statements). + + + + required + + + + **replacements + + expr | stmt + + + + Dictionary mapping identifiers to replacement nodes. + + + + {} + + + + + + + + Returns: + + + + Type + Description + + + + + + list[stmt] + + + + list[ast.stmt]: List of AST statements. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + yields + + +¤ + + + + + + + + + + + + + + + + + + + + YieldType + + +¤ + + + + + + Base class for yield types. + + + + + + + + + + + + + CustomYield + + + + dataclass + + +¤ + + + + + Bases: YieldType + + + A yield from a function marked with @yields. + + + + Attributes: + + + + Name + Type + Description + + + + + type + + Any + + + + The type of yield that was specified in the @yields decorator. + + + + + args + + list[Any] + + + + Positional arguments to the function call. + + + + + kwargs + + dict[str, Any] | None + + + + Keyword arguments to the function call. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GeneratorYield + + + + dataclass + + +¤ + + + + + Bases: YieldType + + + A yield from a generator. + + + + Attributes: + + + + Name + Type + Description + + + + + value + + Any + + + + The value that was yielded from the generator. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + yields + + +¤ +yields(type: Any) + + + + + Returns a decorator that marks functions as a type of yield. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + type + + Any + + + + Opaque type for this yield. + + + + required + + + + + + + + + + + + + + + + no_yields + + +¤ +no_yields(fn) + + + + + Decorator that hints that a function (and anything called +recursively) does not yield. + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + fastapi + + +¤ + + + + Integration of Dispatch programmable endpoints for FastAPI. +Example: +import fastapi +from dispatch.fastapi import Dispatch + +app = fastapi.FastAPI() +dispatch = Dispatch(app, api_key="test-key") + +@dispatch.function() +def my_function(): + return "Hello World!" + +@app.get("/") +def read_root(): + dispatch.call(my_function) + + + + + + + + + + + + + + + + + + + Dispatch + + +¤ +Dispatch( + app: FastAPI, + endpoint: str | None = None, + verification_key: Ed25519PublicKey | None = None, + api_key: str | None = None, + api_url: str | None = None, +) + + + + + Bases: Registry + + + A Dispatch programmable endpoint, powered by FastAPI. + + It mounts a sub-app that implements the Dispatch gRPC interface. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + app + + FastAPI + + + + The FastAPI app to configure. + + + + required + + + + endpoint + + str | None + + + + Full URL of the application the Dispatch programmable +endpoint will be running on. Uses the value of the +DISPATCH_ENDPOINT_URL environment variable by default. + + + + None + + + + verification_key + + Ed25519PublicKey | None + + + + Key to use when verifying signed requests. Uses +the value of the DISPATCH_VERIFICATION_KEY environment variable +by default. The environment variable is expected to carry an +Ed25519 public key in base64 or PEM format. +If not set, request signature verification is disabled (a warning +will be logged by the constructor). + + + + None + + + + api_key + + str | None + + + + Dispatch API key to use for authentication. Uses the value of +the DISPATCH_API_KEY environment variable by default. + + + + None + + + + api_url + + str | None + + + + The URL of the Dispatch API to use. Uses the value of the +DISPATCH_API_URL environment variable if set, otherwise +defaults to the public Dispatch API (DEFAULT_API_URL). + + + + None + + + + + + + + Raises: + + + + Type + Description + + + + + + ValueError + + + + If any of the required arguments are missing. + + + + + + + + + + + + + + + + + + + + + + + + + + function + + +¤ +function() -> Callable[[FunctionType], Function] + + + + + Returns a decorator that registers functions. + + + + + + + + + + + + coroutine + + +¤ +coroutine() -> ( + Callable[[FunctionType], Function | FunctionType] +) + + + + + Returns a decorator that registers coroutine functions. + + + + + + + + + + + + primitive_function + + +¤ +primitive_function() -> ( + Callable[[PrimitiveFunctionType], Function] +) + + + + + Returns a decorator that registers primitive functions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + function + + +¤ + + + + + + + + + + + + + + + + + + + PrimitiveFunctionType + + + + module-attribute + + +¤ +PrimitiveFunctionType: TypeAlias = Callable[[Input], Output] + + + + + A primitive function is a function that accepts a dispatch.function.Input +and unconditionally returns a dispatch.function.Output. It must not raise +exceptions. + + + + + + + + + + + Function + + +¤ +Function( + endpoint: str, + client: Client | None, + name: str, + func: Callable[[Input], Output], +) + + + + + + Callable wrapper around a function meant to be used throughout the +Dispatch Python SDK. + + + + + + + + + + + + + + + + + + + + + dispatch + + +¤ +dispatch(*args, **kwargs) -> DispatchID + + + + + Dispatch a call to the function. +The Registry this function was registered with must be initialized +with a Client / api_key for this call facility to be available. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + *args + + + + + Positional arguments for the function. + + + + () + + + + **kwargs + + + + + Keyword arguments for the function. + + + + {} + + + + + + + + Returns: + + + +Name Type + Description + + + + +DispatchID + DispatchID + + + + ID of the dispatched call. + + + + + + + + + Raises: + + + + Type + Description + + + + + + RuntimeError + + + + if a Dispatch client has not been configured. + + + + + + + + + + + + + + + + + primitive_dispatch + + +¤ +primitive_dispatch(input: Any = None) -> DispatchID + + + + + Dispatch a primitive call. +The Registry this function was registered with must be initialized +with a Client / api_key for this call facility to be available. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + input + + Any + + + + Input to the function. + + + + None + + + + + + + + Returns: + + + +Name Type + Description + + + + +DispatchID + DispatchID + + + + ID of the dispatched call. + + + + + + + + + Raises: + + + + Type + Description + + + + + + RuntimeError + + + + if a Dispatch client has not been configured. + + + + + + + + + + + + + + + + + call_with + + +¤ +call_with( + *args, correlation_id: int | None = None, **kwargs +) -> Call + + + + + Create a Call for this function with the provided input. Useful to +generate calls when polling. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + *args + + + + + Positional arguments for the function. + + + + () + + + + correlation_id + + int | None + + + + optional arbitrary integer the caller can use to +match this call to a call result. + + + + None + + + + **kwargs + + + + + Keyword arguments for the function. + + + + {} + + + + + + + + Returns: + + + +Name Type + Description + + + + +Call + Call + + + + can be passed to Output.poll(). + + + + + + + + + + + + + + + + + primitive_call_with + + +¤ +primitive_call_with( + input: Any, correlation_id: int | None = None +) -> Call + + + + + Create a Call for this function with the provided input. Useful to +generate calls when polling. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + input + + Any + + + + any pickle-able Python value that will be passed as input to +this function. + + + + required + + + + correlation_id + + int | None + + + + optional arbitrary integer the caller can use to +match this call to a call result. + + + + None + + + + + + + + Returns: + + + +Name Type + Description + + + + +Call + Call + + + + can be passed to Output.poll(). + + + + + + + + + + + + + + + + + + + + + + + + + Registry + + +¤ +Registry(endpoint: str, client: Client | None) + + + + + + Registry of local functions. + + + + + Parameters: + + + + Name + Type + Description + Default + + + + + endpoint + + str + + + + URL of the endpoint that the function is accessible from. + + + + required + + + + client + + Client | None + + + + Optional client for the Dispatch API. If provided, calls +to local functions can be dispatched directly. + + + + required + + + + + + + + + + + + + + + + + + + + + + + + + function + + +¤ +function() -> Callable[[FunctionType], Function] + + + + + Returns a decorator that registers functions. + + + + + + + + + + + + coroutine + + +¤ +coroutine() -> ( + Callable[[FunctionType], Function | FunctionType] +) + + + + + Returns a decorator that registers coroutine functions. + + + + + + + + + + + + primitive_function + + +¤ +primitive_function() -> ( + Callable[[PrimitiveFunctionType], Function] +) + + + + + Returns a decorator that registers primitive functions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + id + + +¤ + + + + + + + + + + + + + + + + + + + DispatchID + + + + module-attribute + + +¤ +DispatchID: TypeAlias = str + + + + + Unique identifier in Dispatch. +It should be treated as an opaque value. + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + dispatch + + +¤ + + + + The Dispatch SDK for Python. + + + + + + + + + + + + + + + + + DispatchID + + + + module-attribute + + +¤ +DispatchID: TypeAlias = str + + + + + Unique identifier in Dispatch. +It should be treated as an opaque value. + + + + + + + + + + + Client + + +¤ +Client( + api_key: None | str = None, api_url: None | str = None +) + + + + + + Client for the Dispatch API. + + + + + Parameters: + + + + Name + Type + Description + Default + + + + + api_key + + None | str + + + + Dispatch API key to use for authentication. Uses the value of +the DISPATCH_API_KEY environment variable by default. + + + + None + + + + api_url + + None | str + + + + The URL of the Dispatch API to use. Uses the value of the +DISPATCH_API_URL environment variable if set, otherwise +defaults to the public Dispatch API (DEFAULT_API_URL). + + + + None + + + + + + + + Raises: + + + + Type + Description + + + + + + ValueError + + + + if the API key is missing. + + + + + + + + + + + + + + + + + + + + + + + + + + dispatch + + +¤ +dispatch(calls: Iterable[Call]) -> Iterable[DispatchID] + + + + + Dispatch function calls. + + + + Parameters: + + + + Name + Type + Description + Default + + + + + calls + + Iterable[Call] + + + + Calls to dispatch. + + + + required + + + + + + + + Returns: + + + + Type + Description + + + + + + Iterable[DispatchID] + + + + Identifiers for the function calls, in the same order as the inputs. + + + + + + + + + + + + + + + + + + + + + + + + + Call + + + + dataclass + + +¤ + + + + + + Instruction to call a function. +Though this class can be built manually, it is recommended to use the +with_call method of a Function instead. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Error + + +¤ +Error( + status: Status, type: str | None, message: str | None +) + + + + + + Error when running a function. +This is not a Python exception, but potentially part of a CallResult or +Output. + + + + + Parameters: + + + + Name + Type + Description + Default + + + + + status + + Status + + + + categorization of the error. + + + + required + + + + type + + str | None + + + + arbitrary string, used for humans. Optional. + + + + required + + + + message + + str | None + + + + arbitrary message. Optional. + + + + required + + + + + + + + Raises: + + + + Type + Description + + + + + + ValueError + + + + Neither type or message was provided or status is +invalid. + + + + + + + + + + + + + + + + + + + + + + + + + + from_exception + + + + classmethod + + +¤ +from_exception( + ex: Exception, status: Status | None = None +) -> Error + + + + + Create an Error from a Python exception, using its class qualified +named as type. +The status tries to be inferred, but can be overridden. If it is not +provided or cannot be inferred, it defaults to TEMPORARY_ERROR. + + + + + + + + + + + + + + + + + + + + Input + + +¤ +Input(req: RunRequest) + + + + + + The input to a primitive function. +Functions always take a single argument of type Input. When the function is +run for the first time, it receives the input. When the function is a coroutine +that's resuming after a yield point, it receives the results of the yield +directive. Use the is_first_call and is_resume properties to differentiate +between the two cases. +This class is intended to be used as read-only. + + + + + + + + + + + + + + + + + + + + + input_arguments + + +¤ +input_arguments() -> tuple[list[Any], dict[str, Any]] + + + + + Returns positional and keyword arguments carried by the input. + + + + + + + + + + + + + + + + + + + + Output + + +¤ +Output(proto: RunResponse) + + + + + + The output of a primitive function. +This class is meant to be instantiated and returned by authors of functions +to indicate the follow up action they need to take. Use the various class +methods create an instance of this class. For example Output.value() or +Output.poll(). + + + + + + + + + + + + + + + + + + + + + value + + + + classmethod + + +¤ +value(value: Any, status: Status | None = None) -> Output + + + + + Terminally exit the function with the provided return value. + + + + + + + + + + + + error + + + + classmethod + + +¤ +error(error: Error) -> Output + + + + + Terminally exit the function with the provided error. + + + + + + + + + + + + tail_call + + + + classmethod + + +¤ +tail_call(tail_call: Call) -> Output + + + + + Terminally exit the function, and instruct the orchestrator to +tail call the specified function. + + + + + + + + + + + + exit + + + + classmethod + + +¤ +exit( + result: CallResult | None = None, + tail_call: Call | None = None, + status: Status = Status.OK, +) -> Output + + + + + Terminally exit the function. + + + + + + + + + + + + poll + + + + classmethod + + +¤ +poll(state: Any, calls: None | list[Call] = None) -> Output + + + + + Suspend the function with a set of Calls, instructing the +orchestrator to resume the function with the provided state when +call results are ready. + + + + + + + + + + + + + + + + + + + + Status + + +¤ + + + + + Bases: int, Enum + + + Enumeration of the possible values that can be used in the return status +of functions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + poll + + +¤ +poll(calls: list[Call]) -> list[CallResult] + + + + + Suspend the function with a set of Calls, instructing the +orchestrator to resume the coroutine when call results are ready. + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + http + + +¤ + + + + + + + + + + + + + + + + + + + + + + http_response_code_status + + +¤ +http_response_code_status(code: int) -> Status + + + + + Returns a Status that's broadly equivalent to an HTTP response +status code. + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + httpx + + +¤ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + integrations + + +¤ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + requests + + +¤ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + proto + + +¤ + + + + + + + + + + + + + + + + + + + + Input + + +¤ +Input(req: RunRequest) + + + + + + The input to a primitive function. +Functions always take a single argument of type Input. When the function is +run for the first time, it receives the input. When the function is a coroutine +that's resuming after a yield point, it receives the results of the yield +directive. Use the is_first_call and is_resume properties to differentiate +between the two cases. +This class is intended to be used as read-only. + + + + + + + + + + + + + + + + + + + + + input_arguments + + +¤ +input_arguments() -> tuple[list[Any], dict[str, Any]] + + + + + Returns positional and keyword arguments carried by the input. + + + + + + + + + + + + + + + + + + + + Output + + +¤ +Output(proto: RunResponse) + + + + + + The output of a primitive function. +This class is meant to be instantiated and returned by authors of functions +to indicate the follow up action they need to take. Use the various class +methods create an instance of this class. For example Output.value() or +Output.poll(). + + + + + + + + + + + + + + + + + + + + + value + + + + classmethod + + +¤ +value(value: Any, status: Status | None = None) -> Output + + + + + Terminally exit the function with the provided return value. + + + + + + + + + + + + error + + + + classmethod + + +¤ +error(error: Error) -> Output + + + + + Terminally exit the function with the provided error. + + + + + + + + + + + + tail_call + + + + classmethod + + +¤ +tail_call(tail_call: Call) -> Output + + + + + Terminally exit the function, and instruct the orchestrator to +tail call the specified function. + + + + + + + + + + + + exit + + + + classmethod + + +¤ +exit( + result: CallResult | None = None, + tail_call: Call | None = None, + status: Status = Status.OK, +) -> Output + + + + + Terminally exit the function. + + + + + + + + + + + + poll + + + + classmethod + + +¤ +poll(state: Any, calls: None | list[Call] = None) -> Output + + + + + Suspend the function with a set of Calls, instructing the +orchestrator to resume the function with the provided state when +call results are ready. + + + + + + + + + + + + + + + + + + + + Call + + + + dataclass + + +¤ + + + + + + Instruction to call a function. +Though this class can be built manually, it is recommended to use the +with_call method of a Function instead. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CallResult + + + + dataclass + + +¤ + + + + + + Result of a Call. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Error + + +¤ +Error( + status: Status, type: str | None, message: str | None +) + + + + + + Error when running a function. +This is not a Python exception, but potentially part of a CallResult or +Output. + + + + + Parameters: + + + + Name + Type + Description + Default + + + + + status + + Status + + + + categorization of the error. + + + + required + + + + type + + str | None + + + + arbitrary string, used for humans. Optional. + + + + required + + + + message + + str | None + + + + arbitrary message. Optional. + + + + required + + + + + + + + Raises: + + + + Type + Description + + + + + + ValueError + + + + Neither type or message was provided or status is +invalid. + + + + + + + + + + + + + + + + + + + + + + + + + + from_exception + + + + classmethod + + +¤ +from_exception( + ex: Exception, status: Status | None = None +) -> Error + + + + + Create an Error from a Python exception, using its class qualified +named as type. +The status tries to be inferred, but can be overridden. If it is not +provided or cannot be inferred, it defaults to TEMPORARY_ERROR. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + digest + + +¤ + + + + + + + + + + + + + + + + + + + + + + generate_content_digest + + +¤ +generate_content_digest(body: str | bytes) -> str + + + + + Returns a SHA-512 Content-Digest header, according to +https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-digest-headers-13 + + + + + + + + + + + + verify_content_digest + + +¤ +verify_content_digest( + digest_header: str | bytes, body: str | bytes +) + + + + + Verify a SHA-256 or SHA-512 Content-Digest header matches a +request body. + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + signature + + +¤ + + + + + + + + + + + + + + + + + + + + + + sign_request + + +¤ +sign_request( + request: Request, + key: Ed25519PrivateKey, + created: datetime, +) + + + + + Sign a request using HTTP Message Signatures. +The function adds three additional headers: Content-Digest, +Signature-Input, and Signature. See the following spec for more details: +https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures +The signature covers the request method, the URL host and path, the +Content-Type header, and the request body. At this time, an ED25519 +signature is generated with a hard-coded key ID of "default". + + + + Parameters: + + + + Name + Type + Description + Default + + + + + request + + Request + + + + The request to sign. + + + + required + + + + key + + Ed25519PrivateKey + + + + The Ed25519 private key to use to generate the signature. + + + + required + + + + created + + datetime + + + + The times at which the signature is created. + + + + required + + + + + + + + + + + + + + + + verify_request + + +¤ +verify_request( + request: Request, + key: Ed25519PublicKey, + max_age: timedelta, +) + + + + + Verify a request containing an HTTP Message Signature. +The function checks three additional headers: Content-Digest, +Signature-Input, and Signature. See the following spec for more details: +https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures +The function checks signatures that cover at least the request method, the +URL host and path, the Content-Type header, and the request body (via the +Content-Digest header). At this time, signatures must use a hard-coded key +ID of "default". + + + + Parameters: + + + + Name + Type + Description + Default + + + + + request + + Request + + + + The request to verify. + + + + required + + + + key + + Ed25519PublicKey + + + + The Ed25519 public key to use to verify the signature. + + + + required + + + + max_age + + timedelta + + + + The maximum age of the signature. + + + + required + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + key + + +¤ + + + + + + + + + + + + + + + + + + + + KeyResolver + + + + dataclass + + +¤ + + + + + Bases: HTTPSignatureKeyResolver + + + KeyResolver provides public and private keys. +At this time, multiple keys and/or key types are not supported. +Keys must be Ed25519 keys and have an ID of DEFAULT_KEY_ID. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + public_key_from_pem + + +¤ +public_key_from_pem(pem: str | bytes) -> Ed25519PublicKey + + + + + Returns an Ed25519 public key given a PEM representation. + + + + + + + + + + + + public_key_from_bytes + + +¤ +public_key_from_bytes(key: bytes) -> Ed25519PublicKey + + + + + Returns an Ed25519 public key from 32 raw bytes. + + + + + + + + + + + + private_key_from_pem + + +¤ +private_key_from_pem( + pem: str | bytes, password: bytes | None = None +) -> Ed25519PrivateKey + + + + + Returns an Ed25519 private key given a PEM representation +and optional password. + + + + + + + + + + + + private_key_from_bytes + + +¤ +private_key_from_bytes(key: bytes) -> Ed25519PrivateKey + + + + + Returns an Ed25519 private key from 32 raw bytes. + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + request + + +¤ + + + + + + + + + + + + + + + + + + + + Request + + + + dataclass + + +¤ + + + + + + A framework-agnostic representation of an HTTP request. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + status + + +¤ + + + + + + + + + + + + + + + + + + + + Status + + +¤ + + + + + Bases: int, Enum + + + Enumeration of the possible values that can be used in the return status +of functions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status_for_error + + +¤ +status_for_error(error: Exception) -> Status + + + + + Returns a Status that corresponds to the specified error. + + + + + + + + + + + + status_for_output + + +¤ +status_for_output(output: Any) -> Status + + + + + Returns a Status that corresponds to the specified output value. + + + + + + + + + + + + register_error_type + + +¤ +register_error_type( + error_type: Type[Exception], + handler: Callable[[Exception], Status], +) + + + + + Register an error type, and a handler which derives a Status from +errors of this type. + + + + + + + + + + + + register_output_type + + +¤ +register_output_type( + output_type: Type[Any], handler: Callable[[Any], Status] +) + + + + + Register an output type, and a handler which derives a Status from +outputs of this type. + + + + + + + + + + + + + + + + + + + + + + + + + +