Skip to content

Commit 36620b9

Browse files
committed
feat: Phase 4 - Default rest mode
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent cb07f0e commit 36620b9

33 files changed

Lines changed: 487 additions & 975 deletions

sdk/python/feast/cli/ui.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
from feast.repo_operations import create_feature_store, registry_dump
44

5-
VALID_MODES = ("proto", "rest", "rest-external")
6-
75

86
@click.command()
97
@click.option(
@@ -54,25 +52,6 @@
5452
show_default=False,
5553
help="path to TLS(SSL) certificate public key. You need to pass --key arg as well to start server in TLS mode",
5654
)
57-
@click.option(
58-
"--mode",
59-
"-m",
60-
type=click.Choice(VALID_MODES, case_sensitive=False),
61-
default="proto",
62-
show_default=True,
63-
help=(
64-
"Data serving mode for the UI. "
65-
"'proto' serves the registry as a protobuf blob (current default). "
66-
"'rest' mounts the REST registry API alongside the UI. "
67-
"'rest-external' proxies to an external REST registry API."
68-
),
69-
)
70-
@click.option(
71-
"--rest-api-url",
72-
type=click.STRING,
73-
default="",
74-
help="Base URL of an external REST registry API (required when --mode=rest-external). Example: http://registry-host:6570/api/v1",
75-
)
7655
@click.pass_context
7756
def ui(
7857
ctx: click.Context,
@@ -82,8 +61,6 @@ def ui(
8261
root_path: str = "",
8362
tls_key_path: str = "",
8463
tls_cert_path: str = "",
85-
mode: str = "proto",
86-
rest_api_url: str = "",
8764
):
8865
"""
8966
Shows the Feast UI over the current directory
@@ -92,10 +69,6 @@ def ui(
9269
raise click.BadParameter(
9370
"Please configure --key and --cert args to start the feature server in SSL mode."
9471
)
95-
if mode == "rest-external" and not rest_api_url:
96-
raise click.BadParameter(
97-
"--rest-api-url is required when using --mode=rest-external."
98-
)
9972
store = create_feature_store(ctx)
10073
store.serve_ui(
10174
host=host,
@@ -105,6 +78,4 @@ def ui(
10578
root_path=root_path,
10679
tls_key_path=tls_key_path,
10780
tls_cert_path=tls_cert_path,
108-
mode=mode,
109-
rest_api_url=rest_api_url,
11081
)

sdk/python/feast/feature_store.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3153,15 +3153,8 @@ def serve_ui(
31533153
root_path: str = "",
31543154
tls_key_path: str = "",
31553155
tls_cert_path: str = "",
3156-
mode: str = "proto",
3157-
rest_api_url: str = "",
31583156
) -> None:
3159-
"""Start the UI server locally
3160-
3161-
Args:
3162-
mode: Data serving mode - 'proto' (default), 'rest', or 'rest-external'.
3163-
rest_api_url: Base URL for external REST API (required for 'rest-external' mode).
3164-
"""
3157+
"""Start the UI server locally"""
31653158
if flags_helper.is_test():
31663159
warnings.warn(
31673160
"The Feast UI is an experimental feature. "
@@ -3178,8 +3171,6 @@ def serve_ui(
31783171
root_path=root_path,
31793172
tls_key_path=tls_key_path,
31803173
tls_cert_path=tls_cert_path,
3181-
mode=mode,
3182-
rest_api_url=rest_api_url,
31833174
)
31843175

31853176
def serve_registry(

sdk/python/feast/ui_server.py

Lines changed: 7 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Callable, Optional
66

77
import uvicorn
8-
from fastapi import FastAPI, Request, Response, status
8+
from fastapi import FastAPI, Response, status
99
from fastapi.middleware.cors import CORSMiddleware
1010
from fastapi.staticfiles import StaticFiles
1111

@@ -18,16 +18,12 @@ def _build_projects_list(
1818
store: "feast.FeatureStore",
1919
project_id: str,
2020
root_path: str,
21-
mode: str,
2221
):
23-
"""Build the projects list for the UI, with mode-aware registry paths."""
22+
"""Build the projects list for the UI."""
2423
discovered_projects = []
2524
registry = store.registry.proto()
2625

27-
if mode == "proto":
28-
registry_path_template = f"{root_path}/registry"
29-
else:
30-
registry_path_template = f"{root_path}/api/v1"
26+
registry_path_template = f"{root_path}/api/v1"
3127

3228
if registry and registry.projects and len(registry.projects) > 0:
3329
for proj in registry.projects:
@@ -60,52 +56,7 @@ def _build_projects_list(
6056
}
6157
discovered_projects.insert(0, all_projects_entry)
6258

63-
return {"projects": discovered_projects, "mode": mode}
64-
65-
66-
def _setup_proto_mode(
67-
app: FastAPI, store: "feast.FeatureStore", registry_ttl_secs: int
68-
):
69-
"""Set up the legacy proto-blob serving mode (GET /registry)."""
70-
registry_proto = None
71-
shutting_down = False
72-
active_timer: Optional[threading.Timer] = None
73-
74-
def async_refresh():
75-
store.refresh_registry()
76-
nonlocal registry_proto
77-
registry_proto = store.registry.proto()
78-
if shutting_down:
79-
return
80-
nonlocal active_timer
81-
active_timer = threading.Timer(registry_ttl_secs, async_refresh)
82-
active_timer.start()
83-
84-
@app.on_event("shutdown")
85-
def shutdown_event():
86-
nonlocal shutting_down
87-
shutting_down = True
88-
if active_timer:
89-
active_timer.cancel()
90-
91-
async_refresh()
92-
93-
@app.get("/registry")
94-
def read_registry():
95-
if registry_proto is None:
96-
return Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
97-
return Response(
98-
content=registry_proto.SerializeToString(),
99-
media_type="application/octet-stream",
100-
)
101-
102-
@app.get("/health")
103-
def health():
104-
return (
105-
Response(status_code=status.HTTP_200_OK)
106-
if registry_proto
107-
else Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
108-
)
59+
return {"projects": discovered_projects}
10960

11061

11162
def _setup_rest_mode(app: FastAPI, store: "feast.FeatureStore", registry_ttl_secs: int):
@@ -142,15 +93,6 @@ def shutdown_event():
14293
register_all_routes(rest_app, grpc_handler)
14394
app.mount("/api/v1", rest_app)
14495

145-
@app.get("/registry")
146-
def read_registry():
147-
if registry_proto is None:
148-
return Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
149-
return Response(
150-
content=registry_proto.SerializeToString(),
151-
media_type="application/octet-stream",
152-
)
153-
15496
@app.get("/health")
15597
def health():
15698
return (
@@ -162,106 +104,11 @@ def health():
162104
logger.info("REST registry API mounted at /api/v1")
163105

164106

165-
def _setup_rest_external_mode(
166-
app: FastAPI,
167-
store: "feast.FeatureStore",
168-
rest_api_url: str,
169-
registry_ttl_secs: int,
170-
):
171-
"""Reverse-proxy REST API calls to an external registry server."""
172-
import httpx
173-
174-
rest_api_url = rest_api_url.rstrip("/")
175-
client = httpx.AsyncClient(timeout=60.0)
176-
177-
registry_proto = None
178-
shutting_down = False
179-
active_timer: Optional[threading.Timer] = None
180-
181-
def async_refresh():
182-
store.refresh_registry()
183-
nonlocal registry_proto
184-
registry_proto = store.registry.proto()
185-
if shutting_down:
186-
return
187-
nonlocal active_timer
188-
active_timer = threading.Timer(registry_ttl_secs, async_refresh)
189-
active_timer.start()
190-
191-
@app.on_event("shutdown")
192-
async def shutdown_event():
193-
nonlocal shutting_down
194-
shutting_down = True
195-
if active_timer:
196-
active_timer.cancel()
197-
await client.aclose()
198-
199-
async_refresh()
200-
201-
@app.api_route("/api/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
202-
async def proxy_to_external(request: Request, path: str):
203-
target_url = f"{rest_api_url}/{path}"
204-
query_string = str(request.url.query)
205-
if query_string:
206-
target_url = f"{target_url}?{query_string}"
207-
208-
headers = {
209-
k: v
210-
for k, v in request.headers.items()
211-
if k.lower() not in ("host", "content-length", "transfer-encoding")
212-
}
213-
214-
body = await request.body()
215-
216-
try:
217-
resp = await client.request(
218-
method=request.method,
219-
url=target_url,
220-
headers=headers,
221-
content=body if body else None,
222-
)
223-
return Response(
224-
content=resp.content,
225-
status_code=resp.status_code,
226-
media_type=resp.headers.get("content-type", "application/json"),
227-
)
228-
except httpx.RequestError as e:
229-
logger.error(f"Error proxying to {target_url}: {e}")
230-
return Response(
231-
content=json.dumps(
232-
{"detail": "Failed to reach the upstream registry API"}
233-
),
234-
status_code=status.HTTP_502_BAD_GATEWAY,
235-
media_type="application/json",
236-
)
237-
238-
@app.get("/registry")
239-
def read_registry():
240-
if registry_proto is None:
241-
return Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
242-
return Response(
243-
content=registry_proto.SerializeToString(),
244-
media_type="application/octet-stream",
245-
)
246-
247-
@app.get("/health")
248-
def health():
249-
return (
250-
Response(status_code=status.HTTP_200_OK)
251-
if registry_proto
252-
else Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
253-
)
254-
255-
logger.info(f"REST external proxy configured → {rest_api_url}")
256-
257-
258107
def get_app(
259108
store: "feast.FeatureStore",
260109
project_id: str,
261110
registry_ttl_secs: int,
262111
root_path: str = "",
263-
mode: str = "proto",
264-
rest_api_url: str = "",
265112
):
266113
app = FastAPI()
267114

@@ -273,16 +120,11 @@ def get_app(
273120
allow_headers=["*"],
274121
)
275122

276-
if mode == "rest":
277-
_setup_rest_mode(app, store, registry_ttl_secs)
278-
elif mode == "rest-external":
279-
_setup_rest_external_mode(app, store, rest_api_url, registry_ttl_secs)
280-
else:
281-
_setup_proto_mode(app, store, registry_ttl_secs)
123+
_setup_rest_mode(app, store, registry_ttl_secs)
282124

283125
ui_dir_ref = importlib_resources.files(__spec__.parent) / "ui/build/" # type: ignore[name-defined, arg-type]
284126
with importlib_resources.as_file(ui_dir_ref) as ui_dir:
285-
projects_dict = _build_projects_list(store, project_id, root_path, mode)
127+
projects_dict = _build_projects_list(store, project_id, root_path)
286128
with ui_dir.joinpath("projects-list.json").open(mode="w") as f:
287129
f.write(json.dumps(projects_dict))
288130

@@ -312,19 +154,15 @@ def start_server(
312154
root_path: str = "",
313155
tls_key_path: str = "",
314156
tls_cert_path: str = "",
315-
mode: str = "proto",
316-
rest_api_url: str = "",
317157
):
318158
app = get_app(
319159
store,
320160
project_id,
321161
registry_ttl_sec,
322162
root_path,
323-
mode=mode,
324-
rest_api_url=rest_api_url,
325163
)
326164

327-
logger.info(f"Starting Feast UI server in '{mode}' mode on {host}:{port}")
165+
logger.info(f"Starting Feast UI server on {host}:{port}")
328166

329167
if tls_key_path and tls_cert_path:
330168
uvicorn.run(

0 commit comments

Comments
 (0)