From 55de37e63f0137234928aa3885b29e079497a390 Mon Sep 17 00:00:00 2001 From: ntkathole Date: Sat, 28 Jun 2025 11:22:10 +0530 Subject: [PATCH 1/2] feat: Added Lineage APIs to get registry objects relationships Signed-off-by: ntkathole --- protos/feast/registry/RegistryServer.proto | 40 ++ .../feast/api/registry/rest/__init__.py | 2 + sdk/python/feast/api/registry/rest/lineage.py | 142 +++++++ .../feast/infra/registry/base_registry.py | 140 ++++++- sdk/python/feast/infra/registry/remote.py | 94 ++++- sdk/python/feast/lineage/__init__.py | 4 + sdk/python/feast/lineage/registry_lineage.py | 388 ++++++++++++++++++ .../feast/registry/RegistryServer_pb2.py | 18 +- .../feast/registry/RegistryServer_pb2.pyi | 123 ++++++ .../feast/registry/RegistryServer_pb2_grpc.py | 67 +++ sdk/python/feast/registry_server.py | 38 ++ .../tests/unit/api/test_api_rest_registry.py | 117 ++++++ .../unit/api/test_api_rest_registry_server.py | 3 + .../tests/unit/infra/test_registry_lineage.py | 246 +++++++++++ 14 files changed, 1416 insertions(+), 6 deletions(-) create mode 100644 sdk/python/feast/api/registry/rest/lineage.py create mode 100644 sdk/python/feast/lineage/__init__.py create mode 100644 sdk/python/feast/lineage/registry_lineage.py create mode 100644 sdk/python/tests/unit/infra/test_registry_lineage.py diff --git a/protos/feast/registry/RegistryServer.proto b/protos/feast/registry/RegistryServer.proto index fb68d519dd9..108e0a8aa52 100644 --- a/protos/feast/registry/RegistryServer.proto +++ b/protos/feast/registry/RegistryServer.proto @@ -88,6 +88,10 @@ service RegistryServer{ rpc Refresh (RefreshRequest) returns (google.protobuf.Empty) {} rpc Proto (google.protobuf.Empty) returns (feast.core.Registry) {} + // Lineage RPCs + rpc GetRegistryLineage (GetRegistryLineageRequest) returns (GetRegistryLineageResponse) {} + rpc GetObjectRelationships (GetObjectRelationshipsRequest) returns (GetObjectRelationshipsResponse) {} + } message RefreshRequest { @@ -424,3 +428,39 @@ message DeleteProjectRequest { string name = 1; bool commit = 2; } + +// Lineage + +message EntityReference { + string type = 1; // "dataSource", "entity", "featureView", "featureService" + string name = 2; +} + +message EntityRelation { + EntityReference source = 1; + EntityReference target = 2; +} + +message GetRegistryLineageRequest { + string project = 1; + bool allow_cache = 2; + string filter_object_type = 3; + string filter_object_name = 4; +} + +message GetRegistryLineageResponse { + repeated EntityRelation relationships = 1; + repeated EntityRelation indirect_relationships = 2; +} + +message GetObjectRelationshipsRequest { + string project = 1; + string object_type = 2; + string object_name = 3; + bool include_indirect = 4; + bool allow_cache = 5; +} + +message GetObjectRelationshipsResponse { + repeated EntityRelation relationships = 1; +} diff --git a/sdk/python/feast/api/registry/rest/__init__.py b/sdk/python/feast/api/registry/rest/__init__.py index 9cf3d5af04d..911ae4f8fb3 100644 --- a/sdk/python/feast/api/registry/rest/__init__.py +++ b/sdk/python/feast/api/registry/rest/__init__.py @@ -4,6 +4,7 @@ from feast.api.registry.rest.entities import get_entity_router from feast.api.registry.rest.feature_services import get_feature_service_router from feast.api.registry.rest.feature_views import get_feature_view_router +from feast.api.registry.rest.lineage import get_lineage_router from feast.api.registry.rest.permissions import get_permission_router from feast.api.registry.rest.projects import get_project_router from feast.api.registry.rest.saved_datasets import get_saved_dataset_router @@ -14,6 +15,7 @@ def register_all_routes(app: FastAPI, grpc_handler): app.include_router(get_data_source_router(grpc_handler)) app.include_router(get_feature_service_router(grpc_handler)) app.include_router(get_feature_view_router(grpc_handler)) + app.include_router(get_lineage_router(grpc_handler)) app.include_router(get_permission_router(grpc_handler)) app.include_router(get_project_router(grpc_handler)) app.include_router(get_saved_dataset_router(grpc_handler)) diff --git a/sdk/python/feast/api/registry/rest/lineage.py b/sdk/python/feast/api/registry/rest/lineage.py new file mode 100644 index 00000000000..333bac0090d --- /dev/null +++ b/sdk/python/feast/api/registry/rest/lineage.py @@ -0,0 +1,142 @@ +"""REST API endpoints for registry lineage and relationships.""" + +from typing import Optional + +from fastapi import APIRouter, HTTPException, Query + +from feast.api.registry.rest.rest_utils import grpc_call +from feast.protos.feast.registry import RegistryServer_pb2 + + +def get_lineage_router(grpc_handler) -> APIRouter: + router = APIRouter() + + @router.get("/lineage/registry") + def get_registry_lineage( + project: str = Query(...), + allow_cache: bool = Query(True), + filter_object_type: Optional[str] = Query(None), + filter_object_name: Optional[str] = Query(None), + ): + """ + Get complete registry lineage with relationships and indirect relationships. + Args: + project: Project name + allow_cache: Whether to allow cached data + filter_object_type: Optional filter by object type (dataSource, entity, featureView, featureService) + filter_object_name: Optional filter by object name + Returns: + Dictionary containing relationships and indirect_relationships arrays + """ + req = RegistryServer_pb2.GetRegistryLineageRequest( + project=project, + allow_cache=allow_cache, + filter_object_type=filter_object_type or "", + filter_object_name=filter_object_name or "", + ) + response = grpc_call(grpc_handler.GetRegistryLineage, req) + + return { + "relationships": response.get("relationships", []), + "indirect_relationships": response.get("indirectRelationships", []), + } + + @router.get("/lineage/objects/{object_type}/{object_name}") + def get_object_relationships( + object_type: str, + object_name: str, + project: str = Query(...), + include_indirect: bool = Query(False), + allow_cache: bool = Query(True), + ): + """ + Get relationships for a specific object. + Args: + object_type: Type of object (dataSource, entity, featureView, featureService) + object_name: Name of the object + project: Project name + include_indirect: Whether to include indirect relationships + allow_cache: Whether to allow cached data + Returns: + Dictionary containing relationships array for the specific object + """ + valid_types = ["dataSource", "entity", "featureView", "featureService"] + if object_type not in valid_types: + raise HTTPException( + status_code=400, + detail=f"Invalid object_type. Must be one of: {valid_types}", + ) + + req = RegistryServer_pb2.GetObjectRelationshipsRequest( + project=project, + object_type=object_type, + object_name=object_name, + include_indirect=include_indirect, + allow_cache=allow_cache, + ) + response = grpc_call(grpc_handler.GetObjectRelationships, req) + + return {"relationships": response.get("relationships", [])} + + @router.get("/lineage/complete") + def get_complete_registry_data( + project: str = Query(...), + allow_cache: bool = Query(True), + ): + """ + Get complete registry data. + This endpoint provides all the data the UI currently loads: + - All registry objects + - Relationships + - Indirect relationships + - Merged feature view data + Returns: + Complete registry data structure. + """ + # Get lineage data + lineage_req = RegistryServer_pb2.GetRegistryLineageRequest( + project=project, + allow_cache=allow_cache, + ) + lineage_response = grpc_call(grpc_handler.GetRegistryLineage, lineage_req) + + # Get all registry objects + entities_req = RegistryServer_pb2.ListEntitiesRequest( + project=project, allow_cache=allow_cache + ) + entities_response = grpc_call(grpc_handler.ListEntities, entities_req) + + data_sources_req = RegistryServer_pb2.ListDataSourcesRequest( + project=project, allow_cache=allow_cache + ) + data_sources_response = grpc_call( + grpc_handler.ListDataSources, data_sources_req + ) + + feature_views_req = RegistryServer_pb2.ListAllFeatureViewsRequest( + project=project, allow_cache=allow_cache + ) + feature_views_response = grpc_call( + grpc_handler.ListAllFeatureViews, feature_views_req + ) + + feature_services_req = RegistryServer_pb2.ListFeatureServicesRequest( + project=project, allow_cache=allow_cache + ) + feature_services_response = grpc_call( + grpc_handler.ListFeatureServices, feature_services_req + ) + + return { + "project": project, + "objects": { + "entities": entities_response.get("entities", []), + "dataSources": data_sources_response.get("dataSources", []), + "featureViews": feature_views_response.get("featureViews", []), + "featureServices": feature_services_response.get("featureServices", []), + }, + "relationships": lineage_response.get("relationships", []), + "indirectRelationships": lineage_response.get("indirectRelationships", []), + } + + return router diff --git a/sdk/python/feast/infra/registry/base_registry.py b/sdk/python/feast/infra/registry/base_registry.py index 85810f1fbc1..f177bb420c0 100644 --- a/sdk/python/feast/infra/registry/base_registry.py +++ b/sdk/python/feast/infra/registry/base_registry.py @@ -788,16 +788,152 @@ def refresh(self, project: Optional[str] = None): """Refreshes the state of the registry cache by fetching the registry state from the remote registry store.""" raise NotImplementedError + # Lineage operations + def get_registry_lineage( + self, + project: str, + allow_cache: bool = False, + filter_object_type: Optional[str] = None, + filter_object_name: Optional[str] = None, + ) -> tuple[List[Any], List[Any]]: + """ + Get complete registry lineage with relationships and indirect relationships. + Args: + project: Feast project name + allow_cache: Whether to allow returning data from a cached registry + filter_object_type: Optional filter by object type (dataSource, entity, featureView, featureService) + filter_object_name: Optional filter by object name + Returns: + Tuple of (direct_relationships, indirect_relationships) + """ + from feast.lineage.registry_lineage import RegistryLineageGenerator + + # Create a registry proto with all objects + registry_proto = self._build_registry_proto(project, allow_cache) + + # Generate lineage + lineage_generator = RegistryLineageGenerator() + relationships, indirect_relationships = lineage_generator.generate_lineage( + registry_proto + ) + + # Apply filtering if specified + if filter_object_type and filter_object_name: + relationships = [ + rel + for rel in relationships + if ( + ( + rel.source.type.value == filter_object_type + and rel.source.name == filter_object_name + ) + or ( + rel.target.type.value == filter_object_type + and rel.target.name == filter_object_name + ) + ) + ] + indirect_relationships = [ + rel + for rel in indirect_relationships + if ( + ( + rel.source.type.value == filter_object_type + and rel.source.name == filter_object_name + ) + or ( + rel.target.type.value == filter_object_type + and rel.target.name == filter_object_name + ) + ) + ] + + return relationships, indirect_relationships + + def get_object_relationships( + self, + project: str, + object_type: str, + object_name: str, + include_indirect: bool = False, + allow_cache: bool = False, + ) -> List[Any]: + """ + Get relationships for a specific object. + Args: + project: Feast project name + object_type: Type of object (dataSource, entity, featureView, featureService) + object_name: Name of the object + include_indirect: Whether to include indirect relationships + allow_cache: Whether to allow returning data from a cached registry + Returns: + List of relationships involving the specified object + """ + from feast.lineage.registry_lineage import ( + RegistryLineageGenerator, + ) + + registry_proto = self._build_registry_proto(project, allow_cache) + lineage_generator = RegistryLineageGenerator() + + return lineage_generator.get_object_relationships( + registry_proto, object_type, object_name, include_indirect=include_indirect + ) + + def _build_registry_proto( + self, project: str, allow_cache: bool = False + ) -> RegistryProto: + """Helper method to build a registry proto with all objects.""" + registry = RegistryProto() + + # Add all entities + entities = self.list_entities(project=project, allow_cache=allow_cache) + for entity in entities: + registry.entities.append(entity.to_proto()) + + # Add all data sources + data_sources = self.list_data_sources(project=project, allow_cache=allow_cache) + for data_source in data_sources: + registry.data_sources.append(data_source.to_proto()) + + # Add all feature views + feature_views = self.list_feature_views( + project=project, allow_cache=allow_cache + ) + for feature_view in feature_views: + registry.feature_views.append(feature_view.to_proto()) + + # Add all stream feature views + stream_feature_views = self.list_stream_feature_views( + project=project, allow_cache=allow_cache + ) + for stream_feature_view in stream_feature_views: + registry.stream_feature_views.append(stream_feature_view.to_proto()) + + # Add all on-demand feature views + on_demand_feature_views = self.list_on_demand_feature_views( + project=project, allow_cache=allow_cache + ) + for on_demand_feature_view in on_demand_feature_views: + registry.on_demand_feature_views.append(on_demand_feature_view.to_proto()) + + # Add all feature services + feature_services = self.list_feature_services( + project=project, allow_cache=allow_cache + ) + for feature_service in feature_services: + registry.feature_services.append(feature_service.to_proto()) + + return registry + @staticmethod def _message_to_sorted_dict(message: Message) -> Dict[str, Any]: return json.loads(MessageToJson(message, sort_keys=True)) def to_dict(self, project: str) -> Dict[str, List[Any]]: """Returns a dictionary representation of the registry contents for the specified project. - For each list in the dictionary, the elements are sorted by name, so this method can be used to compare two registries. - Args: project: Feast project to convert to a dict """ diff --git a/sdk/python/feast/infra/registry/remote.py b/sdk/python/feast/infra/registry/remote.py index 4122586046f..05ddccdd38e 100644 --- a/sdk/python/feast/infra/registry/remote.py +++ b/sdk/python/feast/infra/registry/remote.py @@ -1,7 +1,7 @@ import os from datetime import datetime from pathlib import Path -from typing import List, Optional, Union +from typing import Any, List, Optional, Union import grpc from google.protobuf.empty_pb2 import Empty @@ -590,5 +590,97 @@ def refresh(self, project: Optional[str] = None): request = RegistryServer_pb2.RefreshRequest(project=str(project)) self.stub.Refresh(request) + # Lineage operations + def get_registry_lineage( + self, + project: str, + allow_cache: bool = False, + filter_object_type: Optional[str] = None, + filter_object_name: Optional[str] = None, + ) -> tuple[List[Any], List[Any]]: + """Get complete registry lineage via remote registry server.""" + request = RegistryServer_pb2.GetRegistryLineageRequest( + project=project, + allow_cache=allow_cache, + filter_object_type=filter_object_type or "", + filter_object_name=filter_object_name or "", + ) + response = self.stub.GetRegistryLineage(request) + + # Convert protobuf responses back to lineage objects + from feast.lineage.registry_lineage import ( + EntityReference, + EntityRelation, + FeastObjectType, + ) + + relationships = [] + for rel_proto in response.relationships: + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType(rel_proto.source.type), rel_proto.source.name + ), + target=EntityReference( + FeastObjectType(rel_proto.target.type), rel_proto.target.name + ), + ) + ) + + indirect_relationships = [] + for rel_proto in response.indirect_relationships: + indirect_relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType(rel_proto.source.type), rel_proto.source.name + ), + target=EntityReference( + FeastObjectType(rel_proto.target.type), rel_proto.target.name + ), + ) + ) + + return relationships, indirect_relationships + + def get_object_relationships( + self, + project: str, + object_type: str, + object_name: str, + include_indirect: bool = False, + allow_cache: bool = False, + ) -> List[Any]: + """Get relationships for a specific object via remote registry server.""" + request = RegistryServer_pb2.GetObjectRelationshipsRequest( + project=project, + object_type=object_type, + object_name=object_name, + include_indirect=include_indirect, + allow_cache=allow_cache, + ) + response = self.stub.GetObjectRelationships(request) + + # Convert protobuf responses back to lineage objects + from feast.lineage.registry_lineage import ( + EntityReference, + EntityRelation, + FeastObjectType, + ) + + relationships = [] + for rel_proto in response.relationships: + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType(rel_proto.source.type), rel_proto.source.name + ), + target=EntityReference( + FeastObjectType(rel_proto.target.type), rel_proto.target.name + ), + ) + ) + + return relationships + def teardown(self): pass diff --git a/sdk/python/feast/lineage/__init__.py b/sdk/python/feast/lineage/__init__.py new file mode 100644 index 00000000000..138d0969101 --- /dev/null +++ b/sdk/python/feast/lineage/__init__.py @@ -0,0 +1,4 @@ +# Registry lineage generation utilities +from .registry_lineage import EntityReference, EntityRelation, RegistryLineageGenerator + +__all__ = ["RegistryLineageGenerator", "EntityRelation", "EntityReference"] diff --git a/sdk/python/feast/lineage/registry_lineage.py b/sdk/python/feast/lineage/registry_lineage.py new file mode 100644 index 00000000000..02e4083bf7c --- /dev/null +++ b/sdk/python/feast/lineage/registry_lineage.py @@ -0,0 +1,388 @@ +""" +Registry lineage generation for Feast objects. + +This module provides functionality to generate relationship graphs between +Feast objects (entities, feature views, data sources, feature services) +for lineage visualization. +""" + +from dataclasses import dataclass +from enum import Enum +from typing import Dict, List, Tuple + +from feast.protos.feast.core.Registry_pb2 import Registry + + +class FeastObjectType(Enum): + DATA_SOURCE = "dataSource" + ENTITY = "entity" + FEATURE_VIEW = "featureView" + FEATURE_SERVICE = "featureService" + + +@dataclass +class EntityReference: + type: FeastObjectType + name: str + + def to_proto(self): + try: + from feast.protos.feast.registry.RegistryServer_pb2 import ( + EntityReference as EntityReferenceProto, + ) + + return EntityReferenceProto(type=self.type.value, name=self.name) + except ImportError: + return {"type": self.type.value, "name": self.name} + + +@dataclass +class EntityRelation: + source: EntityReference + target: EntityReference + + def to_proto(self): + try: + from feast.protos.feast.registry.RegistryServer_pb2 import ( + EntityRelation as EntityRelationProto, + ) + + return EntityRelationProto( + source=self.source.to_proto(), target=self.target.to_proto() + ) + except ImportError: + # Fallback to dict if protobuf not generated yet + return {"source": self.source.to_proto(), "target": self.target.to_proto()} + + +class RegistryLineageGenerator: + """ + Generates lineage relationships between Feast objects. + """ + + def generate_lineage( + self, registry: Registry + ) -> Tuple[List[EntityRelation], List[EntityRelation]]: + """ + Generate both direct and indirect relationships from registry objects. + Args: + registry: The registry protobuf containing all objects + Returns: + Tuple of (direct_relationships, indirect_relationships) + """ + direct_relationships = self._parse_direct_relationships(registry) + indirect_relationships = self._parse_indirect_relationships( + direct_relationships, registry + ) + + return direct_relationships, indirect_relationships + + def _parse_direct_relationships(self, registry: Registry) -> List[EntityRelation]: + """Parse direct relationships between objects.""" + relationships = [] + + # FeatureService -> FeatureView relationships + for feature_service in registry.feature_services: + if ( + hasattr(feature_service, "spec") + and feature_service.spec + and feature_service.spec.features + ): + for feature in feature_service.spec.features: + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.FEATURE_VIEW, feature.feature_view_name + ), + target=EntityReference( + FeastObjectType.FEATURE_SERVICE, + feature_service.spec.name, + ), + ) + ) + + # Entity -> FeatureView and DataSource -> FeatureView relationships + for feature_view in registry.feature_views: + if hasattr(feature_view, "spec") and feature_view.spec: + # Entity relationships + if hasattr(feature_view.spec, "entities"): + for entity_name in feature_view.spec.entities: + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.ENTITY, entity_name + ), + target=EntityReference( + FeastObjectType.FEATURE_VIEW, feature_view.spec.name + ), + ) + ) + + # Batch source relationship + if ( + hasattr(feature_view.spec, "batch_source") + and feature_view.spec.batch_source + ): + # Try to get the data source name + data_source_name = None + if ( + hasattr(feature_view.spec.batch_source, "name") + and feature_view.spec.batch_source.name + ): + data_source_name = feature_view.spec.batch_source.name + elif ( + hasattr(feature_view.spec.batch_source, "table") + and feature_view.spec.batch_source.table + ): + # Fallback to table name for unnamed data sources + data_source_name = ( + f"table:{feature_view.spec.batch_source.table}" + ) + elif ( + hasattr(feature_view.spec.batch_source, "path") + and feature_view.spec.batch_source.path + ): + # Fallback to path for file-based sources + data_source_name = f"path:{feature_view.spec.batch_source.path}" + else: + # Use a generic identifier + data_source_name = f"unnamed_source_{hash(str(feature_view.spec.batch_source))}" + + if data_source_name: + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.DATA_SOURCE, data_source_name + ), + target=EntityReference( + FeastObjectType.FEATURE_VIEW, feature_view.spec.name + ), + ) + ) + + # OnDemand FeatureView relationships + for odfv in registry.on_demand_feature_views: + if ( + hasattr(odfv, "spec") + and odfv.spec + and hasattr(odfv.spec, "sources") + and odfv.spec.sources + ): + # Handle protobuf map structure + if hasattr(odfv.spec.sources, "items"): + source_items = odfv.spec.sources.items() + else: + # Fallback for different protobuf representations + source_items = [(k, v) for k, v in enumerate(odfv.spec.sources)] + + for source_name, source in source_items: + if ( + hasattr(source, "request_data_source") + and source.request_data_source + ): + if hasattr(source.request_data_source, "name"): + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.DATA_SOURCE, + source.request_data_source.name, + ), + target=EntityReference( + FeastObjectType.FEATURE_VIEW, odfv.spec.name + ), + ) + ) + elif ( + hasattr(source, "feature_view_projection") + and source.feature_view_projection + ): + # Find the source feature view's batch source + if hasattr(source.feature_view_projection, "feature_view_name"): + source_fv = next( + ( + fv + for fv in registry.feature_views + if hasattr(fv, "spec") + and fv.spec + and hasattr(fv.spec, "name") + and fv.spec.name + == source.feature_view_projection.feature_view_name + ), + None, + ) + if ( + source_fv + and hasattr(source_fv, "spec") + and source_fv.spec + and hasattr(source_fv.spec, "batch_source") + and source_fv.spec.batch_source + and hasattr(source_fv.spec.batch_source, "name") + ): + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.DATA_SOURCE, + source_fv.spec.batch_source.name, + ), + target=EntityReference( + FeastObjectType.FEATURE_VIEW, odfv.spec.name + ), + ) + ) + + # Stream FeatureView relationships + for sfv in registry.stream_feature_views: + if hasattr(sfv, "spec") and sfv.spec: + # Stream source + if ( + hasattr(sfv.spec, "stream_source") + and sfv.spec.stream_source + and hasattr(sfv.spec.stream_source, "name") + and sfv.spec.stream_source.name + ): + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.DATA_SOURCE, sfv.spec.stream_source.name + ), + target=EntityReference( + FeastObjectType.FEATURE_VIEW, sfv.spec.name + ), + ) + ) + + # Batch source + if ( + hasattr(sfv.spec, "batch_source") + and sfv.spec.batch_source + and hasattr(sfv.spec.batch_source, "name") + and sfv.spec.batch_source.name + ): + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.DATA_SOURCE, sfv.spec.batch_source.name + ), + target=EntityReference( + FeastObjectType.FEATURE_VIEW, sfv.spec.name + ), + ) + ) + + return relationships + + def _parse_indirect_relationships( + self, direct_relationships: List[EntityRelation], registry: Registry + ) -> List[EntityRelation]: + """Parse indirect relationships (transitive relationships through feature views).""" + indirect_relationships = [] + + # Create Entity -> FeatureService and DataSource -> FeatureService relationships + for feature_service in registry.feature_services: + if ( + hasattr(feature_service, "spec") + and feature_service.spec + and hasattr(feature_service.spec, "features") + and feature_service.spec.features + ): + for feature in feature_service.spec.features: + if hasattr(feature, "feature_view_name"): + # Find all relationships that target this feature view + related_sources = [ + rel.source + for rel in direct_relationships + if rel.target.name == feature.feature_view_name + and rel.target.type == FeastObjectType.FEATURE_VIEW + ] + + # Create indirect relationships to the feature service + for source in related_sources: + indirect_relationships.append( + EntityRelation( + source=source, + target=EntityReference( + FeastObjectType.FEATURE_SERVICE, + feature_service.spec.name, + ), + ) + ) + + return indirect_relationships + + def get_object_relationships( + self, + registry: Registry, + object_type: str, + object_name: str, + include_indirect: bool = False, + ) -> List[EntityRelation]: + """ + Get all relationships for a specific object. + Args: + registry: The registry protobuf + object_type: Type of the object (dataSource, entity, featureView, featureService) + object_name: Name of the object + include_indirect: Whether to include indirect relationships + Returns: + List of relationships involving the specified object + """ + direct_relationships, indirect_relationships = self.generate_lineage(registry) + + all_relationships = direct_relationships[:] + if include_indirect: + all_relationships.extend(indirect_relationships) + + # Filter relationships involving the specified object + filtered_relationships = [] + target_type = FeastObjectType(object_type) + + for rel in all_relationships: + if (rel.source.type == target_type and rel.source.name == object_name) or ( + rel.target.type == target_type and rel.target.name == object_name + ): + filtered_relationships.append(rel) + + return filtered_relationships + + def get_object_lineage_graph( + self, registry: Registry, object_type: str, object_name: str, depth: int = 2 + ) -> Dict: + """ + Get a complete lineage graph for an object up to specified depth. + This can be used for more complex lineage queries and visualization. + """ + direct_relationships, indirect_relationships = self.generate_lineage(registry) + all_relationships = direct_relationships + indirect_relationships + + # Build adjacency graph + graph: Dict[str, List[str]] = {} + for rel in all_relationships: + source_key = f"{rel.source.type.value}:{rel.source.name}" + target_key = f"{rel.target.type.value}:{rel.target.name}" + + if source_key not in graph: + graph[source_key] = [] + graph[source_key].append(target_key) + + # Perform BFS to get subgraph up to specified depth + start_key = f"{object_type}:{object_name}" + visited = set() + result_nodes = set() + result_edges = [] + + def bfs(current_key, current_depth): + if current_depth > depth or current_key in visited: + return + + visited.add(current_key) + result_nodes.add(current_key) + + if current_key in graph: + for neighbor in graph[current_key]: + result_edges.append((current_key, neighbor)) + result_nodes.add(neighbor) + bfs(neighbor, current_depth + 1) + + bfs(start_key, 0) + + return {"nodes": list(result_nodes), "edges": result_edges} diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py index 2d5f7b020ab..37af594eaed 100644 --- a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py @@ -28,7 +28,7 @@ from feast.protos.feast.core import Project_pb2 as feast_dot_core_dot_Project__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#feast/registry/RegistryServer.proto\x12\x0e\x66\x65\x61st.registry\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x19\x66\x65\x61st/core/Registry.proto\x1a\x17\x66\x65\x61st/core/Entity.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\"feast/core/StreamFeatureView.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\x1f\x66\x65\x61st/core/FeatureService.proto\x1a\x1d\x66\x65\x61st/core/SavedDataset.proto\x1a\"feast/core/ValidationProfile.proto\x1a\x1c\x66\x65\x61st/core/InfraObject.proto\x1a\x1b\x66\x65\x61st/core/Permission.proto\x1a\x18\x66\x65\x61st/core/Project.proto\"!\n\x0eRefreshRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\"W\n\x12UpdateInfraRequest\x12 \n\x05infra\x18\x01 \x01(\x0b\x32\x11.feast.core.Infra\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"7\n\x0fGetInfraRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"B\n\x1aListProjectMetadataRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"T\n\x1bListProjectMetadataResponse\x12\x35\n\x10project_metadata\x18\x01 \x03(\x0b\x32\x1b.feast.core.ProjectMetadata\"\xcb\x01\n\x1b\x41pplyMaterializationRequest\x12-\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureView\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\nstart_date\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_date\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\"Y\n\x12\x41pplyEntityRequest\x12\"\n\x06\x65ntity\x18\x01 \x01(\x0b\x32\x12.feast.core.Entity\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"F\n\x10GetEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xa5\x01\n\x13ListEntitiesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12;\n\x04tags\x18\x03 \x03(\x0b\x32-.feast.registry.ListEntitiesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"<\n\x14ListEntitiesResponse\x12$\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x12.feast.core.Entity\"D\n\x13\x44\x65leteEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"f\n\x16\x41pplyDataSourceRequest\x12+\n\x0b\x64\x61ta_source\x18\x01 \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xab\x01\n\x16ListDataSourcesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListDataSourcesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"G\n\x17ListDataSourcesResponse\x12,\n\x0c\x64\x61ta_sources\x18\x01 \x03(\x0b\x32\x16.feast.core.DataSource\"H\n\x17\x44\x65leteDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x81\x02\n\x17\x41pplyFeatureViewRequest\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x12\x0f\n\x07project\x18\x04 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\x42\x13\n\x11\x62\x61se_feature_view\"K\n\x15GetFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xad\x01\n\x17ListFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12?\n\x04tags\x18\x03 \x03(\x0b\x32\x31.feast.registry.ListFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"J\n\x18ListFeatureViewsResponse\x12.\n\rfeature_views\x18\x01 \x03(\x0b\x32\x17.feast.core.FeatureView\"I\n\x18\x44\x65leteFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\xd6\x01\n\x0e\x41nyFeatureView\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x42\x12\n\x10\x61ny_feature_view\"N\n\x18GetAnyFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"U\n\x19GetAnyFeatureViewResponse\x12\x38\n\x10\x61ny_feature_view\x18\x01 \x01(\x0b\x32\x1e.feast.registry.AnyFeatureView\"\xb3\x01\n\x1aListAllFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListAllFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"T\n\x1bListAllFeatureViewsResponse\x12\x35\n\rfeature_views\x18\x01 \x03(\x0b\x32\x1e.feast.registry.AnyFeatureView\"Q\n\x1bGetStreamFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xb9\x01\n\x1dListStreamFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x45\n\x04tags\x18\x03 \x03(\x0b\x32\x37.feast.registry.ListStreamFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x1eListStreamFeatureViewsResponse\x12;\n\x14stream_feature_views\x18\x01 \x03(\x0b\x32\x1d.feast.core.StreamFeatureView\"S\n\x1dGetOnDemandFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xbd\x01\n\x1fListOnDemandFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListOnDemandFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"d\n ListOnDemandFeatureViewsResponse\x12@\n\x17on_demand_feature_views\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureView\"r\n\x1a\x41pplyFeatureServiceRequest\x12\x33\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\x0b\x32\x1a.feast.core.FeatureService\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"N\n\x18GetFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xb3\x01\n\x1aListFeatureServicesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListFeatureServicesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"S\n\x1bListFeatureServicesResponse\x12\x34\n\x10\x66\x65\x61ture_services\x18\x01 \x03(\x0b\x32\x1a.feast.core.FeatureService\"L\n\x1b\x44\x65leteFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"l\n\x18\x41pplySavedDatasetRequest\x12/\n\rsaved_dataset\x18\x01 \x01(\x0b\x32\x18.feast.core.SavedDataset\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"L\n\x16GetSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xaf\x01\n\x18ListSavedDatasetsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12@\n\x04tags\x18\x03 \x03(\x0b\x32\x32.feast.registry.ListSavedDatasetsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"M\n\x19ListSavedDatasetsResponse\x12\x30\n\x0esaved_datasets\x18\x01 \x03(\x0b\x32\x18.feast.core.SavedDataset\"J\n\x19\x44\x65leteSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x81\x01\n\x1f\x41pplyValidationReferenceRequest\x12=\n\x14validation_reference\x18\x01 \x01(\x0b\x32\x1f.feast.core.ValidationReference\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"S\n\x1dGetValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xbd\x01\n\x1fListValidationReferencesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListValidationReferencesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"b\n ListValidationReferencesResponse\x12>\n\x15validation_references\x18\x01 \x03(\x0b\x32\x1f.feast.core.ValidationReference\"Q\n DeleteValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"e\n\x16\x41pplyPermissionRequest\x12*\n\npermission\x18\x01 \x01(\x0b\x32\x16.feast.core.Permission\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetPermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xab\x01\n\x16ListPermissionsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListPermissionsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"F\n\x17ListPermissionsResponse\x12+\n\x0bpermissions\x18\x01 \x03(\x0b\x32\x16.feast.core.Permission\"H\n\x17\x44\x65letePermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"K\n\x13\x41pplyProjectRequest\x12$\n\x07project\x18\x01 \x01(\x0b\x32\x13.feast.core.Project\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\"6\n\x11GetProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"\x94\x01\n\x13ListProjectsRequest\x12\x13\n\x0b\x61llow_cache\x18\x01 \x01(\x08\x12;\n\x04tags\x18\x02 \x03(\x0b\x32-.feast.registry.ListProjectsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"=\n\x14ListProjectsResponse\x12%\n\x08projects\x18\x01 \x03(\x0b\x32\x13.feast.core.Project\"4\n\x14\x44\x65leteProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\x32\xcb \n\x0eRegistryServer\x12K\n\x0b\x41pplyEntity\x12\".feast.registry.ApplyEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\tGetEntity\x12 .feast.registry.GetEntityRequest\x1a\x12.feast.core.Entity\"\x00\x12[\n\x0cListEntities\x12#.feast.registry.ListEntitiesRequest\x1a$.feast.registry.ListEntitiesResponse\"\x00\x12M\n\x0c\x44\x65leteEntity\x12#.feast.registry.DeleteEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyDataSource\x12&.feast.registry.ApplyDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetDataSource\x12$.feast.registry.GetDataSourceRequest\x1a\x16.feast.core.DataSource\"\x00\x12\x64\n\x0fListDataSources\x12&.feast.registry.ListDataSourcesRequest\x1a\'.feast.registry.ListDataSourcesResponse\"\x00\x12U\n\x10\x44\x65leteDataSource\x12\'.feast.registry.DeleteDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x10\x41pplyFeatureView\x12\'.feast.registry.ApplyFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x44\x65leteFeatureView\x12(.feast.registry.DeleteFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x11GetAnyFeatureView\x12(.feast.registry.GetAnyFeatureViewRequest\x1a).feast.registry.GetAnyFeatureViewResponse\"\x00\x12p\n\x13ListAllFeatureViews\x12*.feast.registry.ListAllFeatureViewsRequest\x1a+.feast.registry.ListAllFeatureViewsResponse\"\x00\x12R\n\x0eGetFeatureView\x12%.feast.registry.GetFeatureViewRequest\x1a\x17.feast.core.FeatureView\"\x00\x12g\n\x10ListFeatureViews\x12\'.feast.registry.ListFeatureViewsRequest\x1a(.feast.registry.ListFeatureViewsResponse\"\x00\x12\x64\n\x14GetStreamFeatureView\x12+.feast.registry.GetStreamFeatureViewRequest\x1a\x1d.feast.core.StreamFeatureView\"\x00\x12y\n\x16ListStreamFeatureViews\x12-.feast.registry.ListStreamFeatureViewsRequest\x1a..feast.registry.ListStreamFeatureViewsResponse\"\x00\x12j\n\x16GetOnDemandFeatureView\x12-.feast.registry.GetOnDemandFeatureViewRequest\x1a\x1f.feast.core.OnDemandFeatureView\"\x00\x12\x7f\n\x18ListOnDemandFeatureViews\x12/.feast.registry.ListOnDemandFeatureViewsRequest\x1a\x30.feast.registry.ListOnDemandFeatureViewsResponse\"\x00\x12[\n\x13\x41pplyFeatureService\x12*.feast.registry.ApplyFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x11GetFeatureService\x12(.feast.registry.GetFeatureServiceRequest\x1a\x1a.feast.core.FeatureService\"\x00\x12p\n\x13ListFeatureServices\x12*.feast.registry.ListFeatureServicesRequest\x1a+.feast.registry.ListFeatureServicesResponse\"\x00\x12]\n\x14\x44\x65leteFeatureService\x12+.feast.registry.DeleteFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x41pplySavedDataset\x12(.feast.registry.ApplySavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x0fGetSavedDataset\x12&.feast.registry.GetSavedDatasetRequest\x1a\x18.feast.core.SavedDataset\"\x00\x12j\n\x11ListSavedDatasets\x12(.feast.registry.ListSavedDatasetsRequest\x1a).feast.registry.ListSavedDatasetsResponse\"\x00\x12Y\n\x12\x44\x65leteSavedDataset\x12).feast.registry.DeleteSavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x18\x41pplyValidationReference\x12/.feast.registry.ApplyValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x16GetValidationReference\x12-.feast.registry.GetValidationReferenceRequest\x1a\x1f.feast.core.ValidationReference\"\x00\x12\x7f\n\x18ListValidationReferences\x12/.feast.registry.ListValidationReferencesRequest\x1a\x30.feast.registry.ListValidationReferencesResponse\"\x00\x12g\n\x19\x44\x65leteValidationReference\x12\x30.feast.registry.DeleteValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyPermission\x12&.feast.registry.ApplyPermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetPermission\x12$.feast.registry.GetPermissionRequest\x1a\x16.feast.core.Permission\"\x00\x12\x64\n\x0fListPermissions\x12&.feast.registry.ListPermissionsRequest\x1a\'.feast.registry.ListPermissionsResponse\"\x00\x12U\n\x10\x44\x65letePermission\x12\'.feast.registry.DeletePermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12M\n\x0c\x41pplyProject\x12#.feast.registry.ApplyProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x46\n\nGetProject\x12!.feast.registry.GetProjectRequest\x1a\x13.feast.core.Project\"\x00\x12[\n\x0cListProjects\x12#.feast.registry.ListProjectsRequest\x1a$.feast.registry.ListProjectsResponse\"\x00\x12O\n\rDeleteProject\x12$.feast.registry.DeleteProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x14\x41pplyMaterialization\x12+.feast.registry.ApplyMaterializationRequest\x1a\x16.google.protobuf.Empty\"\x00\x12p\n\x13ListProjectMetadata\x12*.feast.registry.ListProjectMetadataRequest\x1a+.feast.registry.ListProjectMetadataResponse\"\x00\x12K\n\x0bUpdateInfra\x12\".feast.registry.UpdateInfraRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x08GetInfra\x12\x1f.feast.registry.GetInfraRequest\x1a\x11.feast.core.Infra\"\x00\x12:\n\x06\x43ommit\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x07Refresh\x12\x1e.feast.registry.RefreshRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x37\n\x05Proto\x12\x16.google.protobuf.Empty\x1a\x14.feast.core.Registry\"\x00\x42\x35Z3github.com/feast-dev/feast/go/protos/feast/registryb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#feast/registry/RegistryServer.proto\x12\x0e\x66\x65\x61st.registry\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x19\x66\x65\x61st/core/Registry.proto\x1a\x17\x66\x65\x61st/core/Entity.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\"feast/core/StreamFeatureView.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\x1f\x66\x65\x61st/core/FeatureService.proto\x1a\x1d\x66\x65\x61st/core/SavedDataset.proto\x1a\"feast/core/ValidationProfile.proto\x1a\x1c\x66\x65\x61st/core/InfraObject.proto\x1a\x1b\x66\x65\x61st/core/Permission.proto\x1a\x18\x66\x65\x61st/core/Project.proto\"!\n\x0eRefreshRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\"W\n\x12UpdateInfraRequest\x12 \n\x05infra\x18\x01 \x01(\x0b\x32\x11.feast.core.Infra\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"7\n\x0fGetInfraRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"B\n\x1aListProjectMetadataRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"T\n\x1bListProjectMetadataResponse\x12\x35\n\x10project_metadata\x18\x01 \x03(\x0b\x32\x1b.feast.core.ProjectMetadata\"\xcb\x01\n\x1b\x41pplyMaterializationRequest\x12-\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureView\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\nstart_date\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_date\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\"Y\n\x12\x41pplyEntityRequest\x12\"\n\x06\x65ntity\x18\x01 \x01(\x0b\x32\x12.feast.core.Entity\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"F\n\x10GetEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xa5\x01\n\x13ListEntitiesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12;\n\x04tags\x18\x03 \x03(\x0b\x32-.feast.registry.ListEntitiesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"<\n\x14ListEntitiesResponse\x12$\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x12.feast.core.Entity\"D\n\x13\x44\x65leteEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"f\n\x16\x41pplyDataSourceRequest\x12+\n\x0b\x64\x61ta_source\x18\x01 \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xab\x01\n\x16ListDataSourcesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListDataSourcesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"G\n\x17ListDataSourcesResponse\x12,\n\x0c\x64\x61ta_sources\x18\x01 \x03(\x0b\x32\x16.feast.core.DataSource\"H\n\x17\x44\x65leteDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x81\x02\n\x17\x41pplyFeatureViewRequest\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x12\x0f\n\x07project\x18\x04 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\x42\x13\n\x11\x62\x61se_feature_view\"K\n\x15GetFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xad\x01\n\x17ListFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12?\n\x04tags\x18\x03 \x03(\x0b\x32\x31.feast.registry.ListFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"J\n\x18ListFeatureViewsResponse\x12.\n\rfeature_views\x18\x01 \x03(\x0b\x32\x17.feast.core.FeatureView\"I\n\x18\x44\x65leteFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\xd6\x01\n\x0e\x41nyFeatureView\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x42\x12\n\x10\x61ny_feature_view\"N\n\x18GetAnyFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"U\n\x19GetAnyFeatureViewResponse\x12\x38\n\x10\x61ny_feature_view\x18\x01 \x01(\x0b\x32\x1e.feast.registry.AnyFeatureView\"\xb3\x01\n\x1aListAllFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListAllFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"T\n\x1bListAllFeatureViewsResponse\x12\x35\n\rfeature_views\x18\x01 \x03(\x0b\x32\x1e.feast.registry.AnyFeatureView\"Q\n\x1bGetStreamFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xb9\x01\n\x1dListStreamFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x45\n\x04tags\x18\x03 \x03(\x0b\x32\x37.feast.registry.ListStreamFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"]\n\x1eListStreamFeatureViewsResponse\x12;\n\x14stream_feature_views\x18\x01 \x03(\x0b\x32\x1d.feast.core.StreamFeatureView\"S\n\x1dGetOnDemandFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xbd\x01\n\x1fListOnDemandFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListOnDemandFeatureViewsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"d\n ListOnDemandFeatureViewsResponse\x12@\n\x17on_demand_feature_views\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureView\"r\n\x1a\x41pplyFeatureServiceRequest\x12\x33\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\x0b\x32\x1a.feast.core.FeatureService\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"N\n\x18GetFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xb3\x01\n\x1aListFeatureServicesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListFeatureServicesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"S\n\x1bListFeatureServicesResponse\x12\x34\n\x10\x66\x65\x61ture_services\x18\x01 \x03(\x0b\x32\x1a.feast.core.FeatureService\"L\n\x1b\x44\x65leteFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"l\n\x18\x41pplySavedDatasetRequest\x12/\n\rsaved_dataset\x18\x01 \x01(\x0b\x32\x18.feast.core.SavedDataset\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"L\n\x16GetSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xaf\x01\n\x18ListSavedDatasetsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12@\n\x04tags\x18\x03 \x03(\x0b\x32\x32.feast.registry.ListSavedDatasetsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"M\n\x19ListSavedDatasetsResponse\x12\x30\n\x0esaved_datasets\x18\x01 \x03(\x0b\x32\x18.feast.core.SavedDataset\"J\n\x19\x44\x65leteSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x81\x01\n\x1f\x41pplyValidationReferenceRequest\x12=\n\x14validation_reference\x18\x01 \x01(\x0b\x32\x1f.feast.core.ValidationReference\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"S\n\x1dGetValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xbd\x01\n\x1fListValidationReferencesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListValidationReferencesRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"b\n ListValidationReferencesResponse\x12>\n\x15validation_references\x18\x01 \x03(\x0b\x32\x1f.feast.core.ValidationReference\"Q\n DeleteValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"e\n\x16\x41pplyPermissionRequest\x12*\n\npermission\x18\x01 \x01(\x0b\x32\x16.feast.core.Permission\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetPermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xab\x01\n\x16ListPermissionsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListPermissionsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"F\n\x17ListPermissionsResponse\x12+\n\x0bpermissions\x18\x01 \x03(\x0b\x32\x16.feast.core.Permission\"H\n\x17\x44\x65letePermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"K\n\x13\x41pplyProjectRequest\x12$\n\x07project\x18\x01 \x01(\x0b\x32\x13.feast.core.Project\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\"6\n\x11GetProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"\x94\x01\n\x13ListProjectsRequest\x12\x13\n\x0b\x61llow_cache\x18\x01 \x01(\x08\x12;\n\x04tags\x18\x02 \x03(\x0b\x32-.feast.registry.ListProjectsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"=\n\x14ListProjectsResponse\x12%\n\x08projects\x18\x01 \x03(\x0b\x32\x13.feast.core.Project\"4\n\x14\x44\x65leteProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\"-\n\x0f\x45ntityReference\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"r\n\x0e\x45ntityRelation\x12/\n\x06source\x18\x01 \x01(\x0b\x32\x1f.feast.registry.EntityReference\x12/\n\x06target\x18\x02 \x01(\x0b\x32\x1f.feast.registry.EntityReference\"y\n\x19GetRegistryLineageRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x1a\n\x12\x66ilter_object_type\x18\x03 \x01(\t\x12\x1a\n\x12\x66ilter_object_name\x18\x04 \x01(\t\"\x93\x01\n\x1aGetRegistryLineageResponse\x12\x35\n\rrelationships\x18\x01 \x03(\x0b\x32\x1e.feast.registry.EntityRelation\x12>\n\x16indirect_relationships\x18\x02 \x03(\x0b\x32\x1e.feast.registry.EntityRelation\"\x89\x01\n\x1dGetObjectRelationshipsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0bobject_type\x18\x02 \x01(\t\x12\x13\n\x0bobject_name\x18\x03 \x01(\t\x12\x18\n\x10include_indirect\x18\x04 \x01(\x08\x12\x13\n\x0b\x61llow_cache\x18\x05 \x01(\x08\"W\n\x1eGetObjectRelationshipsResponse\x12\x35\n\rrelationships\x18\x01 \x03(\x0b\x32\x1e.feast.registry.EntityRelation2\xb5\"\n\x0eRegistryServer\x12K\n\x0b\x41pplyEntity\x12\".feast.registry.ApplyEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\tGetEntity\x12 .feast.registry.GetEntityRequest\x1a\x12.feast.core.Entity\"\x00\x12[\n\x0cListEntities\x12#.feast.registry.ListEntitiesRequest\x1a$.feast.registry.ListEntitiesResponse\"\x00\x12M\n\x0c\x44\x65leteEntity\x12#.feast.registry.DeleteEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyDataSource\x12&.feast.registry.ApplyDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetDataSource\x12$.feast.registry.GetDataSourceRequest\x1a\x16.feast.core.DataSource\"\x00\x12\x64\n\x0fListDataSources\x12&.feast.registry.ListDataSourcesRequest\x1a\'.feast.registry.ListDataSourcesResponse\"\x00\x12U\n\x10\x44\x65leteDataSource\x12\'.feast.registry.DeleteDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x10\x41pplyFeatureView\x12\'.feast.registry.ApplyFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x44\x65leteFeatureView\x12(.feast.registry.DeleteFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x11GetAnyFeatureView\x12(.feast.registry.GetAnyFeatureViewRequest\x1a).feast.registry.GetAnyFeatureViewResponse\"\x00\x12p\n\x13ListAllFeatureViews\x12*.feast.registry.ListAllFeatureViewsRequest\x1a+.feast.registry.ListAllFeatureViewsResponse\"\x00\x12R\n\x0eGetFeatureView\x12%.feast.registry.GetFeatureViewRequest\x1a\x17.feast.core.FeatureView\"\x00\x12g\n\x10ListFeatureViews\x12\'.feast.registry.ListFeatureViewsRequest\x1a(.feast.registry.ListFeatureViewsResponse\"\x00\x12\x64\n\x14GetStreamFeatureView\x12+.feast.registry.GetStreamFeatureViewRequest\x1a\x1d.feast.core.StreamFeatureView\"\x00\x12y\n\x16ListStreamFeatureViews\x12-.feast.registry.ListStreamFeatureViewsRequest\x1a..feast.registry.ListStreamFeatureViewsResponse\"\x00\x12j\n\x16GetOnDemandFeatureView\x12-.feast.registry.GetOnDemandFeatureViewRequest\x1a\x1f.feast.core.OnDemandFeatureView\"\x00\x12\x7f\n\x18ListOnDemandFeatureViews\x12/.feast.registry.ListOnDemandFeatureViewsRequest\x1a\x30.feast.registry.ListOnDemandFeatureViewsResponse\"\x00\x12[\n\x13\x41pplyFeatureService\x12*.feast.registry.ApplyFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x11GetFeatureService\x12(.feast.registry.GetFeatureServiceRequest\x1a\x1a.feast.core.FeatureService\"\x00\x12p\n\x13ListFeatureServices\x12*.feast.registry.ListFeatureServicesRequest\x1a+.feast.registry.ListFeatureServicesResponse\"\x00\x12]\n\x14\x44\x65leteFeatureService\x12+.feast.registry.DeleteFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x41pplySavedDataset\x12(.feast.registry.ApplySavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x0fGetSavedDataset\x12&.feast.registry.GetSavedDatasetRequest\x1a\x18.feast.core.SavedDataset\"\x00\x12j\n\x11ListSavedDatasets\x12(.feast.registry.ListSavedDatasetsRequest\x1a).feast.registry.ListSavedDatasetsResponse\"\x00\x12Y\n\x12\x44\x65leteSavedDataset\x12).feast.registry.DeleteSavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x18\x41pplyValidationReference\x12/.feast.registry.ApplyValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x16GetValidationReference\x12-.feast.registry.GetValidationReferenceRequest\x1a\x1f.feast.core.ValidationReference\"\x00\x12\x7f\n\x18ListValidationReferences\x12/.feast.registry.ListValidationReferencesRequest\x1a\x30.feast.registry.ListValidationReferencesResponse\"\x00\x12g\n\x19\x44\x65leteValidationReference\x12\x30.feast.registry.DeleteValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyPermission\x12&.feast.registry.ApplyPermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetPermission\x12$.feast.registry.GetPermissionRequest\x1a\x16.feast.core.Permission\"\x00\x12\x64\n\x0fListPermissions\x12&.feast.registry.ListPermissionsRequest\x1a\'.feast.registry.ListPermissionsResponse\"\x00\x12U\n\x10\x44\x65letePermission\x12\'.feast.registry.DeletePermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12M\n\x0c\x41pplyProject\x12#.feast.registry.ApplyProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x46\n\nGetProject\x12!.feast.registry.GetProjectRequest\x1a\x13.feast.core.Project\"\x00\x12[\n\x0cListProjects\x12#.feast.registry.ListProjectsRequest\x1a$.feast.registry.ListProjectsResponse\"\x00\x12O\n\rDeleteProject\x12$.feast.registry.DeleteProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x14\x41pplyMaterialization\x12+.feast.registry.ApplyMaterializationRequest\x1a\x16.google.protobuf.Empty\"\x00\x12p\n\x13ListProjectMetadata\x12*.feast.registry.ListProjectMetadataRequest\x1a+.feast.registry.ListProjectMetadataResponse\"\x00\x12K\n\x0bUpdateInfra\x12\".feast.registry.UpdateInfraRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x08GetInfra\x12\x1f.feast.registry.GetInfraRequest\x1a\x11.feast.core.Infra\"\x00\x12:\n\x06\x43ommit\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x07Refresh\x12\x1e.feast.registry.RefreshRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x37\n\x05Proto\x12\x16.google.protobuf.Empty\x1a\x14.feast.core.Registry\"\x00\x12m\n\x12GetRegistryLineage\x12).feast.registry.GetRegistryLineageRequest\x1a*.feast.registry.GetRegistryLineageResponse\"\x00\x12y\n\x16GetObjectRelationships\x12-.feast.registry.GetObjectRelationshipsRequest\x1a..feast.registry.GetObjectRelationshipsResponse\"\x00\x42\x35Z3github.com/feast-dev/feast/go/protos/feast/registryb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -194,6 +194,18 @@ _globals['_LISTPROJECTSRESPONSE']._serialized_end=6551 _globals['_DELETEPROJECTREQUEST']._serialized_start=6553 _globals['_DELETEPROJECTREQUEST']._serialized_end=6605 - _globals['_REGISTRYSERVER']._serialized_start=6608 - _globals['_REGISTRYSERVER']._serialized_end=10779 + _globals['_ENTITYREFERENCE']._serialized_start=6607 + _globals['_ENTITYREFERENCE']._serialized_end=6652 + _globals['_ENTITYRELATION']._serialized_start=6654 + _globals['_ENTITYRELATION']._serialized_end=6768 + _globals['_GETREGISTRYLINEAGEREQUEST']._serialized_start=6770 + _globals['_GETREGISTRYLINEAGEREQUEST']._serialized_end=6891 + _globals['_GETREGISTRYLINEAGERESPONSE']._serialized_start=6894 + _globals['_GETREGISTRYLINEAGERESPONSE']._serialized_end=7041 + _globals['_GETOBJECTRELATIONSHIPSREQUEST']._serialized_start=7044 + _globals['_GETOBJECTRELATIONSHIPSREQUEST']._serialized_end=7181 + _globals['_GETOBJECTRELATIONSHIPSRESPONSE']._serialized_start=7183 + _globals['_GETOBJECTRELATIONSHIPSRESPONSE']._serialized_end=7270 + _globals['_REGISTRYSERVER']._serialized_start=7273 + _globals['_REGISTRYSERVER']._serialized_end=11678 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi index f4507c02e26..059b572fac3 100644 --- a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi @@ -1316,3 +1316,126 @@ class DeleteProjectRequest(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["commit", b"commit", "name", b"name"]) -> None: ... global___DeleteProjectRequest = DeleteProjectRequest + +class EntityReference(google.protobuf.message.Message): + """Lineage Messages""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + type: builtins.str + """"dataSource", "entity", "featureView", "featureService" """ + name: builtins.str + def __init__( + self, + *, + type: builtins.str = ..., + name: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "type", b"type"]) -> None: ... + +global___EntityReference = EntityReference + +class EntityRelation(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SOURCE_FIELD_NUMBER: builtins.int + TARGET_FIELD_NUMBER: builtins.int + @property + def source(self) -> global___EntityReference: ... + @property + def target(self) -> global___EntityReference: ... + def __init__( + self, + *, + source: global___EntityReference | None = ..., + target: global___EntityReference | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["source", b"source", "target", b"target"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["source", b"source", "target", b"target"]) -> None: ... + +global___EntityRelation = EntityRelation + +class GetRegistryLineageRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + FILTER_OBJECT_TYPE_FIELD_NUMBER: builtins.int + FILTER_OBJECT_NAME_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + filter_object_type: builtins.str + filter_object_name: builtins.str + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + filter_object_type: builtins.str = ..., + filter_object_name: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "filter_object_name", b"filter_object_name", "filter_object_type", b"filter_object_type", "project", b"project"]) -> None: ... + +global___GetRegistryLineageRequest = GetRegistryLineageRequest + +class GetRegistryLineageResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RELATIONSHIPS_FIELD_NUMBER: builtins.int + INDIRECT_RELATIONSHIPS_FIELD_NUMBER: builtins.int + @property + def relationships(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___EntityRelation]: ... + @property + def indirect_relationships(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___EntityRelation]: ... + def __init__( + self, + *, + relationships: collections.abc.Iterable[global___EntityRelation] | None = ..., + indirect_relationships: collections.abc.Iterable[global___EntityRelation] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["indirect_relationships", b"indirect_relationships", "relationships", b"relationships"]) -> None: ... + +global___GetRegistryLineageResponse = GetRegistryLineageResponse + +class GetObjectRelationshipsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + OBJECT_TYPE_FIELD_NUMBER: builtins.int + OBJECT_NAME_FIELD_NUMBER: builtins.int + INCLUDE_INDIRECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + project: builtins.str + object_type: builtins.str + object_name: builtins.str + include_indirect: builtins.bool + allow_cache: builtins.bool + def __init__( + self, + *, + project: builtins.str = ..., + object_type: builtins.str = ..., + object_name: builtins.str = ..., + include_indirect: builtins.bool = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "include_indirect", b"include_indirect", "object_name", b"object_name", "object_type", b"object_type", "project", b"project"]) -> None: ... + +global___GetObjectRelationshipsRequest = GetObjectRelationshipsRequest + +class GetObjectRelationshipsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RELATIONSHIPS_FIELD_NUMBER: builtins.int + @property + def relationships(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___EntityRelation]: ... + def __init__( + self, + *, + relationships: collections.abc.Iterable[global___EntityRelation] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["relationships", b"relationships"]) -> None: ... + +global___GetObjectRelationshipsResponse = GetObjectRelationshipsResponse diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py index bab23c4394e..a0a8d89b9d6 100644 --- a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py @@ -252,6 +252,16 @@ def __init__(self, channel): request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, response_deserializer=feast_dot_core_dot_Registry__pb2.Registry.FromString, ) + self.GetRegistryLineage = channel.unary_unary( + '/feast.registry.RegistryServer/GetRegistryLineage', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetRegistryLineageRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetRegistryLineageResponse.FromString, + ) + self.GetObjectRelationships = channel.unary_unary( + '/feast.registry.RegistryServer/GetObjectRelationships', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetObjectRelationshipsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetObjectRelationshipsResponse.FromString, + ) class RegistryServerServicer(object): @@ -538,6 +548,19 @@ def Proto(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetRegistryLineage(self, request, context): + """Lineage RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetObjectRelationships(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_RegistryServerServicer_to_server(servicer, server): rpc_method_handlers = { @@ -766,6 +789,16 @@ def add_RegistryServerServicer_to_server(servicer, server): request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, response_serializer=feast_dot_core_dot_Registry__pb2.Registry.SerializeToString, ), + 'GetRegistryLineage': grpc.unary_unary_rpc_method_handler( + servicer.GetRegistryLineage, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetRegistryLineageRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetRegistryLineageResponse.SerializeToString, + ), + 'GetObjectRelationships': grpc.unary_unary_rpc_method_handler( + servicer.GetObjectRelationships, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetObjectRelationshipsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetObjectRelationshipsResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'feast.registry.RegistryServer', rpc_method_handlers) @@ -1540,3 +1573,37 @@ def Proto(request, feast_dot_core_dot_Registry__pb2.Registry.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetRegistryLineage(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetRegistryLineage', + feast_dot_registry_dot_RegistryServer__pb2.GetRegistryLineageRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.GetRegistryLineageResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetObjectRelationships(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetObjectRelationships', + feast_dot_registry_dot_RegistryServer__pb2.GetObjectRelationshipsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.GetObjectRelationshipsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index db9c5e9b37f..5d5f4281149 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -745,6 +745,44 @@ def DeleteProject(self, request: RegistryServer_pb2.DeleteProjectRequest, contex self.proxied_registry.delete_project(name=request.name, commit=request.commit) return Empty() + def GetRegistryLineage( + self, request: RegistryServer_pb2.GetRegistryLineageRequest, context + ): + """Get complete registry lineage with relationships and indirect relationships.""" + relationships, indirect_relationships = ( + self.proxied_registry.get_registry_lineage( + project=request.project, + allow_cache=request.allow_cache, + filter_object_type=request.filter_object_type + if request.filter_object_type + else None, + filter_object_name=request.filter_object_name + if request.filter_object_name + else None, + ) + ) + + return RegistryServer_pb2.GetRegistryLineageResponse( + relationships=[rel.to_proto() for rel in relationships], + indirect_relationships=[rel.to_proto() for rel in indirect_relationships], + ) + + def GetObjectRelationships( + self, request: RegistryServer_pb2.GetObjectRelationshipsRequest, context + ): + """Get relationships for a specific object.""" + relationships = self.proxied_registry.get_object_relationships( + project=request.project, + object_type=request.object_type, + object_name=request.object_name, + include_indirect=request.include_indirect, + allow_cache=request.allow_cache, + ) + + return RegistryServer_pb2.GetObjectRelationshipsResponse( + relationships=[rel.to_proto() for rel in relationships] + ) + def Commit(self, request, context): self.proxied_registry.commit() return Empty() diff --git a/sdk/python/tests/unit/api/test_api_rest_registry.py b/sdk/python/tests/unit/api/test_api_rest_registry.py index 706ba701b76..fcf25d4274f 100644 --- a/sdk/python/tests/unit/api/test_api_rest_registry.py +++ b/sdk/python/tests/unit/api/test_api_rest_registry.py @@ -131,3 +131,120 @@ def test_projects_via_rest(fastapi_test_app): def test_permissions_via_rest(fastapi_test_app): response = fastapi_test_app.get("/permissions?project=demo_project") assert response.status_code == 200 + + +def test_lineage_registry_via_rest(fastapi_test_app): + """Test the /lineage/registry endpoint.""" + response = fastapi_test_app.get("/lineage/registry?project=demo_project") + assert response.status_code == 200 + + data = response.json() + assert "relationships" in data + assert "indirect_relationships" in data + assert isinstance(data["relationships"], list) + assert isinstance(data["indirect_relationships"], list) + + +def test_lineage_registry_with_filters_via_rest(fastapi_test_app): + """Test the /lineage/registry endpoint with filters.""" + response = fastapi_test_app.get( + "/lineage/registry?project=demo_project&filter_object_type=featureView" + ) + assert response.status_code == 200 + + response = fastapi_test_app.get( + "/lineage/registry?project=demo_project&filter_object_type=featureView&filter_object_name=user_profile" + ) + assert response.status_code == 200 + + +def test_object_relationships_via_rest(fastapi_test_app): + """Test the /lineage/objects/{object_type}/{object_name} endpoint.""" + response = fastapi_test_app.get( + "/lineage/objects/featureView/user_profile?project=demo_project" + ) + assert response.status_code == 200 + + data = response.json() + assert "relationships" in data + assert isinstance(data["relationships"], list) + + +def test_object_relationships_with_indirect_via_rest(fastapi_test_app): + """Test the object relationships endpoint with indirect relationships.""" + response = fastapi_test_app.get( + "/lineage/objects/featureView/user_profile?project=demo_project&include_indirect=true" + ) + assert response.status_code == 200 + + data = response.json() + assert "relationships" in data + assert isinstance(data["relationships"], list) + + +def test_object_relationships_invalid_type_via_rest(fastapi_test_app): + """Test the object relationships endpoint with invalid object type.""" + response = fastapi_test_app.get( + "/lineage/objects/invalidType/some_name?project=demo_project" + ) + assert response.status_code == 400 + + data = response.json() + assert "detail" in data + assert "Invalid object_type" in data["detail"] + + +def test_complete_registry_data_via_rest(fastapi_test_app): + """Test the /lineage/complete endpoint.""" + response = fastapi_test_app.get("/lineage/complete?project=demo_project") + assert response.status_code == 200 + + data = response.json() + + assert "project" in data + assert data["project"] == "demo_project" + assert "objects" in data + assert "relationships" in data + assert "indirectRelationships" in data + + objects = data["objects"] + assert "entities" in objects + assert "dataSources" in objects + assert "featureViews" in objects + assert "featureServices" in objects + + assert isinstance(objects["entities"], list) + assert isinstance(objects["dataSources"], list) + assert isinstance(objects["featureViews"], list) + assert isinstance(objects["featureServices"], list) + + +def test_complete_registry_data_cache_control_via_rest(fastapi_test_app): + """Test the /lineage/complete endpoint with cache control.""" + response = fastapi_test_app.get( + "/lineage/complete?project=demo_project&allow_cache=false" + ) + assert response.status_code == 200 + + data = response.json() + assert "project" in data + response = fastapi_test_app.get( + "/lineage/complete?project=demo_project&allow_cache=true" + ) + assert response.status_code == 200 + + +def test_lineage_endpoint_error_handling(fastapi_test_app): + """Test error handling in lineage endpoints.""" + # Test missing project parameter + response = fastapi_test_app.get("/lineage/registry") + assert response.status_code == 422 # Validation error + + # Test invalid project + response = fastapi_test_app.get("/lineage/registry?project=nonexistent_project") + # Should still return 200 but with empty results + assert response.status_code == 200 + + # Test object relationships with missing parameters + response = fastapi_test_app.get("/lineage/objects/featureView/test_fv") + assert response.status_code == 422 # Missing required project parameter diff --git a/sdk/python/tests/unit/api/test_api_rest_registry_server.py b/sdk/python/tests/unit/api/test_api_rest_registry_server.py index 1409d15e156..36b784eed6b 100644 --- a/sdk/python/tests/unit/api/test_api_rest_registry_server.py +++ b/sdk/python/tests/unit/api/test_api_rest_registry_server.py @@ -67,3 +67,6 @@ def test_routes_registered_in_app(mock_store_and_registry): assert "/data_sources" in route_paths assert "/saved_datasets" in route_paths assert "/permissions" in route_paths + assert "/lineage/registry" in route_paths + assert "/lineage/objects/{object_type}/{object_name}" in route_paths + assert "/lineage/complete" in route_paths diff --git a/sdk/python/tests/unit/infra/test_registry_lineage.py b/sdk/python/tests/unit/infra/test_registry_lineage.py new file mode 100644 index 00000000000..0175aed565f --- /dev/null +++ b/sdk/python/tests/unit/infra/test_registry_lineage.py @@ -0,0 +1,246 @@ +"""Tests for registry lineage functionality.""" + +from feast.lineage.registry_lineage import ( + EntityReference, + EntityRelation, + FeastObjectType, + RegistryLineageGenerator, +) +from feast.protos.feast.core.DataSource_pb2 import DataSource +from feast.protos.feast.core.Entity_pb2 import Entity, EntitySpecV2 +from feast.protos.feast.core.FeatureService_pb2 import ( + FeatureService, + FeatureServiceSpec, +) +from feast.protos.feast.core.FeatureView_pb2 import FeatureView, FeatureViewSpec +from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( + OnDemandFeatureView, + OnDemandFeatureViewSpec, +) +from feast.protos.feast.core.Registry_pb2 import Registry +from feast.protos.feast.core.StreamFeatureView_pb2 import ( + StreamFeatureView, + StreamFeatureViewSpec, +) + + +class TestRegistryLineage: + def test_lineage_generator_basic(self): + """Test basic lineage generation with simple relationships.""" + + # Create registry with basic objects + registry = Registry() + + # Create entity + entity_spec = EntitySpecV2(name="user_id") + entity = Entity(spec=entity_spec) + registry.entities.append(entity) + + # Create data source + data_source = DataSource() + registry.data_sources.append(data_source) + + # Create feature view + fv_spec = FeatureViewSpec(name="user_features") + fv_spec.entities.append("user_id") + feature_view = FeatureView(spec=fv_spec) + registry.feature_views.append(feature_view) + + # Create feature service + fs_spec = FeatureServiceSpec(name="user_service") + feature_service = FeatureService(spec=fs_spec) + registry.feature_services.append(feature_service) + + # Generate lineage + lineage_generator = RegistryLineageGenerator() + relationships, indirect_relationships = lineage_generator.generate_lineage( + registry + ) + + # Should return valid results without crashing + assert isinstance(relationships, list) + assert isinstance(indirect_relationships, list) + + def test_object_relationships_filtering(self): + """Test filtering relationships for a specific object.""" + + # Create simple registry for testing + registry = Registry() + + # Create a basic feature view + fv_spec = FeatureViewSpec(name="user_features") + feature_view = FeatureView(spec=fv_spec) + registry.feature_views.append(feature_view) + + # Test object relationship filtering + lineage_generator = RegistryLineageGenerator() + relationships = lineage_generator.get_object_relationships( + registry, "featureView", "user_features", include_indirect=False + ) + + # Should return a list (may be empty for simple test) + assert isinstance(relationships, list) + + def test_to_proto_fallback(self): + """Test that to_proto methods work with fallback to dict.""" + + entity_ref = EntityReference(FeastObjectType.ENTITY, "test_entity") + proto_result = entity_ref.to_proto() + + # Should return either protobuf object or dict fallback + assert proto_result is not None + if isinstance(proto_result, dict): + assert proto_result["type"] == "entity" + assert proto_result["name"] == "test_entity" + + relation = EntityRelation( + source=EntityReference(FeastObjectType.ENTITY, "source_entity"), + target=EntityReference(FeastObjectType.FEATURE_VIEW, "target_fv"), + ) + relation_proto = relation.to_proto() + assert relation_proto is not None + + def test_empty_registry(self): + """Test lineage generation with empty registry.""" + registry = Registry() + + lineage_generator = RegistryLineageGenerator() + relationships, indirect_relationships = lineage_generator.generate_lineage( + registry + ) + + assert len(relationships) == 0 + assert len(indirect_relationships) == 0 + + def test_complex_lineage_scenario(self): + """Test complex lineage with multiple feature views and services.""" + registry = Registry() + + # Create multiple entities + entity1_spec = EntitySpecV2(name="user_id") + entity1 = Entity(spec=entity1_spec) + + entity2_spec = EntitySpecV2(name="product_id") + entity2 = Entity(spec=entity2_spec) + + registry.entities.extend([entity1, entity2]) + + # Create multiple data sources + ds1 = DataSource() + ds2 = DataSource() + registry.data_sources.extend([ds1, ds2]) + + # Create feature views + fv1_spec = FeatureViewSpec(name="user_features") + fv1_spec.entities.append("user_id") + fv1 = FeatureView(spec=fv1_spec) + + fv2_spec = FeatureViewSpec(name="product_features") + fv2_spec.entities.append("product_id") + fv2 = FeatureView(spec=fv2_spec) + + registry.feature_views.extend([fv1, fv2]) + + # Generate lineage + lineage_generator = RegistryLineageGenerator() + relationships, indirect_relationships = lineage_generator.generate_lineage( + registry + ) + + # Should return valid results without crashing + assert isinstance(relationships, list) + assert isinstance(indirect_relationships, list) + + def test_on_demand_feature_view_lineage(self): + """Test lineage with on-demand feature views.""" + registry = Registry() + + # Create regular feature view + fv_spec = FeatureViewSpec(name="base_features") + fv_spec.entities.append("user_id") + fv = FeatureView(spec=fv_spec) + registry.feature_views.append(fv) + + # Create on-demand feature view + odfv_spec = OnDemandFeatureViewSpec(name="derived_features") + odfv = OnDemandFeatureView(spec=odfv_spec) + registry.on_demand_feature_views.append(odfv) + + # Generate lineage + lineage_generator = RegistryLineageGenerator() + relationships, indirect_relationships = lineage_generator.generate_lineage( + registry + ) + + # Should handle on-demand feature views without crashing + assert isinstance(relationships, list) + assert isinstance(indirect_relationships, list) + + def test_stream_feature_view_lineage(self): + """Test lineage with stream feature views.""" + registry = Registry() + + # Create stream feature view + sfv_spec = StreamFeatureViewSpec(name="streaming_features") + sfv_spec.entities.append("user_id") + sfv = StreamFeatureView(spec=sfv_spec) + registry.stream_feature_views.append(sfv) + + # Generate lineage + lineage_generator = RegistryLineageGenerator() + relationships, indirect_relationships = lineage_generator.generate_lineage( + registry + ) + + # Should handle stream feature views without crashing + assert isinstance(relationships, list) + assert isinstance(indirect_relationships, list) + + def test_lineage_graph_generation(self): + """Test lineage graph generation for visualization.""" + registry = Registry() + + # Create simple setup + entity_spec = EntitySpecV2(name="user_id") + entity = Entity(spec=entity_spec) + registry.entities.append(entity) + + fv_spec = FeatureViewSpec(name="user_features") + fv_spec.entities.append("user_id") + fv = FeatureView(spec=fv_spec) + registry.feature_views.append(fv) + + # Generate lineage graph + lineage_generator = RegistryLineageGenerator() + graph = lineage_generator.get_object_lineage_graph( + registry, "featureView", "user_features", depth=2 + ) + + assert "nodes" in graph + assert "edges" in graph + assert isinstance(graph["nodes"], list) + assert isinstance(graph["edges"], list) + + def test_missing_object_attributes(self): + """Test lineage generation with objects missing expected attributes.""" + registry = Registry() + + # Create feature view with minimal attributes + fv_spec = FeatureViewSpec(name="incomplete_fv") + fv = FeatureView(spec=fv_spec) + registry.feature_views.append(fv) + + # Create feature service with minimal attributes + fs_spec = FeatureServiceSpec(name="incomplete_fs") + fs = FeatureService(spec=fs_spec) + registry.feature_services.append(fs) + + # Should not crash and should handle gracefully + lineage_generator = RegistryLineageGenerator() + relationships, indirect_relationships = lineage_generator.generate_lineage( + registry + ) + + # Should return empty or minimal relationships without crashing + assert isinstance(relationships, list) + assert isinstance(indirect_relationships, list) From 1e8b65584a25b0b8009916eca0b2926bff3ef80b Mon Sep 17 00:00:00 2001 From: ntkathole Date: Sun, 29 Jun 2025 21:44:35 +0530 Subject: [PATCH 2/2] docs: Added docs for rest api endpoints Signed-off-by: ntkathole --- .../feature-servers/registry-server.md | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) diff --git a/docs/reference/feature-servers/registry-server.md b/docs/reference/feature-servers/registry-server.md index 55a2ac4bf07..ddda5393863 100644 --- a/docs/reference/feature-servers/registry-server.md +++ b/docs/reference/feature-servers/registry-server.md @@ -12,6 +12,276 @@ Feast supports running the Registry Server in three distinct modes: | REST + gRPC | `feast serve_registry --rest-api` | Enables both interfaces | | REST only | `feast serve_registry --rest-api --no-grpc` | Used for REST-only clients like the UI | +## REST API Endpoints + +The REST API provides HTTP/JSON endpoints for accessing all registry metadata. All endpoints are prefixed with `/api/v1` and return JSON responses. + +### Authentication + +The REST API supports Bearer token authentication. Include your token in the Authorization header: + +```bash +Authorization: Bearer +``` + +### Common Query Parameters + +Most endpoints support these common query parameters: + +- `project` (required for most endpoints): The project name +- `allow_cache` (optional, default: `true`): Whether to allow cached data +- `tags` (optional): Filter results by tags in key=value format + +### Entities + +#### List Entities +- **Endpoint**: `GET /api/v1/entities` +- **Description**: Retrieve all entities in a project +- **Parameters**: + - `project` (required): Project name +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/entities?project=my_project" + ``` + +#### Get Entity +- **Endpoint**: `GET /api/v1/entities/{name}` +- **Description**: Retrieve a specific entity by name +- **Parameters**: + - `name` (path): Entity name + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/entities/user_id?project=my_project" + ``` + +### Data Sources + +#### List Data Sources +- **Endpoint**: `GET /api/v1/data_sources` +- **Description**: Retrieve all data sources in a project +- **Parameters**: + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data + - `tags` (optional): Filter by tags +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/data_sources?project=my_project" + ``` + +#### Get Data Source +- **Endpoint**: `GET /api/v1/data_sources/{name}` +- **Description**: Retrieve a specific data source by name +- **Parameters**: + - `name` (path): Data source name + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/data_sources/user_data?project=my_project" + ``` + +### Feature Views + +#### List Feature Views +- **Endpoint**: `GET /api/v1/feature_views` +- **Description**: Retrieve all feature views in a project +- **Parameters**: + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data + - `tags` (optional): Filter by tags +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/feature_views?project=my_project" + ``` + +#### Get Feature View +- **Endpoint**: `GET /api/v1/feature_views/{name}` +- **Description**: Retrieve a specific feature view by name +- **Parameters**: + - `name` (path): Feature view name + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/feature_views/user_features?project=my_project" + ``` + +### Feature Services + +#### List Feature Services +- **Endpoint**: `GET /api/v1/feature_services` +- **Description**: Retrieve all feature services in a project +- **Parameters**: + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data + - `tags` (optional): Filter by tags +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/feature_services?project=my_project" + ``` + +#### Get Feature Service +- **Endpoint**: `GET /api/v1/feature_services/{name}` +- **Description**: Retrieve a specific feature service by name +- **Parameters**: + - `name` (path): Feature service name + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/feature_services/recommendation_service?project=my_project" + ``` + +### Lineage and Relationships + +#### Get Registry Lineage +- **Endpoint**: `GET /api/v1/lineage/registry` +- **Description**: Retrieve complete registry lineage with relationships and indirect relationships +- **Parameters**: + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data + - `filter_object_type` (optional): Filter by object type (`dataSource`, `entity`, `featureView`, `featureService`) + - `filter_object_name` (optional): Filter by object name +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/lineage/registry?project=my_project" + ``` + +#### Get Object Relationships +- **Endpoint**: `GET /api/v1/lineage/objects/{object_type}/{object_name}` +- **Description**: Retrieve relationships for a specific object +- **Parameters**: + - `object_type` (path): Type of object (`dataSource`, `entity`, `featureView`, `featureService`) + - `object_name` (path): Name of the object + - `project` (required): Project name + - `include_indirect` (optional): Whether to include indirect relationships + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/lineage/objects/featureView/user_features?project=my_project&include_indirect=true" + ``` + +#### Get Complete Registry Data +- **Endpoint**: `GET /api/v1/lineage/complete` +- **Description**: Retrieve complete registry data including all objects and relationships (optimized for UI consumption) +- **Parameters**: + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/lineage/complete?project=my_project" + ``` + +### Permissions + +#### List Permissions +- **Endpoint**: `GET /api/v1/permissions` +- **Description**: Retrieve all permissions in a project +- **Parameters**: + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/permissions?project=my_project" + ``` + +#### Get Permission +- **Endpoint**: `GET /api/v1/permissions/{name}` +- **Description**: Retrieve a specific permission by name +- **Parameters**: + - `name` (path): Permission name + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/permissions/read_features?project=my_project" + ``` + +### Projects + +#### List Projects +- **Endpoint**: `GET /api/v1/projects` +- **Description**: Retrieve all projects +- **Parameters**: + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/projects" + ``` + +#### Get Project +- **Endpoint**: `GET /api/v1/projects/{name}` +- **Description**: Retrieve a specific project by name +- **Parameters**: + - `name` (path): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/projects/my_project" + ``` + +### Saved Datasets + +#### List Saved Datasets +- **Endpoint**: `GET /api/v1/saved_datasets` +- **Description**: Retrieve all saved datasets in a project +- **Parameters**: + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data + - `tags` (optional): Filter by tags +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/saved_datasets?project=my_project" + ``` + +#### Get Saved Dataset +- **Endpoint**: `GET /api/v1/saved_datasets/{name}` +- **Description**: Retrieve a specific saved dataset by name +- **Parameters**: + - `name` (path): Saved dataset name + - `project` (required): Project name + - `allow_cache` (optional): Whether to allow cached data +- **Example**: + ```bash + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/saved_datasets/training_data?project=my_project" + ``` + +### Response Formats + +All endpoints return JSON responses with the following general structure: + +- **Success (200)**: Returns the requested data +- **Bad Request (400)**: Invalid parameters or request format +- **Unauthorized (401)**: Missing or invalid authentication token +- **Not Found (404)**: Requested resource does not exist +- **Internal Server Error (500)**: Server-side error + +### Interactive API Documentation + +When the REST API server is running, you can access interactive documentation at: + +- **Swagger UI**: `http://localhost:6572/` (root path) +- **ReDoc**: `http://localhost:6572/docs` +- **OpenAPI Schema**: `http://localhost:6572/openapi.json` ## How to configure the server