Skip to content

Commit c53d871

Browse files
committed
add tornado render server
1 parent e477492 commit c53d871

File tree

9 files changed

+278
-42
lines changed

9 files changed

+278
-42
lines changed

idom/server/base.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
_Self = TypeVar("_Self", bound="AbstractRenderServer[Any, Any]")
1111

1212

13-
class AbstractRenderServer(Generic[_App, _Config]):
13+
class AbstractRenderServer(Generic[_App, _Config], abc.ABC):
1414
"""Base class for all IDOM server application and extension implementations.
1515
1616
It is assumed that IDOM will be used in conjuction with some async-enabled server
@@ -40,17 +40,19 @@ def application(self) -> _App:
4040
raise RuntimeError("No application registered.")
4141
return self._app
4242

43-
def run(self, *args: Any, **kwargs: Any) -> None:
43+
def run(self, host: str, port: int, *args: Any, **kwargs: Any) -> None:
4444
"""Run as a standalone application."""
4545
if self._app is None:
4646
app = self._default_application(self._config)
4747
self.register(app)
4848
else: # pragma: no cover
4949
app = self._app
5050
if not self._daemonized: # pragma: no cover
51-
return self._run_application(app, self._config, args, kwargs)
51+
return self._run_application(self._config, app, host, port, args, kwargs)
5252
else:
53-
return self._run_application_in_thread(app, self._config, args, kwargs)
53+
return self._run_application_in_thread(
54+
self._config, app, host, port, args, kwargs
55+
)
5456

5557
def daemon(self, *args: Any, **kwargs: Any) -> Thread:
5658
"""Run the standalone application in a seperate thread."""
@@ -65,8 +67,10 @@ def daemon(self, *args: Any, **kwargs: Any) -> Thread:
6567

6668
def register(self: _Self, app: Optional[_App]) -> _Self:
6769
"""Register this as an extension."""
68-
self._setup_application(app, self._config)
69-
self._setup_application_did_start_event(app, self._server_did_start)
70+
self._setup_application(self._config, app)
71+
self._setup_application_did_start_event(
72+
self._config, app, self._server_did_start
73+
)
7074
self._app = app
7175
return self
7276

@@ -77,6 +81,10 @@ def wait_until_server_start(self, timeout: float = 3.0):
7781
f"Server did not start within {timeout} seconds"
7882
)
7983

84+
@abc.abstractmethod
85+
def stop(self) -> None:
86+
"""Stop a currently running application"""
87+
8088
@abc.abstractmethod
8189
def _create_config(self, config: Optional[_Config]) -> _Config:
8290
"""Return the default configuration options."""
@@ -87,25 +95,39 @@ def _default_application(self, config: _Config) -> _App:
8795
raise NotImplementedError()
8896

8997
@abc.abstractmethod
90-
def _setup_application(self, app: _App, config: _Config) -> None:
98+
def _setup_application(self, config: _Config, app: _App) -> None:
9199
"""General application setup - add routes, templates, static resource, etc."""
92100
raise NotImplementedError()
93101

94102
@abc.abstractmethod
95-
def _setup_application_did_start_event(self, app: _App, event: Event) -> None:
103+
def _setup_application_did_start_event(
104+
self, config: _Config, app: _App, event: Event
105+
) -> None:
96106
"""Register a callback to the app indicating whether the server has started"""
97107
raise NotImplementedError()
98108

99109
@abc.abstractmethod
100110
def _run_application(
101-
self, app: _App, config: _Config, args: Tuple[Any, ...], kwargs: Dict[str, Any]
111+
self,
112+
config: _Config,
113+
app: _App,
114+
host: str,
115+
port: int,
116+
args: Tuple[Any, ...],
117+
kwargs: Dict[str, Any],
102118
) -> None:
103119
"""Run the application in the main thread"""
104120
raise NotImplementedError()
105121

106122
@abc.abstractmethod
107123
def _run_application_in_thread(
108-
self, app: _App, config: _Config, args: Tuple[Any, ...], kwargs: Dict[str, Any]
124+
self,
125+
config: _Config,
126+
app: _App,
127+
host: str,
128+
port: int,
129+
args: Tuple[Any, ...],
130+
kwargs: Dict[str, Any],
109131
) -> None:
110132
"""This function has been called inside a daemon thread to run the application"""
111133
raise NotImplementedError()

idom/server/flask.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ class FlaskRenderServer(AbstractRenderServer[Flask, Config]):
3535
"""Base class for render servers which use Flask"""
3636

3737
_dispatcher_type: AbstractDispatcher
38+
_wsgi_server: pywsgi.WSGIServer
39+
40+
def stop(self, timeout: Optional[float] = None) -> None:
41+
try:
42+
server = self._wsgi_server
43+
except AttributeError: # pragma: no cover
44+
raise RuntimeError(
45+
f"Application is not running or was not started by {self}"
46+
)
47+
else:
48+
server.stop(timeout)
3849

3950
def _create_config(self, config: Optional[Config]) -> Config:
4051
return Config(
@@ -51,10 +62,10 @@ def _create_config(self, config: Optional[Config]) -> Config:
5162
def _default_application(self, config: Config) -> Flask:
5263
return Flask(config["import_name"])
5364

54-
def _setup_application(self, app: Flask, config: Config) -> None:
65+
def _setup_application(self, config: Config, app: Flask) -> None:
5566
bp = Blueprint("idom", __name__, url_prefix=config["url_prefix"])
5667

57-
self._setup_blueprint_routes(bp, config)
68+
self._setup_blueprint_routes(config, bp)
5869

5970
cors_config = config["cors"]
6071
if cors_config: # pragma: no cover
@@ -84,7 +95,7 @@ def recv() -> LayoutEvent:
8495
None,
8596
)
8697

87-
def _setup_blueprint_routes(self, blueprint: Blueprint, config: Config) -> None:
98+
def _setup_blueprint_routes(self, config: Config, blueprint: Blueprint) -> None:
8899
if config["serve_static_files"]:
89100

90101
@blueprint.route("/client/<path:path>")
@@ -98,21 +109,33 @@ def redirect_to_index():
98109
return redirect(url_for("idom.send_build_dir", path="index.html"))
99110

100111
def _setup_application_did_start_event(
101-
self, app: Flask, event: ThreadEvent
112+
self, config: Config, app: Flask, event: ThreadEvent
102113
) -> None:
103114
@app.before_first_request
104115
def server_did_start():
105116
event.set()
106117

107118
def _run_application(
108-
self, app: Flask, config: Config, args: Tuple[Any, ...], kwargs: Dict[str, Any]
119+
self,
120+
config: Config,
121+
app: Flask,
122+
host: str,
123+
port: int,
124+
args: Tuple[Any, ...],
125+
kwargs: Dict[str, Any],
109126
) -> None:
110-
self._generic_run_application(app, *args, **kwargs)
127+
self._generic_run_application(app, host, port, *args, **kwargs)
111128

112129
def _run_application_in_thread(
113-
self, app: Flask, config: Config, args: Tuple[Any, ...], kwargs: Dict[str, Any]
130+
self,
131+
config: Config,
132+
app: Flask,
133+
host: str,
134+
port: int,
135+
args: Tuple[Any, ...],
136+
kwargs: Dict[str, Any],
114137
) -> None:
115-
self._generic_run_application(app, *args, **kwargs)
138+
self._generic_run_application(app, host, port, *args, **kwargs)
116139

117140
def _generic_run_application(
118141
self,
@@ -121,19 +144,20 @@ def _generic_run_application(
121144
port: int = 5000,
122145
debug: bool = False,
123146
*args: Any,
124-
**kwargs
147+
**kwargs,
125148
):
126149
if debug:
127150
logging.basicConfig(level=logging.DEBUG) # pragma: no cover
128151
logging.debug("Starting server...")
129-
_StartCallbackWSGIServer(
152+
self._wsgi_server = _StartCallbackWSGIServer(
130153
self._server_did_start.set,
131154
(host, port),
132155
app,
133156
*args,
134157
handler_class=WebSocketHandler,
135158
**kwargs,
136-
).serve_forever()
159+
)
160+
self._wsgi_server.serve_forever()
137161

138162

139163
class PerClientStateServer(FlaskRenderServer):

idom/server/sanic.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
SendCoroutine,
1818
RecvCoroutine,
1919
)
20-
from idom.core.layout import LayoutEvent, Layout
20+
from idom.core.layout import LayoutEvent, Layout, LayoutUpdate
2121
from idom.client.manage import BUILD_DIR
2222

2323
from .base import AbstractRenderServer
@@ -54,10 +54,10 @@ def _create_config(self, config: Optional[Config]) -> Config:
5454
def _default_application(self, config: Config) -> Sanic:
5555
return Sanic()
5656

57-
def _setup_application(self, app: Sanic, config: Config) -> None:
57+
def _setup_application(self, config: Config, app: Sanic) -> None:
5858
bp = Blueprint(f"idom_dispatcher_{id(self)}", url_prefix=config["url_prefix"])
5959

60-
self._setup_blueprint_routes(bp, config)
60+
self._setup_blueprint_routes(config, bp)
6161

6262
cors_config = config["cors"]
6363
if cors_config:
@@ -66,25 +66,26 @@ def _setup_application(self, app: Sanic, config: Config) -> None:
6666

6767
app.blueprint(bp)
6868

69-
def _setup_application_did_start_event(self, app: Sanic, event: Event) -> None:
69+
def _setup_application_did_start_event(
70+
self, config: Config, app: Sanic, event: Event
71+
) -> None:
7072
async def server_did_start(app: Sanic, loop: asyncio.AbstractEventLoop) -> None:
7173
event.set()
7274

7375
app.register_listener(server_did_start, "after_server_start")
7476

75-
def _setup_blueprint_routes(self, blueprint: Blueprint, config: Config) -> None:
77+
def _setup_blueprint_routes(self, config: Config, blueprint: Blueprint) -> None:
7678
"""Add routes to the application blueprint"""
7779

7880
@blueprint.websocket("/stream") # type: ignore
7981
async def model_stream(
8082
request: request.Request, socket: WebSocketCommonProtocol
8183
) -> None:
82-
async def sock_send(value: Any) -> None:
84+
async def sock_send(value: LayoutUpdate) -> None:
8385
await socket.send(json.dumps(value))
8486

8587
async def sock_recv() -> LayoutEvent:
86-
event = json.loads(await socket.recv())
87-
return LayoutEvent(event["target"], event["data"])
88+
return LayoutEvent(**json.loads(await socket.recv()))
8889

8990
element_params = {k: request.args.get(k) for k in request.args}
9091
await self._run_dispatcher(sock_send, sock_recv, element_params)
@@ -103,13 +104,25 @@ def redirect_to_index(
103104
)
104105

105106
def _run_application(
106-
self, app: Sanic, config: Config, args: Tuple[Any, ...], kwargs: Dict[str, Any]
107+
self,
108+
config: Config,
109+
app: Sanic,
110+
host: str,
111+
port: int,
112+
args: Tuple[Any, ...],
113+
kwargs: Dict[str, Any],
107114
) -> None:
108115
self._loop = asyncio.get_event_loop()
109-
app.run(*args, **kwargs)
116+
app.run(host, port, *args, **kwargs)
110117

111118
def _run_application_in_thread(
112-
self, app: Sanic, config: Config, args: Tuple[Any, ...], kwargs: Dict[str, Any]
119+
self,
120+
config: Config,
121+
app: Sanic,
122+
host: str,
123+
port: int,
124+
args: Tuple[Any, ...],
125+
kwargs: Dict[str, Any],
113126
) -> None:
114127
try:
115128
loop = asyncio.get_event_loop()
@@ -122,7 +135,9 @@ def _run_application_in_thread(
122135
# what follows was copied from:
123136
# https://github.com/sanic-org/sanic/blob/7028eae083b0da72d09111b9892ddcc00bce7df4/examples/run_async_advanced.py
124137

125-
serv_coro = app.create_server(*args, **kwargs, return_asyncio_server=True)
138+
serv_coro = app.create_server(
139+
host, port, *args, **kwargs, return_asyncio_server=True
140+
)
126141
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
127142
server = loop.run_until_complete(serv_task)
128143
server.after_start()
@@ -167,10 +182,10 @@ class SharedClientStateServer(SanicRenderServer):
167182
_dispatcher_type = SharedViewDispatcher
168183
_dispatcher: SharedViewDispatcher
169184

170-
def _setup_application(self, app: Sanic, config: Config) -> None:
185+
def _setup_application(self, config: Config, app: Sanic) -> None:
171186
app.register_listener(self._activate_dispatcher, "before_server_start")
172187
app.register_listener(self._deactivate_dispatcher, "before_server_stop")
173-
super()._setup_application(app, config)
188+
super()._setup_application(config, app)
174189

175190
async def _activate_dispatcher(
176191
self, app: Sanic, loop: asyncio.AbstractEventLoop

0 commit comments

Comments
 (0)