spaday-regular-table 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spaday_regular_table/__init__.py +48 -0
- spaday_regular_table/components.cem.json +39 -0
- spaday_regular_table/components.py +42 -0
- spaday_regular_table/example.py +267 -0
- spaday_regular_table/extension/cdn/index.js +1202 -0
- spaday_regular_table/extension/cdn/index.js.map +7 -0
- spaday_regular_table/extension/css/material.css +1 -0
- spaday_regular_table/extension/index.html +13 -0
- spaday_regular_table/tests/test_all.py +66 -0
- spaday_regular_table/tests/test_example.py +34 -0
- spaday_regular_table-0.1.0.dist-info/METADATA +111 -0
- spaday_regular_table-0.1.0.dist-info/RECORD +15 -0
- spaday_regular_table-0.1.0.dist-info/WHEEL +4 -0
- spaday_regular_table-0.1.0.dist-info/entry_points.txt +2 -0
- spaday_regular_table-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from spaday import ComponentPackage
|
|
5
|
+
from spaday.component import Child
|
|
6
|
+
|
|
7
|
+
from .components import SpadayRegularTable
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
package = ComponentPackage(
|
|
12
|
+
name="regular-table",
|
|
13
|
+
assets_dir=Path(__file__).parent / "extension",
|
|
14
|
+
assets=(("css", "css/material.css"), ("js", "cdn/index.js")),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RegularTable(SpadayRegularTable):
|
|
19
|
+
"""Python-friendly constructor for the viewport-virtualized table."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*children: Child,
|
|
24
|
+
key: str | None = None,
|
|
25
|
+
columns: Any = None,
|
|
26
|
+
rows: Any = None,
|
|
27
|
+
row_patch: Any = None,
|
|
28
|
+
stream_url: str | None = None,
|
|
29
|
+
virtual_mode: str | None = None,
|
|
30
|
+
row_header: str | None = None,
|
|
31
|
+
row_height: float | None = None,
|
|
32
|
+
**props: Any,
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(
|
|
35
|
+
*children,
|
|
36
|
+
key=key,
|
|
37
|
+
columns=columns,
|
|
38
|
+
rows=rows,
|
|
39
|
+
rowPatch=row_patch,
|
|
40
|
+
streamUrl=stream_url,
|
|
41
|
+
virtualMode=virtual_mode,
|
|
42
|
+
rowHeader=row_header,
|
|
43
|
+
rowHeight=row_height,
|
|
44
|
+
**props,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["RegularTable", "package"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"modules": [
|
|
4
|
+
{
|
|
5
|
+
"path": "src/ts/index.ts",
|
|
6
|
+
"declarations": [
|
|
7
|
+
{
|
|
8
|
+
"kind": "class",
|
|
9
|
+
"name": "RegularTable",
|
|
10
|
+
"customElement": true,
|
|
11
|
+
"tagName": "spaday-regular-table",
|
|
12
|
+
"summary": "Viewport-virtualized table over serializable record rows.",
|
|
13
|
+
"attributes": [
|
|
14
|
+
{ "name": "columns", "type": { "text": "unknown" }, "description": "Column keys or {key, label, cell} definitions." },
|
|
15
|
+
{ "name": "rows", "type": { "text": "unknown" }, "description": "Record rows held in the browser and sliced per viewport." },
|
|
16
|
+
{ "name": "rowPatch", "type": { "text": "unknown" }, "description": "Serializable batch of update, insert, and remove row operations." },
|
|
17
|
+
{ "name": "streamUrl", "type": { "text": "string" }, "description": "Optional SSE endpoint whose messages are row patches." },
|
|
18
|
+
{ "name": "virtualMode", "type": { "text": "'both' | 'horizontal' | 'vertical' | 'none'" }, "default": "'both'" },
|
|
19
|
+
{ "name": "rowHeader", "type": { "text": "string" }, "description": "Optional row field rendered as a sticky row header." },
|
|
20
|
+
{ "name": "rowHeight", "type": { "text": "number" }, "description": "Optional fixed row height in pixels." }
|
|
21
|
+
],
|
|
22
|
+
"events": [
|
|
23
|
+
{ "name": "cell-click", "type": { "text": "CustomEvent" }, "description": "Clicked cell metadata and source row." },
|
|
24
|
+
{ "name": "row-patch", "type": { "text": "CustomEvent" }, "description": "A row patch was applied." },
|
|
25
|
+
{ "name": "row-stream-error", "type": { "text": "CustomEvent" }, "description": "An SSE message was not a valid row patch." },
|
|
26
|
+
{ "name": "table-draw", "type": { "text": "CustomEvent" }, "description": "Fires after each viewport draw so visible cells can be decorated." }
|
|
27
|
+
],
|
|
28
|
+
"members": [
|
|
29
|
+
{ "kind": "method", "name": "updateRow", "description": "Merge fields into one row and redraw the viewport." },
|
|
30
|
+
{ "kind": "method", "name": "insertRow", "description": "Insert one row at a virtual index and redraw the viewport." },
|
|
31
|
+
{ "kind": "method", "name": "removeRow", "description": "Remove one row at a virtual index and redraw the viewport." },
|
|
32
|
+
{ "kind": "method", "name": "getMeta", "description": "Resolve a rendered cell to virtual coordinates and its source row." }
|
|
33
|
+
],
|
|
34
|
+
"slots": []
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Generated by spaday.cem from components.cem.json — do not edit by hand.
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from spaday.component import Child, Component
|
|
7
|
+
|
|
8
|
+
__all__ = ["SpadayRegularTable"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpadayRegularTable(Component):
|
|
12
|
+
"""Viewport-virtualized table over serializable record rows."""
|
|
13
|
+
|
|
14
|
+
tag = "spaday-regular-table"
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*children: Child,
|
|
19
|
+
key: str | None = None,
|
|
20
|
+
columns: Any = None,
|
|
21
|
+
rows: Any = None,
|
|
22
|
+
rowPatch: Any = None,
|
|
23
|
+
streamUrl: str | None = None,
|
|
24
|
+
virtualMode: Literal["both", "horizontal", "vertical", "none"] | None = None,
|
|
25
|
+
rowHeader: str | None = None,
|
|
26
|
+
rowHeight: float | None = None,
|
|
27
|
+
**props: Any,
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(
|
|
30
|
+
*children,
|
|
31
|
+
key=key,
|
|
32
|
+
props={
|
|
33
|
+
"columns": columns,
|
|
34
|
+
"rows": rows,
|
|
35
|
+
"rowPatch": rowPatch,
|
|
36
|
+
"streamUrl": streamUrl,
|
|
37
|
+
"virtualMode": virtualMode,
|
|
38
|
+
"rowHeader": rowHeader,
|
|
39
|
+
"rowHeight": rowHeight,
|
|
40
|
+
},
|
|
41
|
+
**props,
|
|
42
|
+
)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
from spaday import CallEndpoint, SetProp, by_id, element, event_value
|
|
8
|
+
from spaday.backends.starlette import serve
|
|
9
|
+
from starlette.responses import JSONResponse, StreamingResponse
|
|
10
|
+
from starlette.routing import Route
|
|
11
|
+
|
|
12
|
+
from spaday_regular_table import RegularTable, package
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("uvicorn.error")
|
|
15
|
+
|
|
16
|
+
venues = ("XNAS", "XNYS", "ARCX", "BATS")
|
|
17
|
+
states = ("Open", "Working", "Filled")
|
|
18
|
+
rows = [
|
|
19
|
+
{
|
|
20
|
+
"id": index,
|
|
21
|
+
"symbol": f"SYM{index:05d}",
|
|
22
|
+
"venue": venues[index % len(venues)],
|
|
23
|
+
"price": round(98 + index / 100 + (index % 7) * 0.13, 2),
|
|
24
|
+
"change": round(((index % 19) - 9) / 10, 2),
|
|
25
|
+
"volume": 25_000 + (index * 7_919) % 4_000_000,
|
|
26
|
+
"state": states[index % len(states)],
|
|
27
|
+
}
|
|
28
|
+
for index in range(100_000)
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def market_row(index: int, symbol: str | None = None) -> dict[str, Any]:
|
|
33
|
+
return {
|
|
34
|
+
"id": index,
|
|
35
|
+
"symbol": symbol or f"LIVE{index:05d}",
|
|
36
|
+
"venue": venues[index % len(venues)],
|
|
37
|
+
"price": round(102 + (index % 97) * 0.11, 2),
|
|
38
|
+
"change": round(((index % 17) - 8) / 10, 2),
|
|
39
|
+
"volume": 50_000 + (index * 3_571) % 3_000_000,
|
|
40
|
+
"state": states[index % len(states)],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
revision = 0
|
|
45
|
+
subscribers: set[asyncio.Queue] = set()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def publish(*operations: dict[str, Any]) -> dict[str, Any]:
|
|
49
|
+
global revision
|
|
50
|
+
revision += 1
|
|
51
|
+
patch = {"revision": revision, "operations": list(operations)}
|
|
52
|
+
for subscriber in subscribers:
|
|
53
|
+
subscriber.put_nowait(patch)
|
|
54
|
+
return patch
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
next_stream_id = 100_000
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def row_stream(_request):
|
|
61
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
62
|
+
subscribers.add(queue)
|
|
63
|
+
|
|
64
|
+
async def events():
|
|
65
|
+
try:
|
|
66
|
+
while True:
|
|
67
|
+
patch = await queue.get()
|
|
68
|
+
yield f"data: {json.dumps(patch)}\n\n"
|
|
69
|
+
finally:
|
|
70
|
+
subscribers.discard(queue)
|
|
71
|
+
|
|
72
|
+
return StreamingResponse(events(), media_type="text/event-stream", headers={"Cache-Control": "no-cache"})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def stream_rows() -> None:
|
|
76
|
+
global next_stream_id
|
|
77
|
+
while True:
|
|
78
|
+
await asyncio.sleep(2)
|
|
79
|
+
row = market_row(next_stream_id)
|
|
80
|
+
next_stream_id += 1
|
|
81
|
+
rows.insert(0, row)
|
|
82
|
+
rows.pop()
|
|
83
|
+
publish(
|
|
84
|
+
{"type": "insert", "index": 0, "row": row},
|
|
85
|
+
{"type": "remove", "index": 100_000},
|
|
86
|
+
)
|
|
87
|
+
logger.info("Table row streamed from server: %s", row["symbol"])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
next_inserted_id = -1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def table_action(request):
|
|
94
|
+
global next_inserted_id
|
|
95
|
+
action = request.path_params["action"]
|
|
96
|
+
payload = await request.json()
|
|
97
|
+
if action == "click":
|
|
98
|
+
logger.info("Table cell click received from browser: %s", payload)
|
|
99
|
+
row = payload.get("row", {})
|
|
100
|
+
return JSONResponse({"message": f"Server received click on {row.get('symbol', 'a cell')}"})
|
|
101
|
+
|
|
102
|
+
if action == "update":
|
|
103
|
+
changes = {"price": round(float(rows[0]["price"]) + 1, 2), "change": 1}
|
|
104
|
+
rows[0] = {**rows[0], **changes}
|
|
105
|
+
patch = publish({"type": "update", "index": 0, "changes": changes})
|
|
106
|
+
message = f"Server updated {rows[0]['symbol']} to ${rows[0]['price']:.2f}"
|
|
107
|
+
elif action == "insert":
|
|
108
|
+
row = market_row(next_inserted_id, f"NEW{abs(next_inserted_id)}")
|
|
109
|
+
next_inserted_id -= 1
|
|
110
|
+
rows.insert(0, row)
|
|
111
|
+
patch = publish({"type": "insert", "index": 0, "row": row})
|
|
112
|
+
message = f"Server inserted {row['symbol']}"
|
|
113
|
+
elif action == "remove":
|
|
114
|
+
removed = rows.pop(0)
|
|
115
|
+
patch = publish({"type": "remove", "index": 0})
|
|
116
|
+
message = f"Server removed {removed['symbol']}"
|
|
117
|
+
else:
|
|
118
|
+
return JSONResponse({"message": f"Unknown action: {action}"}, status_code=404)
|
|
119
|
+
|
|
120
|
+
logger.info("Table %s received from browser: %s", action, payload)
|
|
121
|
+
return JSONResponse({"message": message, "patch": patch})
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
table = (
|
|
125
|
+
RegularTable(
|
|
126
|
+
columns=[
|
|
127
|
+
{
|
|
128
|
+
"key": "symbol",
|
|
129
|
+
"label": "Symbol",
|
|
130
|
+
"cell": {
|
|
131
|
+
"tag": "button",
|
|
132
|
+
"class": "symbol-button",
|
|
133
|
+
"title": "Send this row to Python",
|
|
134
|
+
"event": "symbol-click",
|
|
135
|
+
"attributes": {"type": "button"},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{"key": "venue", "label": "Venue"},
|
|
139
|
+
{"key": "price", "label": "Last price", "cell": {"format": "number", "digits": 2}},
|
|
140
|
+
{
|
|
141
|
+
"key": "change",
|
|
142
|
+
"label": "Change",
|
|
143
|
+
"cell": {
|
|
144
|
+
"format": "percent",
|
|
145
|
+
"digits": 2,
|
|
146
|
+
"signed": True,
|
|
147
|
+
"positive_class": "positive",
|
|
148
|
+
"negative_class": "negative",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{"key": "volume", "label": "Volume", "cell": {"format": "number"}},
|
|
152
|
+
{"key": "state", "label": "Status", "cell": {"tag": "span", "class": "state-badge"}},
|
|
153
|
+
],
|
|
154
|
+
rows=rows,
|
|
155
|
+
row_header="id",
|
|
156
|
+
row_height=36,
|
|
157
|
+
stream_url="/api/table/stream",
|
|
158
|
+
id="orders",
|
|
159
|
+
)
|
|
160
|
+
.bind("rowPatch", "action_result.body.patch")
|
|
161
|
+
.on("symbol-click", CallEndpoint("POST", "/api/table/click", event_value(), result="action_result"))
|
|
162
|
+
.on("row-patch", SetProp(by_id("stream-status"), "textContent", "Applied server row patch"))
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def action_button(label: str, action: str):
|
|
167
|
+
return (
|
|
168
|
+
element("button")
|
|
169
|
+
.text(label)
|
|
170
|
+
.on(
|
|
171
|
+
"click",
|
|
172
|
+
CallEndpoint("POST", f"/api/table/{action}", {"source": "toolbar"}, result="action_result"),
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
page = element(
|
|
178
|
+
"main",
|
|
179
|
+
element(
|
|
180
|
+
"header",
|
|
181
|
+
element("div", element("p", class_="eyebrow").text("VIRTUALIZED BLOTTER"), element("h1").text("Regular Table")),
|
|
182
|
+
element("div", element("strong").text("100,000"), element("span").text(" browser-side rows"), class_="record-count"),
|
|
183
|
+
class_="page-header",
|
|
184
|
+
),
|
|
185
|
+
element("p", class_="lede").text("Rich cells and granular row patches, authored entirely in Python."),
|
|
186
|
+
element(
|
|
187
|
+
"section",
|
|
188
|
+
element(
|
|
189
|
+
"div",
|
|
190
|
+
action_button("Update first row", "update"),
|
|
191
|
+
action_button("Insert row", "insert"),
|
|
192
|
+
action_button("Remove first row", "remove"),
|
|
193
|
+
class_="toolbar-actions",
|
|
194
|
+
),
|
|
195
|
+
element(
|
|
196
|
+
"div",
|
|
197
|
+
element("span", id="stream-status", class_="status").text("Connecting to server stream"),
|
|
198
|
+
element("span", class_="status action-status").bind("textContent", "action_result.body.message"),
|
|
199
|
+
class_="statuses",
|
|
200
|
+
),
|
|
201
|
+
class_="toolbar",
|
|
202
|
+
),
|
|
203
|
+
element("section", table, class_="table-shell"),
|
|
204
|
+
element("p", class_="footnote").text("Buttons, formatting, REST actions, and live row patches are Python-authored serializable data."),
|
|
205
|
+
class_="page",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
styles = """
|
|
210
|
+
<style>
|
|
211
|
+
body { margin: 0; min-height: 100vh; background: #eef2f6; color: #172033;
|
|
212
|
+
font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
|
|
213
|
+
.page { box-sizing: border-box; max-width: 72rem; margin: 0 auto; padding: 2.5rem 1.25rem; }
|
|
214
|
+
.page-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; }
|
|
215
|
+
.eyebrow { margin: 0; color: #2563eb; font-size: .72rem; font-weight: 800; letter-spacing: .16em; }
|
|
216
|
+
h1 { margin: .2rem 0 0; font-size: clamp(2rem, 5vw, 3rem); letter-spacing: -.04em; }
|
|
217
|
+
.lede { margin: .5rem 0 1.5rem; color: #64748b; }
|
|
218
|
+
.record-count { padding: .7rem 1rem; border: 1px solid #cbd5e1; border-radius: .75rem; background: #fff; box-shadow: 0 6px 18px rgba(15,23,42,.05); }
|
|
219
|
+
.record-count strong { font-size: 1.15rem; } .record-count span { color: #64748b; font-size: .82rem; }
|
|
220
|
+
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .85rem 1rem;
|
|
221
|
+
border: 1px solid #dbe3ee; border-bottom: 0; border-radius: .9rem .9rem 0 0; background: #fff; }
|
|
222
|
+
.toolbar-actions { display: flex; gap: .55rem; }
|
|
223
|
+
button { border: 1px solid #cbd5e1; border-radius: .55rem; padding: .55rem .8rem; background: #fff; color: #334155; cursor: pointer; font-weight: 700; }
|
|
224
|
+
button:hover { border-color: #2563eb; color: #1d4ed8; background: #eff6ff; }
|
|
225
|
+
.status { color: #64748b; font-size: .82rem; text-align: right; }
|
|
226
|
+
.statuses { display: grid; gap: .2rem; }
|
|
227
|
+
.action-status { color: #2563eb; min-height: 1rem; }
|
|
228
|
+
.table-shell { height: min(68vh, 38rem); overflow: hidden; border: 1px solid #dbe3ee; border-radius: 0 0 .9rem .9rem;
|
|
229
|
+
background: #fff; box-shadow: 0 18px 45px rgba(15,23,42,.08); }
|
|
230
|
+
#orders { width: 100%; height: 100%; min-width: 0; }
|
|
231
|
+
#orders regular-table { box-sizing: border-box; padding: 0; background: #fff; scrollbar-color: #94a3b8 #f1f5f9; }
|
|
232
|
+
#orders regular-table table { color: #334155; border-collapse: separate; border-spacing: 0; }
|
|
233
|
+
#orders .rt-col-0 { width: 4rem; min-width: 4rem; max-width: 4rem; }
|
|
234
|
+
#orders .rt-col-1 { width: 12rem; min-width: 12rem; max-width: 12rem; }
|
|
235
|
+
#orders .rt-col-2 { width: 9rem; min-width: 9rem; max-width: 9rem; }
|
|
236
|
+
#orders .rt-col-3 { width: 11rem; min-width: 11rem; max-width: 11rem; }
|
|
237
|
+
#orders .rt-col-4 { width: 10rem; min-width: 10rem; max-width: 10rem; }
|
|
238
|
+
#orders .rt-col-5 { width: 12rem; min-width: 12rem; max-width: 12rem; }
|
|
239
|
+
#orders .rt-col-6 { width: 10rem; min-width: 10rem; max-width: 10rem; }
|
|
240
|
+
#orders regular-table td, #orders regular-table th { height: 36px; padding-inline: 12px; font-size: 13px; border-bottom: 1px solid #edf2f7; }
|
|
241
|
+
#orders regular-table thead tr:last-child th { height: 40px; border-bottom: 1px solid #cbd5e1; background: #f8fafc; color: #475569; font-weight: 800; }
|
|
242
|
+
#orders regular-table tbody tr:hover td { background: #eff6ff; }
|
|
243
|
+
#orders regular-table td.positive { color: #047857; font-weight: 700; }
|
|
244
|
+
#orders regular-table td.negative { color: #be123c; font-weight: 700; }
|
|
245
|
+
.symbol-button { border: 0; padding: .25rem .45rem; background: transparent; color: #1d4ed8; font: inherit; font-weight: 800; }
|
|
246
|
+
.state-badge { display: inline-block; min-width: 3.8rem; padding: .18rem .45rem; border-radius: 999px; background: #e0e7ff; color: #3730a3; text-align: center; font-size: .72rem; font-weight: 800; }
|
|
247
|
+
.footnote { margin: .75rem .25rem 0; color: #64748b; font-size: .8rem; }
|
|
248
|
+
@media (max-width: 700px) { .page { padding: 1rem .5rem; } .page-header, .toolbar { align-items: flex-start; flex-direction: column; }
|
|
249
|
+
.toolbar-actions { flex-wrap: wrap; } .status { text-align: left; } }
|
|
250
|
+
</style>
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
app = serve(
|
|
254
|
+
page,
|
|
255
|
+
packages=[package],
|
|
256
|
+
routes=[
|
|
257
|
+
Route("/api/table/stream", row_stream),
|
|
258
|
+
Route("/api/table/{action}", table_action, methods=["POST"]),
|
|
259
|
+
],
|
|
260
|
+
background=[stream_rows()],
|
|
261
|
+
store={"action_result": {"body": {"message": "Actions round-trip through Python"}}},
|
|
262
|
+
head=styles,
|
|
263
|
+
title="spaday-regular-table example",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
uvicorn.run(app, host="127.0.0.1", port=8014)
|