pgwidgets-python 0.2.3__tar.gz → 0.3.1__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.3 → pgwidgets_python-0.3.1}/PKG-INFO +1 -1
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/WhatsNew.rst +179 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/architecture.rst +3 -1
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/async.rst +23 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/callbacks.rst +19 -13
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/sync.rst +46 -0
- pgwidgets_python-0.3.1/docs/utilities.rst +81 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/web-servers.rst +8 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/widgets.rst +6 -0
- pgwidgets_python-0.3.1/examples/flask-multi-process/README.md +262 -0
- pgwidgets_python-0.3.1/examples/flask-multi-process/gunicorn.conf.py +76 -0
- pgwidgets_python-0.3.1/examples/flask-multi-process/nginx.conf +109 -0
- pgwidgets_python-0.3.1/examples/flask-multi-process/server.py +387 -0
- pgwidgets_python-0.3.1/examples/flask-multi-process/user_app.py +268 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/__init__.py +2 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/async_/application.py +295 -31
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/async_/widget.py +9 -1
- pgwidgets_python-0.3.1/pgwidgets/buffer.py +125 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/extras/file_browser.py +7 -2
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/method_types.py +45 -2
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/sync/application.py +378 -30
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/sync/widget.py +9 -1
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/PKG-INFO +1 -1
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/SOURCES.txt +8 -0
- pgwidgets_python-0.3.1/tests/test_fonts.py +173 -0
- pgwidgets_python-0.3.1/tests/test_resolve_kwargs.py +138 -0
- pgwidgets_python-0.2.3/docs/utilities.rst +0 -40
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/.flake8 +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/.github/workflows/tests.yml +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/.gitignore +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/.readthedocs.yaml +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/LICENSE.md +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/README.md +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/Makefile +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/api/async.rst +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/api/index.rst +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/api/sync.rst +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/conf.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/extras.rst +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/getting-started.rst +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/index.rst +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/subclassing.rst +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/README.md +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/all_widgets.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/all_widgets_async.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/demo_async.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/demo_sync.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/demo_treeview.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/_json.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/async_/Widgets.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/async_/__init__.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/callbacks.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/defs.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/extras/__init__.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/sync/Widgets.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/sync/__init__.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/requires.txt +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/top_level.txt +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pyproject.toml +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/setup.cfg +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/test_pg2.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/__init__.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_defs.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_extras.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_protocol.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_reconstruct.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_session.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_stateful.py +0 -0
- {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_widget_classes.py +0 -0
|
@@ -1,6 +1,185 @@
|
|
|
1
1
|
What's New
|
|
2
2
|
==========
|
|
3
3
|
|
|
4
|
+
Recent changes — since ``v0.3.0``
|
|
5
|
+
---------------------------------
|
|
6
|
+
|
|
7
|
+
Custom fonts: ``Application.register_font`` / ``set_default_font``
|
|
8
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
9
|
+
|
|
10
|
+
Two new methods on ``Application`` let the Python side ship
|
|
11
|
+
custom font files to the browser and apply a document-wide
|
|
12
|
+
default font.
|
|
13
|
+
|
|
14
|
+
.. code-block:: python
|
|
15
|
+
|
|
16
|
+
app.register_font('Roboto', '/path/to/Roboto-Regular.ttf')
|
|
17
|
+
app.register_font('Roboto', '/path/to/Roboto-Bold.ttf',
|
|
18
|
+
weight='bold')
|
|
19
|
+
app.set_default_font('Roboto', size=13)
|
|
20
|
+
|
|
21
|
+
* ``register_font(family, source, *, weight, style)`` -- stores
|
|
22
|
+
the font bytes in an in-memory registry keyed by a monotonic
|
|
23
|
+
id; the built-in HTTP server exposes them at
|
|
24
|
+
``/_pgwidgets/font/<id>`` with ``Cache-Control: immutable``.
|
|
25
|
+
Accepts paths (``str`` / ``os.PathLike``) or raw ``bytes``.
|
|
26
|
+
* ``set_default_font(family, *, size, weight, style)`` --
|
|
27
|
+
writes ``--pg-default-font-{family,size,weight,style}`` CSS
|
|
28
|
+
variables on ``:root`` via a managed ``<style>`` element on
|
|
29
|
+
the JS side. The base widget stylesheet consumes them with
|
|
30
|
+
safe fallbacks so apps that never opt in see no change in
|
|
31
|
+
rendering.
|
|
32
|
+
|
|
33
|
+
Both methods broadcast to every connected session immediately
|
|
34
|
+
and replay the entire registry to every new / reconnecting
|
|
35
|
+
session before ``on_connect`` / ``reconstruct`` runs. That
|
|
36
|
+
guarantees a widget that's reconstructed with
|
|
37
|
+
``set_font(family, ...)`` always finds the face already
|
|
38
|
+
declared on the JS side, even after a full page reload.
|
|
39
|
+
|
|
40
|
+
The JS handler normalises descriptive TTF weight names
|
|
41
|
+
(``thin``, ``light``, ``medium``, ``semibold``, ``extrabold``,
|
|
42
|
+
``black``, ``heavy``, ...) to the numeric CSS values per the
|
|
43
|
+
CSS Fonts spec, so a font registry that knows weights by their
|
|
44
|
+
metadata name (e.g. Ginga's ``font_asst``) can pass weights
|
|
45
|
+
straight through.
|
|
46
|
+
|
|
47
|
+
The matching sync- and async-backend API are identical; the
|
|
48
|
+
async variant schedules sends via
|
|
49
|
+
``asyncio.run_coroutine_threadsafe`` so ``register_font`` is
|
|
50
|
+
callable from outside the event loop.
|
|
51
|
+
|
|
52
|
+
See :doc:`sync` / :doc:`async` for the full API description.
|
|
53
|
+
|
|
54
|
+
Flask multi-process example
|
|
55
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
56
|
+
|
|
57
|
+
``examples/flask-multi-process`` is a new end-to-end demo of
|
|
58
|
+
running pgwidgets-python behind Flask + gunicorn + nginx, with
|
|
59
|
+
one OS process per browser session. Highlights:
|
|
60
|
+
|
|
61
|
+
* ``Application`` accepts ``ws_sock=`` and a session can be
|
|
62
|
+
pre-warmed in the worker process before the browser opens
|
|
63
|
+
its WebSocket -- the worker spawns, runs the user-app's
|
|
64
|
+
``build_ui`` against an empty session, and then waits for
|
|
65
|
+
the WS handshake.
|
|
66
|
+
* Session-aware routing so a Flask front-end can hand each
|
|
67
|
+
browser tab off to a dedicated worker by session ID.
|
|
68
|
+
* Dead-child reaping (``waitpid(WNOHANG)``) to avoid zombie
|
|
69
|
+
worker processes piling up.
|
|
70
|
+
* The user app is a Python class (``app.build_ui(session)``),
|
|
71
|
+
so worker pre-warming, multi-tab fanout, and reconstruction
|
|
72
|
+
reuse the same entry point.
|
|
73
|
+
* The gunicorn / nginx layer serves the bundled pgwidgets-js
|
|
74
|
+
static assets from the pip-installed package (no separate
|
|
75
|
+
jsdelivr fetch needed).
|
|
76
|
+
|
|
77
|
+
See ``examples/flask-multi-process/README.md`` for the full
|
|
78
|
+
production stack walkthrough.
|
|
79
|
+
|
|
80
|
+
file_browser: ``col_key`` on row-activated
|
|
81
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
82
|
+
|
|
83
|
+
``pgwidgets.extras.file_browser`` matches the JS-side
|
|
84
|
+
TreeView/TableView ``activated`` callback signature change:
|
|
85
|
+
``FileBrowser._on_row_activated`` now receives ``col_key`` as
|
|
86
|
+
the 4th argument, so handlers that double-click a file can see
|
|
87
|
+
which column they hit. Existing handlers (3-arg) keep working.
|
|
88
|
+
|
|
89
|
+
``_resolve_kwargs``: skipped-positional kwargs
|
|
90
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
91
|
+
|
|
92
|
+
The kwarg resolver used by every generated widget method now
|
|
93
|
+
allows a caller to skip a positional argument and supply the
|
|
94
|
+
next one by keyword. For example, with a method declared as
|
|
95
|
+
``set_color(bg=None, fg=None)``, callers may now write
|
|
96
|
+
``label.set_color(fg="red")`` directly instead of
|
|
97
|
+
``label.set_color(None, "red")``. Previously the resolver
|
|
98
|
+
would reject the keyword-only form for parameters that
|
|
99
|
+
preceded any supplied keyword.
|
|
100
|
+
|
|
101
|
+
method_types: TreeView colour + cell-selection methods classified as ACTION
|
|
102
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
103
|
+
|
|
104
|
+
Internal: the four ``set_*_color`` overrides and the new
|
|
105
|
+
``select_cell`` / ``select_cells`` / ``clear_cell_selection``
|
|
106
|
+
methods are now ACTION-typed, so the wrapper layer dispatches
|
|
107
|
+
each call straight through to the JS side instead of trying to
|
|
108
|
+
collapse them into a single ``_state`` slot. Matters for
|
|
109
|
+
reconstruction-replay; rarely matters for application code.
|
|
110
|
+
|
|
111
|
+
----
|
|
112
|
+
|
|
113
|
+
Recent changes — since ``v0.2.3``
|
|
114
|
+
---------------------------------
|
|
115
|
+
|
|
116
|
+
Chunked binary transport (both directions)
|
|
117
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
118
|
+
|
|
119
|
+
``Session`` gained ``_send_binary_chunked(wid, method, args, data,
|
|
120
|
+
chunk_size=512KB, shape=None, dtype=None)`` and a matching
|
|
121
|
+
incoming-binary path. Large payloads automatically use the
|
|
122
|
+
chunked transport — :meth:`Image.set_binary_image` (and the
|
|
123
|
+
reconstruction replay of binary state) auto-pick chunked above a
|
|
124
|
+
1 MiB threshold. ``Session._handle_file_chunk`` was rewritten as
|
|
125
|
+
``_handle_binary_chunk`` to consume the new binary-chunk
|
|
126
|
+
envelope: chunks store by ``chunk_index`` (robust to ordering)
|
|
127
|
+
and reassemble as ``bytes``.
|
|
128
|
+
|
|
129
|
+
**Pre-1.0 API break**: drop-end / FileDialog ``activated``
|
|
130
|
+
handlers now receive each file's ``data`` field as raw ``bytes``
|
|
131
|
+
(was a ``"data:<mime>;base64,…"`` string). Each file dict now
|
|
132
|
+
also carries an ``encoding`` field (currently ``"bytes"``;
|
|
133
|
+
reserved for future ``"base64"``).
|
|
134
|
+
|
|
135
|
+
:class:`pgwidgets.Buffer` for typed-array delivery
|
|
136
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
137
|
+
|
|
138
|
+
New :class:`pgwidgets.Buffer` wraps raw bytes with ``shape`` and
|
|
139
|
+
``dtype`` metadata. When passed to a binary-aware method, the
|
|
140
|
+
bytes ride the chunked transport and the receiver on the JS side
|
|
141
|
+
gets a typed array of the right shape (``Uint8Array``,
|
|
142
|
+
``Float32Array``, …) without the per-method casting that used to
|
|
143
|
+
be necessary::
|
|
144
|
+
|
|
145
|
+
from pgwidgets import Buffer
|
|
146
|
+
pixels = ... # 2048 * 2048 * 4 bytes
|
|
147
|
+
viewer.load_buffer(
|
|
148
|
+
Buffer(pixels, shape=(2048, 2048, 4), dtype="uint8"),
|
|
149
|
+
[2048, 2048], cache)
|
|
150
|
+
|
|
151
|
+
Supported dtypes: ``uint8`` / ``uint16`` / ``uint32`` / ``int8``
|
|
152
|
+
/ ``int16`` / ``int32`` / ``float32`` / ``float64``.
|
|
153
|
+
Construction validates that ``len(data) == prod(shape) *
|
|
154
|
+
itemsize``.
|
|
155
|
+
|
|
156
|
+
``Application(ws_sock=…)`` for race-free port allocation
|
|
157
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
158
|
+
|
|
159
|
+
:class:`Application` accepts an optional pre-bound TCP socket via
|
|
160
|
+
``ws_sock=``. When provided, ``Application`` adopts the socket
|
|
161
|
+
directly (hands it to ``websockets.serve(sock=...)``) instead of
|
|
162
|
+
binding ``(host, ws_port)`` itself, and reads ``ws_port`` back
|
|
163
|
+
from the socket for logging. The motivation is TOCTOU-free port
|
|
164
|
+
allocation: the caller can bind in advance, hand the socket in,
|
|
165
|
+
and never release the port between "find" and "use" — no other
|
|
166
|
+
process can grab it in the gap. The existing ``ws_port=`` API
|
|
167
|
+
is unchanged.
|
|
168
|
+
|
|
169
|
+
Flask multi-process demo
|
|
170
|
+
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
171
|
+
|
|
172
|
+
New ``examples/flask-multi-process/`` example: one pgwidgets
|
|
173
|
+
``Application`` per visitor, each in its own OS process with its
|
|
174
|
+
own WebSocket port. Demonstrates the new ``ws_sock=`` parameter
|
|
175
|
+
(child binds, never releases), idle reaping via a grace timer on
|
|
176
|
+
``app.on_disconnect`` that handles the subtle case where
|
|
177
|
+
``app.on_connect`` doesn't fire on session-reconnect (the wrapper
|
|
178
|
+
also hooks ``session.add_connection``), and serving pgwidgets-js's
|
|
179
|
+
static assets from the pip-installed package rather than a CDN.
|
|
180
|
+
|
|
181
|
+
----
|
|
182
|
+
|
|
4
183
|
Recent changes — since ``v0.2.1``
|
|
5
184
|
---------------------------------
|
|
6
185
|
|
|
@@ -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
|
-------------
|
|
@@ -56,6 +56,29 @@ Running
|
|
|
56
56
|
|
|
57
57
|
``await app.close()`` shuts down all sessions and causes ``run()`` to return.
|
|
58
58
|
|
|
59
|
+
Custom fonts
|
|
60
|
+
~~~~~~~~~~~~
|
|
61
|
+
|
|
62
|
+
Same ``register_font`` / ``set_default_font`` API as the sync
|
|
63
|
+
backend (see :doc:`sync`); both are plain sync methods that
|
|
64
|
+
schedule the broadcast onto the event loop via
|
|
65
|
+
``asyncio.run_coroutine_threadsafe``, so they're safe to call
|
|
66
|
+
from both inside and outside the loop.
|
|
67
|
+
|
|
68
|
+
.. code-block:: python
|
|
69
|
+
|
|
70
|
+
app.register_font('Roboto', '/path/to/Roboto-Regular.ttf')
|
|
71
|
+
app.register_font('Roboto', '/path/to/Roboto-Bold.ttf',
|
|
72
|
+
weight='bold')
|
|
73
|
+
app.set_default_font('Roboto', size=13)
|
|
74
|
+
|
|
75
|
+
Bytes are kept in memory on the ``Application`` and served by
|
|
76
|
+
the built-in HTTP server at ``/_pgwidgets/font/<id>``. Fonts
|
|
77
|
+
are replayed to every new and reconnecting session before
|
|
78
|
+
``on_connect`` / ``reconstruct`` runs, so widgets reconstructed
|
|
79
|
+
with ``set_font(family, ...)`` always find the face already
|
|
80
|
+
declared.
|
|
81
|
+
|
|
59
82
|
Session
|
|
60
83
|
-------
|
|
61
84
|
|
|
@@ -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
|
|
|
@@ -71,6 +71,52 @@ Running
|
|
|
71
71
|
|
|
72
72
|
``app.close()`` shuts down all sessions and stops the servers.
|
|
73
73
|
|
|
74
|
+
Custom fonts
|
|
75
|
+
~~~~~~~~~~~~
|
|
76
|
+
|
|
77
|
+
Two methods on ``Application`` push custom font files to the
|
|
78
|
+
browser and apply a document-wide default font. Both broadcast
|
|
79
|
+
to every connected session immediately, and replay the entire
|
|
80
|
+
registry + default font to every new / reconnecting session
|
|
81
|
+
*before* ``on_connect`` / ``reconstruct`` runs -- so any widget
|
|
82
|
+
that takes ``set_font(family, ...)`` always finds the face
|
|
83
|
+
already declared on the JS side.
|
|
84
|
+
|
|
85
|
+
.. code-block:: python
|
|
86
|
+
|
|
87
|
+
# Register one or more faces for a family
|
|
88
|
+
app.register_font('Roboto', '/path/to/Roboto-Regular.ttf')
|
|
89
|
+
app.register_font('Roboto', '/path/to/Roboto-Bold.ttf',
|
|
90
|
+
weight='bold')
|
|
91
|
+
app.register_font('Roboto', '/path/to/Roboto-Italic.ttf',
|
|
92
|
+
style='italic')
|
|
93
|
+
|
|
94
|
+
# Apply a document-wide default
|
|
95
|
+
app.set_default_font('Roboto', size=13)
|
|
96
|
+
|
|
97
|
+
# Later use it on a single widget (or anywhere CSS sees it)
|
|
98
|
+
label.set_font('Roboto', 14)
|
|
99
|
+
|
|
100
|
+
* ``source`` is a path (``str`` / ``os.PathLike``) or raw
|
|
101
|
+
``bytes``. Accepted formats: ``.ttf``, ``.otf``, ``.woff``,
|
|
102
|
+
``.woff2`` -- the Content-Type is set from the extension.
|
|
103
|
+
* ``weight`` accepts CSS keywords, numeric strings, *and*
|
|
104
|
+
common TTF metadata names (``thin``, ``light``, ``medium``,
|
|
105
|
+
``semibold``, ``extrabold``, ``black``, ``heavy``, etc.) --
|
|
106
|
+
the JS handler normalises descriptive names to numeric CSS
|
|
107
|
+
values per the CSS Fonts spec.
|
|
108
|
+
* ``style`` accepts ``'normal'``, ``'italic'``, or
|
|
109
|
+
``'oblique'``; synonyms (``'roman'``, ``'slanted'``) are
|
|
110
|
+
normalised too.
|
|
111
|
+
* ``set_default_font(family=None)`` clears the override.
|
|
112
|
+
|
|
113
|
+
Bytes are served from ``/_pgwidgets/font/<id>`` by the built-in
|
|
114
|
+
HTTP server with ``Cache-Control: immutable``; behind Flask /
|
|
115
|
+
nginx the same route is served from the Python process.
|
|
116
|
+
|
|
117
|
+
See also the JS-side write-up at :ref:`custom-fonts` in the
|
|
118
|
+
pgwidgets-js documentation.
|
|
119
|
+
|
|
74
120
|
Session
|
|
75
121
|
-------
|
|
76
122
|
|
|
@@ -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.
|
|
@@ -276,6 +276,14 @@ Flask
|
|
|
276
276
|
if __name__ == "__main__":
|
|
277
277
|
web.run(host="0.0.0.0", port=8000)
|
|
278
278
|
|
|
279
|
+
For a production-style stack -- gunicorn workers, an nginx
|
|
280
|
+
reverse proxy, one OS process per browser session, and
|
|
281
|
+
session-aware routing -- see ``examples/flask-multi-process``.
|
|
282
|
+
That example also demonstrates pre-warming the per-session
|
|
283
|
+
worker with a Python-class user app before the browser opens
|
|
284
|
+
its WebSocket, and reaping dead child workers so zombies don't
|
|
285
|
+
accumulate.
|
|
286
|
+
|
|
279
287
|
|
|
280
288
|
Apache
|
|
281
289
|
------
|
|
@@ -596,6 +596,12 @@ are pushed per second. ``format`` is one of ``'jpeg'``, ``'png'``,
|
|
|
596
596
|
The latest frame is also stored in widget state so it is replayed
|
|
597
597
|
on reconnect.
|
|
598
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
|
+
|
|
599
605
|
Canvas
|
|
600
606
|
~~~~~~
|
|
601
607
|
|