1
1
from __future__ import annotations
2
2
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 typing import Optional
10
4
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
5
+ from fastapi import FastAPI
21
6
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
32
7
from idom .core .proto import ComponentConstructor
33
8
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"""
9
+ from .starlette import (
10
+ Config ,
11
+ StarletteServer ,
12
+ _setup_common_routes ,
13
+ _setup_config_and_app ,
14
+ _setup_shared_view_dispatcher_route ,
15
+ _setup_single_view_dispatcher_route ,
16
+ )
57
17
58
18
59
19
def PerClientStateServer (
60
20
constructor : ComponentConstructor ,
61
21
config : Optional [Config ] = None ,
62
22
app : Optional [FastAPI ] = None ,
63
- ) -> FastApiServer :
64
- """Return a :class:`FastApiServer ` where each client has its own state.
23
+ ) -> StarletteServer :
24
+ """Return a :class:`StarletteServer ` where each client has its own state.
65
25
66
26
Implements the :class:`~idom.server.proto.ServerFactory` protocol
67
27
@@ -70,20 +30,18 @@ def PerClientStateServer(
70
30
config: Options for configuring server behavior
71
31
app: An application instance (otherwise a default instance is created)
72
32
"""
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 )
33
+ config , app = _setup_config_and_app (config , app , FastAPI )
34
+ _setup_common_routes (config , app )
35
+ _setup_single_view_dispatcher_route (config ["url_prefix" ], app , constructor )
36
+ return StarletteServer (app )
79
37
80
38
81
39
def SharedClientStateServer (
82
40
constructor : ComponentConstructor ,
83
41
config : Optional [Config ] = None ,
84
42
app : Optional [FastAPI ] = None ,
85
- ) -> FastApiServer :
86
- """Return a :class:`FastApiServer ` where each client shares state.
43
+ ) -> StarletteServer :
44
+ """Return a :class:`StarletteServer ` where each client shares state.
87
45
88
46
Implements the :class:`~idom.server.proto.ServerFactory` protocol
89
47
@@ -92,200 +50,7 @@ def SharedClientStateServer(
92
50
config: Options for configuring server behavior
93
51
app: An application instance (otherwise a default instance is created)
94
52
"""
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
53
+ config , app = _setup_config_and_app (config , app , FastAPI )
54
+ _setup_common_routes (config , app )
55
+ _setup_shared_view_dispatcher_route (config ["url_prefix" ], app , constructor )
56
+ return StarletteServer (app )
0 commit comments