Bridge legacy Flask APIs through Engine fallback

This commit is contained in:
2025-10-22 19:59:09 -06:00
parent 7a9feebde5
commit da4cb501e0
4 changed files with 212 additions and 7 deletions

View File

@@ -2,8 +2,11 @@
from __future__ import annotations
import importlib
import logging
import os
from pathlib import Path
from typing import Optional
from typing import Any, Iterable, Optional
from flask import Flask, request, send_from_directory
from flask_cors import CORS
@@ -100,4 +103,135 @@ def create_app(
return app
__all__ = ["create_app"]
def attach_legacy_bridge(
app: Flask,
settings: EngineSettings,
*,
logger: Optional[logging.Logger] = None,
) -> None:
"""Attach the legacy Flask application as a fallback dispatcher.
Borealis ships a mature API surface inside ``Data/Server/server.py``. The
Engine will eventually supersede it, but during the migration the React
frontend still expects the historical endpoints to exist. This helper
attempts to load the legacy application and wires it as a fallback WSGI
dispatcher so any route the Engine does not yet implement transparently
defers to the proven implementation.
"""
log = logger or logging.getLogger("borealis.engine.legacy")
if not _legacy_bridge_enabled():
log.info("legacy-bridge-disabled")
return
legacy = _load_legacy_app(settings, app, log)
if legacy is None:
log.warning("legacy-bridge-unavailable")
return
app.config["ENGINE_LEGACY_BRIDGE_ACTIVE"] = True
app.wsgi_app = _FallbackDispatcher(app.wsgi_app, legacy.wsgi_app) # type: ignore[assignment]
app.extensions["legacy_flask_app"] = legacy
log.info("legacy-bridge-active")
def _legacy_bridge_enabled() -> bool:
raw = os.getenv("BOREALIS_ENGINE_ENABLE_LEGACY_BRIDGE", "1")
return raw.strip().lower() in {"1", "true", "yes", "on"}
def _load_legacy_app(
settings: EngineSettings,
engine_app: Flask,
logger: logging.Logger,
) -> Optional[Flask]:
try:
legacy_module = importlib.import_module("Data.Server.server")
except Exception as exc: # pragma: no cover - defensive
logger.exception("legacy-import-failed", exc_info=exc)
return None
legacy_app = getattr(legacy_module, "app", None)
if not isinstance(legacy_app, Flask):
logger.error("legacy-app-missing")
return None
# Align runtime configuration so both applications share database and
# session state.
try:
setattr(legacy_module, "DB_PATH", str(settings.database_path))
except Exception: # pragma: no cover - defensive
logger.warning("legacy-db-path-sync-failed", extra={"path": str(settings.database_path)})
_synchronise_session_config(engine_app, legacy_app)
return legacy_app
def _synchronise_session_config(engine_app: Flask, legacy_app: Flask) -> None:
legacy_app.secret_key = engine_app.config.get("SECRET_KEY", legacy_app.secret_key)
for key in (
"SESSION_COOKIE_HTTPONLY",
"SESSION_COOKIE_SECURE",
"SESSION_COOKIE_SAMESITE",
"SESSION_COOKIE_DOMAIN",
):
value = engine_app.config.get(key)
if value is not None:
legacy_app.config[key] = value
class _FallbackDispatcher:
"""WSGI dispatcher that retries a secondary app when the primary 404s."""
__slots__ = ("_primary", "_fallback", "_retry_statuses")
def __init__(
self,
primary: Any,
fallback: Any,
*,
retry_statuses: Iterable[int] = (404,),
) -> None:
self._primary = primary
self._fallback = fallback
self._retry_statuses = {int(status) for status in retry_statuses}
def __call__(self, environ: dict[str, Any], start_response: Any) -> Iterable[bytes]:
captured_body: list[bytes] = []
captured_status: dict[str, Any] = {}
def _capture_start_response(status: str, headers: list[tuple[str, str]], exc_info: Any = None):
captured_status["status"] = status
captured_status["headers"] = headers
captured_status["exc_info"] = exc_info
def _write(data: bytes) -> None:
captured_body.append(data)
return _write
primary_iterable = self._primary(environ, _capture_start_response)
try:
for chunk in primary_iterable:
captured_body.append(chunk)
finally:
close = getattr(primary_iterable, "close", None)
if callable(close):
close()
status_line = str(captured_status.get("status") or "500 Internal Server Error")
try:
status_code = int(status_line.split()[0])
except Exception: # pragma: no cover - defensive
status_code = 500
if status_code not in self._retry_statuses:
start_response(status_line, captured_status.get("headers", []), captured_status.get("exc_info"))
return captured_body
return self._fallback(environ, start_response)
__all__ = ["attach_legacy_bridge", "create_app"]