| # | |
| # 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. | |
| # | |
| import logging | |
| from collections.abc import Callable | |
| from datetime import timedelta | |
| from typing import Any | |
| from typing import Optional | |
| from typing import TypeVar | |
| from typing import Union | |
| import apache_beam as beam | |
| from apache_beam.coders import coders | |
| from apache_beam.io.requestresponse import DEFAULT_CACHE_ENTRY_TTL_SEC | |
| from apache_beam.io.requestresponse import DEFAULT_TIMEOUT_SECS | |
| from apache_beam.io.requestresponse import Caller | |
| from apache_beam.io.requestresponse import DefaultThrottler | |
| from apache_beam.io.requestresponse import ExponentialBackOffRepeater | |
| from apache_beam.io.requestresponse import PreCallThrottler | |
| from apache_beam.io.requestresponse import RedisCache | |
| from apache_beam.io.requestresponse import Repeater | |
| from apache_beam.io.requestresponse import RequestResponseIO | |
| __all__ = [ | |
| "EnrichmentSourceHandler", | |
| "Enrichment", | |
| "cross_join", | |
| ] | |
| InputT = TypeVar('InputT') | |
| OutputT = TypeVar('OutputT') | |
| JoinFn = Callable[[dict[str, Any], dict[str, Any]], beam.Row] | |
| _LOGGER = logging.getLogger(__name__) | |
| def has_valid_redis_address(host: str, port: int) -> bool: | |
| """returns `True` if both host and port are not `None`.""" | |
| if host and port: | |
| return True | |
| return False | |
| def cross_join(left: dict[str, Any], right: dict[str, Any]) -> beam.Row: | |
| """performs a cross join on two `dict` objects. | |
| Joins the columns of the right row onto the left row. | |
| Args: | |
| left (dict[str, Any]): input request dictionary. | |
| right (dict[str, Any]): response dictionary from the API. | |
| Returns: | |
| `beam.Row` containing the merged columns. | |
| """ | |
| for k, v in right.items(): | |
| if k not in left: | |
| # Don't override the values in left. | |
| left[k] = v | |
| elif left[k] != v: | |
| _LOGGER.warning( | |
| '%s exists in the input row as well the row fetched ' | |
| 'from API but have different values - %s and %s. Using the input ' | |
| 'value (%s) for the enriched row. You can override this behavior by ' | |
| 'passing a custom `join_fn` to Enrichment transform.' % | |
| (k, left[k], v, left[k])) | |
| return beam.Row(**left) | |
| class EnrichmentSourceHandler(Caller[InputT, OutputT]): | |
| """Wrapper class for `apache_beam.io.requestresponse.Caller`. | |
| Ensure that the implementation of ``__call__`` method returns a tuple | |
| of `beam.Row` objects. | |
| """ | |
| def get_cache_key(self, request: InputT) -> str: | |
| """Returns the request to be cached. This is how the response will be | |
| looked up in the cache as well. | |
| Implement this method to provide the key for the cache. | |
| By default, the entire request is stored as the cache key. | |
| For example, in `BigTableEnrichmentHandler`, the row key for the element | |
| is returned here. | |
| """ | |
| return "request: %s" % request | |
| class Enrichment(beam.PTransform[beam.PCollection[InputT], | |
| beam.PCollection[OutputT]]): | |
| """A :class:`apache_beam.transforms.enrichment.Enrichment` transform to | |
| enrich elements in a PCollection. | |
| Uses the :class:`apache_beam.transforms.enrichment.EnrichmentSourceHandler` | |
| to enrich elements by joining the metadata from external source. | |
| Processes an input :class:`~apache_beam.pvalue.PCollection` of `beam.Row` by | |
| applying a :class:`apache_beam.transforms.enrichment.EnrichmentSourceHandler` | |
| to each element and returning the enriched | |
| :class:`~apache_beam.pvalue.PCollection`. | |
| Args: | |
| source_handler: Handles source lookup and metadata retrieval. | |
| Implements the | |
| :class:`apache_beam.transforms.enrichment.EnrichmentSourceHandler` | |
| join_fn: A lambda function to join original element with lookup metadata. | |
| Defaults to `CROSS_JOIN`. | |
| timeout: (Optional) timeout for source requests. Defaults to 30 seconds. | |
| repeater: provides method to repeat failed requests to API due to service | |
| errors. Defaults to | |
| :class:`apache_beam.io.requestresponse.ExponentialBackOffRepeater` to | |
| repeat requests with exponential backoff. | |
| throttler: provides methods to pre-throttle a request. Defaults to | |
| :class:`apache_beam.io.requestresponse.DefaultThrottler` for | |
| client-side adaptive throttling using | |
| :class:`apache_beam.io.components.adaptive_throttler.AdaptiveThrottler`. | |
| """ | |
| def __init__( | |
| self, | |
| source_handler: EnrichmentSourceHandler, | |
| join_fn: Optional[JoinFn] = None, | |
| timeout: Optional[float] = DEFAULT_TIMEOUT_SECS, | |
| repeater: Repeater = ExponentialBackOffRepeater(), | |
| throttler: PreCallThrottler = DefaultThrottler(), | |
| use_custom_types: bool = False): | |
| self._cache = None | |
| self._source_handler = source_handler | |
| self._join_fn = ( | |
| join_fn if join_fn else source_handler.join_fn if hasattr( | |
| source_handler, 'join_fn') else cross_join) | |
| self._timeout = timeout | |
| self._repeater = repeater | |
| self._throttler = throttler | |
| self._use_custom_types = ( | |
| source_handler.use_custom_types if hasattr( | |
| source_handler, 'use_custom_types') else use_custom_types) | |
| def expand(self, | |
| input_row: beam.PCollection[InputT]) -> beam.PCollection[OutputT]: | |
| # For caching with enrichment transform, enrichment handlers provide a | |
| # get_cache_key() method that returns a unique string formatted | |
| # request for that row. | |
| request_coder = coders.StrUtf8Coder() | |
| if self._cache: | |
| self._cache.request_coder = request_coder | |
| fetched_data = ( | |
| input_row | |
| | "Enrichment-RRIO" >> RequestResponseIO( | |
| caller=self._source_handler, | |
| timeout=self._timeout, | |
| repeater=self._repeater, | |
| cache=self._cache, | |
| throttler=self._throttler)) | |
| # EnrichmentSourceHandler returns a tuple of (request,response). | |
| return ( | |
| fetched_data | |
| | "enrichment_join" >> beam.Map( | |
| lambda x: self._join_fn(x[0]._asdict(), x[1]._asdict()) | |
| if not self._use_custom_types else self._join_fn(x[0], x[1]))) | |
| def with_redis_cache( | |
| self, | |
| host: str, | |
| port: int, | |
| time_to_live: Union[int, timedelta] = DEFAULT_CACHE_ENTRY_TTL_SEC, | |
| *, | |
| request_coder: Optional[coders.Coder] = None, | |
| response_coder: Optional[coders.Coder] = None, | |
| **kwargs, | |
| ): | |
| """Configure the Redis cache to use with enrichment transform. | |
| Args: | |
| host (str): The hostname or IP address of the Redis server. | |
| port (int): The port number of the Redis server. | |
| time_to_live: `(Union[int, timedelta])` The time-to-live (TTL) for | |
| records stored in Redis. Provide an integer (in seconds) or a | |
| `datetime.timedelta` object. | |
| request_coder: (Optional[`coders.Coder`]) coder for requests stored | |
| in Redis. | |
| response_coder: (Optional[`coders.Coder`]) coder for decoding responses | |
| received from Redis. | |
| kwargs: Optional additional keyword arguments that | |
| are required to connect to your redis server. Same as `redis.Redis()`. | |
| """ | |
| if has_valid_redis_address(host, port): | |
| self._cache = RedisCache( # type: ignore[assignment] | |
| host=host, | |
| port=port, | |
| time_to_live=time_to_live, | |
| request_coder=request_coder, | |
| response_coder=response_coder, | |
| **kwargs) | |
| return self |