1- from __future__ import annotations
1+ from typing import Optional
22
3- import asyncio
4- import json
5- import logging
6- import sys
7- from asyncio import Future
8- from threading import Event , Thread , current_thread
9- from typing import Any , Dict , Optional , Tuple , Union
3+ from fastapi import FastAPI
104
11- from fastapi import APIRouter , FastAPI , Request , WebSocket
12- from fastapi .middleware .cors import CORSMiddleware
13- from fastapi .responses import RedirectResponse
14- from fastapi .staticfiles import StaticFiles
15- from mypy_extensions import TypedDict
16- from starlette .websockets import WebSocketDisconnect
17- from uvicorn .config import Config as UvicornConfig
18- from uvicorn .server import Server as UvicornServer
19- from uvicorn .supervisors .multiprocess import Multiprocess
20- from uvicorn .supervisors .statreload import StatReload as ChangeReload
21-
22- from idom .config import IDOM_WED_MODULES_DIR
23- from idom .core .dispatcher import (
24- RecvCoroutine ,
25- SendCoroutine ,
26- SharedViewDispatcher ,
27- VdomJsonPatch ,
28- dispatch_single_view ,
29- ensure_shared_view_dispatcher_future ,
30- )
31- from idom .core .layout import Layout , LayoutEvent
325from idom .core .proto import ComponentConstructor
336
34- from .utils import CLIENT_BUILD_DIR , poll , threaded
35-
36-
37- logger = logging .getLogger (__name__ )
38-
39-
40- class Config (TypedDict , total = False ):
41- """Config for :class:`FastApiRenderServer`"""
42-
43- cors : Union [bool , Dict [str , Any ]]
44- """Enable or configure Cross Origin Resource Sharing (CORS)
45-
46- For more information see docs for ``fastapi.middleware.cors.CORSMiddleware``
47- """
48-
49- redirect_root_to_index : bool
50- """Whether to redirect the root URL (with prefix) to ``index.html``"""
51-
52- serve_static_files : bool
53- """Whether or not to serve static files (i.e. web modules)"""
54-
55- url_prefix : str
56- """The URL prefix where IDOM resources will be served from"""
7+ from .starlette import (
8+ Config ,
9+ StarletteServer ,
10+ _setup_common_routes ,
11+ _setup_config_and_app ,
12+ _setup_shared_view_dispatcher_route ,
13+ _setup_single_view_dispatcher_route ,
14+ )
5715
5816
5917def PerClientStateServer (
6018 constructor : ComponentConstructor ,
6119 config : Optional [Config ] = None ,
6220 app : Optional [FastAPI ] = None ,
63- ) -> FastApiServer :
64- """Return a :class:`FastApiServer ` where each client has its own state.
21+ ) -> StarletteServer :
22+ """Return a :class:`StarletteServer ` where each client has its own state.
6523
6624 Implements the :class:`~idom.server.proto.ServerFactory` protocol
6725
@@ -70,20 +28,18 @@ def PerClientStateServer(
7028 config: Options for configuring server behavior
7129 app: An application instance (otherwise a default instance is created)
7230 """
73- config , app = _setup_config_and_app (config , app )
74- router = APIRouter (prefix = config ["url_prefix" ])
75- _setup_common_routes (app , router , config )
76- _setup_single_view_dispatcher_route (router , constructor )
77- app .include_router (router )
78- return FastApiServer (app )
31+ config , app = _setup_config_and_app (config , app , FastAPI )
32+ _setup_common_routes (config , app )
33+ _setup_single_view_dispatcher_route (config ["url_prefix" ], app , constructor )
34+ return StarletteServer (app )
7935
8036
8137def SharedClientStateServer (
8238 constructor : ComponentConstructor ,
8339 config : Optional [Config ] = None ,
8440 app : Optional [FastAPI ] = None ,
85- ) -> FastApiServer :
86- """Return a :class:`FastApiServer ` where each client shares state.
41+ ) -> StarletteServer :
42+ """Return a :class:`StarletteServer ` where each client shares state.
8743
8844 Implements the :class:`~idom.server.proto.ServerFactory` protocol
8945
@@ -92,200 +48,7 @@ def SharedClientStateServer(
9248 config: Options for configuring server behavior
9349 app: An application instance (otherwise a default instance is created)
9450 """
95- config , app = _setup_config_and_app (config , app )
96- router = APIRouter (prefix = config ["url_prefix" ])
97- _setup_common_routes (app , router , config )
98- _setup_shared_view_dispatcher_route (app , router , constructor )
99- app .include_router (router )
100- return FastApiServer (app )
101-
102-
103- class FastApiServer :
104- """A thin wrapper for running a FastAPI application
105-
106- See :class:`idom.server.proto.Server` for more info
107- """
108-
109- _server : UvicornServer
110- _current_thread : Thread
111-
112- def __init__ (self , app : FastAPI ) -> None :
113- self .app = app
114- self ._did_stop = Event ()
115- app .on_event ("shutdown" )(self ._server_did_stop )
116-
117- def run (self , host : str , port : int , * args : Any , ** kwargs : Any ) -> None :
118- self ._current_thread = current_thread ()
119-
120- self ._server = server = UvicornServer (
121- UvicornConfig (
122- self .app , host = host , port = port , loop = "asyncio" , * args , ** kwargs
123- )
124- )
125-
126- # The following was copied from the uvicorn source with minimal modification. We
127- # shouldn't need to do this, but unfortunately there's no easy way to gain access to
128- # the server instance so you can stop it.
129- # BUG: https://github.com/encode/uvicorn/issues/742
130- config = server .config
131-
132- if (config .reload or config .workers > 1 ) and not isinstance (
133- server .config .app , str
134- ): # pragma: no cover
135- logger = logging .getLogger ("uvicorn.error" )
136- logger .warning (
137- "You must pass the application as an import string to enable 'reload' or "
138- "'workers'."
139- )
140- sys .exit (1 )
141-
142- if config .should_reload : # pragma: no cover
143- sock = config .bind_socket ()
144- supervisor = ChangeReload (config , target = server .run , sockets = [sock ])
145- supervisor .run ()
146- elif config .workers > 1 : # pragma: no cover
147- sock = config .bind_socket ()
148- supervisor = Multiprocess (config , target = server .run , sockets = [sock ])
149- supervisor .run ()
150- else :
151- import asyncio
152-
153- asyncio .set_event_loop (asyncio .new_event_loop ())
154- server .run ()
155-
156- run_in_thread = threaded (run )
157-
158- def wait_until_started (self , timeout : Optional [float ] = 3.0 ) -> None :
159- poll (
160- f"start { self .app } " ,
161- 0.01 ,
162- timeout ,
163- lambda : hasattr (self , "_server" ) and self ._server .started ,
164- )
165-
166- def stop (self , timeout : Optional [float ] = 3.0 ) -> None :
167- self ._server .should_exit = True
168- self ._did_stop .wait (timeout )
169-
170- async def _server_did_stop (self ) -> None :
171- self ._did_stop .set ()
172-
173-
174- def _setup_config_and_app (
175- config : Optional [Config ],
176- app : Optional [FastAPI ],
177- ) -> Tuple [Config , FastAPI ]:
178- return (
179- {
180- "cors" : False ,
181- "url_prefix" : "" ,
182- "serve_static_files" : True ,
183- "redirect_root_to_index" : True ,
184- ** (config or {}), # type: ignore
185- },
186- app or FastAPI (),
187- )
188-
189-
190- def _setup_common_routes (app : FastAPI , router : APIRouter , config : Config ) -> None :
191- cors_config = config ["cors" ]
192- if cors_config : # pragma: no cover
193- cors_params = (
194- cors_config if isinstance (cors_config , dict ) else {"allow_origins" : ["*" ]}
195- )
196- app .add_middleware (CORSMiddleware , ** cors_params )
197-
198- # This really should be added to the APIRouter, but there's a bug in FastAPI
199- # BUG: https://github.com/tiangolo/fastapi/issues/1469
200- url_prefix = config ["url_prefix" ]
201- if config ["serve_static_files" ]:
202- app .mount (
203- f"{ url_prefix } /client" ,
204- StaticFiles (
205- directory = str (CLIENT_BUILD_DIR ),
206- html = True ,
207- check_dir = True ,
208- ),
209- name = "idom_static_files" ,
210- )
211- app .mount (
212- f"{ url_prefix } /modules" ,
213- StaticFiles (
214- directory = str (IDOM_WED_MODULES_DIR .current ),
215- html = True ,
216- check_dir = True ,
217- ),
218- name = "idom_static_files" ,
219- )
220-
221- if config ["redirect_root_to_index" ]:
222-
223- @app .route (f"{ url_prefix } /" )
224- def redirect_to_index (request : Request ) -> RedirectResponse :
225- return RedirectResponse (
226- f"{ url_prefix } /client/index.html?{ request .query_params } "
227- )
228-
229-
230- def _setup_single_view_dispatcher_route (
231- router : APIRouter , constructor : ComponentConstructor
232- ) -> None :
233- @router .websocket ("/stream" )
234- async def model_stream (socket : WebSocket ) -> None :
235- await socket .accept ()
236- send , recv = _make_send_recv_callbacks (socket )
237- try :
238- await dispatch_single_view (
239- Layout (constructor (** dict (socket .query_params ))), send , recv
240- )
241- except WebSocketDisconnect as error :
242- logger .info (f"WebSocket disconnect: { error .code } " )
243-
244-
245- def _setup_shared_view_dispatcher_route (
246- app : FastAPI , router : APIRouter , constructor : ComponentConstructor
247- ) -> None :
248- dispatcher_future : Future [None ]
249- dispatch_coroutine : SharedViewDispatcher
250-
251- @app .on_event ("startup" )
252- async def activate_dispatcher () -> None :
253- nonlocal dispatcher_future
254- nonlocal dispatch_coroutine
255- dispatcher_future , dispatch_coroutine = ensure_shared_view_dispatcher_future (
256- Layout (constructor ())
257- )
258-
259- @app .on_event ("shutdown" )
260- async def deactivate_dispatcher () -> None :
261- logger .debug ("Stopping dispatcher - server is shutting down" )
262- dispatcher_future .cancel ()
263- await asyncio .wait ([dispatcher_future ])
264-
265- @router .websocket ("/stream" )
266- async def model_stream (socket : WebSocket ) -> None :
267- await socket .accept ()
268-
269- if socket .query_params :
270- raise ValueError (
271- "SharedClientState server does not support per-client view parameters"
272- )
273-
274- send , recv = _make_send_recv_callbacks (socket )
275-
276- try :
277- await dispatch_coroutine (send , recv )
278- except WebSocketDisconnect as error :
279- logger .info (f"WebSocket disconnect: { error .code } " )
280-
281-
282- def _make_send_recv_callbacks (
283- socket : WebSocket ,
284- ) -> Tuple [SendCoroutine , RecvCoroutine ]:
285- async def sock_send (value : VdomJsonPatch ) -> None :
286- await socket .send_text (json .dumps (value ))
287-
288- async def sock_recv () -> LayoutEvent :
289- return LayoutEvent (** json .loads (await socket .receive_text ()))
290-
291- return sock_send , sock_recv
51+ config , app = _setup_config_and_app (config , app , FastAPI )
52+ _setup_common_routes (config , app )
53+ _setup_shared_view_dispatcher_route (config ["url_prefix" ], app , constructor )
54+ return StarletteServer (app )
0 commit comments