Skip to content

Server Proto #370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ async def forward_to_index(request):
"redirect_root_to_index": False,
"url_prefix": IDOM_MODEL_SERVER_URL_PREFIX,
},
).register(app)
app,
)


if __name__ == "__main__":
Expand Down
8 changes: 3 additions & 5 deletions docs/source/core-concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,8 @@ Layout Server

The :ref:`Dispatcher <Layout Dispatcher>` allows you to animate the layout, but we still
need to get the models on the screen. One of the last steps in that journey is to send
them over the wire. To do that you need an
:class:`~idom.server.base.AbstractRenderServer` implementation. Presently, IDOM comes
with support for the following web servers:
them over the wire. To do that you need a :class:`~idom.server.proto.ServerFactory`
implementation. Presently, IDOM comes with support for the following web servers:

- :class:`sanic.app.Sanic` (``pip install idom[sanic]``)

Expand Down Expand Up @@ -244,8 +243,7 @@ The implementation registers hooks into the application to serve the model once
def View(self):
return idom.html.h1(["Hello World"])

per_client_state = PerClientStateServer(View)
per_client_state.register(app)
per_client_state = PerClientStateServer(View, app=app)

app.run("localhost", 5000)

Expand Down
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def test_style(session: Session) -> None:
".",
"--check",
"--exclude",
rf"/({black_default_exclude}|node_modules)/",
rf"/({black_default_exclude}|venv|node_modules)/",
)
session.run("isort", ".", "--check-only")

Expand Down
2 changes: 2 additions & 0 deletions scripts/live_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def wrap_builder(old_builder):
# This is the bit that we're injecting to get the example components to reload too
def new_builder():
[s.stop() for s in _running_idom_servers]
[s.wait_until_stopped() for s in _running_idom_servers]

