11import json
2+ import logging
23import threading
34from importlib import resources as importlib_resources
45from typing import Callable , Optional
1011
1112import feast
1213
14+ logger = logging .getLogger (__name__ )
1315
14- def get_app (
16+
17+ def _build_projects_list (
1518 store : "feast.FeatureStore" ,
1619 project_id : str ,
17- registry_ttl_secs : int ,
18- root_path : str = "" ,
20+ root_path : str ,
1921):
20- app = FastAPI ()
22+ """Build the projects list for the UI."""
23+ discovered_projects = []
24+ registry = store .registry .proto ()
2125
22- app .add_middleware (
23- CORSMiddleware ,
24- allow_origins = ["*" ],
25- allow_credentials = True ,
26- allow_methods = ["*" ],
27- allow_headers = ["*" ],
28- )
26+ registry_path_template = f"{ root_path } /api/v1"
27+
28+ if registry and registry .projects and len (registry .projects ) > 0 :
29+ for proj in registry .projects :
30+ if proj .spec and proj .spec .name :
31+ discovered_projects .append (
32+ {
33+ "name" : proj .spec .name .replace ("_" , " " ).title (),
34+ "description" : proj .spec .description
35+ or f"Project: { proj .spec .name } " ,
36+ "id" : proj .spec .name ,
37+ "registryPath" : registry_path_template ,
38+ }
39+ )
40+ else :
41+ discovered_projects .append (
42+ {
43+ "name" : "Project" ,
44+ "description" : "Test project" ,
45+ "id" : project_id ,
46+ "registryPath" : registry_path_template ,
47+ }
48+ )
49+
50+ if len (discovered_projects ) > 1 :
51+ all_projects_entry = {
52+ "name" : "All Projects" ,
53+ "description" : "View data across all projects" ,
54+ "id" : "all" ,
55+ "registryPath" : registry_path_template ,
56+ }
57+ discovered_projects .insert (0 , all_projects_entry )
58+
59+ return {"projects" : discovered_projects }
60+
61+
62+ def _setup_rest_mode (app : FastAPI , store : "feast.FeatureStore" , registry_ttl_secs : int ):
63+ """Mount the REST registry API routes on the UI server under /api/v1."""
64+ from feast .api .registry .rest import register_all_routes
65+ from feast .registry_server import RegistryServer
2966
30- # Asynchronously refresh registry, notifying shutdown and canceling the active timer if the app is shutting down
3167 registry_proto = None
3268 shutting_down = False
3369 active_timer : Optional [threading .Timer ] = None
@@ -51,61 +87,11 @@ def shutdown_event():
5187
5288 async_refresh ()
5389
54- ui_dir_ref = importlib_resources .files (__spec__ .parent ) / "ui/build/" # type: ignore[name-defined, arg-type]
55- with importlib_resources .as_file (ui_dir_ref ) as ui_dir :
56- # Initialize with the projects-list.json file
57- with ui_dir .joinpath ("projects-list.json" ).open (mode = "w" ) as f :
58- # Get all projects from the registry
59- discovered_projects = []
60- registry = store .registry .proto ()
61-
62- # Use the projects list from the registry
63- if registry and registry .projects and len (registry .projects ) > 0 :
64- for proj in registry .projects :
65- if proj .spec and proj .spec .name :
66- discovered_projects .append (
67- {
68- "name" : proj .spec .name .replace ("_" , " " ).title (),
69- "description" : proj .spec .description
70- or f"Project: { proj .spec .name } " ,
71- "id" : proj .spec .name ,
72- "registryPath" : f"{ root_path } /registry" ,
73- }
74- )
75- else :
76- # If no projects in registry, use the current project from feature_store.yaml
77- discovered_projects .append (
78- {
79- "name" : "Project" ,
80- "description" : "Test project" ,
81- "id" : project_id ,
82- "registryPath" : f"{ root_path } /registry" ,
83- }
84- )
90+ grpc_handler = RegistryServer (store .registry )
8591
86- # Add "All Projects" option at the beginning if there are multiple projects
87- if len (discovered_projects ) > 1 :
88- all_projects_entry = {
89- "name" : "All Projects" ,
90- "description" : "View data across all projects" ,
91- "id" : "all" ,
92- "registryPath" : f"{ root_path } /registry" ,
93- }
94- discovered_projects .insert (0 , all_projects_entry )
95-
96- projects_dict = {"projects" : discovered_projects }
97- f .write (json .dumps (projects_dict ))
98-
99- @app .get ("/registry" )
100- def read_registry ():
101- if registry_proto is None :
102- return Response (
103- status_code = status .HTTP_503_SERVICE_UNAVAILABLE
104- ) # Service Unavailable
105- return Response (
106- content = registry_proto .SerializeToString (),
107- media_type = "application/octet-stream" ,
108- )
92+ rest_app = FastAPI (root_path = "/api/v1" )
93+ register_all_routes (rest_app , grpc_handler )
94+ app .mount ("/api/v1" , rest_app )
10995
11096 @app .get ("/health" )
11197 def health ():
@@ -115,14 +101,38 @@ def health():
115101 else Response (status_code = status .HTTP_503_SERVICE_UNAVAILABLE )
116102 )
117103
118- # For all other paths (such as paths that would otherwise be handled by react router), pass to React
104+ logger .info ("REST registry API mounted at /api/v1" )
105+
106+
107+ def get_app (
108+ store : "feast.FeatureStore" ,
109+ project_id : str ,
110+ registry_ttl_secs : int ,
111+ root_path : str = "" ,
112+ ):
113+ app = FastAPI ()
114+
115+ app .add_middleware (
116+ CORSMiddleware ,
117+ allow_origins = ["*" ],
118+ allow_credentials = True ,
119+ allow_methods = ["*" ],
120+ allow_headers = ["*" ],
121+ )
122+
123+ _setup_rest_mode (app , store , registry_ttl_secs )
124+
125+ ui_dir_ref = importlib_resources .files (__spec__ .parent ) / "ui/build/" # type: ignore[name-defined, arg-type]
126+ with importlib_resources .as_file (ui_dir_ref ) as ui_dir :
127+ projects_dict = _build_projects_list (store , project_id , root_path )
128+ with ui_dir .joinpath ("projects-list.json" ).open (mode = "w" ) as f :
129+ f .write (json .dumps (projects_dict ))
130+
119131 @app .api_route ("/p/{path_name:path}" , methods = ["GET" ])
120132 def catch_all ():
121133 filename = ui_dir .joinpath ("index.html" )
122-
123134 with open (filename ) as f :
124135 content = f .read ()
125-
126136 return Response (content , media_type = "text/html" )
127137
128138 app .mount (
@@ -151,6 +161,9 @@ def start_server(
151161 registry_ttl_sec ,
152162 root_path ,
153163 )
164+
165+ logger .info (f"Starting Feast UI server on { host } :{ port } " )
166+
154167 if tls_key_path and tls_cert_path :
155168 uvicorn .run (
156169 app ,
0 commit comments