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
13 changes: 12 additions & 1 deletion remotestate-py/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
## Version 0.3.3 (in development)

- Improved usability of the Python `Store` class:
- Introduced public protocol `StoreAt` now returned
from `Store.at` instead of internal `_StoreAt` type.
- Added `StoreAt.value` read-only property to access the value
of a node returned by `Store.at`.

- The Python `ServeResult` class now renders as a HTML table in notebooks.


## Version 0.3.2

- Fixed issue in TypeScript library. (#42)
- Fixed issue in remotestate TypeScript library. (#42)

## Version 0.3.1

Expand Down
16 changes: 8 additions & 8 deletions remotestate-py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,13 @@ rs.serve(CounterService(), ui_dist="my-ui/dist")

The public Python API is exported from `remotestate`:

- `Store`
- `Service`
- `ServeResult`
- `action`
- `query`
- `Store` and `StoreAt`
- `Service` and `ServeResult`
- `action` and `query`
- `serve`
- `path`

## Store
## Store and StoreAt

`Store(initial, *, default_factory=None)` holds the Python-side application state.

Expand All @@ -87,8 +85,10 @@ The public Python API is exported from `remotestate`:
- `set(path, value)` writes a value and notifies subscribers
- `store[path]` and `store[path] = value` are notebook-friendly aliases for
`get()` and `set()`
- `store.at.some.path = value` is notebook-friendly sugar for nested
`set()` calls; use item syntax for keys that are not valid identifiers
- `store.at` returns a notebook-friendly accessor of type `StoreAt`
for nested state variables using item or attribute syntax
- `store.at.some.path = value` for example executes nested
`set()` calls; use item syntax for arrays or keys that are not valid identifiers
- `subscribe(callback)` receives batched path-to-value updates after changes flush
- `default_factory` can materialize missing parents while setting nested values

Expand Down
145 changes: 136 additions & 9 deletions remotestate-py/notebooks/demo.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@
"import remotestate as rs"
]
},
{
"cell_type": "markdown",
"id": "07f01128-58ac-4f12-a2b7-27aa1f4e0dad",
"metadata": {},
"source": [
"Define a store that hold's our UI's data state"
]
},
{
"cell_type": "code",
"execution_count": 2,
Expand All @@ -60,6 +68,14 @@
"store = rs.Store({\"count\": 42})"
]
},
{
"cell_type": "markdown",
"id": "8e1ac855-9ecf-4638-8325-ec34fee9d6ff",
"metadata": {},
"source": [
"Define a service that provides actions and/or queries upon the store"
]
},
{
"cell_type": "code",
"execution_count": 3,
Expand All @@ -70,12 +86,23 @@
"class MyService(rs.Service):\n",
" @rs.action\n",
" async def increment(self):\n",
" self.store.set(\"count\", self.store.get(\"count\") + 1)\n"
" # Using 'at':\n",
" self.store.at.count = self.store.at.count.value + 1\n",
" # Using 'set()' / 'get()':\n",
" # self.store.set(\"count\", self.store.get(\"count\") + 1)\n"
]
},
{
"cell_type": "markdown",
"id": "faa078d2-a579-4a44-8dd6-53280dec5b17",
"metadata": {},
"source": [
"Serve the app and display the UI as a side-effect"
]
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 4,
"id": "979719a8-99c0-484e-ba5a-7f41a9f9c0a4",
"metadata": {},
"outputs": [
Expand All @@ -86,15 +113,15 @@
" <iframe\n",
" width=\"100%\"\n",
" height=\"300\"\n",
" src=\"http://localhost:49692?t=1782044457&ws=ws%3A%2F%2Flocalhost%3A49692%2Fws\"\n",
" src=\"http://localhost:61303?t=1783061078&ws=ws%3A%2F%2Flocalhost%3A61303%2Fws\"\n",
" frameborder=\"0\"\n",
" allowfullscreen\n",
" \n",
" ></iframe>\n",
" "
],
"text/plain": [
"<IPython.lib.display.IFrame at 0x29bff3bd310>"
"<IPython.lib.display.IFrame at 0x1e4b905dbe0>"
]
},
"metadata": {},
Expand All @@ -103,48 +130,148 @@
],
"source": [
"serve_result = rs.serve(\n",
" # If we wouldn't need actions/queries, we'd have passed rs.Service(store) here\n",
" MyService(store), \n",
" # The app's build directory or its (dev) URL, e.g., \"http://localhost:5173/\"\n",
" ui_dist=\"../../remotestate-demo/dist\",\n",
" # The height of the generated UI in CSS pixels\n",
" height=300,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "a22e5777-2bf7-48c5-aaec-ddc67234360c",
"metadata": {},
"source": [
"We can now change state in the store"
]
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 5,
"id": "5a8d1487-8f9f-492b-bc77-69ac1320cd18",
"metadata": {},
"outputs": [],
"source": [
"store.set(\"count\", 100)"
]
},
{
"cell_type": "markdown",
"id": "2172ac31-6123-4b70-a89b-7c2a4d2cecaa",
"metadata": {},
"source": [
"or via 'at'"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "407879ae-4f90-4c89-ac72-029395e3ce9d",
"metadata": {},
"outputs": [],
"source": [
"store.at.count = 200"
]
},
{
"cell_type": "markdown",
"id": "c83498b4-6107-46fb-8b6f-13bbcdf0a023",
"metadata": {},
"source": [
"And get state values from the store"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "59bfc45c-450a-4f64-b073-50381b5b986d",
"id": "a8914148-4262-4785-93bb-66445c05730c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"ServeResult(host='localhost', port=49692, server_url='http://localhost:49692', ws_url='ws://localhost:49692/ws', ui_base_url='http://localhost:49692', ui_url='http://localhost:49692?t=1782044457&ws=ws%3A%2F%2Flocalhost%3A49692%2Fws', app=<fastapi.applications.FastAPI object at 0x0000029BFF309FD0>, server=<uvicorn.server.Server object at 0x0000029BFF0FE5D0>, thread=<Thread(Thread-4 (run), started daemon 19724)>)"
"200"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"store.get(\"count\")"
]
},
{
"cell_type": "markdown",
"id": "a5cf7aab-a314-4829-86b4-a1eea49cfb5b",
"metadata": {},
"source": [
"or via 'at' "
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "cff59797-15fa-42c0-9738-0250d798ea1d",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<pre>200</pre>"
],
"text/plain": [
"200"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"store.at.count"
]
},
{
"cell_type": "markdown",
"id": "905e4677-eb1d-46e1-9dd1-0968f7fc9d2d",
"metadata": {},
"source": [
"The serve result carries important info for clients using remotestore"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "59bfc45c-450a-4f64-b073-50381b5b986d",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<table><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody><tr><th>Host</th><td>localhost</td></tr><tr><th>Port</th><td>61303</td></tr><tr><th>Server URL</th><td><a href=\"http://localhost:61303\">http://localhost:61303</a></td></tr><tr><th>WebSocket URL</th><td><a href=\"ws://localhost:61303/ws\">ws://localhost:61303/ws</a></td></tr><tr><th>UI Base URL</th><td><a href=\"http://localhost:61303\">http://localhost:61303</a></td></tr><tr><th>UI URL</th><td><a href=\"http://localhost:61303?t=1783061078&amp;ws=ws%3A%2F%2Flocalhost%3A61303%2Fws\">http://localhost:61303?t=1783061078&amp;ws=ws%3A%2F%2Flocalhost%3A61303%2Fws</a></td></tr></tbody></table>"
],
"text/plain": [
"ServeResult(host='localhost', port=61303, server_url='http://localhost:61303', ws_url='ws://localhost:61303/ws', ui_base_url='http://localhost:61303', ui_url='http://localhost:61303?t=1783061078&ws=ws%3A%2F%2Flocalhost%3A61303%2Fws', app=<fastapi.applications.FastAPI object at 0x000001E4B8F4CD70>, server=<uvicorn.server.Server object at 0x000001E4B8F4DE80>, thread=<Thread(Thread-4 (run), started daemon 31108)>)"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"serve_result"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "407879ae-4f90-4c89-ac72-029395e3ce9d",
"id": "29f7f7ef-0294-48cc-a5fe-36e864d5ced0",
"metadata": {},
"outputs": [],
"source": []
Expand All @@ -166,7 +293,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.5"
"version": "3.14.6"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion remotestate-py/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "remotestate"
version = "0.3.2"
version = "0.3.3.dev0"
authors = [{name = "forman"}]
description = "Python state, React UI."
readme = "README.md"
Expand Down
3 changes: 2 additions & 1 deletion remotestate-py/src/remotestate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from . import path
from .serve import ServeResult, serve
from .service import Service, action, query
from .store import Store
from .store import Store, StoreAt

__version__ = version("remotestate")

__all__ = [
"Store",
"StoreAt",
"Service",
"ServeResult",
"action",
Expand Down
28 changes: 28 additions & 0 deletions remotestate-py/src/remotestate/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import webbrowser
from collections.abc import Callable
from dataclasses import dataclass, field
from html import escape
from typing import Any, Literal, TypeGuard
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

Expand Down Expand Up @@ -56,6 +57,33 @@ def stop(self, *, timeout: float = 5.0) -> None:
_wait_for_port_free(self.host, self.port, timeout=timeout)
_servers.pop(self.registry_key, None)

def _repr_html_(self) -> str:
rows = [
("Host", self.host, False),
("Port", self.port, False),
("Server URL", self.server_url, True),
("WebSocket URL", self.ws_url, True),
("UI Base URL", self.ui_base_url, True),
("UI URL", self.ui_url, True),
]

def format_value(value: object, is_url: bool) -> str:
escaped = escape(str(value))
if is_url:
return f'<a href="{escaped}">{escaped}</a>'
return escaped

body = "".join(
f"<tr><th>{escape(label)}</th><td>{format_value(value, is_url)}</td></tr>"
for label, value, is_url in rows
)
return (
"<table>"
"<thead><tr><th>Field</th><th>Value</th></tr></thead>"
f"<tbody>{body}</tbody>"
"</table>"
)


Display = DisplayMode | Callable[[ServeResult], None]

Expand Down
Loading