Skip to content
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
7 changes: 7 additions & 0 deletions remotestate-py/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## Version 0.1.1 (in development)

- Added `__version__` attribute to main package.

- Changed signature and behavior of `serve()` function:
- Renamed `iframe_heigh` argument into `height`, and added `width`.
- It is no longer using FastAPI/Uvicorn default
logging. Instead, all server logs are written to `server.log`
and logging to stdout/stderr is suppressed.

## Version 0.1.0

Expand Down
4 changes: 4 additions & 0 deletions remotestate-py/src/remotestate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""Public package exports for building and serving ``remotestate`` apps."""

from importlib.metadata import version

from .service import Service, action, query
from .serve import serve
from .store import Store

__version__ = version("remotestate")

__all__ = [
"Store",
"Service",
Expand Down
52 changes: 46 additions & 6 deletions remotestate-py/src/remotestate/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

DEFAULT_HOST = "localhost"
DEFAULT_PORT = 9753
DEFAULT_IFRAME_WIDTH = "100%"
DEFAULT_IFRAME_HEIGHT = 400

_servers: dict[str, uvicorn.Server] = {}
Expand All @@ -41,7 +42,8 @@ def serve(
app: FastAPI | None = None,
open_browser: bool | None = None,
open_iframe: bool | None = None,
iframe_height: int = DEFAULT_IFRAME_HEIGHT,
width: int | str = DEFAULT_IFRAME_WIDTH,
height: int | str = DEFAULT_IFRAME_HEIGHT,
# --- Uvicorn
host: str = DEFAULT_HOST,
port: int = DEFAULT_PORT,
Expand All @@ -59,12 +61,13 @@ def serve(
`fastapi.staticfiles.StaticFiles` object or a directory path.
app: A FastAPI instance to use. If not provided,
a new instance is created and passed to `Service.init_app(app)`
so that it can by initialized by the user.
so that it can be initialized by the user.
open_browser: Open the UI in the default browser after starting.
Defaults to True when not running in Jupyter.
open_iframe: Render the UI as an IFrame in the Jupyter notebook.
Defaults to True when running in Jupyter.
iframe_height: Height of the IFrame in pixels.
width: Width of the IFrame.
height: Height of the IFrame.
host: Host to bind the server to.
port: Port to bind the server to.
uvicorn_settings: Additional [uvicorn settings]((https://uvicorn.dev/settings/)
Expand Down Expand Up @@ -93,10 +96,14 @@ def serve(
else:
mounts_["/"] = StaticFiles(directory=ui_dist, html=True)

remotestate_server = Server(service=service, mounts=mounts_, app=app)
rs_server = Server(service=service, mounts=mounts_, app=app)

uvicorn_settings.update(host=host, port=port)
uvicorn_config = uvicorn.Config(remotestate_server.app, **uvicorn_settings)
if "log_config" not in uvicorn_settings:
# disable uvicorn's default logging setup
uvicorn_settings.update(log_config=_get_log_config())

uvicorn_config = uvicorn.Config(rs_server.app, **uvicorn_settings)
uvicorn_server = uvicorn.Server(uvicorn_config)
_servers[registry_key] = uvicorn_server

Expand All @@ -119,7 +126,7 @@ def serve(
if should_open_iframe:
from IPython.display import IFrame, display

display(IFrame(src=ui_dist_url, width="100%", height=iframe_height))
display(IFrame(src=ui_dist_url, width=width, height=height))
elif should_open_browser:
webbrowser.open(ui_dist_url)

Expand Down Expand Up @@ -158,6 +165,7 @@ def _add_ui_url_params(ui_dist_url: str, *, host: str, port: int) -> str:
("ws", f"ws://{host}:{port}/ws"),
]
)
# noinspection PyTypeChecker
return urlunsplit(url_parts._replace(query=urlencode(query)))


Expand All @@ -179,3 +187,35 @@ def _wait_for_port_free(host: str, port: int, timeout: float = 5.0) -> None:
pass # port still in use
time.sleep(0.05)
raise TimeoutError(f"Port {port} did not become free within {timeout}s")


def _get_log_config(log_file: str | PathLike = "server.log") -> dict[str, Any]:
return {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"file": {
"class": "logging.FileHandler",
"filename": str(log_file),
"formatter": "default",
}
},
"formatters": {
"default": {
"format": "%(asctime)s %(levelname)s %(name)s: %(message)s",
}
},
"loggers": {
"uvicorn": {"handlers": ["file"], "level": "INFO", "propagate": False},
"uvicorn.error": {
"handlers": ["file"],
"level": "INFO",
"propagate": False,
},
"uvicorn.access": {
"handlers": ["file"],
"level": "INFO",
"propagate": False,
},
},
}
1 change: 1 addition & 0 deletions remotestate-py/tests/test_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pytest

# noinspection PyProtectedMember
from remotestate.serve import (
_add_ui_url_params,
_get_cell_id,
Expand Down