pgwidgets-python 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/PKG-INFO +1 -1
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/WhatsNew.rst +145 -8
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/architecture.rst +3 -1
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/callbacks.rst +19 -13
- pgwidgets_python-0.3.0/docs/utilities.rst +81 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/widgets.rst +61 -10
- pgwidgets_python-0.3.0/examples/flask-multi-process/README.md +72 -0
- pgwidgets_python-0.3.0/examples/flask-multi-process/server.py +203 -0
- pgwidgets_python-0.3.0/examples/flask-multi-process/user_app.py +230 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/__init__.py +2 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/async_/application.py +245 -43
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/async_/widget.py +38 -6
- pgwidgets_python-0.3.0/pgwidgets/buffer.py +125 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/method_types.py +49 -2
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/sync/application.py +281 -46
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/sync/widget.py +42 -6
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/PKG-INFO +1 -1
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/SOURCES.txt +4 -0
- pgwidgets_python-0.2.1/docs/utilities.rst +0 -40
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/.flake8 +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/.github/workflows/tests.yml +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/.gitignore +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/.readthedocs.yaml +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/LICENSE.md +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/README.md +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/Makefile +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/api/async.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/api/index.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/api/sync.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/async.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/conf.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/extras.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/getting-started.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/index.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/subclassing.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/sync.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/docs/web-servers.rst +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/README.md +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/all_widgets.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/all_widgets_async.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/demo_async.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/demo_sync.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/examples/demo_treeview.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/_json.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/async_/Widgets.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/async_/__init__.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/callbacks.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/defs.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/extras/__init__.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/extras/file_browser.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/sync/Widgets.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets/sync/__init__.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/requires.txt +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pgwidgets_python.egg-info/top_level.txt +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/pyproject.toml +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/setup.cfg +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/test_pg2.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/__init__.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_defs.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_extras.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_protocol.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_reconstruct.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_session.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_stateful.py +0 -0
- {pgwidgets_python-0.2.1 → pgwidgets_python-0.3.0}/tests/test_widget_classes.py +0 -0
|
@@ -1,13 +1,146 @@
|
|
|
1
1
|
What's New
|
|
2
2
|
==========
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Recent changes — since ``v0.2.3``
|
|
5
|
+
---------------------------------
|
|
6
|
+
|
|
7
|
+
Chunked binary transport (both directions)
|
|
8
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
9
|
+
|
|
10
|
+
``Session`` gained ``_send_binary_chunked(wid, method, args, data,
|
|
11
|
+
chunk_size=512KB, shape=None, dtype=None)`` and a matching
|
|
12
|
+
incoming-binary path. Large payloads automatically use the
|
|
13
|
+
chunked transport — :meth:`Image.set_binary_image` (and the
|
|
14
|
+
reconstruction replay of binary state) auto-pick chunked above a
|
|
15
|
+
1 MiB threshold. ``Session._handle_file_chunk`` was rewritten as
|
|
16
|
+
``_handle_binary_chunk`` to consume the new binary-chunk
|
|
17
|
+
envelope: chunks store by ``chunk_index`` (robust to ordering)
|
|
18
|
+
and reassemble as ``bytes``.
|
|
19
|
+
|
|
20
|
+
**Pre-1.0 API break**: drop-end / FileDialog ``activated``
|
|
21
|
+
handlers now receive each file's ``data`` field as raw ``bytes``
|
|
22
|
+
(was a ``"data:<mime>;base64,…"`` string). Each file dict now
|
|
23
|
+
also carries an ``encoding`` field (currently ``"bytes"``;
|
|
24
|
+
reserved for future ``"base64"``).
|
|
25
|
+
|
|
26
|
+
:class:`pgwidgets.Buffer` for typed-array delivery
|
|
27
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
28
|
+
|
|
29
|
+
New :class:`pgwidgets.Buffer` wraps raw bytes with ``shape`` and
|
|
30
|
+
``dtype`` metadata. When passed to a binary-aware method, the
|
|
31
|
+
bytes ride the chunked transport and the receiver on the JS side
|
|
32
|
+
gets a typed array of the right shape (``Uint8Array``,
|
|
33
|
+
``Float32Array``, …) without the per-method casting that used to
|
|
34
|
+
be necessary::
|
|
35
|
+
|
|
36
|
+
from pgwidgets import Buffer
|
|
37
|
+
pixels = ... # 2048 * 2048 * 4 bytes
|
|
38
|
+
viewer.load_buffer(
|
|
39
|
+
Buffer(pixels, shape=(2048, 2048, 4), dtype="uint8"),
|
|
40
|
+
[2048, 2048], cache)
|
|
41
|
+
|
|
42
|
+
Supported dtypes: ``uint8`` / ``uint16`` / ``uint32`` / ``int8``
|
|
43
|
+
/ ``int16`` / ``int32`` / ``float32`` / ``float64``.
|
|
44
|
+
Construction validates that ``len(data) == prod(shape) *
|
|
45
|
+
itemsize``.
|
|
46
|
+
|
|
47
|
+
``Application(ws_sock=…)`` for race-free port allocation
|
|
48
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
49
|
+
|
|
50
|
+
:class:`Application` accepts an optional pre-bound TCP socket via
|
|
51
|
+
``ws_sock=``. When provided, ``Application`` adopts the socket
|
|
52
|
+
directly (hands it to ``websockets.serve(sock=...)``) instead of
|
|
53
|
+
binding ``(host, ws_port)`` itself, and reads ``ws_port`` back
|
|
54
|
+
from the socket for logging. The motivation is TOCTOU-free port
|
|
55
|
+
allocation: the caller can bind in advance, hand the socket in,
|
|
56
|
+
and never release the port between "find" and "use" — no other
|
|
57
|
+
process can grab it in the gap. The existing ``ws_port=`` API
|
|
58
|
+
is unchanged.
|
|
59
|
+
|
|
60
|
+
Flask multi-process demo
|
|
61
|
+
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
62
|
+
|
|
63
|
+
New ``examples/flask-multi-process/`` example: one pgwidgets
|
|
64
|
+
``Application`` per visitor, each in its own OS process with its
|
|
65
|
+
own WebSocket port. Demonstrates the new ``ws_sock=`` parameter
|
|
66
|
+
(child binds, never releases), idle reaping via a grace timer on
|
|
67
|
+
``app.on_disconnect`` that handles the subtle case where
|
|
68
|
+
``app.on_connect`` doesn't fire on session-reconnect (the wrapper
|
|
69
|
+
also hooks ``session.add_connection``), and serving pgwidgets-js's
|
|
70
|
+
static assets from the pip-installed package rather than a CDN.
|
|
71
|
+
|
|
72
|
+
----
|
|
73
|
+
|
|
74
|
+
Recent changes — since ``v0.2.1``
|
|
75
|
+
---------------------------------
|
|
76
|
+
|
|
77
|
+
Reliable ``get_size()`` / ``get_position()`` on every visual widget
|
|
78
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
79
|
+
|
|
80
|
+
The auto-sync layer that backs widget getters has been reworked so
|
|
81
|
+
``widget.get_size()`` (and ``get_position()`` where supported)
|
|
82
|
+
returns the current layout-determined value for any visual widget,
|
|
83
|
+
not just ones that opted into the ``resizable`` / ``moveable``
|
|
84
|
+
options. The binding now installs a *passive* resize listener on
|
|
85
|
+
every visual widget — the value is captured into local state for
|
|
86
|
+
the getter, but it is **not** pushed to other connected browsers or
|
|
87
|
+
replayed on reconstruction. Explicit ``widget.resize(w, h)`` calls
|
|
88
|
+
are still replayed, as is interactive resize on widgets that opted
|
|
89
|
+
into active sync.
|
|
90
|
+
|
|
91
|
+
The user-visible upshot: code that calls ``image.get_size()`` on an
|
|
92
|
+
``Image`` placed in a flex container now returns the real pixel
|
|
93
|
+
size the browser laid the widget out at, instead of ``None`` or a
|
|
94
|
+
stale value. And widgets like ``Image`` with
|
|
95
|
+
``set_expanding(True, True)`` no longer get pinned to pixel
|
|
96
|
+
dimensions on reconnect (a regression in earlier auto-sync work).
|
|
97
|
+
|
|
98
|
+
The same guard now applies in both reconstruction paths
|
|
99
|
+
(top-level state replay AND ``_transfer_proxy`` for factory-created
|
|
100
|
+
widgets like ``ToolBarAction``), which fixes a "toolbar items
|
|
101
|
+
drift farther apart after each reconnect" bug.
|
|
102
|
+
|
|
103
|
+
``map`` callback survives reconstruction
|
|
104
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
105
|
+
|
|
106
|
+
The Python side now forwards ``map`` callbacks during a reconstruct
|
|
107
|
+
window instead of suppressing them along with the rest of the
|
|
108
|
+
state-replay echoes. ``map`` is a one-shot lifecycle event tied to
|
|
109
|
+
the widget first becoming visually present; missing it on the
|
|
110
|
+
Python side meant a user's map handler stayed un-fired until the
|
|
111
|
+
window happened to be resized. Pairs with the JS-side reliability
|
|
112
|
+
work for the same callback.
|
|
113
|
+
|
|
114
|
+
Sensible defaults for more getters
|
|
115
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
116
|
+
|
|
117
|
+
Getters now return useful zero-value defaults before any state has
|
|
118
|
+
been set or reported from the browser, instead of ``None``:
|
|
119
|
+
|
|
120
|
+
- ``get_scroll_position()`` → ``(0.0, 0.0)``
|
|
121
|
+
- ``get_scroll_percent()`` → ``(0.0, 0.0)`` (or ``0.0`` on
|
|
122
|
+
``ScrollBar``, which uses a single-axis API)
|
|
123
|
+
- ``get_thumb_percent()`` → ``(0.0, 0.0)`` (or ``0.0`` on
|
|
124
|
+
``ScrollBar``)
|
|
125
|
+
- ``get_expanding()`` → ``(False, False)``
|
|
126
|
+
- ``get_enabled()`` → ``True``
|
|
127
|
+
- ``get_state()`` → ``False``
|
|
128
|
+
- ``get_volume()`` → ``1.0`` (HTMLMediaElement convention: 0.0
|
|
129
|
+
muted, 1.0 full).
|
|
130
|
+
|
|
131
|
+
Configured in ``STATE_KEY_DEFAULTS`` / ``STATE_DEFAULTS`` in
|
|
132
|
+
``method_types.py``.
|
|
133
|
+
|
|
134
|
+
----
|
|
135
|
+
|
|
136
|
+
Earlier — since ``v0.1.3``
|
|
137
|
+
--------------------------
|
|
5
138
|
|
|
6
139
|
Major changes
|
|
7
|
-
|
|
140
|
+
~~~~~~~~~~~~~
|
|
8
141
|
|
|
9
142
|
TreeView / TableView: dict-tree model
|
|
10
|
-
|
|
143
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
11
144
|
|
|
12
145
|
Mirroring the JS-side rewrite, ``TreeView`` and ``TableView`` now
|
|
13
146
|
work with hierarchies of dicts keyed by stable string identifiers.
|
|
@@ -63,7 +196,7 @@ Highlights:
|
|
|
63
196
|
See :doc:`widgets` for the full reference.
|
|
64
197
|
|
|
65
198
|
Window controls (TopLevel) and shade (MDISubWindow)
|
|
66
|
-
|
|
199
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
67
200
|
|
|
68
201
|
``TopLevel`` gains the same window controls that ``MDISubWindow``
|
|
69
202
|
has, plus a "shade" (roll up to title bar) state on both.
|
|
@@ -96,7 +229,7 @@ Maximize, Close). The menu supports both click-release and
|
|
|
96
229
|
press-drag-release, like a menubar.
|
|
97
230
|
|
|
98
231
|
Image: binary-frame protocol
|
|
99
|
-
|
|
232
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
100
233
|
|
|
101
234
|
New method ``Image.set_binary_image(data, format='jpeg')`` sends raw
|
|
102
235
|
bytes (``bytes`` / ``bytearray`` / ``memoryview``) via a WebSocket
|
|
@@ -106,7 +239,7 @@ Useful for animation/streaming. ``format`` is one of ``"jpeg"``,
|
|
|
106
239
|
widget state and replayed on reconnect.
|
|
107
240
|
|
|
108
241
|
Callbacks base class
|
|
109
|
-
|
|
242
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
110
243
|
|
|
111
244
|
New module ``pgwidgets.callbacks`` exposes ``Callbacks``, a small
|
|
112
245
|
base class that provides the same callback API (``add_callback``,
|
|
@@ -121,7 +254,7 @@ supports both ``add_callback("activated", ...)`` and
|
|
|
121
254
|
See :ref:`callbacks-base`.
|
|
122
255
|
|
|
123
256
|
Robustness improvements
|
|
124
|
-
|
|
257
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
125
258
|
|
|
126
259
|
- ``Session._send`` and ``_send_binary`` no longer hang when the
|
|
127
260
|
asyncio loop refuses a coroutine (loop closed mid-call,
|
|
@@ -139,7 +272,7 @@ Robustness improvements
|
|
|
139
272
|
internal ``ScrollBar`` widgets in its constructor.
|
|
140
273
|
|
|
141
274
|
Other notable changes
|
|
142
|
-
|
|
275
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
143
276
|
|
|
144
277
|
- ``ColorDialog`` now exposes ``popup``, ``set_position``,
|
|
145
278
|
``set_modal``, and the ``move`` / ``close`` callbacks (inherited
|
|
@@ -160,3 +293,7 @@ Other notable changes
|
|
|
160
293
|
though the JS side preserves columns on clear).
|
|
161
294
|
- ``FileBrowser`` migrated to the new dict-tree ``TableView`` API
|
|
162
295
|
internally and now subclasses ``Callbacks``.
|
|
296
|
+
- ``FixedLayout`` container added (auto-generated from the JS
|
|
297
|
+
definition). Use ``W.FixedLayout()`` and
|
|
298
|
+
``layout.add_widget(child, x, y)`` to place children at fixed
|
|
299
|
+
pixel offsets; ``remove(child)`` works as on any other container.
|
|
@@ -68,7 +68,9 @@ All messages are JSON objects with a ``type`` field.
|
|
|
68
68
|
- ``{"type": "error", "id": 1, "error": "..."}`` -- method error.
|
|
69
69
|
- ``{"type": "callback", "wid": 1, "action": "activated", "args": [...]}`` --
|
|
70
70
|
user interaction.
|
|
71
|
-
- ``{"type": "
|
|
71
|
+
- ``{"type": "binary-chunk", ...}`` -- one chunk of a chunked binary
|
|
72
|
+
transfer (used in both directions; file uploads, large image
|
|
73
|
+
payloads, etc.; see :doc:`callbacks`).
|
|
72
74
|
|
|
73
75
|
Session Model
|
|
74
76
|
-------------
|
|
@@ -127,9 +127,10 @@ The protocol works as follows:
|
|
|
127
127
|
metadata (names, sizes, MIME types) but no file data.
|
|
128
128
|
2. The framework fires a **drop-start** callback with the metadata so you
|
|
129
129
|
can show progress UI.
|
|
130
|
-
3. The browser sends ``
|
|
130
|
+
3. The browser sends ``binary-chunk`` JSON headers, each followed by a raw
|
|
131
|
+
binary WebSocket frame carrying the chunk's bytes (no base64 inflation).
|
|
131
132
|
4. The framework fires **drop-progress** callbacks with transfer status.
|
|
132
|
-
5. When all chunks arrive, the framework reassembles the
|
|
133
|
+
5. When all chunks arrive, the framework reassembles the bytes and fires the
|
|
133
134
|
**drop-end** callback with the complete payload.
|
|
134
135
|
|
|
135
136
|
drop-start
|
|
@@ -175,22 +176,28 @@ drop-end
|
|
|
175
176
|
~~~~~~~~
|
|
176
177
|
|
|
177
178
|
Fires when all file data has been received. The handler receives the full
|
|
178
|
-
payload
|
|
179
|
+
payload; each file dict carries:
|
|
180
|
+
|
|
181
|
+
- ``name`` (str) — original filename
|
|
182
|
+
- ``size`` (int) — byte size
|
|
183
|
+
- ``type`` (str) — MIME type from the browser (e.g. ``"image/png"``)
|
|
184
|
+
- ``encoding`` (str) — wire format of ``data``. Currently always
|
|
185
|
+
``"bytes"`` (``data`` is raw bytes); reserved for ``"base64"`` if a
|
|
186
|
+
future sender chooses to deliver the file body without binary
|
|
187
|
+
reassembly.
|
|
188
|
+
- ``data`` (bytes or None) — file contents. ``None`` on read error;
|
|
189
|
+
in that case an additional ``error`` field carries the message.
|
|
179
190
|
|
|
180
191
|
.. code-block:: python
|
|
181
192
|
|
|
182
|
-
import base64
|
|
183
|
-
|
|
184
193
|
def on_drop(payload):
|
|
185
194
|
for f in payload["files"]:
|
|
195
|
+
if f["encoding"] != "bytes":
|
|
196
|
+
continue # currently only "bytes" is emitted
|
|
186
197
|
name = f["name"]
|
|
187
198
|
size = f["size"]
|
|
188
|
-
mime = f["type"]
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# Decode the file content
|
|
192
|
-
b64 = data_uri.split(",", 1)[1]
|
|
193
|
-
content = base64.b64decode(b64)
|
|
199
|
+
mime = f["type"] # e.g. "image/png"
|
|
200
|
+
content = f["data"] # bytes
|
|
194
201
|
print(f"Received {name}: {len(content)} bytes")
|
|
195
202
|
|
|
196
203
|
widget.on("drop-end", on_drop)
|
|
@@ -210,8 +217,7 @@ Example: File Drop Zone
|
|
|
210
217
|
|
|
211
218
|
def on_drop(payload):
|
|
212
219
|
f = payload["files"][0]
|
|
213
|
-
|
|
214
|
-
text = base64.b64decode(b64).decode("utf-8", errors="replace")
|
|
220
|
+
text = f["data"].decode("utf-8", errors="replace")
|
|
215
221
|
textarea.set_text(text)
|
|
216
222
|
drop_label.set_text(f"Loaded: {f['name']}")
|
|
217
223
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Utilities
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
The ``Widget`` base class exposes a few static utility methods that are
|
|
5
|
+
useful when working with local files on the server side.
|
|
6
|
+
|
|
7
|
+
.. code-block:: python
|
|
8
|
+
|
|
9
|
+
from pgwidgets.sync.widget import Widget # or pgwidgets.async_.widget
|
|
10
|
+
|
|
11
|
+
to_data_uri
|
|
12
|
+
------------
|
|
13
|
+
|
|
14
|
+
.. code-block:: python
|
|
15
|
+
|
|
16
|
+
Widget.to_data_uri(path)
|
|
17
|
+
|
|
18
|
+
Read a local file, base64-encode its contents, and return a
|
|
19
|
+
``data:<mime>;base64,…`` string. The MIME type is guessed from the file
|
|
20
|
+
extension (falls back to ``application/octet-stream``).
|
|
21
|
+
|
|
22
|
+
The returned string can be passed directly to any widget method that
|
|
23
|
+
expects a URL, such as ``set_image()``, ``set_icon()``, or
|
|
24
|
+
``add_cursor()``.
|
|
25
|
+
|
|
26
|
+
.. code-block:: python
|
|
27
|
+
|
|
28
|
+
# Load a local image into an Image widget
|
|
29
|
+
img.set_image(Widget.to_data_uri("/path/to/photo.png"))
|
|
30
|
+
|
|
31
|
+
# Set a button icon from a local file
|
|
32
|
+
btn.set_icon(Widget.to_data_uri("/path/to/icon.svg"))
|
|
33
|
+
|
|
34
|
+
.. note::
|
|
35
|
+
|
|
36
|
+
Methods listed in the ``_FILE_ARG_METHODS`` set (currently
|
|
37
|
+
``set_icon``, ``set_image``, and ``set_icon_gutter``) already detect
|
|
38
|
+
local file paths and convert them automatically. Use
|
|
39
|
+
``to_data_uri()`` explicitly when you need the data URI for other
|
|
40
|
+
purposes or want to cache/reuse it.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Buffer
|
|
44
|
+
------
|
|
45
|
+
|
|
46
|
+
.. code-block:: python
|
|
47
|
+
|
|
48
|
+
from pgwidgets import Buffer
|
|
49
|
+
|
|
50
|
+
pixels = numpy_array.tobytes() # 2048 * 2048 * 4 bytes
|
|
51
|
+
buf = Buffer(pixels,
|
|
52
|
+
shape=(2048, 2048, 4),
|
|
53
|
+
dtype="uint8")
|
|
54
|
+
viewer.load_buffer(buf, [2048, 2048], cache)
|
|
55
|
+
|
|
56
|
+
A ``Buffer`` wraps raw ``bytes`` with ``shape`` and ``dtype``
|
|
57
|
+
metadata, so a method receiving the buffer on the JavaScript side
|
|
58
|
+
gets a properly-sized typed array (``Uint8Array``, ``Float32Array``,
|
|
59
|
+
…) instead of a raw ``ArrayBuffer``. Use it for image pixel data,
|
|
60
|
+
scientific arrays, vertex buffers — anything where the receiver
|
|
61
|
+
needs to know how to interpret the bytes without a hand-rolled
|
|
62
|
+
convention per method.
|
|
63
|
+
|
|
64
|
+
A ``Buffer`` always ships via the chunked binary transport
|
|
65
|
+
(``binary-call-chunked`` + ``binary-chunk`` messages), so multi-
|
|
66
|
+
megabyte payloads stream in 512 KiB chunks and don't block other
|
|
67
|
+
WebSocket traffic. ``shape`` and ``dtype`` ride on the announce
|
|
68
|
+
header — see :doc:`architecture`.
|
|
69
|
+
|
|
70
|
+
Supported dtypes:
|
|
71
|
+
|
|
72
|
+
- ``"uint8"`` / ``"uint16"`` / ``"uint32"``
|
|
73
|
+
- ``"int8"`` / ``"int16"`` / ``"int32"``
|
|
74
|
+
- ``"float32"`` / ``"float64"``
|
|
75
|
+
|
|
76
|
+
Methods that don't opt into ``Buffer`` keep receiving plain
|
|
77
|
+
``bytes`` — :class:`Buffer` is purely additive. Construction
|
|
78
|
+
validates that the byte length equals
|
|
79
|
+
``prod(shape) * dtype.itemsize``; pass ``shape=(len(data),)`` and
|
|
80
|
+
``dtype="uint8"`` if you just want a flat byte buffer with a
|
|
81
|
+
known length.
|
|
@@ -140,6 +140,17 @@ Grid layout.
|
|
|
140
140
|
``append_column(widgets)``, ``delete_column(index)``
|
|
141
141
|
- **Callbacks:** ``child-added``, ``child-removed``
|
|
142
142
|
|
|
143
|
+
FixedLayout
|
|
144
|
+
~~~~~~~~~~~
|
|
145
|
+
|
|
146
|
+
Absolute-positioning container. Each child is placed at a fixed
|
|
147
|
+
``(x, y)`` pixel offset within the container and renders at its
|
|
148
|
+
natural size unless ``resize()`` has been called on it.
|
|
149
|
+
|
|
150
|
+
- **Options:** *(none)*
|
|
151
|
+
- **Methods:** ``add_widget(child, x, y)``
|
|
152
|
+
- **Callbacks:** ``child-added``, ``child-removed``
|
|
153
|
+
|
|
143
154
|
Splitter
|
|
144
155
|
~~~~~~~~
|
|
145
156
|
|
|
@@ -415,24 +426,58 @@ TextSource
|
|
|
415
426
|
|
|
416
427
|
Source code editor with line numbers, syntax tags, and gutter icons.
|
|
417
428
|
|
|
429
|
+
Positions in the buffer are expressed as ``TextBufferRef`` instances —
|
|
430
|
+
live references that track edits. ``create_ref(offset, gravity)`` is
|
|
431
|
+
the only place a caller deals in raw integer offsets; all other
|
|
432
|
+
position-taking and position-returning methods on the public API use
|
|
433
|
+
refs.
|
|
434
|
+
|
|
435
|
+
.. note::
|
|
436
|
+
|
|
437
|
+
Refs are JS-side live objects. When using ``TextSource`` over the
|
|
438
|
+
remote interface (pgwidgets-python proper), refs cannot currently
|
|
439
|
+
round-trip across the wire — a ref-handle protocol is planned for
|
|
440
|
+
a future release. In the meantime, the ref-based API is fully
|
|
441
|
+
usable from JS-direct and pyodide contexts.
|
|
442
|
+
|
|
418
443
|
- **Args:** ``text``
|
|
419
444
|
- **Options:** ``wrap``, ``line_numbers``, ``icon_gutter``, ``editable``,
|
|
420
445
|
``font_family``, ``font_size``
|
|
421
446
|
- **Methods:** ``set_text(text)``, ``get_text()``, ``get_length()``,
|
|
422
|
-
``
|
|
447
|
+
``get_text_range(start_ref, end_ref)``,
|
|
448
|
+
``insert_text(ref, text, tags)``, ``delete_range(start_ref, end_ref)``,
|
|
423
449
|
``clear()``, ``set_editable(tf)``, ``set_wrap(mode)``,
|
|
424
450
|
``set_line_numbers(tf)``, ``set_icon_gutter(tf)``,
|
|
425
|
-
``set_icon(
|
|
426
|
-
``
|
|
451
|
+
``set_icon(ref, icon_url)``, ``get_cursor()``, ``set_cursor(ref)``,
|
|
452
|
+
``get_selection_range()``, ``set_selection_range(start_ref, end_ref)``,
|
|
427
453
|
``create_tag(name, attrs)``, ``remove_tag_def(name)``,
|
|
428
|
-
``
|
|
429
|
-
``
|
|
430
|
-
``
|
|
431
|
-
``
|
|
432
|
-
``
|
|
454
|
+
``has_tag(name)``,
|
|
455
|
+
``apply_tag(name, start_ref, end_ref)``,
|
|
456
|
+
``remove_tag(name, start_ref, end_ref)``,
|
|
457
|
+
``get_tags_at(ref)``, ``get_tags_range(start_ref, end_ref)``,
|
|
458
|
+
``create_ref(offset, gravity)``, ``remove_ref(ref)``,
|
|
459
|
+
``create_named_ref(name, offset, gravity)``,
|
|
460
|
+
``get_named_ref(name)``, ``remove_named_ref(name)``,
|
|
461
|
+
``get_ref_start()``, ``get_ref_end()``, ``get_ref_bounds()``,
|
|
462
|
+
``get_ref_line_start(lineno)``, ``get_ref_line_end(lineno)``,
|
|
463
|
+
``undo()``, ``redo()``, ``can_undo()``, ``can_redo()``,
|
|
464
|
+
``find(query, opts)``, ``find_all(query, opts)``,
|
|
465
|
+
``replace(query, replacement, opts)``, ``scroll_to_ref(ref)``,
|
|
433
466
|
``scroll_to_cursor()``
|
|
434
|
-
- **Callbacks:** ``changed``, ``cursor_moved
|
|
435
|
-
``icon_clicked``
|
|
467
|
+
- **Callbacks:** ``changed``, ``cursor_moved`` (fires with a fresh
|
|
468
|
+
ref), ``line_clicked``, ``icon_clicked`` (fires with
|
|
469
|
+
``(line, ref)``)
|
|
470
|
+
|
|
471
|
+
A ``TextBufferRef`` itself supports the following methods:
|
|
472
|
+
|
|
473
|
+
- **Inspection:** ``get_offset()``, ``get_gravity()``, ``is_valid()``,
|
|
474
|
+
``get_line()``, ``get_line_column()``
|
|
475
|
+
- **Absolute position:** ``set_offset(offset)``, ``set_line(lineno)``,
|
|
476
|
+
``to_ref(other)``, ``copy()``
|
|
477
|
+
- **Relative movement:** ``to_line_start()``, ``to_line_end()``,
|
|
478
|
+
``to_next_line()``, ``to_prev_line()``, ``to_next_char()``,
|
|
479
|
+
``to_prev_char()`` (movement methods clamp at buffer boundaries
|
|
480
|
+
and are no-ops past them; mutating an invalidated ref raises)
|
|
436
481
|
|
|
437
482
|
Selectors
|
|
438
483
|
---------
|
|
@@ -551,6 +596,12 @@ are pushed per second. ``format`` is one of ``'jpeg'``, ``'png'``,
|
|
|
551
596
|
The latest frame is also stored in widget state so it is replayed
|
|
552
597
|
on reconnect.
|
|
553
598
|
|
|
599
|
+
Payloads larger than ~1 MiB automatically use the *chunked* binary
|
|
600
|
+
transport (``binary-call-chunked`` + per-chunk ``binary-chunk``
|
|
601
|
+
messages) so the WebSocket can interleave control traffic while a
|
|
602
|
+
large frame streams. No API change for callers — pass ``bytes``
|
|
603
|
+
and the framework picks the right transport.
|
|
604
|
+
|
|
554
605
|
Canvas
|
|
555
606
|
~~~~~~
|
|
556
607
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Flask multi-process pgwidgets demo
|
|
2
|
+
|
|
3
|
+
One pgwidgets `Application` per visitor, each in a fresh OS process
|
|
4
|
+
with its own WebSocket port.
|
|
5
|
+
|
|
6
|
+
- **`server.py`** — Flask front-end. Each request to `/` spawns a
|
|
7
|
+
child process running a fresh `Application`, waits for the child
|
|
8
|
+
to report its WebSocket port through a `multiprocessing.Queue`,
|
|
9
|
+
then returns an HTML page whose `RemoteInterface` connects back
|
|
10
|
+
to it.
|
|
11
|
+
- **`user_app.py`** — The per-visitor `Application` definition.
|
|
12
|
+
The child itself binds an ephemeral TCP socket, hands it directly
|
|
13
|
+
to `Application(ws_sock=...)` (so the port is held continuously —
|
|
14
|
+
no race with another process), and reports the resulting port
|
|
15
|
+
back through the queue. Edit the body of `setup(session)` to
|
|
16
|
+
build your UI. Can also be run standalone (`python user_app.py`)
|
|
17
|
+
for quick UI debugging without Flask in the loop; the port is
|
|
18
|
+
auto-allocated and logged.
|
|
19
|
+
|
|
20
|
+
## Run
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
pip install Flask pgwidgets-python pgwidgets-js
|
|
24
|
+
python server.py
|
|
25
|
+
# browse to http://localhost:5000/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every browser tab triggers a brand-new process. When the tab is
|
|
29
|
+
closed (or the browser navigates away), the child waits a grace
|
|
30
|
+
period — `IDLE_GRACE_SECONDS` in `user_app.py`, default 5 minutes —
|
|
31
|
+
to absorb page refreshes, brief network drops, and the user
|
|
32
|
+
closing/reopening a laptop, then exits. Reconnecting within the
|
|
33
|
+
grace window cancels the pending shutdown.
|
|
34
|
+
|
|
35
|
+
### Listening on other interfaces
|
|
36
|
+
|
|
37
|
+
By default Flask and each per-visitor WebSocket bind to
|
|
38
|
+
``127.0.0.1`` (loopback only). To accept connections from other
|
|
39
|
+
machines:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
python server.py --host 0.0.0.0 --port 8080
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The bind interface and the WebSocket URL are decoupled — the
|
|
46
|
+
browser is given a ``ws://`` URL derived from the same hostname it
|
|
47
|
+
used to reach Flask, so the same server can be reached via
|
|
48
|
+
``localhost``, by IP address, or by DNS name with no further
|
|
49
|
+
configuration.
|
|
50
|
+
|
|
51
|
+
## What the browser loads
|
|
52
|
+
|
|
53
|
+
Each HTML response embeds the per-visitor WebSocket URL. The
|
|
54
|
+
pgwidgets-js bundle (`Widgets.js`, `Widgets.css`, the `modules/`
|
|
55
|
+
subtree, icons) is served by Flask itself, from the pip-installed
|
|
56
|
+
`pgwidgets-js` Python package — `pgwidgets_js.get_static_path()`
|
|
57
|
+
returns the directory and a `/pgwidgets-js/<path>` route in
|
|
58
|
+
`server.py` exposes it. No CDN, no network dependency at runtime,
|
|
59
|
+
and the version on the wire always matches the one in your venv.
|
|
60
|
+
|
|
61
|
+
To point at an in-progress working copy of pgwidgets-js instead of
|
|
62
|
+
the installed package, either ``pip install -e .`` from the
|
|
63
|
+
pgwidgets-js source tree (so `get_static_path()` resolves to the
|
|
64
|
+
working copy), or replace `PGWIDGETS_JS_ROOT` in `server.py` with
|
|
65
|
+
a hard path to that tree's `static/` directory.
|
|
66
|
+
|
|
67
|
+
## Caveats
|
|
68
|
+
|
|
69
|
+
- **Flask debug mode is off.** The reloader spawns a second
|
|
70
|
+
process, which combined with `multiprocessing` can produce
|
|
71
|
+
surprising behavior on Windows / macOS. Restart `server.py` by
|
|
72
|
+
hand after edits.
|