working fastapi app support

This commit is contained in:
Jakob Pinterits
2024-10-20 21:42:50 +02:00
parent 1bc0fcb7a0
commit bf298f6034
3 changed files with 64 additions and 23 deletions

View File

@@ -326,15 +326,26 @@ class FastapiServer(fastapi.FastAPI, AbstractAppServer):
# The route that serves the index.html will be registered later, so that
# it has a lower priority than user-created routes.
#
# This keeps track of whether the fallback route has already been
# registered.
self._index_hmtl_route_registered = False
async def __call__(self, scope, receive, send) -> None:
# Because this is a single page application, all other routes should
# serve the index page. The session will determine which components
# should be shown.
self.add_api_route(
"/{initial_route_str:path}", self._serve_index, methods=["GET"]
)
#
# This route is registered last, so that it has the lowest priority.
# This allows the user to add custom routes that take precedence.
if not self._index_hmtl_route_registered:
self._index_hmtl_route_registered = True
self.add_api_route(
"/{initial_route_str:path}", self._serve_index, methods=["GET"]
)
# Delegate to FastAPI
return await super().__call__(scope, receive, send)
@contextlib.asynccontextmanager

View File

@@ -253,7 +253,7 @@ def load_user_app(
if isinstance(var, rio.app_server.fastapi_server.FastapiServer):
as_fastapi_apps.append((var_name, var))
if isinstance(var, rio.App):
elif isinstance(var, rio.App):
rio_apps.append((var_name, var))
# Prepare the main file name
@@ -262,10 +262,12 @@ def load_user_app(
else:
main_file_reference = f"The file `{Path(app_module.__file__).relative_to(proj.project_directory)}`"
print(as_fastapi_apps, rio_apps)
# Which type of app do we have?
#
# Case: FastAPI app
if len(as_fastapi_apps) > 1:
if len(as_fastapi_apps) > 0:
app_list = as_fastapi_apps
app_server = as_fastapi_apps[0][1]
app_instance = app_server.app

View File

@@ -5,6 +5,7 @@ import typing as t
import revel
import uvicorn
import uvicorn.lifespan.on
from starlette.types import Receive, Scope, Send
import rio
import rio.app_server.fastapi_server
@@ -44,27 +45,47 @@ class UvicornWorker:
# either a Rio app or a FastAPI app (derived from a Rio app).
self.app_server = app_server
def _create_and_store_app_server(self) -> None:
app_server = self.app._as_fastapi(
debug_mode=self.debug_mode,
running_in_window=self.run_in_window,
internal_on_app_start=lambda: self.on_server_is_ready_or_failed.set_result(
None
),
base_url=self.base_url,
)
assert isinstance(
app_server, rio.app_server.fastapi_server.FastapiServer
)
self.app_server = app_server
async def run(self) -> None:
rio.cli._logger.debug("Uvicorn worker is starting")
# Set up a uvicorn server, but don't start it yet
# Create the app server
if self.app_server is None:
app_server = self.app._as_fastapi(
debug_mode=self.debug_mode,
running_in_window=self.run_in_window,
internal_on_app_start=lambda: self.on_server_is_ready_or_failed.set_result(
None
),
base_url=self.base_url,
)
assert isinstance(
app_server, rio.app_server.fastapi_server.FastapiServer
)
self.app_server = app_server
del app_server
self._create_and_store_app_server()
assert self.app_server is not None
# Instead of using the ASGI app directly, create a transparent shim that
# redirect's to the worker's currently stored app server. This allows
# replacing the app server at will because the shim always remains the
# same.
#
# ASGI is a bitch about function signatures. This function cannot be a
# simple method, because the added `self` parameter seems to confused
# whoever the caller is. Hence the nested function.
async def _asgi_shim(
scope: Scope,
receive: Receive,
send: Send,
) -> None:
assert self.app_server is not None
await self.app_server(scope, receive, send)
# Set up a uvicorn server, but don't start it yet
config = uvicorn.Config(
self.app_server,
app=_asgi_shim,
log_config=None, # Prevent uvicorn from configuring global logging
log_level="error" if self.quiet else "info",
timeout_graceful_shutdown=1, # Without a timeout the server sometimes deadlocks
@@ -135,7 +156,14 @@ class UvicornWorker:
worker must already be running for this to work.
"""
assert self.app_server is not None
rio.cli._logger.debug("Replacing the app in the server")
self.app_server.app = app
# TODO: What to do with the new server here?
# Store the new app
self.app = app
# And create a new app server. This is necessary, because the mounted
# sub-apps may have changed. This ensures they're up to date.
self._create_and_store_app_server()
# There is no need to inject the new app or server anywhere. Since
# uvicorn was fed a shim function instead of the app directly, any
# requests will automatically be redirected to the new server instance.