Skip to content

Commit 19f3622

Browse files
committed
sdks/python: enrich data with tecton feature store
1 parent 626be06 commit 19f3622

1 file changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
from dataclasses import dataclass, field
18+
import logging
19+
from collections.abc import Callable
20+
from collections.abc import Mapping
21+
from typing import Any, Dict
22+
from typing import Optional
23+
24+
import apache_beam as beam
25+
from apache_beam.transforms.enrichment import EnrichmentSourceHandler
26+
from apache_beam.transforms.enrichment_handlers.utils import ExceptionLevel
27+
from tecton_client import TectonClient, MetadataOptions, RequestOptions
28+
29+
__all__ = [
30+
'TectonFeatureStoreEnrichmentHandler',
31+
]
32+
33+
EntityRowFn = Callable[[beam.Row], Mapping[str, Any]]
34+
35+
_LOGGER = logging.getLogger(__name__)
36+
37+
38+
@dataclass
39+
class TectonConnectionConfig:
40+
"""Configuration dataclass for Tecton connection parameters.
41+
42+
This dataclass contains the essential connection parameters needed to
43+
establish a connection with a Tecton feature store instance.
44+
45+
Attributes:
46+
url: The URL of the Tecton instance to connect to.
47+
Example: 'https://your-instance.tecton.ai'
48+
default_workspace_name: The name of the workspace containing the feature
49+
service. This is the workspace where your feature definitions are stored.
50+
api_key: The API key for authenticating with the Tecton instance.
51+
This should be a valid API key with appropriate permissions.
52+
kwargs: Additional keyword arguments for write operations. Enables forward
53+
compatibility with future Tecton connection parameters.
54+
"""
55+
url: str
56+
default_workspace_name: str
57+
api_key: str
58+
kwargs: Dict[str, Any] = field(default_factory=dict)
59+
60+
def __post_init__(self):
61+
if not self.url:
62+
raise ValueError('Please provide a Tecton instance URL (`url`).')
63+
64+
if not self.default_workspace_name:
65+
raise ValueError(
66+
'Please provide a workspace name (`default_workspace_name`).')
67+
68+
if not self.api_key:
69+
raise ValueError('Please provide an API key (`api_key`).')
70+
71+
@dataclass
72+
class TectonFeaturesRetrievalConfig:
73+
"""Configuration dataclass for Tecton feature retrieval parameters.
74+
75+
This dataclass contains the parameters needed to retrieve features from
76+
a Tecton feature store, including entity identification and feature
77+
service configuration.
78+
79+
Attributes:
80+
feature_service_name: The name of the feature service containing the
81+
features to fetch from the online Tecton feature store. This should
82+
match a feature service defined in your Tecton workspace.
83+
entity_id: The entity name for the entity associated with the features.
84+
The `entity_id` is used to extract the entity value from the input row.
85+
Please provide exactly one of `entity_id` or `entity_row_fn`.
86+
entity_row_fn: A lambda function that takes an input `beam.Row` and
87+
returns a dictionary with a mapping from the entity key column name to
88+
entity key value. It is used to build/extract the entity dict for
89+
feature retrieval. Please provide exactly one of `entity_id` or
90+
`entity_row_fn`.
91+
request_context_map: Optional mapping of request context parameters
92+
to pass to Tecton for feature computation. These are typically used
93+
for real-time features that depend on request-time data.
94+
workspace_name: Optional workspace name override. If not provided,
95+
uses the workspace from the connection config.
96+
allow_partial_results: Whether to allow partial results if some features
97+
fail to compute. Defaults to False.
98+
request_options: Optional RequestOptions for controlling request behavior.
99+
Defaults to None.
100+
metadata_options: Optional MetadataOptions for controlling what metadata
101+
is returned. Defaults to
102+
MetadataOptions(include_names=True, include_data_types=True).
103+
kwargs: Additional keyword arguments for feature retrieval. Enables forward
104+
compatibility with future Tecton feature retrieval parameters.
105+
"""
106+
feature_service_name: str
107+
entity_id: str = ""
108+
entity_row_fn: Optional[EntityRowFn] = None
109+
request_context_map: Optional[Mapping[str, Any]] = None
110+
workspace_name: Optional[str] = None
111+
allow_partial_results: bool = False
112+
request_options: Optional[RequestOptions] = None
113+
metadata_options: Optional[MetadataOptions] = field(
114+
default_factory=lambda: MetadataOptions(include_names=True,
115+
include_data_types=True))
116+
kwargs: Dict[str, Any] = field(default_factory=dict)
117+
118+
def __post_init__(self):
119+
if not self.feature_service_name:
120+
raise ValueError(
121+
'Please provide a feature service name for the Tecton '
122+
'online feature store (`feature_service_name`).')
123+
124+
if ((not self.entity_row_fn and not self.entity_id) or
125+
bool(self.entity_row_fn and self.entity_id)):
126+
raise ValueError(
127+
"Please specify exactly one of a `entity_id` or a lambda "
128+
"function with `entity_row_fn` to extract the entity id "
129+
"from the input row.")
130+
131+
class TectonFeatureStoreEnrichmentHandler(EnrichmentSourceHandler[beam.Row,
132+
beam.Row]):
133+
"""Enrichment handler to interact with the Tecton feature store.
134+
135+
This handler fetches features from Tecton's online feature store using
136+
a feature service name.
137+
138+
Use this handler with :class:`apache_beam.transforms.enrichment.Enrichment`
139+
transform. To filter the features to enrich, use the `join_fn` param in
140+
:class:`apache_beam.transforms.enrichment.Enrichment`.
141+
"""
142+
def __init__(
143+
self,
144+
connection_config: TectonConnectionConfig,
145+
features_retrieval_config: TectonFeaturesRetrievalConfig,
146+
*,
147+
exception_level: ExceptionLevel = ExceptionLevel.WARN,
148+
):
149+
"""Initializes an instance of `TectonFeatureStoreEnrichmentHandler`.
150+
151+
Args:
152+
connection_config: A `TectonConnectionConfig` dataclass containing
153+
connection parameters (url, workspace_name, api_key).
154+
features_retrieval_config: A `TectonFeaturesRetrievalConfig` dataclass
155+
containing feature retrieval parameters (feature_service_name,
156+
entity_id, entity_row_fn).
157+
exception_level: a `enum.Enum` value from
158+
`apache_beam.transforms.enrichment_handlers.utils.ExceptionLevel`
159+
to set the level when `None` feature values are fetched from the
160+
online Tecton store. Defaults to `ExceptionLevel.WARN`.
161+
"""
162+
self._connection_config = connection_config
163+
self._features_retrieval_config = features_retrieval_config
164+
self._exception_level = exception_level
165+
166+
def __enter__(self):
167+
"""Connect with the Tecton feature store."""
168+
self._client = TectonClient(
169+
**unpack_dataclass_with_kwargs(self._connection_config))
170+
171+
def __call__(self, request: beam.Row, *args, **kwargs):
172+
"""Fetches feature values for an entity-id from the Tecton feature store.
173+
174+
Args:
175+
request: the input `beam.Row` to enrich.
176+
"""
177+
if self._features_retrieval_config.entity_row_fn:
178+
entity = self._features_retrieval_config.entity_row_fn(request)
179+
else:
180+
request_dict = request._asdict()
181+
entity = {
182+
self._features_retrieval_config.entity_id:
183+
request_dict[self._features_retrieval_config.entity_id]
184+
}
185+
186+
try:
187+
config = unpack_dataclass_with_kwargs(self._features_retrieval_config)
188+
config.pop('entity_id', None)
189+
config.pop('entity_row_fn', None)
190+
response = self._client.get_features(**config,join_key_map=entity)
191+
feature_values = response.get_features_dict()
192+
except Exception as e:
193+
if self._exception_level == ExceptionLevel.RAISE:
194+
raise RuntimeError(
195+
f'Failed to fetch features from Tecton feature store: {e}')
196+
elif self._exception_level == ExceptionLevel.WARN:
197+
_LOGGER.warning(
198+
f'Failed to fetch features from Tecton feature store: {e}')
199+
feature_values = {}
200+
else: # ExceptionLevel.QUIET
201+
feature_values = {}
202+
203+
return request, beam.Row(**feature_values)
204+
205+
def __exit__(self, exc_type, exc_val, exc_tb):
206+
"""Clean the instantiated Tecton client."""
207+
self._client._client.close()
208+
self._client = None
209+
210+
def get_cache_key(self, request: beam.Row) -> str:
211+
"""Returns a string formatted with unique entity-id for the feature values.
212+
"""
213+
if self._features_retrieval_config.entity_row_fn:
214+
entity = self._features_retrieval_config.entity_row_fn(request)
215+
entity_id = list(entity.keys())[0]
216+
else:
217+
entity_id = self._features_retrieval_config.entity_id
218+
return f'entity_id: {request._asdict()[entity_id]}'
219+
220+
221+
def unpack_dataclass_with_kwargs(dataclass_instance):
222+
"""Unpacks dataclass fields into a flat dict, merging kwargs with precedence.
223+
224+
Args:
225+
dataclass_instance: Dataclass instance to unpack.
226+
227+
Returns:
228+
dict: Flattened dictionary with kwargs taking precedence over fields.
229+
"""
230+
params: dict = dataclass_instance.__dict__.copy()
231+
nested_kwargs = params.pop('kwargs', {})
232+
return {**params, **nested_kwargs}

0 commit comments

Comments
 (0)