Skip to content

Commit 51b105d

Browse files
committed
initial work converting to new server interface
1 parent a3fbbad commit 51b105d

File tree

8 files changed

+310
-571
lines changed

8 files changed

+310
-571
lines changed

src/idom/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .core.layout import Layout
1717
from .core.vdom import vdom
1818
from .sample import run_sample_app
19-
from .server.develop import develop
19+
from .server.any import run
2020
from .utils import Ref, html_to_vdom
2121
from .widgets import hotswap, multiview
2222

@@ -38,6 +38,7 @@
3838
"multiview",
3939
"Ref",
4040
"run_sample_app",
41+
"run",
4142
"Stop",
4243
"types",
4344
"use_callback",

src/idom/sample.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from . import html
77
from .core.component import component
88
from .core.types import VdomDict
9-
from .server.develop import develop
9+
from .server.any import run
1010

1111

1212
@component
@@ -37,4 +37,4 @@ def run_sample_app(
3737
port: the port on the host to serve from
3838
open_browser: whether to open a browser window after starting the server
3939
"""
40-
develop(App, None, host, port, open_browser=open_browser)
40+
run(App, host, port, open_browser=open_browser)

src/idom/server/any.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import warnings
5+
import webbrowser
6+
from importlib import import_module
7+
from typing import Any, Awaitable, Iterator, TypeVar, runtime_checkable
8+
9+
from typing_extensions import Protocol
10+
11+
from idom.types import ComponentConstructor
12+
13+
from .utils import find_available_port
14+
15+
16+
SUPPORTED_PACKAGES = (
17+
"starlette",
18+
"fastapi",
19+
"sanic",
20+
"flask",
21+
"tornado",
22+
)
23+
24+
25+
def run(
26+
component: ComponentConstructor,
27+
host: str = "127.0.0.1",
28+
port: int | None = None,
29+
open_browser: bool = True,
30+
) -> None:
31+
"""Run a component with a development server"""
32+
33+
warnings.warn(
34+
"You are running a development server, be sure to change this before deploying in production!",
35+
UserWarning,
36+
stacklevel=2,
37+
)
38+
39+
try:
40+
implementation = next(all_implementations())
41+
except StopIteration:
42+
raise RuntimeError( # pragma: no cover
43+
f"Found no built-in server implementation installed {SUPPORTED_PACKAGES}"
44+
)
45+
46+
app = implementation.create_development_app()
47+
implementation.configure(app, component)
48+
49+
coros: list[Awaitable] = []
50+
51+
host = host
52+
port = port or find_available_port(host)
53+
started = asyncio.Event()
54+
55+
coros.append(implementation.serve_development_app(app, host, port, started))
56+
57+
if open_browser:
58+
59+
async def _open_browser_after_server() -> None:
60+
await started.wait()
61+
webbrowser.open(f"http://{host}:{port}")
62+
63+
coros.append(_open_browser_after_server())
64+
65+
asyncio.get_event_loop().run_forever(asyncio.gather(*coros))
66+
67+
68+
def configure(app: Any, component: ComponentConstructor) -> None:
69+
return get_implementation().configure(app, component)
70+
71+
72+
def create_development_app() -> Any:
73+
return get_implementation().create_development_app()
74+
75+
76+
async def serve_development_app(
77+
app: Any, host: str, port: int, started: asyncio.Event
78+
) -> None:
79+
return await get_implementation().serve_development_app(app, host, port, started)
80+
81+
82+
def get_implementation() -> Implementation:
83+
"""Get the first available server implementation"""
84+
try:
85+
return next(all_implementations())
86+
except StopIteration:
87+
raise RuntimeError("No built-in server implementation installed.")
88+
89+
90+
def all_implementations() -> Iterator[Implementation]:
91+
"""Yield all available server implementations"""
92+
for name in SUPPORTED_PACKAGES:
93+
try:
94+
module = import_module(f"idom.server.{name}")
95+
except ImportError: # pragma: no cover
96+
continue
97+
98+
if not isinstance(module, Implementation):
99+
raise TypeError(f"{module.__name__!r} is an invalid implementation")
100+
101+
yield module
102+
103+
104+
_App = TypeVar("_App")
105+
106+
107+
@runtime_checkable
108+
class Implementation(Protocol):
109+
"""Common interface for IDOM's builti-in server implementations"""
110+
111+
def configure(self, app: _App, component: ComponentConstructor) -> None:
112+
"""Configure the given app instance to display the given component"""
113+
114+
def create_development_app(self) -> _App:
115+
"""Create an application instance for development purposes"""
116+
117+
async def serve_development_app(
118+
self, app: _App, host: str, port: int, started: asyncio.Event
119+
) -> None:
120+
"""Run an application using a development server"""

src/idom/server/develop.py

Lines changed: 0 additions & 133 deletions
This file was deleted.

src/idom/server/fastapi.py

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,39 @@
1-
from typing import Optional
1+
from __future__ import annotations
22

33
from fastapi import FastAPI
44

5+
from idom.config import IDOM_DEBUG_MODE
56
from idom.core.types import ComponentConstructor
67

78
from .starlette import (
8-
Config,
9-
StarletteServer,
9+
Options,
1010
_setup_common_routes,
11-
_setup_config_and_app,
12-
_setup_shared_view_dispatcher_route,
11+
_setup_options,
1312
_setup_single_view_dispatcher_route,
13+
serve_development_app,
1414
)
1515

1616

17-
def PerClientStateServer(
18-
constructor: ComponentConstructor,
19-
config: Optional[Config] = None,
20-
app: Optional[FastAPI] = None,
21-
) -> StarletteServer:
22-
"""Return a :class:`StarletteServer` where each client has its own state.
23-
24-
Implements the :class:`~idom.server.proto.ServerFactory` protocol
17+
__all__ = "configure", "serve_development_app", "create_development_app"
2518

26-
Parameters:
27-
constructor: A component constructor
28-
config: Options for configuring server behavior
29-
app: An application instance (otherwise a default instance is created)
30-
"""
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)
3519

36-
37-
def SharedClientStateServer(
20+
def configure(
21+
app: FastAPI,
3822
constructor: ComponentConstructor,
39-
config: Optional[Config] = None,
40-
app: Optional[FastAPI] = None,
41-
) -> StarletteServer:
42-
"""Return a :class:`StarletteServer` where each client shares state.
43-
44-
Implements the :class:`~idom.server.proto.ServerFactory` protocol
23+
options: Options | None = None,
24+
) -> None:
25+
"""Prepare a :class:`FastAPI` server to serve the given component
4526
4627
Parameters:
28+
app: An application instance
4729
constructor: A component constructor
4830
config: Options for configuring server behavior
49-
app: An application instance (otherwise a default instance is created)
31+
5032
"""
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)
33+
options = _setup_options(options)
34+
_setup_common_routes(options, app)
35+
_setup_single_view_dispatcher_route(options["url_prefix"], app, constructor)
36+
37+
38+
def create_development_app() -> FastAPI:
39+
return FastAPI(debug=IDOM_DEBUG_MODE.current)

0 commit comments

Comments
 (0)