|
1 | 1 | from datetime import datetime |
2 | | -from typing import Dict, List, Optional, Union |
| 2 | +from typing import TYPE_CHECKING, Dict, List, Optional, Union |
3 | 3 |
|
4 | 4 | from google.protobuf.json_format import MessageToJson |
5 | 5 | from typeguard import typechecked |
6 | 6 |
|
7 | 7 | from feast.base_feature_view import BaseFeatureView |
8 | | -from feast.errors import FeatureViewMissingDuringFeatureServiceInference |
| 8 | +from feast.errors import ( |
| 9 | + FeastObjectNotFoundException, |
| 10 | + FeatureViewMissingDuringFeatureServiceInference, |
| 11 | +) |
9 | 12 | from feast.feature_logging import LoggingConfig |
10 | 13 | from feast.feature_view import FeatureView |
11 | 14 | from feast.feature_view_projection import FeatureViewProjection |
| 15 | +from feast.field import Field |
12 | 16 | from feast.labeling.label_view import LabelView |
13 | 17 | from feast.on_demand_feature_view import OnDemandFeatureView |
14 | 18 | from feast.protos.feast.core.FeatureService_pb2 import ( |
|
21 | 25 | FeatureServiceSpec as FeatureServiceSpecProto, |
22 | 26 | ) |
23 | 27 |
|
| 28 | +if TYPE_CHECKING: |
| 29 | + from feast.infra.registry.base_registry import BaseRegistry |
| 30 | + |
24 | 31 |
|
25 | 32 | @typechecked |
26 | 33 | class FeatureService: |
@@ -161,6 +168,119 @@ def infer_features( |
161 | 168 | f'{type(feature_grouping)} as part of the "features" argument.)' |
162 | 169 | ) |
163 | 170 |
|
| 171 | + def prepare_for_apply( |
| 172 | + self, |
| 173 | + registry: "BaseRegistry", |
| 174 | + project: str, |
| 175 | + allow_cache: bool = False, |
| 176 | + ) -> "FeatureService": |
| 177 | + """ |
| 178 | + Materialize feature view projections before registry apply. |
| 179 | +
|
| 180 | + Uses the same FeatureService construction and ``infer_features`` path as |
| 181 | + ``FeatureStore.apply`` for SDK-defined services. |
| 182 | +
|
| 183 | + When the service is already fully resolved (SDK path where _features is |
| 184 | + set and projections already have features populated via infer_features, |
| 185 | + OR the proto deserialization path where projections carry full dtype |
| 186 | + info), this is a no-op. |
| 187 | + """ |
| 188 | + from feast.types import Invalid |
| 189 | + |
| 190 | + if self._features and all(p.features for p in self.feature_view_projections): |
| 191 | + return self |
| 192 | + |
| 193 | + if ( |
| 194 | + not self._features |
| 195 | + and self.feature_view_projections |
| 196 | + and all( |
| 197 | + p.features and all(f.dtype != Invalid for f in p.features) |
| 198 | + for p in self.feature_view_projections |
| 199 | + ) |
| 200 | + ): |
| 201 | + return self |
| 202 | + |
| 203 | + fvs_to_update: Dict[str, Union[FeatureView, BaseFeatureView]] = {} |
| 204 | + |
| 205 | + if self._features: |
| 206 | + for feature_grouping in self._features: |
| 207 | + if isinstance(feature_grouping, BaseFeatureView): |
| 208 | + fvs_to_update[feature_grouping.name] = ( |
| 209 | + registry.get_any_feature_view( |
| 210 | + feature_grouping.name, project, allow_cache=allow_cache |
| 211 | + ) |
| 212 | + ) |
| 213 | + self.infer_features(fvs_to_update=fvs_to_update) |
| 214 | + return self |
| 215 | + |
| 216 | + resolved_features: List[Union[FeatureView, OnDemandFeatureView, LabelView]] = [] |
| 217 | + for projection in self.feature_view_projections: |
| 218 | + try: |
| 219 | + feature_view = registry.get_any_feature_view( |
| 220 | + projection.name, project, allow_cache=allow_cache |
| 221 | + ) |
| 222 | + except FeastObjectNotFoundException as exc: |
| 223 | + raise FeastObjectNotFoundException( |
| 224 | + f"Feature view '{projection.name}' not found in project '{project}'" |
| 225 | + ) from exc |
| 226 | + |
| 227 | + if not isinstance( |
| 228 | + feature_view, (FeatureView, OnDemandFeatureView, LabelView) |
| 229 | + ): |
| 230 | + raise ValueError( |
| 231 | + f"Cannot resolve projection for feature view '{projection.name}'" |
| 232 | + ) |
| 233 | + |
| 234 | + fvs_to_update[feature_view.name] = feature_view |
| 235 | + features_by_name = { |
| 236 | + feature.name: feature for feature in feature_view.features |
| 237 | + } |
| 238 | + |
| 239 | + if self._projection_matches_registry_features(projection, features_by_name): |
| 240 | + resolved_features.append(feature_view.with_projection(projection)) |
| 241 | + elif projection.desired_features: |
| 242 | + resolved_features.append( |
| 243 | + feature_view[list(projection.desired_features)] |
| 244 | + ) |
| 245 | + elif not projection.features: |
| 246 | + resolved_features.append(feature_view) |
| 247 | + else: |
| 248 | + resolved_features.append( |
| 249 | + feature_view[[feature.name for feature in projection.features]] |
| 250 | + ) |
| 251 | + |
| 252 | + prepared = FeatureService( |
| 253 | + name=self.name, |
| 254 | + features=resolved_features, |
| 255 | + tags=self.tags, |
| 256 | + description=self.description, |
| 257 | + owner=self.owner, |
| 258 | + logging_config=self.logging_config, |
| 259 | + precompute_online=self.precompute_online, |
| 260 | + ) |
| 261 | + prepared.created_timestamp = self.created_timestamp |
| 262 | + prepared.last_updated_timestamp = self.last_updated_timestamp |
| 263 | + prepared.infer_features(fvs_to_update=fvs_to_update) |
| 264 | + |
| 265 | + self._features = prepared._features |
| 266 | + self.feature_view_projections = prepared.feature_view_projections |
| 267 | + return self |
| 268 | + |
| 269 | + @staticmethod |
| 270 | + def _projection_matches_registry_features( |
| 271 | + projection: FeatureViewProjection, |
| 272 | + features_by_name: Dict[str, Field], |
| 273 | + ) -> bool: |
| 274 | + if not projection.features: |
| 275 | + return False |
| 276 | + |
| 277 | + for feature in projection.features: |
| 278 | + if feature.name not in features_by_name: |
| 279 | + return False |
| 280 | + if feature != features_by_name[feature.name]: |
| 281 | + return False |
| 282 | + return True |
| 283 | + |
164 | 284 | def __repr__(self): |
165 | 285 | items = (f"{k} = {v}" for k, v in self.__dict__.items()) |
166 | 286 | return f"<{self.__class__.__name__}({', '.join(items)})>" |
@@ -259,6 +379,48 @@ def to_proto(self) -> FeatureServiceProto: |
259 | 379 |
|
260 | 380 | return FeatureServiceProto(spec=spec, meta=meta) |
261 | 381 |
|
| 382 | + @classmethod |
| 383 | + def build_apply_request( |
| 384 | + cls, |
| 385 | + *, |
| 386 | + name: str, |
| 387 | + project: str, |
| 388 | + feature_view_refs: List[tuple[str, Optional[List[str]]]], |
| 389 | + description: str = "", |
| 390 | + tags: Optional[Dict[str, str]] = None, |
| 391 | + owner: str = "", |
| 392 | + commit: bool = True, |
| 393 | + ): |
| 394 | + """Build an unresolved ApplyFeatureServiceRequest from feature view refs.""" |
| 395 | + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 |
| 396 | + from feast.protos.feast.core.FeatureViewProjection_pb2 import ( |
| 397 | + FeatureViewProjection as FeatureViewProjectionProto, |
| 398 | + ) |
| 399 | + from feast.protos.feast.registry import RegistryServer_pb2 |
| 400 | + |
| 401 | + projections = [] |
| 402 | + for feature_view_name, feature_names in feature_view_refs: |
| 403 | + projection = FeatureViewProjectionProto( |
| 404 | + feature_view_name=feature_view_name, |
| 405 | + ) |
| 406 | + if feature_names: |
| 407 | + for feature_name in feature_names: |
| 408 | + projection.feature_columns.append(FeatureSpecV2(name=feature_name)) |
| 409 | + projections.append(projection) |
| 410 | + |
| 411 | + spec = FeatureServiceSpecProto( |
| 412 | + name=name, |
| 413 | + features=projections, |
| 414 | + tags=tags or {}, |
| 415 | + description=description, |
| 416 | + owner=owner, |
| 417 | + ) |
| 418 | + return RegistryServer_pb2.ApplyFeatureServiceRequest( |
| 419 | + feature_service=FeatureServiceProto(spec=spec), |
| 420 | + project=project, |
| 421 | + commit=commit, |
| 422 | + ) |
| 423 | + |
262 | 424 | def validate(self): |
263 | 425 | if not self.precompute_online: |
264 | 426 | return |
|
0 commit comments