danvas 0.5.1__py3-none-win_amd64.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.
- danvas-0.5.1.data/purelib/danvas/__init__.py +105 -0
- danvas-0.5.1.data/purelib/danvas/_bin/danvasd.exe +0 -0
- danvas-0.5.1.data/purelib/danvas/_dialin.py +92 -0
- danvas-0.5.1.data/purelib/danvas/_events.py +50 -0
- danvas-0.5.1.data/purelib/danvas/_factories.py +288 -0
- danvas-0.5.1.data/purelib/danvas/_flags.py +76 -0
- danvas-0.5.1.data/purelib/danvas/_hotreload_monitor.py +27 -0
- danvas-0.5.1.data/purelib/danvas/_layout.py +571 -0
- danvas-0.5.1.data/purelib/danvas/_ledger.py +173 -0
- danvas-0.5.1.data/purelib/danvas/_livepatch.py +267 -0
- danvas-0.5.1.data/purelib/danvas/_net.py +52 -0
- danvas-0.5.1.data/purelib/danvas/_onedit.py +169 -0
- danvas-0.5.1.data/purelib/danvas/_procown.py +119 -0
- danvas-0.5.1.data/purelib/danvas/_protocol.py +163 -0
- danvas-0.5.1.data/purelib/danvas/_trace.py +255 -0
- danvas-0.5.1.data/purelib/danvas/arrow.py +132 -0
- danvas-0.5.1.data/purelib/danvas/autopanel.py +367 -0
- danvas-0.5.1.data/purelib/danvas/bake.py +325 -0
- danvas-0.5.1.data/purelib/danvas/bridge.py +2600 -0
- danvas-0.5.1.data/purelib/danvas/canvas.py +2883 -0
- danvas-0.5.1.data/purelib/danvas/canvas.pyi +378 -0
- danvas-0.5.1.data/purelib/danvas/components/__init__.py +51 -0
- danvas-0.5.1.data/purelib/danvas/components/_doc.py +52 -0
- danvas-0.5.1.data/purelib/danvas/components/_jsx.py +23 -0
- danvas-0.5.1.data/purelib/danvas/components/_routing.py +271 -0
- danvas-0.5.1.data/purelib/danvas/components/_theme.py +74 -0
- danvas-0.5.1.data/purelib/danvas/components/audio.py +92 -0
- danvas-0.5.1.data/purelib/danvas/components/base.py +1320 -0
- danvas-0.5.1.data/purelib/danvas/components/button.py +94 -0
- danvas-0.5.1.data/purelib/danvas/components/chat.py +88 -0
- danvas-0.5.1.data/purelib/danvas/components/custom.py +354 -0
- danvas-0.5.1.data/purelib/danvas/components/download.py +183 -0
- danvas-0.5.1.data/purelib/danvas/components/filebrowser.py +246 -0
- danvas-0.5.1.data/purelib/danvas/components/histogram.py +92 -0
- danvas-0.5.1.data/purelib/danvas/components/image.py +197 -0
- danvas-0.5.1.data/purelib/danvas/components/inspector.py +701 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/audio.jsx +82 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/button.jsx +12 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/chat.jsx +100 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/download.jsx +32 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/file_browser.jsx +35 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/image.jsx +11 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/inspector.jsx +233 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/label.jsx +11 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/live_plot.jsx +71 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/markdown.jsx +11 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/plot.jsx +44 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/slider.jsx +61 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/table.jsx +313 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/text_field.jsx +35 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/toggle.jsx +22 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/upload.jsx +70 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/video.jsx +22 -0
- danvas-0.5.1.data/purelib/danvas/components/jsx/webview.jsx +11 -0
- danvas-0.5.1.data/purelib/danvas/components/label.py +72 -0
- danvas-0.5.1.data/purelib/danvas/components/liveplot.py +336 -0
- danvas-0.5.1.data/purelib/danvas/components/markdown.py +173 -0
- danvas-0.5.1.data/purelib/danvas/components/model3d.py +2043 -0
- danvas-0.5.1.data/purelib/danvas/components/plot.py +52 -0
- danvas-0.5.1.data/purelib/danvas/components/react.py +631 -0
- danvas-0.5.1.data/purelib/danvas/components/slider.py +110 -0
- danvas-0.5.1.data/purelib/danvas/components/table.py +509 -0
- danvas-0.5.1.data/purelib/danvas/components/textfield.py +108 -0
- danvas-0.5.1.data/purelib/danvas/components/toggle.py +82 -0
- danvas-0.5.1.data/purelib/danvas/components/upload.py +236 -0
- danvas-0.5.1.data/purelib/danvas/components/video.py +89 -0
- danvas-0.5.1.data/purelib/danvas/components/webview.py +85 -0
- danvas-0.5.1.data/purelib/danvas/dispatch.py +631 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-C-brsJOE.js +175 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-C-brsJOE.js.br +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-C-brsJOE.js.gz +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-DA1g5i2i.css +1 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-DA1g5i2i.css.br +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-DA1g5i2i.css.gz +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-sRVW3Fwz.js +14 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-sRVW3Fwz.js.br +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-sRVW3Fwz.js.gz +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/plotly-cartesian.min-Dzd4OOBc.js +127 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/plotly-cartesian.min-Dzd4OOBc.js.br +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/plotly-cartesian.min-Dzd4OOBc.js.gz +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/index.html +39 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/index.html.br +0 -0
- danvas-0.5.1.data/purelib/danvas/frontend/dist/index.html.gz +0 -0
- danvas-0.5.1.data/purelib/danvas/hotreload.py +415 -0
- danvas-0.5.1.data/purelib/danvas/kernel.py +208 -0
- danvas-0.5.1.data/purelib/danvas/merge.py +1569 -0
- danvas-0.5.1.data/purelib/danvas/py.typed +0 -0
- danvas-0.5.1.data/purelib/danvas/remote.py +826 -0
- danvas-0.5.1.data/purelib/danvas/serial.py +196 -0
- danvas-0.5.1.data/purelib/danvas/server.py +708 -0
- danvas-0.5.1.data/purelib/danvas/shapes.py +480 -0
- danvas-0.5.1.data/purelib/danvas/source.py +370 -0
- danvas-0.5.1.data/purelib/danvas/templates/components.json +960 -0
- danvas-0.5.1.data/purelib/danvas/tunnel.py +186 -0
- danvas-0.5.1.dist-info/METADATA +1659 -0
- danvas-0.5.1.dist-info/RECORD +107 -0
- danvas-0.5.1.dist-info/WHEEL +5 -0
- danvas-0.5.1.dist-info/licenses/LICENSE +661 -0
- danvas-0.5.1.dist-info/licenses/THIRD_PARTY_LICENSES.md +54 -0
- danvas-0.5.1.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""danvas: a browser-based spatial canvas driven entirely from Python."""
|
|
2
|
+
|
|
3
|
+
from .autopanel import autopanel
|
|
4
|
+
from .canvas import Canvas
|
|
5
|
+
from .components import (
|
|
6
|
+
AudioFeed,
|
|
7
|
+
BaseComponent,
|
|
8
|
+
Button,
|
|
9
|
+
Chat,
|
|
10
|
+
Custom,
|
|
11
|
+
Download,
|
|
12
|
+
FileBrowser,
|
|
13
|
+
Histogram,
|
|
14
|
+
Image,
|
|
15
|
+
Inspector,
|
|
16
|
+
Label,
|
|
17
|
+
LivePlot,
|
|
18
|
+
Markdown,
|
|
19
|
+
Plot,
|
|
20
|
+
React,
|
|
21
|
+
Slider,
|
|
22
|
+
Table,
|
|
23
|
+
TextField,
|
|
24
|
+
Toggle,
|
|
25
|
+
Upload,
|
|
26
|
+
UploadedFile,
|
|
27
|
+
VideoFeed,
|
|
28
|
+
Model3D,
|
|
29
|
+
WebView,
|
|
30
|
+
)
|
|
31
|
+
from .dispatch import panel_for
|
|
32
|
+
from .shapes import (
|
|
33
|
+
BaseShape, DrawingShape,
|
|
34
|
+
Geo, Text, Note, Draw, Highlight, Line, Frame,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Single source of truth is the installed package metadata (pyproject `version`).
|
|
38
|
+
# Fall back to 0.0.0 when running straight from a source tree that was never
|
|
39
|
+
# installed, so the import never fails just to read a version string.
|
|
40
|
+
try:
|
|
41
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
42
|
+
|
|
43
|
+
__version__ = _version("danvas")
|
|
44
|
+
except PackageNotFoundError:
|
|
45
|
+
__version__ = "0.0.0"
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"Canvas",
|
|
49
|
+
"autopanel",
|
|
50
|
+
"panel_for",
|
|
51
|
+
"Merge",
|
|
52
|
+
"SourceClient",
|
|
53
|
+
"Slider",
|
|
54
|
+
"Button",
|
|
55
|
+
"Label",
|
|
56
|
+
"VideoFeed",
|
|
57
|
+
"AudioFeed",
|
|
58
|
+
"Chat",
|
|
59
|
+
"Custom",
|
|
60
|
+
"Download",
|
|
61
|
+
"FileBrowser",
|
|
62
|
+
"React",
|
|
63
|
+
"Markdown",
|
|
64
|
+
"Image",
|
|
65
|
+
"Table",
|
|
66
|
+
"TextField",
|
|
67
|
+
"Toggle",
|
|
68
|
+
"Plot",
|
|
69
|
+
"LivePlot",
|
|
70
|
+
"Histogram",
|
|
71
|
+
"Inspector",
|
|
72
|
+
"Upload",
|
|
73
|
+
"UploadedFile",
|
|
74
|
+
"Model3D",
|
|
75
|
+
"WebView",
|
|
76
|
+
"BaseComponent",
|
|
77
|
+
"BaseShape",
|
|
78
|
+
"DrawingShape",
|
|
79
|
+
"Geo",
|
|
80
|
+
"Text",
|
|
81
|
+
"Note",
|
|
82
|
+
"Draw",
|
|
83
|
+
"Highlight",
|
|
84
|
+
"Line",
|
|
85
|
+
"Frame",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def __getattr__(name):
|
|
90
|
+
# Lazily expose ``danvas.Merge`` / ``danvas.SourceClient`` so importing the
|
|
91
|
+
# package doesn't pull in the websocket *client* stack (only the merge
|
|
92
|
+
# aggregator and the dial-in source client need it).
|
|
93
|
+
if name == "Merge":
|
|
94
|
+
from .merge import Merge
|
|
95
|
+
return Merge
|
|
96
|
+
if name == "SourceClient":
|
|
97
|
+
from .source import SourceClient
|
|
98
|
+
return SourceClient
|
|
99
|
+
if name == "connect":
|
|
100
|
+
from .remote import connect
|
|
101
|
+
return connect
|
|
102
|
+
if name == "RemoteCanvas":
|
|
103
|
+
from .remote import RemoteCanvas
|
|
104
|
+
return RemoteCanvas
|
|
105
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Client-side dial-in helpers: normalise a source spec, probe it for auth, and
|
|
2
|
+
run its password flow.
|
|
3
|
+
|
|
4
|
+
These are pure-stdlib (http.client / urllib) and used by BOTH a dial-in source
|
|
5
|
+
(``danvas.SourceClient`` / ``danvas.connect``) and the merge hub that dials out.
|
|
6
|
+
They live here — not in merge.py, which pulls the FastAPI server stack — so a
|
|
7
|
+
light client/broker install can dial into a canvas without that dependency. One
|
|
8
|
+
definition, every caller.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from http.client import HTTPConnection, HTTPSConnection
|
|
13
|
+
from urllib.parse import quote, urlsplit
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_source(spec):
|
|
17
|
+
"""Normalise a source spec to ``(ws_uri, http_parts, label)``.
|
|
18
|
+
|
|
19
|
+
``http_parts`` is ``(scheme, host, port, is_tls)`` for the source's HTTP
|
|
20
|
+
origin -- used to probe whether the canvas is password-protected and to run
|
|
21
|
+
its ``/__auth__`` login flow. Accepts a bare port (``8001`` / ``":8001"``),
|
|
22
|
+
a ``host:port``, or a full URL (``https://x.loca.lt`` / ``wss://host/ws``).
|
|
23
|
+
"""
|
|
24
|
+
if isinstance(spec, int):
|
|
25
|
+
ws_uri, label = f"ws://localhost:{spec}/ws", f"localhost:{spec}"
|
|
26
|
+
else:
|
|
27
|
+
text = str(spec).strip()
|
|
28
|
+
if "://" in text:
|
|
29
|
+
scheme, _, rest = text.partition("://")
|
|
30
|
+
scheme = {"http": "ws", "https": "wss"}.get(scheme.lower(), scheme.lower())
|
|
31
|
+
rest = rest.rstrip("/")
|
|
32
|
+
label = rest.split("/", 1)[0]
|
|
33
|
+
if not rest.endswith("/ws"):
|
|
34
|
+
rest += "/ws"
|
|
35
|
+
ws_uri = f"{scheme}://{rest}"
|
|
36
|
+
else:
|
|
37
|
+
if text.startswith(":"):
|
|
38
|
+
text = "localhost" + text
|
|
39
|
+
if ":" in text:
|
|
40
|
+
host, _, port = text.rpartition(":")
|
|
41
|
+
host = host or "localhost"
|
|
42
|
+
else:
|
|
43
|
+
host, port = "localhost", text
|
|
44
|
+
ws_uri, label = f"ws://{host}:{int(port)}/ws", f"{host}:{port}"
|
|
45
|
+
u = urlsplit(ws_uri)
|
|
46
|
+
tls = u.scheme == "wss"
|
|
47
|
+
host = u.hostname or "localhost"
|
|
48
|
+
port = u.port or (443 if tls else 80)
|
|
49
|
+
http_parts = ("https" if tls else "http", host, port, tls)
|
|
50
|
+
return ws_uri, http_parts, label
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _http_conn(http_parts, timeout=6):
|
|
54
|
+
_scheme, host, port, tls = http_parts
|
|
55
|
+
cls = HTTPSConnection if tls else HTTPConnection
|
|
56
|
+
return cls(host, port, timeout=timeout)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _probe_source(http_parts):
|
|
60
|
+
"""Classify a source (blocking; run in an executor): ``"open"`` (reachable, no
|
|
61
|
+
auth), ``"auth"`` (password-protected -> HTTP 401), or ``"offline"``."""
|
|
62
|
+
try:
|
|
63
|
+
conn = _http_conn(http_parts)
|
|
64
|
+
conn.request("GET", "/")
|
|
65
|
+
resp = conn.getresponse()
|
|
66
|
+
resp.read()
|
|
67
|
+
conn.close()
|
|
68
|
+
return "auth" if resp.status == 401 else "open"
|
|
69
|
+
except Exception:
|
|
70
|
+
return "offline"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _authenticate(http_parts, password):
|
|
74
|
+
"""Run a source's ``/__auth__`` password flow (blocking; run in an executor).
|
|
75
|
+
|
|
76
|
+
Returns the ``pc_session`` cookie token on success, or ``None`` on a wrong
|
|
77
|
+
password / unreachable host. The canvas replies to a correct password with a
|
|
78
|
+
303 redirect carrying ``Set-Cookie: pc_session=...``.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
conn = _http_conn(http_parts)
|
|
82
|
+
body = "password=" + quote(password or "", safe="")
|
|
83
|
+
conn.request("POST", "/__auth__", body=body,
|
|
84
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
|
85
|
+
resp = conn.getresponse()
|
|
86
|
+
set_cookie = resp.getheader("Set-Cookie") or ""
|
|
87
|
+
resp.read()
|
|
88
|
+
conn.close()
|
|
89
|
+
m = re.search(r"pc_session=([^;]+)", set_cookie)
|
|
90
|
+
return m.group(1) if m else None
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""canvas.emit / canvas.on_event: the backend's universal trigger.
|
|
2
|
+
|
|
3
|
+
The frontend already has one universal trigger: a panel calls
|
|
4
|
+
``canvas.send({...})`` and Python handlers fire. This is the same idea
|
|
5
|
+
pointed the other way — *any* Python code (a watchdog thread, a timer, a
|
|
6
|
+
serial reader, an MQTT callback, another handler) calls ``canvas.emit(name,
|
|
7
|
+
data)`` and every ``@canvas.on_event(name)`` handler fires::
|
|
8
|
+
|
|
9
|
+
@canvas.on_event("part-dropped")
|
|
10
|
+
def _(path):
|
|
11
|
+
viewer.update(path)
|
|
12
|
+
|
|
13
|
+
# anywhere, any thread:
|
|
14
|
+
canvas.emit("part-dropped", "drop/bracket.glb")
|
|
15
|
+
|
|
16
|
+
Emits are funnelled through the same shared dispatch thread as browser
|
|
17
|
+
input, so an inline handler never races an ``on_change`` — an emit is an
|
|
18
|
+
event like any other. And because each event name's handler list is a
|
|
19
|
+
headless peer of a panel's (it *borrows* the component dispatch machinery
|
|
20
|
+
wholesale), handlers get the identical feature set: ``threaded=True``,
|
|
21
|
+
``dedicated=True`` with ``queue="fifo"/"latest"`` backpressure, ``async
|
|
22
|
+
def`` support, an optional trailing ``viewer`` argument (``{}`` for
|
|
23
|
+
backend emits), and visibility in the Inspector's dispatch trace.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .components.base import BaseComponent
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EventChannel:
|
|
30
|
+
"""The handler list for one event name — a headless peer of a panel.
|
|
31
|
+
|
|
32
|
+
Borrows :class:`BaseComponent`'s registration/dispatch methods verbatim
|
|
33
|
+
rather than reimplementing the threaded/dedicated/queue/async branching;
|
|
34
|
+
the attributes below are exactly the state those methods touch.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_register_callback = BaseComponent._register_callback
|
|
38
|
+
_dispatch_callbacks = BaseComponent._dispatch_callbacks
|
|
39
|
+
_accepts_viewer = staticmethod(BaseComponent._accepts_viewer)
|
|
40
|
+
_traced = staticmethod(BaseComponent._traced)
|
|
41
|
+
|
|
42
|
+
def __init__(self, canvas, name):
|
|
43
|
+
self._canvas = canvas
|
|
44
|
+
self.name = name # labels dispatch-trace rows
|
|
45
|
+
self._callbacks = []
|
|
46
|
+
self._dedicated_kernels = {}
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def _bridge(self):
|
|
50
|
+
return self._canvas._bridge
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Component factory methods for :class:`~danvas.canvas.Canvas`
|
|
2
|
+
(``canvas.slider`` / ``button`` / ``react`` / …).
|
|
3
|
+
|
|
4
|
+
Split out of canvas.py: each is a thin wrapper that builds a component and hands
|
|
5
|
+
it to ``Canvas.insert`` via ``_make``. Mixed into Canvas as :class:`_FactoryMixin`,
|
|
6
|
+
so the methods run on the real Canvas instance — ``self.insert`` /
|
|
7
|
+
``self._auto_name`` / ``self._show_seq`` resolve there.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from ._flags import LAYOUT_FLAGS
|
|
15
|
+
from .components import (
|
|
16
|
+
AudioFeed,
|
|
17
|
+
Button,
|
|
18
|
+
Chat,
|
|
19
|
+
Custom,
|
|
20
|
+
Download,
|
|
21
|
+
FileBrowser,
|
|
22
|
+
Histogram,
|
|
23
|
+
Image,
|
|
24
|
+
Inspector,
|
|
25
|
+
Label,
|
|
26
|
+
LivePlot,
|
|
27
|
+
Markdown,
|
|
28
|
+
Plot,
|
|
29
|
+
React,
|
|
30
|
+
Slider,
|
|
31
|
+
Table,
|
|
32
|
+
TextField,
|
|
33
|
+
Toggle,
|
|
34
|
+
Upload,
|
|
35
|
+
VideoFeed,
|
|
36
|
+
Model3D,
|
|
37
|
+
WebView,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from typing_extensions import Unpack
|
|
42
|
+
|
|
43
|
+
from .canvas import Place
|
|
44
|
+
|
|
45
|
+
# Keyword names consumed by ``Canvas.insert`` itself. A factory splits these off
|
|
46
|
+
# and forwards everything else to the component constructor. ``name`` is
|
|
47
|
+
# intentionally absent: it is the component's identity (set in its constructor),
|
|
48
|
+
# not a placement option. The lock/chrome flags come from the shared LAYOUT_FLAGS
|
|
49
|
+
# table; ``queue`` lives here so all factories accept it uniformly.
|
|
50
|
+
_INSERT_KEYS = ("x", "y", "w", "h", "width", "height", "rotation", "queue",
|
|
51
|
+
"below", "above", "right_of", "left_of", "gap",
|
|
52
|
+
"roles", "lock_for", "decorative", *LAYOUT_FLAGS)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _FactoryMixin:
|
|
56
|
+
"""The ``canvas.<component>(...)`` factories. Mixed into :class:`Canvas`."""
|
|
57
|
+
|
|
58
|
+
def _make(self, cls, *args, **kw):
|
|
59
|
+
place = {k: kw.pop(k) for k in _INSERT_KEYS if k in kw}
|
|
60
|
+
return self.insert(cls(*args, **kw), **place)
|
|
61
|
+
|
|
62
|
+
def slider(self, name="slider", min=0, max=100, default=None, step=1,
|
|
63
|
+
on_release=False, label=None, **place: Unpack[Place]):
|
|
64
|
+
"""Insert a :class:`~danvas.Slider`. See :meth:`insert` for ``place``.
|
|
65
|
+
|
|
66
|
+
``step`` sets the granularity and the int-vs-float behaviour (a
|
|
67
|
+
fractional step like ``0.1`` makes it a float slider). ``on_release=True``
|
|
68
|
+
reports only the settled value when the user lets go, instead of every
|
|
69
|
+
value during the drag.
|
|
70
|
+
"""
|
|
71
|
+
return self._make(Slider, name=name, min=min, max=max, default=default,
|
|
72
|
+
step=step, on_release=on_release, label=label, **place)
|
|
73
|
+
|
|
74
|
+
def toggle(self, options, name="toggle", default=None, label=None, **place: Unpack[Place]):
|
|
75
|
+
"""Insert a :class:`~danvas.Toggle`. See :meth:`insert` for ``place``."""
|
|
76
|
+
return self._make(Toggle, options, name=name, default=default, label=label,
|
|
77
|
+
**place)
|
|
78
|
+
|
|
79
|
+
def button(self, name="button", text=None, label=None, **place: Unpack[Place]):
|
|
80
|
+
"""Insert a :class:`~danvas.Button`. See :meth:`insert` for ``place``."""
|
|
81
|
+
return self._make(Button, name=name, text=text, label=label, **place)
|
|
82
|
+
|
|
83
|
+
def label(self, name="label", value="", label=None, **place: Unpack[Place]):
|
|
84
|
+
"""Insert a :class:`~danvas.Label`. See :meth:`insert` for ``place``."""
|
|
85
|
+
return self._make(Label, name=name, value=value, label=label, **place)
|
|
86
|
+
|
|
87
|
+
def video(self, name="video", quality=70, label=None, **place: Unpack[Place]):
|
|
88
|
+
"""Insert a :class:`~danvas.VideoFeed`. See :meth:`insert` for ``place``."""
|
|
89
|
+
return self._make(VideoFeed, name=name, quality=quality, label=label, **place)
|
|
90
|
+
|
|
91
|
+
def audio(self, name="audio", sample_rate=16000, channels=1, label=None, **place: Unpack[Place]):
|
|
92
|
+
"""Insert an :class:`~danvas.AudioFeed`. See :meth:`insert` for ``place``."""
|
|
93
|
+
return self._make(AudioFeed, name=name, sample_rate=sample_rate,
|
|
94
|
+
channels=channels, label=label, **place)
|
|
95
|
+
|
|
96
|
+
def chat(self, name="chat", label=None, **place: Unpack[Place]):
|
|
97
|
+
"""Insert a :class:`~danvas.Chat` panel. See :meth:`insert` for ``place``."""
|
|
98
|
+
return self._make(Chat, name=name, label=label, **place)
|
|
99
|
+
|
|
100
|
+
def custom(self, html=None, path=None, css=None, js=None, name="custom",
|
|
101
|
+
label=None, **place: Unpack[Place]):
|
|
102
|
+
"""Insert a :class:`~danvas.Custom`. See :meth:`insert` for ``place``.
|
|
103
|
+
|
|
104
|
+
``html``/``css``/``js`` may be given as separate strings (e.g. pasted
|
|
105
|
+
from uiverse.io) — they are composed into one document under the hood.
|
|
106
|
+
Size the panel with ``w``/``h`` in ``place``.
|
|
107
|
+
"""
|
|
108
|
+
return self._make(Custom, html=html, path=path, css=css, js=js,
|
|
109
|
+
name=name, label=label, **place)
|
|
110
|
+
|
|
111
|
+
def download(self, name="download", source=None, filename=None, text=None, label=None,
|
|
112
|
+
**place: Unpack[Place]):
|
|
113
|
+
"""Insert a :class:`~danvas.Download` button. See :meth:`insert` for ``place``.
|
|
114
|
+
|
|
115
|
+
Clicking it downloads ``source`` — a file path or ``bytes`` — to the
|
|
116
|
+
viewer's machine. For content generated fresh on each click, omit
|
|
117
|
+
``source`` and register a provider with ``@download.provide``.
|
|
118
|
+
``filename`` sets the saved name (otherwise a path's basename, or the
|
|
119
|
+
panel name, is used). The host code chooses what each click serves, so
|
|
120
|
+
nothing the viewer sends selects a path.
|
|
121
|
+
"""
|
|
122
|
+
return self._make(Download, name=name, source=source, filename=filename,
|
|
123
|
+
text=text, label=label, **place)
|
|
124
|
+
|
|
125
|
+
def upload(self, name="upload", text=None, label=None, dest=None,
|
|
126
|
+
accept=None, multiple=False, max_size=None, **place: Unpack[Place]):
|
|
127
|
+
"""Insert an :class:`~danvas.Upload` panel. See :meth:`insert` for ``place``.
|
|
128
|
+
|
|
129
|
+
A click-or-drop zone that receives a viewer's file into Python; wire it
|
|
130
|
+
with ``@upload.on_upload``. By default the bytes arrive in memory
|
|
131
|
+
(``file.data``); pass ``dest=`` a directory to stream each upload to disk
|
|
132
|
+
instead (``file.path``), which keeps memory flat for large files.
|
|
133
|
+
``accept`` filters the picker (e.g. ``".csv"``), ``multiple=True`` allows
|
|
134
|
+
several at once, and ``max_size`` (bytes) rejects oversized uploads — set
|
|
135
|
+
it on any public/tunneled canvas.
|
|
136
|
+
"""
|
|
137
|
+
return self._make(Upload, name=name, text=text, label=label, dest=dest,
|
|
138
|
+
accept=accept, multiple=multiple, max_size=max_size,
|
|
139
|
+
**place)
|
|
140
|
+
|
|
141
|
+
def file_browser(self, name="filebrowser", root=".", label=None, pattern=None,
|
|
142
|
+
show_hidden=False, **place: Unpack[Place]):
|
|
143
|
+
"""Insert a :class:`~danvas.FileBrowser`. See :meth:`insert` for ``place``.
|
|
144
|
+
|
|
145
|
+
Navigation is confined to ``root``. ``pattern`` (an fnmatch glob like
|
|
146
|
+
``"*.csv"``) filters which files are shown. Size it with ``w``/``h`` in
|
|
147
|
+
``place``.
|
|
148
|
+
"""
|
|
149
|
+
return self._make(FileBrowser, name=name, root=root, label=label,
|
|
150
|
+
pattern=pattern, show_hidden=show_hidden, **place)
|
|
151
|
+
|
|
152
|
+
def react(self, source=None, path=None, jsx=None, css=None, css_path=None,
|
|
153
|
+
name="react", label=None, props=None, scope=None,
|
|
154
|
+
wasm=None, wasm_path=None, **place: Unpack[Place]):
|
|
155
|
+
"""Insert a :class:`~danvas.React` panel — the workhorse for custom UI.
|
|
156
|
+
|
|
157
|
+
``source`` is JSX defining ``function Component(...)`` (or load it from a
|
|
158
|
+
file with ``path=``); alternatively pass just ``jsx`` markup plus optional
|
|
159
|
+
``css`` and the Component wrapper is added under the hood. ``css`` also
|
|
160
|
+
works with ``source=`` (it rides as a ``<style>`` the host renders), so a
|
|
161
|
+
full component can keep its styles in a separate string — or load it from a
|
|
162
|
+
file with ``css_path=`` (the ``css`` twin of ``path=``), so a panel keeps
|
|
163
|
+
both halves in sibling files. Use :meth:`React.from_uiverse` to convert a
|
|
164
|
+
uiverse.io styled-components snippet. ``props`` is the initial props dict; ``scope`` is third-party
|
|
165
|
+
library names (e.g. ``["d3"]``) loaded as ESM and exposed as ``libs``.
|
|
166
|
+
|
|
167
|
+
``wasm`` accepts raw ``.wasm`` bytes; ``wasm_path=`` loads them from a
|
|
168
|
+
file. The module is compiled once in the browser and its exports are
|
|
169
|
+
available as ``await canvas.wasm`` inside the JSX component.
|
|
170
|
+
|
|
171
|
+
Placement, visibility (``roles`` / ``lock_for``), the lock/chrome flags,
|
|
172
|
+
and ``queue`` all flow through ``**place`` — see :meth:`insert` (and the
|
|
173
|
+
:class:`Place` keys your editor now autocompletes).
|
|
174
|
+
"""
|
|
175
|
+
return self._make(React, source=source, path=path, jsx=jsx, css=css,
|
|
176
|
+
css_path=css_path, name=name, label=label, props=props,
|
|
177
|
+
scope=scope, wasm=wasm, wasm_path=wasm_path, **place)
|
|
178
|
+
|
|
179
|
+
def markdown(self, text="", name="markdown", label=None, **place: Unpack[Place]):
|
|
180
|
+
"""Insert a :class:`~danvas.Markdown` panel. See :meth:`insert` for ``place``."""
|
|
181
|
+
return self._make(Markdown, text=text, name=name, label=label, **place)
|
|
182
|
+
|
|
183
|
+
def image(self, src, name="image", label=None, fit="contain", **place: Unpack[Place]):
|
|
184
|
+
"""Insert an :class:`~danvas.Image` panel. See :meth:`insert` for ``place``.
|
|
185
|
+
|
|
186
|
+
``src`` is a path, URL, image bytes, Matplotlib/PIL figure, or array.
|
|
187
|
+
"""
|
|
188
|
+
return self._make(Image, src, name=name, label=label, fit=fit, **place)
|
|
189
|
+
|
|
190
|
+
def table(self, data, name="table", label=None, **place: Unpack[Place]):
|
|
191
|
+
"""Insert a :class:`~danvas.Table` panel. See :meth:`insert` for ``place``.
|
|
192
|
+
|
|
193
|
+
``data`` is a pandas DataFrame/Series, a list of dicts/rows, or a dict.
|
|
194
|
+
"""
|
|
195
|
+
return self._make(Table, data, name=name, label=label, **place)
|
|
196
|
+
|
|
197
|
+
def text_field(self, name="text_field", placeholder="", default="", multiline=False,
|
|
198
|
+
label=None, **place: Unpack[Place]):
|
|
199
|
+
"""Insert a :class:`~danvas.TextField`. See :meth:`insert` for ``place``.
|
|
200
|
+
|
|
201
|
+
Single-line (default): fires ``on_change`` on Enter or focus-loss.
|
|
202
|
+
Pass ``multiline=True`` for a textarea that fires on focus-loss.
|
|
203
|
+
"""
|
|
204
|
+
return self._make(TextField, name=name, placeholder=placeholder,
|
|
205
|
+
default=default, multiline=multiline, label=label,
|
|
206
|
+
**place)
|
|
207
|
+
|
|
208
|
+
def show(self, value, name=None, label=None, **kw):
|
|
209
|
+
"""Auto-render any value as the best-fitting panel and insert it.
|
|
210
|
+
|
|
211
|
+
Picks the component the way a notebook decides how to render an output
|
|
212
|
+
(DataFrame → table, figure/array → image, Plotly → plot, rich
|
|
213
|
+
``_repr_*`` → its HTML, dict/list → JSON, string → label/markdown, else a
|
|
214
|
+
repr label) via :func:`danvas.panel_for`. With no ``name`` a unique one
|
|
215
|
+
is generated; re-using a ``name`` replaces that panel in place. Returns
|
|
216
|
+
the inserted component. See :meth:`insert` for ``place``.
|
|
217
|
+
|
|
218
|
+
Any keyword arguments not used for placement are forwarded to the
|
|
219
|
+
chosen component's constructor when it accepts them and silently ignored
|
|
220
|
+
otherwise — so ``editable=True`` enables editing on a table but is a
|
|
221
|
+
no-op if the value renders as an image or label.
|
|
222
|
+
"""
|
|
223
|
+
from .dispatch import panel_for
|
|
224
|
+
# Split placement kwargs from component-specific ones.
|
|
225
|
+
place = {k: kw.pop(k) for k in _INSERT_KEYS if k in kw}
|
|
226
|
+
if name is None:
|
|
227
|
+
self._show_seq += 1
|
|
228
|
+
name = f"panel_{self._show_seq}"
|
|
229
|
+
comp = panel_for(value, name=name, label=label, **kw)
|
|
230
|
+
# insert() handles eviction of whatever currently holds this name, so
|
|
231
|
+
# re-showing under the same name replaces in place on its own.
|
|
232
|
+
return self.insert(comp, **place)
|
|
233
|
+
|
|
234
|
+
def webview(self, url, name="webview", label=None, **place: Unpack[Place]):
|
|
235
|
+
"""Insert a :class:`~danvas.WebView`. See :meth:`insert` for ``place``."""
|
|
236
|
+
return self._make(WebView, url, name=name, label=label, **place)
|
|
237
|
+
|
|
238
|
+
def model3d(self, name="model3d", label=None, color=None,
|
|
239
|
+
**place: Unpack[Place]):
|
|
240
|
+
"""Insert a :class:`~danvas.Model3D` — the prebuilt CAD/3D viewer:
|
|
241
|
+
``viewer.update(glb_bytes_or_path_or_trimesh)`` shows the model with
|
|
242
|
+
orbit, snap measurements, and a section plane. See :meth:`insert`."""
|
|
243
|
+
return self._make(Model3D, name=name, label=label, color=color,
|
|
244
|
+
**place)
|
|
245
|
+
|
|
246
|
+
def plot(self, name="plot", label=None, **place: Unpack[Place]):
|
|
247
|
+
"""Insert a :class:`~danvas.Plot`. See :meth:`insert` for ``place``."""
|
|
248
|
+
return self._make(Plot, name=name, label=label, **place)
|
|
249
|
+
|
|
250
|
+
def live_plot(self, name="liveplot", traces=None, max_points=300,
|
|
251
|
+
mode="lines", layout=None, smoothing=0.0, label=None,
|
|
252
|
+
color=None, **place: Unpack[Place]):
|
|
253
|
+
"""Insert a :class:`~danvas.LivePlot`. See :meth:`insert` for ``place``.
|
|
254
|
+
|
|
255
|
+
``traces`` only fixes the legend order — pushing an unseen key adds a
|
|
256
|
+
trace on the fly. ``smoothing`` (0–1) is an EMA over each trace;
|
|
257
|
+
``max_points`` caps the rolling window per trace.
|
|
258
|
+
"""
|
|
259
|
+
return self._make(LivePlot, name=name, traces=traces,
|
|
260
|
+
max_points=max_points, mode=mode, layout=layout,
|
|
261
|
+
smoothing=smoothing, label=label, color=color,
|
|
262
|
+
**place)
|
|
263
|
+
|
|
264
|
+
def histogram(self, name="histogram", bins=30, mode="heatmap",
|
|
265
|
+
value_range=None, max_steps=200, label=None, color=None,
|
|
266
|
+
**place: Unpack[Place]):
|
|
267
|
+
"""Insert a :class:`~danvas.Histogram` — a distribution-over-time panel.
|
|
268
|
+
See :meth:`insert` for ``place``.
|
|
269
|
+
|
|
270
|
+
Feed it with ``panel.add(values, step)``; needs ``plotly``. ``bins``
|
|
271
|
+
sets the resolution, ``value_range`` pins the bin edges, ``max_steps``
|
|
272
|
+
caps the rolling history, and ``mode`` is ``"heatmap"`` or ``"overlay"``.
|
|
273
|
+
"""
|
|
274
|
+
return self._make(Histogram, name=name, bins=bins, mode=mode,
|
|
275
|
+
value_range=value_range, max_steps=max_steps,
|
|
276
|
+
label=label, color=color, **place)
|
|
277
|
+
|
|
278
|
+
def inspector(self, name="inspector", refresh=1.0, source="components",
|
|
279
|
+
namespace=None, label=None, **place: Unpack[Place]):
|
|
280
|
+
"""Insert an :class:`~danvas.Inspector`. See :meth:`insert` for ``place``.
|
|
281
|
+
|
|
282
|
+
``refresh`` is the auto-refresh period in seconds, so the table stays live
|
|
283
|
+
as panel values and positions change (default ``1.0``); the rebuild only
|
|
284
|
+
runs while serving with a browser connected, so an idle inspector is free.
|
|
285
|
+
Pass ``refresh=None`` to make it manual (the **Refresh** button only).
|
|
286
|
+
"""
|
|
287
|
+
return self._make(Inspector, name=name, refresh=refresh, source=source,
|
|
288
|
+
namespace=namespace, label=label, **place)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Single source of truth for a panel's lock / chrome flags.
|
|
2
|
+
|
|
3
|
+
Every panel shares the same set of boolean flags that gate how a *viewer* may
|
|
4
|
+
interact with it (drag, resize, operate its controls, select it) and whether it
|
|
5
|
+
draws its card chrome. Each flag is described once here as a :class:`Flag` and
|
|
6
|
+
the same table drives:
|
|
7
|
+
|
|
8
|
+
- the read/write properties and ``set_layout`` on
|
|
9
|
+
:class:`~danvas.components.base.BaseComponent`,
|
|
10
|
+
- the ``register`` message built in :mod:`danvas.bridge`,
|
|
11
|
+
- ``Canvas.insert`` and the save/load formation in :mod:`danvas.canvas`.
|
|
12
|
+
|
|
13
|
+
So adding a new flag is a single edit here rather than a change threaded through
|
|
14
|
+
seven places. ``wire`` is the key sent to the browser; it is deliberately kept
|
|
15
|
+
distinct from the Python name (e.g. ``draggable`` → ``movable``) and **must not**
|
|
16
|
+
change without updating the frontend's ``lockMeta`` handling. ``default`` is both
|
|
17
|
+
the initial value and the "don't bother sending it" baseline in the register
|
|
18
|
+
message.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from collections import namedtuple
|
|
22
|
+
|
|
23
|
+
from ._protocol import FLAG_WIRE_KEYS
|
|
24
|
+
|
|
25
|
+
Flag = namedtuple("Flag", "wire attr default doc")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Python name -> Flag(wire key, backing attribute, default, property docstring).
|
|
29
|
+
# Insertion order is the canonical order used wherever the flags are iterated.
|
|
30
|
+
# The ``wire`` keys come from the canonical danvas/_protocol.py (the same
|
|
31
|
+
# definition the frontend's protocol.generated.js is rendered from) so the
|
|
32
|
+
# browser-facing names can't drift; this module owns the backing attr / default /
|
|
33
|
+
# docstring.
|
|
34
|
+
LAYOUT_FLAGS = {
|
|
35
|
+
"locked": Flag(
|
|
36
|
+
FLAG_WIRE_KEYS["locked"], "_locked", False,
|
|
37
|
+
"Whether the panel is fully locked (no move/resize/interaction)."),
|
|
38
|
+
"draggable": Flag(
|
|
39
|
+
FLAG_WIRE_KEYS["draggable"], "_draggable", True,
|
|
40
|
+
"Whether the user can drag the panel. Control interaction is "
|
|
41
|
+
"unaffected."),
|
|
42
|
+
"resizable": Flag(
|
|
43
|
+
FLAG_WIRE_KEYS["resizable"], "_resizable", True,
|
|
44
|
+
"Whether the user can resize the panel. Interaction is unaffected."),
|
|
45
|
+
"operable": Flag(
|
|
46
|
+
FLAG_WIRE_KEYS["operable"], "_operable", True,
|
|
47
|
+
"Whether the user can operate the panel's controls from the UI.\n\n"
|
|
48
|
+
" Set to ``False`` to make the controls inert to the user while "
|
|
49
|
+
"the panel\n stays unlocked, so Python ``update()`` calls still "
|
|
50
|
+
"render live (e.g. a\n slider thumb that tracks an automatic "
|
|
51
|
+
"value the user mustn't drag). The\n panel can still be "
|
|
52
|
+
"moved/selected; use ``locked`` to freeze everything.\n "),
|
|
53
|
+
"grabbable": Flag(
|
|
54
|
+
FLAG_WIRE_KEYS["grabbable"], "_grabbable", True,
|
|
55
|
+
"Whether the user can grab/select this panel at all.\n\n"
|
|
56
|
+
" Content-heavy panels (Custom, React, WebView, plots…) normally "
|
|
57
|
+
"need a\n first click to select the panel before their content "
|
|
58
|
+
"becomes\n interactive. Set to ``False`` to drop that cover "
|
|
59
|
+
"*and* make the panel\n invisible to selection: the content is "
|
|
60
|
+
"live (hover and clicks work)\n from the start, and no click, "
|
|
61
|
+
"marquee, or select-all ever highlights\n or selects the panel — "
|
|
62
|
+
"only the widget itself reacts. The trade-off is\n that the user "
|
|
63
|
+
"can't move or resize it; do that from Python (or flip\n "
|
|
64
|
+
"``grabbable`` back on).\n "),
|
|
65
|
+
"frame": Flag(
|
|
66
|
+
FLAG_WIRE_KEYS["frame"], "_frame", True,
|
|
67
|
+
"Whether the panel draws its rectangular card chrome.\n\n"
|
|
68
|
+
" Set to ``False`` to strip the card entirely — background, "
|
|
69
|
+
"border,\n shadow, padding, label header, and the "
|
|
70
|
+
"hover/selection highlight\n rectangle — so the component's "
|
|
71
|
+
"content appears to float directly on\n the canvas. The panel "
|
|
72
|
+
"still occupies its w×h box and can be moved or\n resized as "
|
|
73
|
+
"usual (marquee select still works). Pair with\n "
|
|
74
|
+
"``grabbable=False`` if clicks on the content should never select it.\n"
|
|
75
|
+
" "),
|
|
76
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Clean monitor entry-point for ``serve(hot_reload=True)``.
|
|
2
|
+
|
|
3
|
+
Spawned as a subprocess by ``canvas._maybe_handoff_reload`` so the monitor
|
|
4
|
+
process never contains user code or user-launched daemon threads. The original
|
|
5
|
+
``python script.py`` process exits (killing its threads) after spawning this;
|
|
6
|
+
all file-watching and worker management happens here.
|
|
7
|
+
|
|
8
|
+
Usage (internal only):
|
|
9
|
+
python -m danvas._hotreload_monitor <main_file> <port> <tunnel 0|1> <provider> [broker 0|1]
|
|
10
|
+
|
|
11
|
+
Extra ``serve(watch=...)`` globs ride in the ``_danvas_RELOAD_WATCH`` env var
|
|
12
|
+
(a JSON list) rather than argv, so they don't collide with the positional args.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
from danvas.hotreload import run_monitor
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main_file, port_s, tunnel_s, provider = sys.argv[1:5]
|
|
23
|
+
broker = bool(int(sys.argv[5])) if len(sys.argv) > 5 else False
|
|
24
|
+
watch = json.loads(os.environ.get("_danvas_RELOAD_WATCH", "[]"))
|
|
25
|
+
run_monitor(main_file, port=int(port_s),
|
|
26
|
+
tunnel=bool(int(tunnel_s)), tunnel_provider=provider,
|
|
27
|
+
watch=watch, broker=broker)
|