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.
Files changed (70) hide show
  1. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/PKG-INFO +1 -1
  2. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/WhatsNew.rst +179 -0
  3. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/architecture.rst +3 -1
  4. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/async.rst +23 -0
  5. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/callbacks.rst +19 -13
  6. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/sync.rst +46 -0
  7. pgwidgets_python-0.3.1/docs/utilities.rst +81 -0
  8. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/web-servers.rst +8 -0
  9. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/widgets.rst +6 -0
  10. pgwidgets_python-0.3.1/examples/flask-multi-process/README.md +262 -0
  11. pgwidgets_python-0.3.1/examples/flask-multi-process/gunicorn.conf.py +76 -0
  12. pgwidgets_python-0.3.1/examples/flask-multi-process/nginx.conf +109 -0
  13. pgwidgets_python-0.3.1/examples/flask-multi-process/server.py +387 -0
  14. pgwidgets_python-0.3.1/examples/flask-multi-process/user_app.py +268 -0
  15. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/__init__.py +2 -0
  16. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/async_/application.py +295 -31
  17. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/async_/widget.py +9 -1
  18. pgwidgets_python-0.3.1/pgwidgets/buffer.py +125 -0
  19. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/extras/file_browser.py +7 -2
  20. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/method_types.py +45 -2
  21. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/sync/application.py +378 -30
  22. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/sync/widget.py +9 -1
  23. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/PKG-INFO +1 -1
  24. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/SOURCES.txt +8 -0
  25. pgwidgets_python-0.3.1/tests/test_fonts.py +173 -0
  26. pgwidgets_python-0.3.1/tests/test_resolve_kwargs.py +138 -0
  27. pgwidgets_python-0.2.3/docs/utilities.rst +0 -40
  28. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/.flake8 +0 -0
  29. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/.github/workflows/tests.yml +0 -0
  30. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/.gitignore +0 -0
  31. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/.readthedocs.yaml +0 -0
  32. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/LICENSE.md +0 -0
  33. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/README.md +0 -0
  34. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/Makefile +0 -0
  35. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/api/async.rst +0 -0
  36. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/api/index.rst +0 -0
  37. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/api/sync.rst +0 -0
  38. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/conf.py +0 -0
  39. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/extras.rst +0 -0
  40. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/getting-started.rst +0 -0
  41. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/index.rst +0 -0
  42. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/docs/subclassing.rst +0 -0
  43. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/README.md +0 -0
  44. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/all_widgets.py +0 -0
  45. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/all_widgets_async.py +0 -0
  46. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/demo_async.py +0 -0
  47. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/demo_sync.py +0 -0
  48. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/examples/demo_treeview.py +0 -0
  49. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/_json.py +0 -0
  50. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/async_/Widgets.py +0 -0
  51. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/async_/__init__.py +0 -0
  52. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/callbacks.py +0 -0
  53. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/defs.py +0 -0
  54. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/extras/__init__.py +0 -0
  55. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/sync/Widgets.py +0 -0
  56. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets/sync/__init__.py +0 -0
  57. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
  58. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/requires.txt +0 -0
  59. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pgwidgets_python.egg-info/top_level.txt +0 -0
  60. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/pyproject.toml +0 -0
  61. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/setup.cfg +0 -0
  62. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/test_pg2.py +0 -0
  63. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/__init__.py +0 -0
  64. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_defs.py +0 -0
  65. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_extras.py +0 -0
  66. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_protocol.py +0 -0
  67. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_reconstruct.py +0 -0
  68. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_session.py +0 -0
  69. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_stateful.py +0 -0
  70. {pgwidgets_python-0.2.3 → pgwidgets_python-0.3.1}/tests/test_widget_classes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgwidgets-python
3
- Version: 0.2.3
3
+ Version: 0.3.1
4
4
  Summary: Python bindings for the pgwidgets JavaScript widget library
5
5
  Author: PGWidgets Developers
6
6
  License: BSD-3-Clause
@@ -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": "file-chunk", ...}`` -- chunked file data (see :doc:`callbacks`).
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 ``file-chunk`` messages with base64-encoded data.
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 data and fires 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 with base64 data URIs:
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
- data_uri = f["data"] # "data:<mime>;base64,<data>"
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
- b64 = f["data"].split(",", 1)[1]
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