|
16 | 16 | import uuid as uuid_module |
17 | 17 | from collections import defaultdict |
18 | 18 | from datetime import datetime |
19 | | -from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple |
| 19 | +from typing import ( |
| 20 | + Any, |
| 21 | + Callable, |
| 22 | + Dict, |
| 23 | + List, |
| 24 | + Literal, |
| 25 | + Mapping, |
| 26 | + Optional, |
| 27 | + Sequence, |
| 28 | + Tuple, |
| 29 | + Union, |
| 30 | +) |
20 | 31 |
|
21 | 32 | import requests |
22 | 33 | from pydantic import StrictStr |
23 | 34 |
|
24 | 35 | from feast import Entity, FeatureView, RepoConfig |
| 36 | +from feast.feature_service import FeatureService |
25 | 37 | from feast.infra.online_stores.helpers import _to_naive_utc |
26 | 38 | from feast.infra.online_stores.online_store import OnlineStore |
| 39 | +from feast.infra.registry.base_registry import BaseRegistry |
| 40 | +from feast.online_response import OnlineResponse |
27 | 41 | from feast.permissions.client.http_auth_requests_wrapper import HttpSessionManager |
| 42 | +from feast.protos.feast.serving.ServingService_pb2 import ( |
| 43 | + FieldStatus, |
| 44 | + GetOnlineFeaturesResponse, |
| 45 | + GetOnlineFeaturesResponseMetadata, |
| 46 | +) |
28 | 47 | from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto |
| 48 | +from feast.protos.feast.types.Value_pb2 import RepeatedValue |
29 | 49 | from feast.protos.feast.types.Value_pb2 import Value as ValueProto |
30 | 50 | from feast.repo_config import FeastConfigBaseModel |
31 | 51 | from feast.rest_error_handler import rest_error_handling_decorator |
@@ -132,6 +152,88 @@ def _proto_value_to_transport_value(proto_value: ValueProto) -> Any: |
132 | 152 |
|
133 | 153 | return feast_value_type_to_python_type(proto_value) |
134 | 154 |
|
| 155 | + _STATUS_MAP = { |
| 156 | + "PRESENT": FieldStatus.PRESENT, |
| 157 | + "NOT_FOUND": FieldStatus.NOT_FOUND, |
| 158 | + "NULL_VALUE": FieldStatus.NULL_VALUE, |
| 159 | + "OUTSIDE_MAX_AGE": FieldStatus.OUTSIDE_MAX_AGE, |
| 160 | + } |
| 161 | + |
| 162 | + def get_online_features( |
| 163 | + self, |
| 164 | + config: RepoConfig, |
| 165 | + features: Union[List[str], FeatureService], |
| 166 | + entity_rows: Union[ |
| 167 | + List[Dict[str, Any]], |
| 168 | + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], |
| 169 | + ], |
| 170 | + registry: BaseRegistry, |
| 171 | + project: str, |
| 172 | + full_feature_names: bool = False, |
| 173 | + include_feature_view_version_metadata: bool = False, |
| 174 | + ) -> OnlineResponse: |
| 175 | + assert isinstance(config.online_store, RemoteOnlineStoreConfig) |
| 176 | + |
| 177 | + if isinstance(entity_rows, list): |
| 178 | + columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()} |
| 179 | + for entity_row in entity_rows: |
| 180 | + for key, value in entity_row.items(): |
| 181 | + columnar[key].append(value) |
| 182 | + entity_rows = columnar |
| 183 | + |
| 184 | + entities: Dict[str, List[Any]] = {} |
| 185 | + for k, vals in entity_rows.items(): |
| 186 | + iterable = vals.val if isinstance(vals, RepeatedValue) else vals |
| 187 | + entities[k] = [_json_safe(v) for v in iterable] |
| 188 | + |
| 189 | + req_body: Dict[str, Any] = { |
| 190 | + "entities": entities, |
| 191 | + "full_feature_names": full_feature_names, |
| 192 | + "include_feature_view_version_metadata": include_feature_view_version_metadata, |
| 193 | + } |
| 194 | + |
| 195 | + if isinstance(features, FeatureService): |
| 196 | + req_body["feature_service"] = features.name |
| 197 | + else: |
| 198 | + req_body["features"] = features |
| 199 | + |
| 200 | + response = get_remote_online_features(config=config, req_body=req_body) |
| 201 | + |
| 202 | + if response.status_code != 200: |
| 203 | + raise RuntimeError( |
| 204 | + f"Failed to get online features: {response.status_code} {response.text}" |
| 205 | + ) |
| 206 | + |
| 207 | + resp_json = response.json() |
| 208 | + return self._build_online_response_from_json(resp_json) |
| 209 | + |
| 210 | + def _build_online_response_from_json( |
| 211 | + self, resp_json: Dict[str, Any] |
| 212 | + ) -> OnlineResponse: |
| 213 | + proto = GetOnlineFeaturesResponse() |
| 214 | + |
| 215 | + metadata = GetOnlineFeaturesResponseMetadata() |
| 216 | + feature_names = resp_json.get("metadata", {}).get("feature_names", []) |
| 217 | + metadata.feature_names.val.extend(feature_names) |
| 218 | + proto.metadata.CopyFrom(metadata) |
| 219 | + |
| 220 | + for result in resp_json.get("results", []): |
| 221 | + fv = GetOnlineFeaturesResponse.FeatureVector() |
| 222 | + for val in result.get("values", []): |
| 223 | + if val is None: |
| 224 | + fv.values.append(ValueProto()) |
| 225 | + else: |
| 226 | + protos = python_values_to_proto_values([val]) |
| 227 | + fv.values.append(protos[0]) |
| 228 | + for status_str in result.get("statuses", []): |
| 229 | + fv.statuses.append( |
| 230 | + self._STATUS_MAP.get(status_str, FieldStatus.INVALID) |
| 231 | + ) |
| 232 | + proto.results.append(fv) |
| 233 | + |
| 234 | + proto.status = True |
| 235 | + return OnlineResponse(proto) |
| 236 | + |
135 | 237 | def online_write_batch( |
136 | 238 | self, |
137 | 239 | config: RepoConfig, |
|
0 commit comments