pyclay 1.0.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.
- pyclay/__init__.py +335 -0
- pyclay/_cli.py +153 -0
- pyclay/_renderer.py +1325 -0
- pyclay/_runtime.py +70 -0
- pyclay/_server.py +95 -0
- pyclay-1.0.0.dist-info/METADATA +159 -0
- pyclay-1.0.0.dist-info/RECORD +10 -0
- pyclay-1.0.0.dist-info/WHEEL +4 -0
- pyclay-1.0.0.dist-info/entry_points.txt +2 -0
- pyclay-1.0.0.dist-info/licenses/LICENSE +203 -0
pyclay/__init__.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyclay – public API
|
|
3
|
+
====================
|
|
4
|
+
Every function appends a component dict to the runtime tree.
|
|
5
|
+
An optional ``style`` dict lets callers control CSS per-element.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from pyclay import _runtime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Page config
|
|
15
|
+
|
|
16
|
+
_VALID_THEMES = {"ivory", "nebula", "arctic", "obsidian"}
|
|
17
|
+
|
|
18
|
+
def page_config(*, title="My App", theme="ivory", favicon="", custom_css=""):
|
|
19
|
+
"""Set page-level settings. Call once, at the top of your script.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
title: Browser tab title.
|
|
23
|
+
theme: ``"ivory"``, ``"nebula"``, ``"arctic"``, or ``"obsidian"``.
|
|
24
|
+
favicon: URL to a favicon image (optional).
|
|
25
|
+
custom_css: Path or URL to a custom CSS file (optional).
|
|
26
|
+
"""
|
|
27
|
+
assert theme in _VALID_THEMES, f"theme must be one of {_VALID_THEMES}"
|
|
28
|
+
_runtime.set_page_config(title=title, theme=theme, favicon=favicon, custom_css=custom_css)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def page(name):
|
|
32
|
+
"""Switch context to build a new page. Subsequent elements belong to this page.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
name: Unique page identifier/label (e.g. "Docs" or "Components").
|
|
36
|
+
"""
|
|
37
|
+
_runtime.set_current_page(name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Text
|
|
41
|
+
|
|
42
|
+
def heading(text, level=1, *, style=None):
|
|
43
|
+
assert 1 <= level <= 6, "Heading level must be 1-6"
|
|
44
|
+
_runtime.add({"type": "heading", "text": text, "level": level, "style": style})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def text(content, *, style=None):
|
|
48
|
+
_runtime.add({"type": "paragraph", "text": content, "style": style})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def bold(content, *, style=None):
|
|
52
|
+
_runtime.add({"type": "bold", "text": content, "style": style})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def italic(content, *, style=None):
|
|
56
|
+
_runtime.add({"type": "italic", "text": content, "style": style})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def blockquote(content, *, style=None):
|
|
60
|
+
_runtime.add({"type": "blockquote", "text": content, "style": style})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def kbd(text_content):
|
|
64
|
+
"""Render a keyboard-key style element."""
|
|
65
|
+
_runtime.add({"type": "kbd", "text": text_content})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Lists
|
|
69
|
+
|
|
70
|
+
def bullet_list(items, *, style=None):
|
|
71
|
+
assert isinstance(items, list), "bullet_list expects a list"
|
|
72
|
+
_runtime.add({"type": "bullet_list", "items": items, "style": style})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def ordered_list(items, *, style=None):
|
|
76
|
+
assert isinstance(items, list), "ordered_list expects a list"
|
|
77
|
+
_runtime.add({"type": "ordered_list", "items": items, "style": style})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Media / embeds
|
|
81
|
+
|
|
82
|
+
def image(src, *, alt="", width=None, height=None, style=None):
|
|
83
|
+
_runtime.add({"type": "image", "src": src, "alt": alt, "width": width, "height": height, "style": style})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def video(src, *, autoplay=False, loop=False, controls=True, width=None, height=None, style=None):
|
|
87
|
+
"""Render a video element.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
src: URL or path to the video (e.g. "assets/video.mp4").
|
|
91
|
+
autoplay: If True, play automatically (will be muted for browser compliance).
|
|
92
|
+
loop: If True, play in a loop.
|
|
93
|
+
controls: If True, display playback controls.
|
|
94
|
+
width: Optional width of the video.
|
|
95
|
+
height: Optional height of the video.
|
|
96
|
+
style: Optional style overrides dictionary.
|
|
97
|
+
"""
|
|
98
|
+
_runtime.add({
|
|
99
|
+
"type": "video",
|
|
100
|
+
"src": src,
|
|
101
|
+
"autoplay": autoplay,
|
|
102
|
+
"loop": loop,
|
|
103
|
+
"controls": controls,
|
|
104
|
+
"width": width,
|
|
105
|
+
"height": height,
|
|
106
|
+
"style": style
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def link(text, href, *, style=None):
|
|
112
|
+
_runtime.add({"type": "link", "text": text, "href": href, "style": style})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def code_block(code, *, language="", style=None):
|
|
116
|
+
_runtime.add({"type": "code_block", "code": code, "language": language, "style": style})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def html_raw(markup):
|
|
120
|
+
"""Inject arbitrary HTML. Use with care."""
|
|
121
|
+
_runtime.add({"type": "raw", "html": markup})
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Decorative
|
|
125
|
+
|
|
126
|
+
def divider(*, style=None):
|
|
127
|
+
_runtime.add({"type": "divider", "style": style})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def spacer(height="1rem"):
|
|
131
|
+
_runtime.add({"type": "spacer", "height": height})
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Data display
|
|
135
|
+
|
|
136
|
+
def table(headers, rows, *, style=None):
|
|
137
|
+
"""Render an HTML table.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
headers: List of column header strings.
|
|
141
|
+
rows: List of rows, each row a list of cell values.
|
|
142
|
+
"""
|
|
143
|
+
_runtime.add({"type": "table", "headers": headers, "rows": rows, "style": style})
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def metric(label, value, *, delta=None, delta_color=None, style=None):
|
|
147
|
+
"""Streamlit-style metric card.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
label: Metric title.
|
|
151
|
+
value: Primary value to display.
|
|
152
|
+
delta: Optional delta string (e.g. "+12%").
|
|
153
|
+
delta_color: ``"green"`` (default for positive) or ``"red"``.
|
|
154
|
+
"""
|
|
155
|
+
_runtime.add({
|
|
156
|
+
"type": "metric",
|
|
157
|
+
"label": label,
|
|
158
|
+
"value": value,
|
|
159
|
+
"delta": delta,
|
|
160
|
+
"delta_color": delta_color,
|
|
161
|
+
"style": style,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def progress_bar(value, *, max_value=100, label="", style=None):
|
|
166
|
+
_runtime.add({
|
|
167
|
+
"type": "progress_bar",
|
|
168
|
+
"value": value,
|
|
169
|
+
"max": max_value,
|
|
170
|
+
"label": label,
|
|
171
|
+
"style": style,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# UI elements
|
|
176
|
+
|
|
177
|
+
def badge(text, *, color="#6366f1", style=None):
|
|
178
|
+
_runtime.add({"type": "badge", "text": text, "color": color, "style": style})
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def alert(text, *, variant="info", style=None):
|
|
182
|
+
"""Alert box. ``variant`` is one of: info, success, warning, error."""
|
|
183
|
+
_runtime.add({"type": "alert", "text": text, "variant": variant, "style": style})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def button(label, *, style=None):
|
|
187
|
+
"""Visual-only styled button (no click handler)."""
|
|
188
|
+
_runtime.add({"type": "button", "label": label, "style": style})
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def link_button(label, href, *, style=None):
|
|
192
|
+
"""An anchor element styled as a button. Navigates to *href* on click."""
|
|
193
|
+
_runtime.add({"type": "link_button", "label": label, "href": href, "style": style})
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def card(title="", body="", *, style=None):
|
|
197
|
+
"""A bordered card container. Accepts plain text title/body."""
|
|
198
|
+
_runtime.add({"type": "card", "title": title, "body": body, "style": style})
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Collapsible / Tabs
|
|
202
|
+
|
|
203
|
+
def accordion(items, *, style=None):
|
|
204
|
+
"""Collapsible accordion using ``<details>/<summary>``.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
items: List of ``{"title": "...", "content": "..."}`` dicts.
|
|
208
|
+
"""
|
|
209
|
+
assert isinstance(items, list), "accordion expects a list of items"
|
|
210
|
+
_runtime.add({"type": "accordion", "items": items, "style": style})
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def tabs(labels, *, style=None):
|
|
214
|
+
"""Begin a tabbed section. Follow with ``tab_panel()`` context managers.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
labels: List of tab label strings, one per panel.
|
|
218
|
+
"""
|
|
219
|
+
assert isinstance(labels, list) and len(labels) >= 1, "tabs expects a list of labels"
|
|
220
|
+
comp = {"type": "tabs", "labels": labels, "panels": [], "style": style}
|
|
221
|
+
_runtime.add(comp)
|
|
222
|
+
_runtime._tabs_stack.append(comp)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@contextmanager
|
|
226
|
+
def tab_panel(*, style=None):
|
|
227
|
+
"""Context manager for a single tab panel inside a ``tabs()`` block."""
|
|
228
|
+
assert _runtime._tabs_stack, "tab_panel() must be used after a tabs() call"
|
|
229
|
+
panel = {"type": "container", "children": [], "style": style}
|
|
230
|
+
_runtime._tabs_stack[-1]["panels"].append(panel)
|
|
231
|
+
_runtime._container_stack.append(panel["children"])
|
|
232
|
+
try:
|
|
233
|
+
yield
|
|
234
|
+
finally:
|
|
235
|
+
_runtime._container_stack.pop()
|
|
236
|
+
# Pop tabs_stack when all panels are defined
|
|
237
|
+
if len(_runtime._tabs_stack[-1]["panels"]) >= len(_runtime._tabs_stack[-1]["labels"]):
|
|
238
|
+
_runtime._tabs_stack.pop()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Navbar & Footer
|
|
242
|
+
|
|
243
|
+
_VALID_NAV_VARIANTS = {"simple", "centered"}
|
|
244
|
+
_VALID_FOOTER_VARIANTS = {"simple", "columns"}
|
|
245
|
+
|
|
246
|
+
def navbar(title="", links=None, *, variant="simple", style=None):
|
|
247
|
+
"""Add a navigation bar to the top of the page.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
title: Brand / logo text.
|
|
251
|
+
links: List of ``{"text": "...", "href": "..."}`` dicts.
|
|
252
|
+
variant: ``"simple"`` (logo left, links right) or
|
|
253
|
+
``"centered"`` (logo centred, links below).
|
|
254
|
+
"""
|
|
255
|
+
assert variant in _VALID_NAV_VARIANTS, f"navbar variant must be one of {_VALID_NAV_VARIANTS}"
|
|
256
|
+
_runtime.add({
|
|
257
|
+
"type": "navbar",
|
|
258
|
+
"title": title,
|
|
259
|
+
"links": links or [],
|
|
260
|
+
"variant": variant,
|
|
261
|
+
"style": style,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def footer(text="", links=None, *, variant="simple", columns_data=None, style=None):
|
|
266
|
+
"""Add a footer to the bottom of the page.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
text: Main footer text (copyright line, etc.).
|
|
270
|
+
links: List of ``{"text": "...", "href": "..."}`` dicts.
|
|
271
|
+
variant: ``"simple"`` (single line) or ``"columns"`` (multi-column).
|
|
272
|
+
columns_data: For ``"columns"`` variant only -- list of
|
|
273
|
+
``{"heading": "...", "links": [{"text": ..., "href": ...}, ...]}`` dicts.
|
|
274
|
+
"""
|
|
275
|
+
assert variant in _VALID_FOOTER_VARIANTS, f"footer variant must be one of {_VALID_FOOTER_VARIANTS}"
|
|
276
|
+
_runtime.add({
|
|
277
|
+
"type": "footer",
|
|
278
|
+
"text": text,
|
|
279
|
+
"links": links or [],
|
|
280
|
+
"variant": variant,
|
|
281
|
+
"columns_data": columns_data or [],
|
|
282
|
+
"style": style,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# Layout: container & columns
|
|
287
|
+
|
|
288
|
+
@contextmanager
|
|
289
|
+
def container(*, style=None):
|
|
290
|
+
"""Context manager -- wraps children in a styled ``<div>``."""
|
|
291
|
+
comp = {"type": "container", "children": [], "style": style}
|
|
292
|
+
_runtime.push_container(comp)
|
|
293
|
+
try:
|
|
294
|
+
yield
|
|
295
|
+
finally:
|
|
296
|
+
_runtime.pop_container()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@contextmanager
|
|
300
|
+
def columns(ratios=None, *, gap="1rem", style=None):
|
|
301
|
+
"""Context manager -- CSS-grid columns.
|
|
302
|
+
|
|
303
|
+
``ratios`` is a list of relative widths, e.g. ``[1, 2]`` -> ``1fr 2fr``.
|
|
304
|
+
Use nested ``column()`` calls inside.
|
|
305
|
+
"""
|
|
306
|
+
if ratios is None:
|
|
307
|
+
ratios = [1, 1]
|
|
308
|
+
template = " ".join(f"{r}fr" for r in ratios)
|
|
309
|
+
merged = style or {}
|
|
310
|
+
merged = {
|
|
311
|
+
"display": "grid",
|
|
312
|
+
"grid_template_columns": template,
|
|
313
|
+
"gap": gap,
|
|
314
|
+
**merged,
|
|
315
|
+
}
|
|
316
|
+
comp = {"type": "container", "children": [], "style": merged}
|
|
317
|
+
_runtime.push_container(comp)
|
|
318
|
+
try:
|
|
319
|
+
yield
|
|
320
|
+
finally:
|
|
321
|
+
_runtime.pop_container()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@contextmanager
|
|
325
|
+
def column(*, style=None):
|
|
326
|
+
"""A single column inside a ``columns()`` block."""
|
|
327
|
+
merged = style or {}
|
|
328
|
+
merged = {"min_width": "0", **merged}
|
|
329
|
+
comp = {"type": "container", "children": [], "style": merged}
|
|
330
|
+
_runtime.push_container(comp)
|
|
331
|
+
try:
|
|
332
|
+
yield
|
|
333
|
+
finally:
|
|
334
|
+
_runtime.pop_container()
|
|
335
|
+
|
pyclay/_cli.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import traceback
|
|
5
|
+
from watchdog.observers import Observer
|
|
6
|
+
from watchdog.events import FileSystemEventHandler
|
|
7
|
+
from pyclay import _runtime, _renderer, _server
|
|
8
|
+
|
|
9
|
+
def run_script(script_path):
|
|
10
|
+
_runtime.reset()
|
|
11
|
+
try:
|
|
12
|
+
with open(script_path, encoding="utf-8") as f:
|
|
13
|
+
code = f.read()
|
|
14
|
+
exec(code, {"__name__": "__main__"})
|
|
15
|
+
html = _renderer.render_page()
|
|
16
|
+
return html
|
|
17
|
+
except Exception:
|
|
18
|
+
# Format a friendly error page instead of crashing
|
|
19
|
+
tb = traceback.format_exc()
|
|
20
|
+
print(f"\n{'='*60}")
|
|
21
|
+
print(f" ERROR in {script_path}")
|
|
22
|
+
print(f"{'='*60}")
|
|
23
|
+
print(tb)
|
|
24
|
+
print(f"{'='*60}\n")
|
|
25
|
+
error_html = f"""<!DOCTYPE html>
|
|
26
|
+
<html><head><meta charset="UTF-8"><title>pyclay – Error</title>
|
|
27
|
+
<style>
|
|
28
|
+
body {{ font-family: 'Inter', -apple-system, sans-serif; background: #0f0f17; color: #e4e4f0; margin: 0; padding: 2rem; }}
|
|
29
|
+
.error-box {{ background: #1a1a2e; border: 1px solid #ef4444; border-radius: 10px; padding: 2rem; max-width: 800px; margin: 2rem auto; }}
|
|
30
|
+
h1 {{ color: #ef4444; margin-top: 0; font-size: 1.4rem; }}
|
|
31
|
+
pre {{ background: #111; padding: 1.2rem; border-radius: 8px; overflow-x: auto; font-size: 0.85rem; line-height: 1.6; color: #fca5a5; }}
|
|
32
|
+
p {{ color: #888; font-size: 0.9rem; }}
|
|
33
|
+
</style></head>
|
|
34
|
+
<body>
|
|
35
|
+
<div class="error-box">
|
|
36
|
+
<h1>\u26a0 Build Error</h1>
|
|
37
|
+
<pre>{_renderer._esc(tb)}</pre>
|
|
38
|
+
<p>Fix the error and save your file - the page will auto-reload.</p>
|
|
39
|
+
</div>
|
|
40
|
+
</body></html>"""
|
|
41
|
+
return error_html
|
|
42
|
+
|
|
43
|
+
class _ReloadHandler(FileSystemEventHandler):
|
|
44
|
+
def __init__(self, script_path):
|
|
45
|
+
self.script_path = script_path
|
|
46
|
+
|
|
47
|
+
def on_modified(self, event):
|
|
48
|
+
if event.src_path.endswith(self.script_path.lstrip("./")):
|
|
49
|
+
print(f"Change detected - reloading {self.script_path}")
|
|
50
|
+
html = run_script(self.script_path)
|
|
51
|
+
_server.update_html(html) # push new HTML, bump timestamp
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
if len(sys.argv) < 2:
|
|
55
|
+
print("Usage: pyclay <command> [args]")
|
|
56
|
+
print("")
|
|
57
|
+
print("Commands:")
|
|
58
|
+
print(" run <script.py> Start dev server with hot reload")
|
|
59
|
+
print(" build <script.py> [--out DIR] Export static HTML")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
command = sys.argv[1]
|
|
63
|
+
|
|
64
|
+
if command == "build":
|
|
65
|
+
if len(sys.argv) < 3:
|
|
66
|
+
print("Usage: pyclay build <script.py> [--out DIR]")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
script_path = sys.argv[2]
|
|
70
|
+
|
|
71
|
+
# Parse --out flag
|
|
72
|
+
out_dir = "dist"
|
|
73
|
+
if "--out" in sys.argv:
|
|
74
|
+
idx = sys.argv.index("--out")
|
|
75
|
+
if idx + 1 < len(sys.argv):
|
|
76
|
+
out_dir = sys.argv[idx + 1]
|
|
77
|
+
|
|
78
|
+
_runtime.reset()
|
|
79
|
+
try:
|
|
80
|
+
with open(script_path, encoding="utf-8") as f:
|
|
81
|
+
code = f.read()
|
|
82
|
+
exec(code, {"__name__": "__main__"})
|
|
83
|
+
html = _renderer.render_page()
|
|
84
|
+
except Exception:
|
|
85
|
+
print(f"\nBuild failed for {script_path}:")
|
|
86
|
+
traceback.print_exc()
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
# Strip hot-reload JS from production build
|
|
90
|
+
import re
|
|
91
|
+
html = re.sub(r'<script>\s*let _pc_last.*?</script>', '', html, flags=re.DOTALL)
|
|
92
|
+
|
|
93
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
94
|
+
out_path = os.path.join(out_dir, "index.html")
|
|
95
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
96
|
+
f.write(html)
|
|
97
|
+
|
|
98
|
+
# Copy assets/ directory to output directory if it exists
|
|
99
|
+
if os.path.exists("assets"):
|
|
100
|
+
import shutil
|
|
101
|
+
out_assets_dir = os.path.join(out_dir, "assets")
|
|
102
|
+
try:
|
|
103
|
+
if os.path.exists(out_assets_dir):
|
|
104
|
+
shutil.rmtree(out_assets_dir)
|
|
105
|
+
shutil.copytree("assets", out_assets_dir)
|
|
106
|
+
print(f" Assets: copied assets/ -> {out_assets_dir}")
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f" Warning: failed to copy assets folder: {e}")
|
|
109
|
+
|
|
110
|
+
abs_path = os.path.abspath(out_path)
|
|
111
|
+
print(f"Built successfully -> {abs_path}")
|
|
112
|
+
print(f" Size: {len(html):,} bytes")
|
|
113
|
+
|
|
114
|
+
elif command == "run":
|
|
115
|
+
if len(sys.argv) < 3:
|
|
116
|
+
print("Usage: pyclay run <script.py> [--port PORT]")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
script_path = sys.argv[2]
|
|
120
|
+
|
|
121
|
+
# Parse --port flag
|
|
122
|
+
port = 8501
|
|
123
|
+
if "--port" in sys.argv:
|
|
124
|
+
idx = sys.argv.index("--port")
|
|
125
|
+
if idx + 1 < len(sys.argv):
|
|
126
|
+
port = int(sys.argv[idx + 1])
|
|
127
|
+
|
|
128
|
+
# first render
|
|
129
|
+
html = run_script(script_path)
|
|
130
|
+
server = _server.serve(html, port=port)
|
|
131
|
+
|
|
132
|
+
# watch for file changes
|
|
133
|
+
handler = _ReloadHandler(script_path)
|
|
134
|
+
observer = Observer()
|
|
135
|
+
observer.schedule(handler, path=".", recursive=True)
|
|
136
|
+
observer.start()
|
|
137
|
+
|
|
138
|
+
print("Watching for changes... (Ctrl+C to stop)")
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
while True:
|
|
142
|
+
time.sleep(1)
|
|
143
|
+
except KeyboardInterrupt:
|
|
144
|
+
print("\nStopped.")
|
|
145
|
+
observer.stop()
|
|
146
|
+
server.shutdown()
|
|
147
|
+
|
|
148
|
+
observer.join()
|
|
149
|
+
|
|
150
|
+
else:
|
|
151
|
+
print(f"Unknown command: {command}")
|
|
152
|
+
print("Available commands: run, build")
|
|
153
|
+
sys.exit(1)
|