Skip to content

Commit a8bbeaa

Browse files
committed
sdks/python: enrich data with tecton feature store
1 parent c4f9998 commit a8bbeaa

1 file changed

Lines changed: 227 additions & 0 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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, Union
22+
from typing import Optional
23+
import numpy
24+
25+
import apache_beam as beam
26+
from apache_beam.transforms.enrichment import EnrichmentSourceHandler
27+
from apache_beam.transforms.enrichment_handlers.utils import ExceptionLevel
28+
import tecton
29+
30+
import sys
31+
from io import StringIO
32+
33+
__all__ = [
34+
'TectonFeatureStoreEnrichmentHandler',
35+
]
36+
37+
EntityRowFn = Callable[[beam.Row], Mapping[str, Any]]
38+
39+
_LOGGER = logging.getLogger(__name__)
40+
41+
42+
@dataclass
43+
class TectonConnectionConfig:
44+
"""Configuration dataclass for Tecton connection parameters.
45+
46+
This dataclass contains the essential connection parameters needed to
47+
establish a connection with a Tecton feature store instance.
48+
49+
Attributes:
50+
url: The URL of the Tecton instance to connect to.
51+
Example: 'https://your-instance.tecton.ai'
52+
api_key: The API key for authenticating with the Tecton instance.
53+
This should be a valid API key with appropriate permissions.
54+
"""
55+
url: str
56+
api_key: str
57+
58+
def __post_init__(self):
59+
if not self.url:
60+
raise ValueError('Please provide a Tecton instance URL (`url`).')
61+
62+
if not self.api_key:
63+
raise ValueError('Please provide an API key (`api_key`).')
64+
65+
@dataclass
66+
class TectonFeaturesRetrievalConfig:
67+
"""Configuration dataclass for Tecton feature retrieval parameters.
68+
69+
This dataclass contains the parameters needed to retrieve features from
70+
a Tecton feature store, including entity identification and feature
71+
service configuration.
72+
73+
Attributes:
74+
feature_service_name: The name of the feature service containing the
75+
features to fetch from the online Tecton feature store. This should
76+
match a feature service defined in your Tecton workspace.
77+
workspace_name: workspace name to use for feature retrieval.
78+
entity_id: The entity name for the entity associated with the features.
79+
The `entity_id` is used to extract the entity value from the input row.
80+
Please provide exactly one of `entity_id` or `entity_row_fn`.
81+
entity_row_fn: A lambda function that takes an input `beam.Row` and
82+
returns a dictionary with a mapping from the entity key column name to
83+
entity key value. It is used to build/extract the entity dict for
84+
feature retrieval. Please provide exactly one of `entity_id` or
85+
`entity_row_fn`.
86+
include_join_keys_in_response: Whether to include join keys as part of
87+
the response FeatureVector. Defaults to False.
88+
request_data: Optional mapping of request context parameters to pass to
89+
Tecton for real-time feature computation. These are typically used for
90+
RealtimeFeatureViews that depend on request-time data. Defaults to None.
91+
return_effective_times: Whether to include effective times when converting
92+
FeatureVector to dictionary. Effective times indicate when each feature
93+
value became valid. Defaults to False.
94+
"""
95+
feature_service_name: str
96+
workspace_name: str
97+
entity_id: str = ""
98+
entity_row_fn: Optional[EntityRowFn] = None
99+
include_join_keys_in_response: bool = False
100+
request_data: Optional[Mapping[str, Any]] = None
101+
return_effective_times: bool = False
102+
103+
def __post_init__(self):
104+
if not self.feature_service_name:
105+
raise ValueError(
106+
'Please provide a feature service name for the Tecton '
107+
'online feature store (`feature_service_name`).')
108+
109+
if not self.workspace_name:
110+
raise ValueError(
111+
'Please provide a workspace name for the Tecton '
112+
'online feature store (`workspace_name`).')
113+
114+
if ((not self.entity_row_fn and not self.entity_id) or
115+
bool(self.entity_row_fn and self.entity_id)):
116+
raise ValueError(
117+
"Please specify exactly one of a `entity_id` or a lambda "
118+
"function with `entity_row_fn` to extract the entity id "
119+
"from the input row.")
120+
121+
class TectonFeatureStoreEnrichmentHandler(EnrichmentSourceHandler[beam.Row,
122+
beam.Row]):
123+
"""Enrichment handler to interact with the Tecton feature store.
124+
125+
This handler fetches features from Tecton's online feature store using
126+
a feature service name.
127+
128+
Use this handler with :class:`apache_beam.transforms.enrichment.Enrichment`
129+
transform. To filter the features to enrich, use the `join_fn` param in
130+
:class:`apache_beam.transforms.enrichment.Enrichment`.
131+
"""
132+
def __init__(
133+
self,
134+
connection_config: TectonConnectionConfig,
135+
features_retrieval_config: TectonFeaturesRetrievalConfig,
136+
*,
137+
exception_level: ExceptionLevel = ExceptionLevel.WARN,
138+
):
139+
"""Initializes an instance of `TectonFeatureStoreEnrichmentHandler`.
140+
141+
Args:
142+
connection_config: A `TectonConnectionConfig` dataclass containing
143+
connection parameters (url, workspace_name, api_key).
144+
features_retrieval_config: A `TectonFeaturesRetrievalConfig` dataclass
145+
containing feature retrieval parameters (feature_service_name,
146+
entity_id, entity_row_fn).
147+
exception_level: a `enum.Enum` value from
148+
`apache_beam.transforms.enrichment_handlers.utils.ExceptionLevel`
149+
to set the level when `None` feature values are fetched from the
150+
online Tecton store. Defaults to `ExceptionLevel.WARN`.
151+
"""
152+
self._connection_config = connection_config
153+
self._features_retrieval_config = features_retrieval_config
154+
self._exception_level = exception_level
155+
156+
def __enter__(self):
157+
"""Connect to the Tecton feature store."""
158+
# Suppress Tecton SDK output to avoid cluttering test output. Redirect
159+
# stdout to suppress success messages.
160+
original_stdout = sys.stdout
161+
sys.stdout = StringIO()
162+
try:
163+
tecton.login(
164+
tecton_url=self._connection_config.url,
165+
tecton_api_key=self._connection_config.api_key)
166+
finally:
167+
sys.stdout = original_stdout
168+
169+
self._feature_service = tecton.get_feature_service(
170+
name=self._features_retrieval_config.feature_service_name,
171+
workspace=self._features_retrieval_config.workspace_name)
172+
173+
def __call__(self, request: beam.Row, *args, **kwargs):
174+
"""Fetches feature values for an entity-id from the Tecton feature store."""
175+
if self._features_retrieval_config.entity_row_fn:
176+
entity = self._features_retrieval_config.entity_row_fn(request)
177+
else:
178+
request_dict = request._asdict()
179+
entity = {
180+
self._features_retrieval_config.entity_id:
181+
request_dict[self._features_retrieval_config.entity_id]
182+
}
183+
184+
try:
185+
response = self._feature_service.get_online_features(
186+
join_keys=entity,
187+
include_join_keys_in_response=self._features_retrieval_config.include_join_keys_in_response,
188+
request_data=self._features_retrieval_config.request_data
189+
)
190+
191+
feature_values = response.to_dict(
192+
self._features_retrieval_config.return_effective_times
193+
)
194+
except Exception as e:
195+
if self._exception_level == ExceptionLevel.RAISE:
196+
raise RuntimeError(
197+
f'Failed to fetch features from Tecton feature store: {e}')
198+
elif self._exception_level == ExceptionLevel.WARN:
199+
_LOGGER.warning(
200+
f'Failed to fetch features from Tecton feature store: {e}')
201+
feature_values = {}
202+
else: # ExceptionLevel.QUIET
203+
feature_values = {}
204+
205+
return request, beam.Row(**feature_values)
206+
207+
def __exit__(self, exc_type, exc_val, exc_tb):
208+
"""Clean the Tecton feature store connection."""
209+
# Suppress Tecton SDK output during teardown. Redirect stdout to suppress
210+
# logout messages.
211+
original_stdout = sys.stdout
212+
sys.stdout = StringIO()
213+
try:
214+
tecton.logout()
215+
finally:
216+
sys.stdout = original_stdout
217+
218+
def get_cache_key(self, request: beam.Row) -> str:
219+
"""Returns a string formatted with unique entity-id for the feature values.
220+
"""
221+
if self._features_retrieval_config.entity_row_fn:
222+
entity = self._features_retrieval_config.entity_row_fn(request)
223+
entity_id = list(entity.keys())[0]
224+
else:
225+
entity_id = self._features_retrieval_config.entity_id
226+
return f'entity_id: {request._asdict()[entity_id]}'
227+

0 commit comments

Comments
 (0)