structile 6.0.0__py3-none-any.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.
- structile/__init__.py +544 -0
- structile/__main__.py +57 -0
- structile/_dom_lite.py +128 -0
- structile/_log.py +33 -0
- structile/_paths.py +148 -0
- structile/_plugins.py +230 -0
- structile/config.py +272 -0
- structile/convert.py +247 -0
- structile/dom_lite.js +70 -0
- structile/interpreters.py +430 -0
- structile/render.py +535 -0
- structile/serialize.py +88 -0
- structile/static/structile.prod.html +188 -0
- structile/static/widget.js +185 -0
- structile/widget.py +406 -0
- structile-6.0.0.dist-info/METADATA +471 -0
- structile-6.0.0.dist-info/RECORD +20 -0
- structile-6.0.0.dist-info/WHEEL +5 -0
- structile-6.0.0.dist-info/licenses/LICENSE +201 -0
- structile-6.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: structile
|
|
3
|
+
Version: 6.0.0
|
|
4
|
+
Summary: A spatial editor and diff tool for structured data, inline in Jupyter
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: anywidget>=0.9
|
|
10
|
+
Requires-Dist: traitlets>=5
|
|
11
|
+
Provides-Extra: numpy
|
|
12
|
+
Requires-Dist: numpy; extra == "numpy"
|
|
13
|
+
Provides-Extra: convert
|
|
14
|
+
Requires-Dist: mini-racer; extra == "convert"
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest; extra == "test"
|
|
17
|
+
Requires-Dist: pytest-xdist; extra == "test"
|
|
18
|
+
Requires-Dist: numpy; extra == "test"
|
|
19
|
+
Requires-Dist: mini-racer; extra == "test"
|
|
20
|
+
Requires-Dist: build; extra == "test"
|
|
21
|
+
Provides-Extra: browser-test
|
|
22
|
+
Requires-Dist: playwright; extra == "browser-test"
|
|
23
|
+
Provides-Extra: notebook-test
|
|
24
|
+
Requires-Dist: nbformat; extra == "notebook-test"
|
|
25
|
+
Requires-Dist: nbclient; extra == "notebook-test"
|
|
26
|
+
Requires-Dist: ipykernel; extra == "notebook-test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Structile
|
|
30
|
+
|
|
31
|
+
*A spatial editor and diff tool for structured data.*
|
|
32
|
+
|
|
33
|
+
A viewer for JSON (and Python-repr, and XML/HTML, and any other text format
|
|
34
|
+
you attach a small interpreter script to) data — inline editing, undo/redo,
|
|
35
|
+
search, and a spatial "packing" layout that lays dicts and tables out as
|
|
36
|
+
nested boxes instead of an indented tree. Renders wherever you're running:
|
|
37
|
+
an inline Jupyter widget, a browser tab, a written HTML file, or a terminal
|
|
38
|
+
summary.
|
|
39
|
+
|
|
40
|
+
## Table of contents
|
|
41
|
+
|
|
42
|
+
- [Install](#install)
|
|
43
|
+
- [Basic use](#basic-use)
|
|
44
|
+
- [Renderers](#renderers)
|
|
45
|
+
- [Command line](#command-line)
|
|
46
|
+
- [Editing and saving back to Python](#editing-and-saving-back-to-python)
|
|
47
|
+
- [Custom formats via an interpreter](#custom-formats-via-an-interpreter)
|
|
48
|
+
- [Distributing interpreters as a package](#distributing-interpreters-as-a-package)
|
|
49
|
+
- [Comparing two files (`structile.diff`)](#comparing-two-files-structilediff)
|
|
50
|
+
- [Converting between formats](#converting-between-formats)
|
|
51
|
+
- [Configuring the viewer](#configuring-the-viewer)
|
|
52
|
+
- [Embedding the viewer in your own host](#embedding-the-viewer-in-your-own-host)
|
|
53
|
+
- [Logging](#logging)
|
|
54
|
+
- [Current limitations](#current-limitations)
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install structile
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Add `pip install "structile[numpy]"` if you also want numpy scalar
|
|
63
|
+
support, or `pip install mini-racer` if you plan to use `convert()`'s
|
|
64
|
+
non-JSON/Python directions (see [Converting between formats](#converting-between-formats))
|
|
65
|
+
— everything else works with no extra dependency beyond `anywidget`/
|
|
66
|
+
`traitlets`, which `pip install structile` already pulls in.
|
|
67
|
+
|
|
68
|
+
## Basic use
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import structile as st
|
|
72
|
+
|
|
73
|
+
st.open({"a": 1, "nested": {"b": 2, "tags": ["x", "y"]}}) # a literal value
|
|
74
|
+
st.open("results.json") # or load a file by path
|
|
75
|
+
st.__version__ # package/viewer version
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Call it as the last expression in a cell (or wrap it in
|
|
79
|
+
`IPython.display.display(...)`) to render it. It always returns a
|
|
80
|
+
`RenderHandle` — see [Renderers](#renderers) for what that is.
|
|
81
|
+
|
|
82
|
+
`obj` can be any mix of `dict`, `list`, `tuple`, `set`/`frozenset`, `None`,
|
|
83
|
+
`bool`, `int` (any size — huge ints are preserved exactly), `float`, `str`,
|
|
84
|
+
and numpy scalars (if numpy is installed). If `obj` is a `pathlib.Path`, or
|
|
85
|
+
a plain `str` that happens to name an existing file, it's read from disk
|
|
86
|
+
and parsed instead of shown as a literal value — as JSON if that succeeds,
|
|
87
|
+
otherwise as plain text. When it does succeed, the source pane (see below)
|
|
88
|
+
shows/saves the file's own literal text, preserving its original
|
|
89
|
+
formatting, rather than a re-serialization.
|
|
90
|
+
|
|
91
|
+
`structile.normalize(obj)` runs just that same value-preparation step —
|
|
92
|
+
big-int/set handling included — and returns the resulting JSON-safe
|
|
93
|
+
structure directly, with no rendering at all, if that's all you want.
|
|
94
|
+
|
|
95
|
+
## Renderers
|
|
96
|
+
|
|
97
|
+
`open()` doesn't hardcode "inline Jupyter widget" — it renders
|
|
98
|
+
through whichever **renderer** is active, matplotlib-backend style
|
|
99
|
+
(`matplotlib.use(...)` ⟷ `structile.use(...)`).
|
|
100
|
+
|
|
101
|
+
| Renderer | What it does | Auto-selected when |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `widget` | The original inline anywidget path (editable, embedded mode). | An IPython kernel with a rich frontend (`ZMQInteractiveShell`) — Jupyter classic/lab, VS Code notebooks, Colab. |
|
|
104
|
+
| `browser` | Writes one self-contained HTML file (viewer + data + interpreter, all inlined — no server, no CDN) and opens it with `webbrowser.open()`. Settings bar, editing, undo/redo, and client-side Save all work directly in that file. | Outside a notebook, with a display available. |
|
|
105
|
+
| `file` | Same HTML as `browser`, but just writes it and returns/prints the path — never opens a tab. | Headless: no `DISPLAY`/`WAYLAND_DISPLAY` on Linux, `SSH_CONNECTION` set, or `webbrowser.get()` raises. |
|
|
106
|
+
| `none` | Renders nothing. | `CI` or `PYTEST_CURRENT_TEST` is set — so stray debug calls in a test suite never spawn browser tabs. |
|
|
107
|
+
| `text` | A compact terminal summary (type/counts, top-level keys, a truncated tree). | Never auto-selected; opt in with `renderer="text"`. |
|
|
108
|
+
|
|
109
|
+
Resolved in increasing precedence: `renderer=` (per call) → the
|
|
110
|
+
`STRUCTILE_RENDERER` env var → `config=`/
|
|
111
|
+
`structile.options.renderer` (`set_option("renderer", ...)` /
|
|
112
|
+
`structile.use(...)`) → auto-detect.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
st.open(my_data, renderer="browser") # force it, this call only
|
|
116
|
+
st.use("file") # module default, matplotlib.use()-style
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`browser`/`file` write into a stable per-process temp directory that is
|
|
120
|
+
**never auto-deleted** (the browser may not have finished loading the file
|
|
121
|
+
by the time the process exits). `out=` writes the same standalone HTML
|
|
122
|
+
somewhere specific instead of/in addition to that temp file, for any
|
|
123
|
+
renderer; `auto_open=False` stops the `browser` renderer from opening a tab
|
|
124
|
+
(it still writes the file):
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
st.open(my_data, out="snapshot.html") # write only, don't open
|
|
128
|
+
st.open(my_data, renderer="browser", auto_open=False) # write to the temp dir, don't open
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Every call returns a `RenderHandle` — never `None` — regardless of
|
|
132
|
+
renderer:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
h = st.open(my_data)
|
|
136
|
+
h.path # where the standalone HTML was last written, or None
|
|
137
|
+
h.value # the displayed value — live (updates on Save) for `widget`, a static snapshot otherwise
|
|
138
|
+
h.widget # the underlying StructileWidget for `widget`, else None
|
|
139
|
+
h.open() # open a browser tab (builds+writes to the temp dir first if nothing has been written yet)
|
|
140
|
+
h.to_html() # the standalone HTML as a string, built lazily and cached
|
|
141
|
+
h.save(path) # write that HTML to `path`
|
|
142
|
+
repr(h) # e.g. <structile: dict, 9 keys -> C:\...\a3f2b91c.html>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`h.to_html()`/`.open()`/`.save()` work the same way **regardless of which
|
|
146
|
+
renderer actually ran** — a `widget` render can still be popped out into a
|
|
147
|
+
full standalone browser tab via `h.open()`.
|
|
148
|
+
|
|
149
|
+
## Command line
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
python -m structile data.json
|
|
153
|
+
python -m structile data.json --renderer file --out snapshot.html
|
|
154
|
+
python -m structile data.json --no-open # write, don't open a browser tab
|
|
155
|
+
python -m structile --version
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Same renderer resolution/auto-detection as calling `open()` from a
|
|
159
|
+
script. There's no `--format`/registry flag on the CLI: `--interpreter` is
|
|
160
|
+
inferred from the file's own extension the same way `open()` infers
|
|
161
|
+
it.
|
|
162
|
+
|
|
163
|
+
## Editing and saving back to Python
|
|
164
|
+
|
|
165
|
+
The `widget` renderer is read-write. Edit inline as usual, then **Save**
|
|
166
|
+
(or Ctrl+S) sends the edited content back to Python instead of downloading
|
|
167
|
+
it. (The `browser`/`file` renderers use the *standalone* viewer instead,
|
|
168
|
+
where Save prompts for a new data file directly from the browser — there's
|
|
169
|
+
no Python-side channel for those to report back through, so `h.value` for
|
|
170
|
+
them stays a static snapshot of what was originally rendered.)
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
h = st.open("results.json") # loaded from a path: Save writes back to it
|
|
174
|
+
h = st.open(my_data, path="out.json") # in-memory obj: Save writes to out.json
|
|
175
|
+
h = st.open(my_data) # in-memory, no path: Save only updates h.value
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`h.value` holds the last-saved (parsed) value and is updated on every save;
|
|
179
|
+
`h.widget.dirty` / `h.widget.dirty_count` mirror the viewer's own
|
|
180
|
+
unsaved-edit indicator if you want to poll it. Nothing is synced live
|
|
181
|
+
per-keystroke — only an explicit Save commits.
|
|
182
|
+
|
|
183
|
+
Double-clicking a value or table cell opens a reviewable Edit diff
|
|
184
|
+
(original on the left, your edit on the right) — Save commits it back to
|
|
185
|
+
Python (as above); Cancel discards it. Key/column/name renames are the one
|
|
186
|
+
exception: they still commit immediately, live (no review step).
|
|
187
|
+
|
|
188
|
+
**Gotcha**: editing never mutates the object you passed in. `st.open(my_dict)`
|
|
189
|
+
normalizes `my_dict` into a separate structure the widget owns; nothing
|
|
190
|
+
ever writes back into `my_dict` itself. Read `h.value` after saving to get
|
|
191
|
+
the edited result, or reassign `my_dict = h.value`.
|
|
192
|
+
|
|
193
|
+
## Custom formats via an interpreter
|
|
194
|
+
|
|
195
|
+
Pass `interpreter=` — a path to a `.js` file, raw JS source, or a list of
|
|
196
|
+
candidates tried in order — to hand the viewer raw text instead of a
|
|
197
|
+
Python value; it's parsed (and, on save, serialized back) with that script
|
|
198
|
+
client-side. Register one once at import time (`register_interpreter`) so
|
|
199
|
+
later calls don't need `interpreter=` at all:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
st.register_interpreter(".xml", "my_interpreter.js")
|
|
203
|
+
st.open("config.xml") # auto-selected from the registry, no interpreter= needed
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`interpreter=` (or a `register_interpreter()` registration) can also be a
|
|
207
|
+
**list** of candidates, tried in order — useful when you have more than
|
|
208
|
+
one schema in play and don't want to say up front which file is which; the
|
|
209
|
+
first candidate that doesn't raise on a given file wins, independently per
|
|
210
|
+
file.
|
|
211
|
+
|
|
212
|
+
An interpreter script implements one of two contracts, depending on the
|
|
213
|
+
data:
|
|
214
|
+
|
|
215
|
+
- **Markup** (`format="xml"`/`"html"`, or a `.xml`/`.html`/`.htm` path):
|
|
216
|
+
`interpretXML(xmlDocument)` (and optionally `serializeXML(value)` to
|
|
217
|
+
support saving back), given a browser-parsed DOM document. A minimal
|
|
218
|
+
example, reading a flat `<config>` element's attributes as key/value
|
|
219
|
+
pairs:
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
function interpretXML(doc) {
|
|
223
|
+
const root = doc.documentElement;
|
|
224
|
+
if (root.tagName !== "config") throw new Error("expected a <config> root element");
|
|
225
|
+
const result = {};
|
|
226
|
+
for (const attr of root.attributes) result[attr.name] = attr.value;
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- **Anything else** (any other `format=`, e.g. `"ini"`, or a matching file
|
|
232
|
+
extension): `interpretText(text)` (and optionally `serializeText(value)`),
|
|
233
|
+
given the raw text directly — no DOM involved at all.
|
|
234
|
+
|
|
235
|
+
`open()` never needs anything beyond its own dependencies for
|
|
236
|
+
either case: every renderer (including `widget`) forwards the interpreter
|
|
237
|
+
source(s) as-is and lets the browser try them client-side. `convert()`
|
|
238
|
+
(below), and calling `select_interpreter`/`select_interpreter_any_format`
|
|
239
|
+
directly, are the only two cases that need the optional `mini-racer`
|
|
240
|
+
package (`pip install mini-racer`) — both need the actual parsed Python
|
|
241
|
+
value back, which only running the interpreter (in an embedded JS engine —
|
|
242
|
+
a real V8, via a prebuilt wheel; no Node.js, no npm, no system install) can
|
|
243
|
+
produce.
|
|
244
|
+
|
|
245
|
+
### Distributing interpreters as a package
|
|
246
|
+
|
|
247
|
+
An organisation with its own in-house schema can distribute its
|
|
248
|
+
interpreter(s) as an ordinary `pip install`-able package instead of a
|
|
249
|
+
`.js` file callers have to know the location of — via a standard
|
|
250
|
+
[entry point](https://packaging.python.org/en/latest/specifications/entry-points/)
|
|
251
|
+
in the group `structile.interpreters`. Once installed, this just works,
|
|
252
|
+
with no import of the plugin package and no registration call:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
import structile as st
|
|
256
|
+
st.open("blotter.axml") # correct interpreter chosen automatically
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The plugin package's entry point resolves to a callable taking one
|
|
260
|
+
argument, a small facade exposing only `register_interpreter()`:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
# pyproject.toml: [project.entry-points."structile.interpreters"]
|
|
264
|
+
# blotter = "acme_structile_formats:register"
|
|
265
|
+
|
|
266
|
+
import importlib.resources
|
|
267
|
+
from structile import InterpreterSource
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def register(registry) -> None:
|
|
271
|
+
text = (importlib.resources.files("acme_structile_formats") / "interpreters" / "blotter.js").read_text(encoding="utf-8")
|
|
272
|
+
registry.register_interpreter(".axml", InterpreterSource(text, name="acme_structile_formats"))
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`InterpreterSource(text, name=)` wraps JS source text loaded directly
|
|
276
|
+
(rather than a path) — accepted anywhere `interpreter=` is. Discovery is
|
|
277
|
+
lazy (never triggered by `import structile`) and runs at most once per
|
|
278
|
+
process; a broken plugin logs a warning and is skipped rather than
|
|
279
|
+
breaking anyone else's call. An explicit `interpreter=` or your own
|
|
280
|
+
`register_interpreter()` call always takes precedence over a plugin's.
|
|
281
|
+
`st.plugins()` (or `python -m structile --plugins`) lists what's actually
|
|
282
|
+
installed and what it registered.
|
|
283
|
+
|
|
284
|
+
## Comparing two files (`structile.diff`)
|
|
285
|
+
|
|
286
|
+
`structile.diff(left, right, ...)` shows a field-by-field diff —
|
|
287
|
+
`left`/`right` each accept anything `open()`'s `obj` does, and it
|
|
288
|
+
returns a `DiffRenderHandle` (`.path`/`.open()`/`.to_html()`/`.save(path)`,
|
|
289
|
+
same shape as `open()`'s own `RenderHandle`).
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
import structile as st
|
|
293
|
+
|
|
294
|
+
st.diff({"a": 1, "b": 2}, {"a": 1, "b": 3})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`interpreter=`/`format=`/`name=` each accept a single value (applied to
|
|
298
|
+
both sides — two files in the same format/schema, the common case) or a
|
|
299
|
+
`(left, right)` tuple, so the two sides can be genuinely different, even
|
|
300
|
+
mutually-incompatible, schemas — each resolved through its own
|
|
301
|
+
interpreter. Give `interpreter=`/`format=` as a `(value, None)` tuple so
|
|
302
|
+
only one side is treated as markup, when comparing e.g. an XML file
|
|
303
|
+
against a plain JSON value:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
st.diff(
|
|
307
|
+
"left.xml", "right.json",
|
|
308
|
+
interpreter=("my_interpreter.js", None),
|
|
309
|
+
format=("xml", None),
|
|
310
|
+
)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`renderer=`/`out=`/`auto_open=` work the same as `open()`'s (`height=`
|
|
314
|
+
too — iframe height, `widget` only). `view=` (`"unified"`/`"split"`)
|
|
315
|
+
switches between the two diff presentations. `key_columns=` sets initial
|
|
316
|
+
per-table row-key overrides for table alignment.
|
|
317
|
+
|
|
318
|
+
`renderer="widget"` renders an inline two-sided diff widget in Jupyter. The
|
|
319
|
+
diff GRAPH is always read-only, but each side's own source pane can still
|
|
320
|
+
be independently edited and saved — `h.left_value`/`h.right_value` read
|
|
321
|
+
through to the live widget, updated on that side's own Save:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
h = st.diff({"a": 1}, {"a": 2}, renderer="widget")
|
|
325
|
+
h.left_value # {"a": 1} — updates if the left side is edited+saved in the widget
|
|
326
|
+
h.right_value # {"a": 2} — likewise for the right side
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Converting between formats
|
|
330
|
+
|
|
331
|
+
`structile.convert(src, dst_format, interpreter=None)` parses `src` and
|
|
332
|
+
re-serializes it as `dst_format`, returning the converted text — no widget,
|
|
333
|
+
no notebook, just a format conversion.
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
import structile as st
|
|
337
|
+
|
|
338
|
+
st.convert("data.json", "python") # -> Python-repr text
|
|
339
|
+
st.convert("data.py", "json") # -> JSON text
|
|
340
|
+
st.convert("{'a': 1, 'b': {1, 2, 3}}", "json") # literal text works too
|
|
341
|
+
|
|
342
|
+
st.convert("data.xml", "json", interpreter="my_interpreter.js")
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`src` is a path (format inferred from its extension: `.json`/`.py`/`.xml`/
|
|
346
|
+
`.html` — or, once an interpreter is attached to it, any other extension
|
|
347
|
+
too) or a literal string (a literal string only works for `"json"`/
|
|
348
|
+
`"python"`/`"xml"`/`"html"`, since there's no way to reliably sniff an
|
|
349
|
+
arbitrary text format from content alone). `dst_format` is `"json"`,
|
|
350
|
+
`"python"`, `"xml"`, `"html"`, or any other string an interpreter is
|
|
351
|
+
registered/provided for. `"json"` ↔ `"python"` needs no extra dependency;
|
|
352
|
+
anything else needs the optional **`mini-racer` package**
|
|
353
|
+
(`pip install mini-racer`) — a real embedded V8 engine via a prebuilt
|
|
354
|
+
wheel, no Node.js, no npm, no system install at all.
|
|
355
|
+
|
|
356
|
+
## Configuring the viewer
|
|
357
|
+
|
|
358
|
+
The viewer's own settings (`gap`, `theme`, and the packing-layout knobs)
|
|
359
|
+
can be set three ways, in increasing order of precedence:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
import structile as st
|
|
363
|
+
|
|
364
|
+
st.set_option("theme", "dark") # module-level default, applies to every call after this
|
|
365
|
+
st.options.gap = 8 # equivalent, attribute style
|
|
366
|
+
st.get_option("theme") # read a module-level default back ("dark") — None if unset
|
|
367
|
+
st.reset_option("theme") # unset it again, back to the viewer's own built-in default
|
|
368
|
+
|
|
369
|
+
st.open(my_data, gap=8, theme="dark") # per-call override
|
|
370
|
+
st.open(my_data, config=st.Options()) # ...or pass an Options instance
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Any setting left unset everywhere (module default and per-call both
|
|
374
|
+
`None`) falls back to the viewer's own built-in default. Passing an
|
|
375
|
+
unknown option name, or a value of the wrong type, raises immediately.
|
|
376
|
+
|
|
377
|
+
Precedence for viewer settings (`gap`/`theme`/layout knobs): per-call
|
|
378
|
+
keyword (`st.open(data, gap=8)`) > per-call `config=Options()` >
|
|
379
|
+
module-level default (`set_option`/`options.gap = 8`) > the viewer's own
|
|
380
|
+
built-in default. Precedence for `renderer`: per-call `renderer=` >
|
|
381
|
+
`STRUCTILE_RENDERER` env var > per-call `config=Options()` >
|
|
382
|
+
module-level default (`use()`/`set_option("renderer", ...)`) >
|
|
383
|
+
auto-detect. The two precedence chains are resolved completely
|
|
384
|
+
independently — `renderer` is Python-side dispatch only and has no
|
|
385
|
+
corresponding concept in the viewer itself.
|
|
386
|
+
|
|
387
|
+
VS Code's Jupyter renderer may draw a white output background around
|
|
388
|
+
ipywidgets even in a dark theme. Add this notebook cell before rendering
|
|
389
|
+
widgets if you want that wrapper to be transparent:
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
%%html
|
|
393
|
+
<style>
|
|
394
|
+
.cell-output-ipywidget-background { background-color: transparent !important; }
|
|
395
|
+
.jp-OutputArea-output { background-color: transparent !important; }
|
|
396
|
+
</style>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Embedding the viewer in your own host
|
|
400
|
+
|
|
401
|
+
The `widget` renderer's underlying iframe protocol is available directly
|
|
402
|
+
if you want to embed the viewer as a controlled editing surface inside
|
|
403
|
+
your own page (an `<iframe>`, or a `srcdoc` document) — the host supplies
|
|
404
|
+
the data/config instead of the user picking a file, and the viewer reports
|
|
405
|
+
state back instead of downloading files itself.
|
|
406
|
+
|
|
407
|
+
**Turning it on** — either works, and either activates it independent of
|
|
408
|
+
the other:
|
|
409
|
+
|
|
410
|
+
- Load the page with `?embed=1` in the URL, or
|
|
411
|
+
- Just post a `structile-embed-init` message to it.
|
|
412
|
+
|
|
413
|
+
**Message protocol** (plain objects, matched on `type`):
|
|
414
|
+
|
|
415
|
+
| Direction | Type | Payload |
|
|
416
|
+
|---|---|---|
|
|
417
|
+
| host to viewer | `structile-embed-init` | `{ value? \| text?+format?, name?, interpreterSource?, config?, hostManagesSave?, disableSourceView? }` |
|
|
418
|
+
| host to viewer | `structile-embed-diff-init` | `{ left:{text,format,name?}, right:{text,format,name?}, leftInterpreterSource?, rightInterpreterSource?, config?, keyColumns?, diff?:{view}, hostManagesSave?, disableSourceView? }` — two-sided diff mode |
|
|
419
|
+
| host to viewer | `structile-embed-interpreter` | `{ source, name? }` — send a companion interpreter after the fact |
|
|
420
|
+
| host to viewer | `structile-embed-request-save` | no payload — pull current content on the host's own initiative |
|
|
421
|
+
| viewer to host | `structile-dirty-state-changed` | `{ dirty, totalCount, containers: [{path, label, count}] }` — single-value mode only, never sent for diff mode |
|
|
422
|
+
| viewer to host | `structile-save-requested` | `{ format, ext, fileName, isSaveAs:false, content }` \| `{ ..., side }` (diff mode — `side` is `"left"`/`"right"`) \| `{ error }` |
|
|
423
|
+
|
|
424
|
+
For `structile-embed-init`: pass either an already-parsed `value`, or raw `text`
|
|
425
|
+
plus `format` — `"json"` and `"python"` use the two built-in parsers;
|
|
426
|
+
anything else (`"xml"`, `"html"`, or a custom format) runs through an
|
|
427
|
+
interpreter. `config` is a settings payload (e.g.
|
|
428
|
+
`{ settings: { valMax: 22, n: 3 } }`), plus an optional top-level
|
|
429
|
+
`theme: "light" | "dark"`. `hostManagesSave: true` hides the Save button
|
|
430
|
+
and stops Ctrl/Cmd+S from being handled inside the iframe at all — for a
|
|
431
|
+
host that wants its own save keybinding to win instead; that host is
|
|
432
|
+
expected to pull current content on demand via `structile-embed-request-save`
|
|
433
|
+
rather than waiting for a push. `disableSourceView: true` hides the
|
|
434
|
+
read-only raw-source-text toggle/pane entirely.
|
|
435
|
+
|
|
436
|
+
In embedded mode, **Save never touches the filesystem or opens a tab** — it
|
|
437
|
+
only posts `structile-save-requested` with the serialized content, and it's the
|
|
438
|
+
host's job to persist it.
|
|
439
|
+
|
|
440
|
+
> **Note**: the viewer posts with `targetOrigin: "*"` (it doesn't know the
|
|
441
|
+
> host's origin ahead of time) and listens for messages from any origin —
|
|
442
|
+
> fine for a same-app iframe/widget, but don't embed untrusted third-party
|
|
443
|
+
> pages this way without adding your own origin checks on both ends.
|
|
444
|
+
|
|
445
|
+
## Logging
|
|
446
|
+
|
|
447
|
+
`structile` emits to `logging.getLogger("structile")` — DEBUG for
|
|
448
|
+
internal decisions (which renderer/interpreter was picked and why), INFO
|
|
449
|
+
for real actions (a file written, a browser tab opened), WARNING for
|
|
450
|
+
recoverable hiccups (an interpreter candidate failed, trying the next
|
|
451
|
+
one). It never configures handlers/levels itself:
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
import logging
|
|
455
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Current limitations
|
|
459
|
+
|
|
460
|
+
- Fixed iframe height (`height=`, default `600`) — no auto-resize to content
|
|
461
|
+
yet.
|
|
462
|
+
- Plain numpy arrays aren't handled (only numpy *scalars*) — call
|
|
463
|
+
`.tolist()` first if you need to show one.
|
|
464
|
+
- A source dict with keys that collide once stringified (e.g. both `1` and
|
|
465
|
+
`"1"` as separate keys) will collide in the displayed result too — this
|
|
466
|
+
is a fundamental JSON limitation (object keys are always strings), not
|
|
467
|
+
something `structile` works around.
|
|
468
|
+
- `browser`/`file`/`none`/`text` renders are one-way — there's no channel
|
|
469
|
+
back to the Python process the way `widget`'s embedded-mode protocol has,
|
|
470
|
+
so `h.value` for those stays whatever was originally rendered, regardless
|
|
471
|
+
of what happens later in the browser tab or file.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
structile/__init__.py,sha256=xfqqOlmMk0YtB-Li-Sz5nN-FeH_NsYKngp0W0YeNuE0,27445
|
|
2
|
+
structile/__main__.py,sha256=i97EC-10gdHyzcOWTdt5qkWUOgKMSoaTOEO9UYWdV-0,2490
|
|
3
|
+
structile/_dom_lite.py,sha256=qM4rA93TtMrmFsARMWpb0W4PlS82mWCum0Lao5c337w,5764
|
|
4
|
+
structile/_log.py,sha256=1himEH9SIMoaERyCE5AfYprHEeIUjPM0zWoXPBgYtLg,1753
|
|
5
|
+
structile/_paths.py,sha256=ZsrvqnfWkLmjM128TrqCQMEVKCWj1vj64dyBmmhJTdo,7206
|
|
6
|
+
structile/_plugins.py,sha256=wm5J2Kqd8lvXiP20cTIgVCbh1LNPEIPmpIaJi-ZYj-c,10527
|
|
7
|
+
structile/config.py,sha256=LMy8HroNpM0gMr9ov3bFVootYr8FhxSyjhUGAOSE3WM,11456
|
|
8
|
+
structile/convert.py,sha256=MrsmAJVX94TNIc7ewYFbuwYOpDmmSpV3d6Ti8ULyJ50,13018
|
|
9
|
+
structile/dom_lite.js,sha256=ZGXp9r17gnfZ-SEpFRwZKpigVK3WDWaTh1ZTC5YxSTA,3540
|
|
10
|
+
structile/interpreters.py,sha256=HKrVBQO3YSqvJs8fqYnvt42b-B8WSP9qNvaVoUqmxfc,21291
|
|
11
|
+
structile/render.py,sha256=miffCvS2jx7JtIKoXeJR_JBgEekln7b94p0lO_Vrs5M,23792
|
|
12
|
+
structile/serialize.py,sha256=BhytkiYvJcU8n7oWLN90oz36hQbGHKi3m7aBpi0Lsh8,3658
|
|
13
|
+
structile/widget.py,sha256=IZigXuCdz7iccVfpS-TLt5tQhFdBVysjkiwPq2w9Bw0,21139
|
|
14
|
+
structile/static/structile.prod.html,sha256=1-3H5DM6iTa-15oLTxl4nEDA6mGLH-Y6L1x9nD1x-Mw,255703
|
|
15
|
+
structile/static/widget.js,sha256=AlLecseyRWav_z74YrW3gPFokAGpDRbCf61tYJ-MelA,8608
|
|
16
|
+
structile-6.0.0.dist-info/licenses/LICENSE,sha256=iBabAjFJR2jp731S3Fm3S39_7irqQWrQ8o1Pl9uozBw,11553
|
|
17
|
+
structile-6.0.0.dist-info/METADATA,sha256=N3TxUoDc5Rvq9lWzFC-B2P8jJlPlvB2S7YUvqvMX-9Q,22207
|
|
18
|
+
structile-6.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
19
|
+
structile-6.0.0.dist-info/top_level.txt,sha256=DqseT8IMSScv4QJ7m9hFADKIaZmwkPmFsmuHP-SBgRs,10
|
|
20
|
+
structile-6.0.0.dist-info/RECORD,,
|