16
16
Flask ,
17
17
Request ,
18
18
copy_current_request_context ,
19
- redirect ,
20
19
request ,
21
20
send_from_directory ,
22
- url_for ,
23
21
)
24
22
from flask_cors import CORS
25
23
from flask_sock import Sock
26
- from gevent import pywsgi
27
- from geventwebsocket .handler import WebSocketHandler
28
24
from simple_websocket import Server as WebSocket
25
+ from werkzeug .serving import BaseWSGIServer , make_server
29
26
30
27
import idom
31
28
from idom .config import IDOM_WEB_MODULES_DIR
32
29
from idom .core .hooks import Context , create_context , use_context
33
30
from idom .core .layout import LayoutEvent , LayoutUpdate
34
31
from idom .core .serve import serve_json_patch
35
32
from idom .core .types import ComponentType , RootComponentConstructor
33
+ from idom .utils import Ref
36
34
37
35
from .utils import CLIENT_BUILD_DIR , client_build_dir_path
38
36
39
37
40
38
logger = logging .getLogger (__name__ )
41
39
42
- RequestContext : type [Context [Request | None ]] = create_context (None , "RequestContext" )
40
+ ConnectionContext : type [Context [Connection | None ]] = create_context (
41
+ None , "ConnectionContext"
42
+ )
43
43
44
44
45
45
def configure (
@@ -57,10 +57,10 @@ def configure(
57
57
options = options or Options ()
58
58
blueprint = Blueprint ("idom" , __name__ , url_prefix = options .url_prefix )
59
59
60
- _setup_common_routes (blueprint , options )
61
-
62
60
# this route should take priority so set up it up first
63
- _setup_single_view_dispatcher_route (app , options , component )
61
+ _setup_single_view_dispatcher_route (blueprint , options , component )
62
+
63
+ _setup_common_routes (blueprint , options )
64
64
65
65
app .register_blueprint (blueprint )
66
66
@@ -82,21 +82,15 @@ async def serve_development_app(
82
82
loop = asyncio .get_event_loop ()
83
83
stopped = asyncio .Event ()
84
84
85
- server : pywsgi . WSGIServer
85
+ server : Ref [ BaseWSGIServer ] = Ref ()
86
86
87
87
def run_server () -> None : # pragma: no cover
88
88
# we don't cover this function because coverage doesn't work right in threads
89
- nonlocal server
90
- server = pywsgi .WSGIServer (
91
- (host , port ),
92
- app ,
93
- handler_class = WebSocketHandler ,
94
- )
95
- server .start ()
89
+ server .current = make_server (host , port , app , threaded = True )
96
90
if started :
97
91
loop .call_soon_threadsafe (started .set )
98
92
try :
99
- server .serve_forever ()
93
+ server .current . serve_forever ()
100
94
finally :
101
95
loop .call_soon_threadsafe (stopped .set )
102
96
@@ -110,27 +104,27 @@ def run_server() -> None: # pragma: no cover
110
104
await stopped .wait ()
111
105
finally :
112
106
# we may have exitted because this task was cancelled
113
- server .stop ( 3 )
107
+ server .current . shutdown ( )
114
108
# the thread should eventually join
115
109
thread .join (timeout = 3 )
116
110
# just double check it happened
117
111
if thread .is_alive (): # pragma: no cover
118
112
raise RuntimeError ("Failed to shutdown server." )
119
113
120
114
121
- def use_request () -> Request :
122
- """Get the current ``Request` `"""
123
- request = use_context (RequestContext )
124
- if request is None :
115
+ def use_connection () -> Connection :
116
+ """Get the current :class:`Connection `"""
117
+ connection = use_context (ConnectionContext )
118
+ if connection is None :
125
119
raise RuntimeError ( # pragma: no cover
126
- "No request . Are you running with a Flask server?"
120
+ "No connection . Are you running with a Flask server?"
127
121
)
128
- return request
122
+ return connection
129
123
130
124
131
125
def use_scope () -> dict [str , Any ]:
132
126
"""Get the current WSGI environment"""
133
- return use_request () .environ
127
+ return use_connection (). request .environ
134
128
135
129
136
130
@dataclass
@@ -143,9 +137,6 @@ class Options:
143
137
For more information see docs for ``flask_cors.CORS``
144
138
"""
145
139
146
- redirect_root : bool = True
147
- """Whether to redirect the root URL (with prefix) to ``index.html``"""
148
-
149
140
serve_static_files : bool = True
150
141
"""Whether or not to serve static files (i.e. web modules)"""
151
142
@@ -161,35 +152,29 @@ def _setup_common_routes(blueprint: Blueprint, options: Options) -> None:
161
152
162
153
if options .serve_static_files :
163
154
164
- @blueprint .route ("/app " )
165
- @blueprint .route ("/app <path:path>" )
155
+ @blueprint .route ("/" )
156
+ @blueprint .route ("/<path:path>" )
166
157
def send_client_dir (path : str = "" ) -> Any :
167
158
return send_from_directory (
168
159
str (CLIENT_BUILD_DIR ),
169
160
client_build_dir_path (path ),
170
161
)
171
162
172
- @blueprint .route ("/modules/<path:path>" )
173
- def send_modules_dir (path : str ) -> Any :
163
+ @blueprint .route (r"/_api/modules/<path:path>" )
164
+ @blueprint .route (r"<path:_>/_api/modules/<path:path>" )
165
+ def send_modules_dir (
166
+ path : str ,
167
+ _ : str = "" , # this is not used
168
+ ) -> Any :
174
169
return send_from_directory (str (IDOM_WEB_MODULES_DIR .current ), path )
175
170
176
- if options .redirect_root :
177
-
178
- @blueprint .route ("/" )
179
- def redirect_to_index () -> Any :
180
- return redirect (
181
- url_for ("idom.send_client_dir" , path = "" , ** request .args )
182
- )
183
-
184
171
185
172
def _setup_single_view_dispatcher_route (
186
- app : Flask , options : Options , constructor : RootComponentConstructor
173
+ blueprint : Blueprint , options : Options , constructor : RootComponentConstructor
187
174
) -> None :
188
- sockets = Sock (app )
175
+ sock = Sock (blueprint )
189
176
190
- @sockets .route (_join_url_paths (options .url_prefix , "/app/_stream" ))
191
- @sockets .route (_join_url_paths (options .url_prefix , "/app/<path:path>/_stream" ))
192
- def model_stream (ws : WebSocket ) -> None :
177
+ def model_stream (ws : WebSocket , path : str = "" ) -> None :
193
178
def send (value : Any ) -> None :
194
179
ws .send (json .dumps (value ))
195
180
@@ -200,10 +185,15 @@ def recv() -> Optional[LayoutEvent]:
200
185
else :
201
186
return None
202
187
203
- dispatch_in_thread (constructor (), send , recv )
188
+ dispatch_in_thread (ws , path , constructor (), send , recv )
189
+
190
+ sock .route ("/_api/stream" , endpoint = "without_path" )(model_stream )
191
+ sock .route ("/<path:path>/_api/stream" , endpoint = "with_path" )(model_stream )
204
192
205
193
206
194
def dispatch_in_thread (
195
+ websocket : WebSocket ,
196
+ path : str ,
207
197
component : ComponentType ,
208
198
send : Callable [[Any ], None ],
209
199
recv : Callable [[], Optional [LayoutEvent ]],
@@ -227,7 +217,11 @@ async def recv_coro() -> Any:
227
217
228
218
async def main () -> None :
229
219
await serve_json_patch (
230
- idom .Layout (RequestContext (component , value = request )),
220
+ idom .Layout (
221
+ ConnectionContext (
222
+ component , value = Connection (request , websocket , path )
223
+ )
224
+ ),
231
225
send_coro ,
232
226
recv_coro ,
233
227
)
@@ -277,6 +271,20 @@ def run_send() -> None:
277
271
return None
278
272
279
273
274
+ @dataclass
275
+ class Connection :
276
+ """A simple wrapper for holding connection information"""
277
+
278
+ request : Request
279
+ """The current request object"""
280
+
281
+ websocket : WebSocket
282
+ """A handle to the current websocket"""
283
+
284
+ path : str
285
+ """The current path being served"""
286
+
287
+
280
288
class _DispatcherThreadInfo (NamedTuple ):
281
289
dispatch_loop : asyncio .AbstractEventLoop
282
290
dispatch_future : "asyncio.Future[Any]"
0 commit comments