# we need to set this before `docs.main` does
IDOM_CLIENT_IMPORT_SOURCE_URL.current = (
Expand All @@ -39,6 +40,7 @@ def new_builder():
server = PerClientStateServer(main.component, {"cors": True})
_running_idom_servers.append(server)
server.run_in_thread("127.0.0.1", 5555, debug=True)
server.wait_until_started()
old_builder()

return new_builder
Expand Down
8 changes: 4 additions & 4 deletions src/idom/core/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ async def dispatch_single_view(
task_group.start_soon(_single_incoming_loop, layout, recv)


SharedViewDispatcher = Callable[[SendCoroutine, RecvCoroutine], Awaitable[None]]
_SharedViewDispatcherFuture = Callable[[SendCoroutine, RecvCoroutine], "Future[None]"]
_SharedViewDispatcherCoro = Callable[[SendCoroutine, RecvCoroutine], Awaitable[None]]


@asynccontextmanager
Expand Down Expand Up @@ -93,8 +93,8 @@ def dispatch_shared_view_soon(

def ensure_shared_view_dispatcher_future(
layout: Layout,
) -> Tuple[Future[None], _SharedViewDispatcherCoro]:
dispatcher_future: Future[_SharedViewDispatcherCoro] = Future()
) -> Tuple[Future[None], SharedViewDispatcher]:
dispatcher_future: Future[SharedViewDispatcher] = Future()

async def dispatch_shared_view_forever() -> None:
with layout:
Expand All @@ -121,7 +121,7 @@ async def dispatch(send: SendCoroutine, recv: RecvCoroutine) -> None:

async def _make_shared_view_dispatcher(
layout: Layout,
) -> Tuple[_SharedViewDispatcherCoro, Ref[Any], WeakSet[Queue[LayoutUpdate]]]:
) -> Tuple[SharedViewDispatcher, Ref[Any], WeakSet[Queue[LayoutUpdate]]]:
initial_update = await layout.render()
model_state = Ref(initial_update.apply_to({}))

Expand Down
7 changes: 2 additions & 5 deletions src/idom/server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
from .base import AbstractRenderServer
from .prefab import hotswap_server, multiview_server, run


__all__ = [
"default",
"run",
"multiview_server",
"hotswap_server",
"AbstractRenderServer",
"multiview_server",
"run",
]
65 changes: 29 additions & 36 deletions src/idom/server/base.py
Original file line number Diff line number Diff line change
@@ -1,90 +1,78 @@
import abc
from threading import Event, Thread
from typing import Any, Dict, Generic, Optional, Tuple, TypeVar
from typing import Any, Dict, Optional, Tuple, TypeVar

from idom.core.component import ComponentConstructor

from .proto import ServerFactory


_App = TypeVar("_App", bound=Any)
_Config = TypeVar("_Config", bound=Any)
_Self = TypeVar("_Self", bound="AbstractRenderServer[Any, Any]")


class AbstractRenderServer(Generic[_App, _Config], abc.ABC):
class AbstractRenderServer(ServerFactory[_App, _Config], abc.ABC):
"""Base class for all IDOM server application and extension implementations.

It is assumed that IDOM will be used in conjuction with some async-enabled server
library (e.g. ``sanic`` or ``tornado``) so these server implementations should work
standalone and as an extension to an existing application.

Standalone usage:
:meth:`~AbstractServerExtension.run` or :meth:`~AbstractServerExtension.run_in_thread`
Register an extension:
:meth:`~AbstractServerExtension.register`
Construct the server then call ``:meth:`~AbstractRenderServer.run` or
:meth:`~AbstractRenderServer.run_in_thread`
Register as an extension:
Simply construct the :meth:`~AbstractRenderServer` and pass it an ``app``
instance.
"""

def __init__(
self,
constructor: ComponentConstructor,
config: Optional[_Config] = None,
app: Optional[_App] = None,
) -> None:
self._app: Optional[_App] = None
self._root_component_constructor = constructor
self._daemon_thread: Optional[Thread] = None
self._config = self._create_config(config)
self._server_did_start = Event()

@property
def application(self) -> _App:
if self._app is None:
raise RuntimeError("No application registered.")
return self._app
self.app = app or self._default_application(self._config)
self._setup_application(self._config, self.app)
self._setup_application_did_start_event(
self._config, self.app, self._server_did_start
)

def run(self, host: str, port: int, *args: Any, **kwargs: Any) -> None:
"""Run as a standalone application."""
if self._app is None:
app = self._default_application(self._config)
self.register(app)
else: # pragma: no cover
app = self._app
if self._daemon_thread is None: # pragma: no cover
return self._run_application(self._config, app, host, port, args, kwargs)
return self._run_application(
self._config, self.app, host, port, args, kwargs
)
else:
return self._run_application_in_thread(
self._config, app, host, port, args, kwargs
self._config, self.app, host, port, args, kwargs
)

def run_in_thread(self, *args: Any, **kwargs: Any) -> Thread:
def run_in_thread(self, host: str, port: int, *args: Any, **kwargs: Any) -> Thread:
"""Run the standalone application in a seperate thread."""
self._daemon_thread = thread = Thread(
target=lambda: self.run(*args, **kwargs), daemon=True
target=lambda: self.run(host, port, *args, **kwargs), daemon=True
)

thread.start()
self.wait_until_server_start()
self.wait_until_started()

return thread

def register(self: _Self, app: Optional[_App]) -> _Self:
"""Register this as an extension."""
if self._app is not None:
raise RuntimeError(f"Already registered {self._app}")
self._setup_application(self._config, app)
self._setup_application_did_start_event(
self._config, app, self._server_did_start
)
self._app = app
return self

def wait_until_server_start(self, timeout: float = 3.0) -> None:
def wait_until_started(self, timeout: Optional[float] = 3.0) -> None:
"""Block until the underlying application has started"""
if not self._server_did_start.wait(timeout=timeout):
raise RuntimeError( # pragma: no cover
f"Server did not start within {timeout} seconds"
)

@abc.abstractmethod
def stop(self) -> None:
def stop(self, timeout: Optional[float] = None) -> None:
"""Stop a currently running application"""
raise NotImplementedError()

Expand Down Expand Up @@ -135,3 +123,8 @@ def _run_application_in_thread(
) -> None:
"""This function has been called inside a daemon thread to run the application"""
raise NotImplementedError()

def __repr__(self) -> str:
cls = type(self)
full_name = f"{cls.__module__}.{cls.__name__}"
return f"{full_name}({self._config})"
2 changes: 1 addition & 1 deletion src/idom/server/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class FastApiRenderServer(AbstractRenderServer[FastAPI, Config]):

_server: UvicornServer

def stop(self, timeout: float = 3) -> None:
def stop(self, timeout: Optional[float] = 3.0) -> None:
"""Stop the running application"""
self._server.should_exit
if self._daemon_thread is not None:
Expand Down
47 changes: 24 additions & 23 deletions src/idom/server/prefab.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,33 @@
"""

import logging
from typing import Any, Dict, Optional, Tuple, Type, TypeVar
from typing import Any, Dict, Optional, Tuple, TypeVar

from idom.core.component import ComponentConstructor
from idom.widgets.utils import MountFunc, MultiViewMount, hotswap, multiview

from .base import AbstractRenderServer
from .proto import Server, ServerFactory
from .utils import find_available_port, find_builtin_server_type


DEFAULT_SERVER_FACTORY = find_builtin_server_type("PerClientStateServer")

logger = logging.getLogger(__name__)
_S = TypeVar("_S", bound=AbstractRenderServer[Any, Any])

_App = TypeVar("_App")
_Config = TypeVar("_Config")


def run(
component: ComponentConstructor,
server_type: Type[_S] = find_builtin_server_type("PerClientStateServer"),
server_type: ServerFactory[_App, _Config] = DEFAULT_SERVER_FACTORY,
host: str = "127.0.0.1",
port: Optional[int] = None,
server_config: Optional[Any] = None,
run_kwargs: Optional[Dict[str, Any]] = None,
app: Optional[Any] = None,
daemon: bool = False,
) -> _S:
) -> Server[_App]:
"""A utility for quickly running a render server with minimal boilerplate

Parameters:
Expand All @@ -41,8 +45,8 @@ def run(
server_config:
Options passed to configure the server.
run_kwargs:
Keyword arguments passed to the :meth:`AbstractRenderServer.run`
or :meth:`AbstractRenderServer.run_in_thread` methods of the server
Keyword arguments passed to the :meth:`~idom.server.proto.Server.run`
or :meth:`~idom.server.proto.Server.run_in_thread` methods of the server
depending on whether ``daemon`` is set or not.
app:
Register the server to an existing application and run that.
Expand All @@ -58,27 +62,24 @@ def run(
if port is None: # pragma: no cover
port = find_available_port(host)

logger.info(f"Using {server_type.__module__}.{server_type.__name__}")

server = server_type(component, server_config)

if app is not None: # pragma: no cover
server.register(app)
server = server_type(component, server_config, app)
logger.info(f"Using {server}")

run_server = server.run if not daemon else server.run_in_thread
run_server(host, port, **(run_kwargs or {})) # type: ignore
server.wait_until_started()

return server


def multiview_server(
server_type: Type[_S],
server_type: ServerFactory[_App, _Config] = DEFAULT_SERVER_FACTORY,
host: str = "127.0.0.1",
port: Optional[int] = None,
server_config: Optional[Any] = None,
server_config: Optional[_Config] = None,
run_kwargs: Optional[Dict[str, Any]] = None,
app: Optional[Any] = None,
) -> Tuple[MultiViewMount, _S]:
) -> Tuple[MultiViewMount, Server[_App]]:
"""Set up a server where views can be dynamically added.

In other words this allows the user to work with IDOM in an imperative manner.
Expand All @@ -89,8 +90,8 @@ def multiview_server(
server: The server type to start up as a daemon
host: The server hostname
port: The server port number
server_config: Value passed to :meth:`AbstractRenderServer.configure`
run_kwargs: Keyword args passed to :meth:`AbstractRenderServer.run_in_thread`
server_config: Value passed to :meth:`~idom.server.proto.ServerFactory`
run_kwargs: Keyword args passed to :meth:`~idom.server.proto.Server.run_in_thread`
app: Optionally provide a prexisting application to register to

Returns:
Expand All @@ -114,14 +115,14 @@ def multiview_server(


def hotswap_server(
server_type: Type[_S],
server_type: ServerFactory[_App, _Config] = DEFAULT_SERVER_FACTORY,
host: str = "127.0.0.1",
port: Optional[int] = None,
server_config: Optional[Any] = None,
server_config: Optional[_Config] = None,
run_kwargs: Optional[Dict[str, Any]] = None,
app: Optional[Any] = None,
sync_views: bool = False,
) -> Tuple[MountFunc, _S]:
) -> Tuple[MountFunc, Server[_App]]:
"""Set up a server where views can be dynamically swapped out.

In other words this allows the user to work with IDOM in an imperative manner.
Expand All @@ -132,8 +133,8 @@ def hotswap_server(
server: The server type to start up as a daemon
host: The server hostname
port: The server port number
server_config: Value passed to :meth:`AbstractRenderServer.configure`
run_kwargs: Keyword args passed to :meth:`AbstractRenderServer.run_in_thread`
server_config: Value passed to :meth:`~idom.server.proto.ServerFactory`
run_kwargs: Keyword args passed to :meth:`~idom.server.proto.Server.run_in_thread`
app: Optionally provide a prexisting application to register to
sync_views: Whether to update all displays with newly mounted components

Expand Down
Loading