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.
Files changed (107) hide show
  1. danvas-0.5.1.data/purelib/danvas/__init__.py +105 -0
  2. danvas-0.5.1.data/purelib/danvas/_bin/danvasd.exe +0 -0
  3. danvas-0.5.1.data/purelib/danvas/_dialin.py +92 -0
  4. danvas-0.5.1.data/purelib/danvas/_events.py +50 -0
  5. danvas-0.5.1.data/purelib/danvas/_factories.py +288 -0
  6. danvas-0.5.1.data/purelib/danvas/_flags.py +76 -0
  7. danvas-0.5.1.data/purelib/danvas/_hotreload_monitor.py +27 -0
  8. danvas-0.5.1.data/purelib/danvas/_layout.py +571 -0
  9. danvas-0.5.1.data/purelib/danvas/_ledger.py +173 -0
  10. danvas-0.5.1.data/purelib/danvas/_livepatch.py +267 -0
  11. danvas-0.5.1.data/purelib/danvas/_net.py +52 -0
  12. danvas-0.5.1.data/purelib/danvas/_onedit.py +169 -0
  13. danvas-0.5.1.data/purelib/danvas/_procown.py +119 -0
  14. danvas-0.5.1.data/purelib/danvas/_protocol.py +163 -0
  15. danvas-0.5.1.data/purelib/danvas/_trace.py +255 -0
  16. danvas-0.5.1.data/purelib/danvas/arrow.py +132 -0
  17. danvas-0.5.1.data/purelib/danvas/autopanel.py +367 -0
  18. danvas-0.5.1.data/purelib/danvas/bake.py +325 -0
  19. danvas-0.5.1.data/purelib/danvas/bridge.py +2600 -0
  20. danvas-0.5.1.data/purelib/danvas/canvas.py +2883 -0
  21. danvas-0.5.1.data/purelib/danvas/canvas.pyi +378 -0
  22. danvas-0.5.1.data/purelib/danvas/components/__init__.py +51 -0
  23. danvas-0.5.1.data/purelib/danvas/components/_doc.py +52 -0
  24. danvas-0.5.1.data/purelib/danvas/components/_jsx.py +23 -0
  25. danvas-0.5.1.data/purelib/danvas/components/_routing.py +271 -0
  26. danvas-0.5.1.data/purelib/danvas/components/_theme.py +74 -0
  27. danvas-0.5.1.data/purelib/danvas/components/audio.py +92 -0
  28. danvas-0.5.1.data/purelib/danvas/components/base.py +1320 -0
  29. danvas-0.5.1.data/purelib/danvas/components/button.py +94 -0
  30. danvas-0.5.1.data/purelib/danvas/components/chat.py +88 -0
  31. danvas-0.5.1.data/purelib/danvas/components/custom.py +354 -0
  32. danvas-0.5.1.data/purelib/danvas/components/download.py +183 -0
  33. danvas-0.5.1.data/purelib/danvas/components/filebrowser.py +246 -0
  34. danvas-0.5.1.data/purelib/danvas/components/histogram.py +92 -0
  35. danvas-0.5.1.data/purelib/danvas/components/image.py +197 -0
  36. danvas-0.5.1.data/purelib/danvas/components/inspector.py +701 -0
  37. danvas-0.5.1.data/purelib/danvas/components/jsx/audio.jsx +82 -0
  38. danvas-0.5.1.data/purelib/danvas/components/jsx/button.jsx +12 -0
  39. danvas-0.5.1.data/purelib/danvas/components/jsx/chat.jsx +100 -0
  40. danvas-0.5.1.data/purelib/danvas/components/jsx/download.jsx +32 -0
  41. danvas-0.5.1.data/purelib/danvas/components/jsx/file_browser.jsx +35 -0
  42. danvas-0.5.1.data/purelib/danvas/components/jsx/image.jsx +11 -0
  43. danvas-0.5.1.data/purelib/danvas/components/jsx/inspector.jsx +233 -0
  44. danvas-0.5.1.data/purelib/danvas/components/jsx/label.jsx +11 -0
  45. danvas-0.5.1.data/purelib/danvas/components/jsx/live_plot.jsx +71 -0
  46. danvas-0.5.1.data/purelib/danvas/components/jsx/markdown.jsx +11 -0
  47. danvas-0.5.1.data/purelib/danvas/components/jsx/plot.jsx +44 -0
  48. danvas-0.5.1.data/purelib/danvas/components/jsx/slider.jsx +61 -0
  49. danvas-0.5.1.data/purelib/danvas/components/jsx/table.jsx +313 -0
  50. danvas-0.5.1.data/purelib/danvas/components/jsx/text_field.jsx +35 -0
  51. danvas-0.5.1.data/purelib/danvas/components/jsx/toggle.jsx +22 -0
  52. danvas-0.5.1.data/purelib/danvas/components/jsx/upload.jsx +70 -0
  53. danvas-0.5.1.data/purelib/danvas/components/jsx/video.jsx +22 -0
  54. danvas-0.5.1.data/purelib/danvas/components/jsx/webview.jsx +11 -0
  55. danvas-0.5.1.data/purelib/danvas/components/label.py +72 -0
  56. danvas-0.5.1.data/purelib/danvas/components/liveplot.py +336 -0
  57. danvas-0.5.1.data/purelib/danvas/components/markdown.py +173 -0
  58. danvas-0.5.1.data/purelib/danvas/components/model3d.py +2043 -0
  59. danvas-0.5.1.data/purelib/danvas/components/plot.py +52 -0
  60. danvas-0.5.1.data/purelib/danvas/components/react.py +631 -0
  61. danvas-0.5.1.data/purelib/danvas/components/slider.py +110 -0
  62. danvas-0.5.1.data/purelib/danvas/components/table.py +509 -0
  63. danvas-0.5.1.data/purelib/danvas/components/textfield.py +108 -0
  64. danvas-0.5.1.data/purelib/danvas/components/toggle.py +82 -0
  65. danvas-0.5.1.data/purelib/danvas/components/upload.py +236 -0
  66. danvas-0.5.1.data/purelib/danvas/components/video.py +89 -0
  67. danvas-0.5.1.data/purelib/danvas/components/webview.py +85 -0
  68. danvas-0.5.1.data/purelib/danvas/dispatch.py +631 -0
  69. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-C-brsJOE.js +175 -0
  70. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-C-brsJOE.js.br +0 -0
  71. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-C-brsJOE.js.gz +0 -0
  72. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-DA1g5i2i.css +1 -0
  73. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-DA1g5i2i.css.br +0 -0
  74. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-DA1g5i2i.css.gz +0 -0
  75. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-sRVW3Fwz.js +14 -0
  76. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-sRVW3Fwz.js.br +0 -0
  77. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/index-sRVW3Fwz.js.gz +0 -0
  78. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  79. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  80. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  81. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  82. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  83. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  84. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  85. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/plotly-cartesian.min-Dzd4OOBc.js +127 -0
  86. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/plotly-cartesian.min-Dzd4OOBc.js.br +0 -0
  87. danvas-0.5.1.data/purelib/danvas/frontend/dist/assets/plotly-cartesian.min-Dzd4OOBc.js.gz +0 -0
  88. danvas-0.5.1.data/purelib/danvas/frontend/dist/index.html +39 -0
  89. danvas-0.5.1.data/purelib/danvas/frontend/dist/index.html.br +0 -0
  90. danvas-0.5.1.data/purelib/danvas/frontend/dist/index.html.gz +0 -0
  91. danvas-0.5.1.data/purelib/danvas/hotreload.py +415 -0
  92. danvas-0.5.1.data/purelib/danvas/kernel.py +208 -0
  93. danvas-0.5.1.data/purelib/danvas/merge.py +1569 -0
  94. danvas-0.5.1.data/purelib/danvas/py.typed +0 -0
  95. danvas-0.5.1.data/purelib/danvas/remote.py +826 -0
  96. danvas-0.5.1.data/purelib/danvas/serial.py +196 -0
  97. danvas-0.5.1.data/purelib/danvas/server.py +708 -0
  98. danvas-0.5.1.data/purelib/danvas/shapes.py +480 -0
  99. danvas-0.5.1.data/purelib/danvas/source.py +370 -0
  100. danvas-0.5.1.data/purelib/danvas/templates/components.json +960 -0
  101. danvas-0.5.1.data/purelib/danvas/tunnel.py +186 -0
  102. danvas-0.5.1.dist-info/METADATA +1659 -0
  103. danvas-0.5.1.dist-info/RECORD +107 -0
  104. danvas-0.5.1.dist-info/WHEEL +5 -0
  105. danvas-0.5.1.dist-info/licenses/LICENSE +661 -0
  106. danvas-0.5.1.dist-info/licenses/THIRD_PARTY_LICENSES.md +54 -0
  107. 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}")
@@ -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)