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.
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+