| # | |
| # Licensed to the Apache Software Foundation (ASF) under one or more | |
| # contributor license agreements. See the NOTICE file distributed with | |
| # this work for additional information regarding copyright ownership. | |
| # The ASF licenses this file to You under the Apache License, Version 2.0 | |
| # (the "License"); you may not use this file except in compliance with | |
| # the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # | |
| """For internal use only; no backwards-compatibility guarantees.""" | |
| # pytype: skip-file | |
| # TODO(https://github.com/apache/beam/issues/18399): Issue with dill + local | |
| # classes + abc metaclass | |
| # import abc | |
| import inspect | |
| from typing import TYPE_CHECKING | |
| from typing import Any | |
| from typing import Callable | |
| from typing import Optional | |
| from typing import TypeVar | |
| from typing import Union | |
| from typing import overload | |
| from google.protobuf import message | |
| from google.protobuf import wrappers_pb2 | |
| from apache_beam.internal import pickler | |
| from apache_beam.portability.api import beam_runner_api_pb2 | |
| from apache_beam.utils import proto_utils | |
| if TYPE_CHECKING: | |
| from apache_beam.runners.pipeline_context import PipelineContext | |
| T = TypeVar('T') | |
| RunnerApiFnT = TypeVar('RunnerApiFnT', bound='RunnerApiFn') | |
| ConstructorFn = Callable[[Union['message.Message', bytes], 'PipelineContext'], | |
| Any] | |
| class RunnerApiFn(object): | |
| """Abstract base class that provides urn registration utilities. | |
| A class that inherits from this class will get a registration-based | |
| from_runner_api and to_runner_api method that convert to and from | |
| beam_runner_api_pb2.FunctionSpec. | |
| Additionally, register_pickle_urn can be called from the body of a class | |
| to register serialization via pickling. | |
| """ | |
| # TODO(https://github.com/apache/beam/issues/18399): Issue with dill + local | |
| # classes + abc metaclass | |
| # __metaclass__ = abc.ABCMeta | |
| _known_urns: dict[str, tuple[Optional[type], ConstructorFn]] = {} | |
| # @abc.abstractmethod is disabled here to avoid an error with mypy. mypy | |
| # performs abc.abtractmethod/property checks even if a class does | |
| # not use abc.ABCMeta, however, functions like `register_pickle_urn` | |
| # dynamically patch `to_runner_api_parameter`, which mypy cannot track, so | |
| # mypy incorrectly infers that this method has not been overridden with a | |
| # concrete implementation. | |
| # @abc.abstractmethod | |
| def to_runner_api_parameter( | |
| self, unused_context: 'PipelineContext') -> tuple[str, Any]: | |
| """Returns the urn and payload for this Fn. | |
| The returned urn(s) should be registered with `register_urn`. | |
| """ | |
| raise NotImplementedError | |
| @classmethod | |
| @overload | |
| def register_urn( | |
| cls, | |
| urn: str, | |
| parameter_type: type[T], | |
| ) -> Callable[[Callable[[T, 'PipelineContext'], Any]], | |
| Callable[[T, 'PipelineContext'], Any]]: | |
| pass | |
| @classmethod | |
| @overload | |
| def register_urn( | |
| cls, | |
| urn: str, | |
| parameter_type: None, | |
| ) -> Callable[[Callable[[bytes, 'PipelineContext'], Any]], | |
| Callable[[bytes, 'PipelineContext'], Any]]: | |
| pass | |
| @classmethod | |
| @overload | |
| def register_urn( | |
| cls, | |
| urn: str, | |
| parameter_type: type[T], | |
| fn: Callable[[T, 'PipelineContext'], Any]) -> None: | |
| pass | |
| @classmethod | |
| @overload | |
| def register_urn( | |
| cls, | |
| urn: str, | |
| parameter_type: None, | |
| fn: Callable[[bytes, 'PipelineContext'], Any]) -> None: | |
| pass | |
| @classmethod | |
| def register_urn(cls, urn, parameter_type, fn=None): | |
| """Registers a urn with a constructor. | |
| For example, if 'beam:fn:foo' had parameter type FooPayload, one could | |
| write `RunnerApiFn.register_urn('bean:fn:foo', FooPayload, foo_from_proto)` | |
| where foo_from_proto took as arguments a FooPayload and a PipelineContext. | |
| This function can also be used as a decorator rather than passing the | |
| callable in as the final parameter. | |
| A corresponding to_runner_api_parameter method would be expected that | |
| returns the tuple ('beam:fn:foo', FooPayload) | |
| """ | |
| def register(fn): | |
| cls._known_urns[urn] = parameter_type, fn | |
| return fn | |
| if fn: | |
| # Used as a statement. | |
| register(fn) | |
| else: | |
| # Used as a decorator. | |
| return register | |
| @classmethod | |
| def register_pickle_urn(cls, pickle_urn): | |
| """Registers and implements the given urn via pickling. | |
| """ | |
| inspect.currentframe().f_back.f_locals['to_runner_api_parameter'] = ( | |
| lambda self, context: | |
| (pickle_urn, wrappers_pb2.BytesValue(value=pickler.dumps(self)))) | |
| cls.register_urn( | |
| pickle_urn, | |
| wrappers_pb2.BytesValue, | |
| lambda proto, unused_context: pickler.loads(proto.value)) | |
| def to_runner_api( | |
| self, context: 'PipelineContext') -> beam_runner_api_pb2.FunctionSpec: | |
| """Returns an FunctionSpec encoding this Fn. | |
| Prefer overriding self.to_runner_api_parameter. | |
| """ | |
| urn, typed_param = self.to_runner_api_parameter(context) | |
| return beam_runner_api_pb2.FunctionSpec( | |
| urn=urn, | |
| payload=typed_param.SerializeToString() if isinstance( | |
| typed_param, message.Message) else typed_param) | |
| @classmethod | |
| def from_runner_api( | |
| cls: type[RunnerApiFnT], | |
| fn_proto: beam_runner_api_pb2.FunctionSpec, | |
| context: 'PipelineContext') -> RunnerApiFnT: | |
| """Converts from an FunctionSpec to a Fn object. | |
| Prefer registering a urn with its parameter type and constructor. | |
| """ | |
| parameter_type, constructor = cls._known_urns[fn_proto.urn] | |
| return constructor( | |
| proto_utils.parse_Bytes(fn_proto.payload, parameter_type), context) |