fletchr-studio 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. fletchr_studio-0.1.0/.gitignore +28 -0
  2. fletchr_studio-0.1.0/CHANGELOG.md +70 -0
  3. fletchr_studio-0.1.0/LICENSE +21 -0
  4. fletchr_studio-0.1.0/PKG-INFO +172 -0
  5. fletchr_studio-0.1.0/README.md +140 -0
  6. fletchr_studio-0.1.0/examples/make_demo.py +156 -0
  7. fletchr_studio-0.1.0/frontend/index.html +13 -0
  8. fletchr_studio-0.1.0/frontend/package-lock.json +1982 -0
  9. fletchr_studio-0.1.0/frontend/package.json +23 -0
  10. fletchr_studio-0.1.0/frontend/public/favicon.svg +7 -0
  11. fletchr_studio-0.1.0/frontend/src/App.tsx +824 -0
  12. fletchr_studio-0.1.0/frontend/src/BlockNode.tsx +79 -0
  13. fletchr_studio-0.1.0/frontend/src/FileDialog.tsx +130 -0
  14. fletchr_studio-0.1.0/frontend/src/Inspector.tsx +516 -0
  15. fletchr_studio-0.1.0/frontend/src/PaletteSidebar.tsx +102 -0
  16. fletchr_studio-0.1.0/frontend/src/PreviewPanel.tsx +101 -0
  17. fletchr_studio-0.1.0/frontend/src/TopBar.tsx +207 -0
  18. fletchr_studio-0.1.0/frontend/src/api.ts +102 -0
  19. fletchr_studio-0.1.0/frontend/src/layout.ts +85 -0
  20. fletchr_studio-0.1.0/frontend/src/main.tsx +6 -0
  21. fletchr_studio-0.1.0/frontend/src/styles.css +988 -0
  22. fletchr_studio-0.1.0/frontend/src/types.ts +114 -0
  23. fletchr_studio-0.1.0/frontend/tsconfig.json +16 -0
  24. fletchr_studio-0.1.0/frontend/vite.config.ts +15 -0
  25. fletchr_studio-0.1.0/pyproject.toml +102 -0
  26. fletchr_studio-0.1.0/src/fletchr_studio/__init__.py +108 -0
  27. fletchr_studio-0.1.0/src/fletchr_studio/_pkgversion.py +9 -0
  28. fletchr_studio-0.1.0/src/fletchr_studio/_version.py +34 -0
  29. fletchr_studio-0.1.0/src/fletchr_studio/branding.py +42 -0
  30. fletchr_studio-0.1.0/src/fletchr_studio/codegen.py +401 -0
  31. fletchr_studio-0.1.0/src/fletchr_studio/exceptions.py +38 -0
  32. fletchr_studio-0.1.0/src/fletchr_studio/graph.py +246 -0
  33. fletchr_studio-0.1.0/src/fletchr_studio/palette.py +479 -0
  34. fletchr_studio-0.1.0/src/fletchr_studio/preview.py +74 -0
  35. fletchr_studio-0.1.0/src/fletchr_studio/runner.py +318 -0
  36. fletchr_studio-0.1.0/src/fletchr_studio/server.py +245 -0
  37. fletchr_studio-0.1.0/src/fletchr_studio/sessions.py +220 -0
  38. fletchr_studio-0.1.0/src/fletchr_studio/static/assets/index-BNeWOtLI.css +1 -0
  39. fletchr_studio-0.1.0/src/fletchr_studio/static/assets/index-CL3dBVkr.js +170 -0
  40. fletchr_studio-0.1.0/src/fletchr_studio/static/favicon.svg +7 -0
  41. fletchr_studio-0.1.0/src/fletchr_studio/static/index.html +14 -0
  42. fletchr_studio-0.1.0/src/fletchr_studio/validate.py +268 -0
  43. fletchr_studio-0.1.0/tests/conftest.py +18 -0
  44. fletchr_studio-0.1.0/tests/test_codegen.py +302 -0
  45. fletchr_studio-0.1.0/tests/test_graph.py +84 -0
  46. fletchr_studio-0.1.0/tests/test_palette.py +148 -0
  47. fletchr_studio-0.1.0/tests/test_preview.py +35 -0
  48. fletchr_studio-0.1.0/tests/test_runner.py +249 -0
  49. fletchr_studio-0.1.0/tests/test_server.py +198 -0
  50. fletchr_studio-0.1.0/tests/test_validate.py +226 -0
@@ -0,0 +1,28 @@
1
+ .venv
2
+ __pycache__
3
+ .hypothesis
4
+ .pytest_cache
5
+ .ipynb_checkpoints
6
+ *.ipynb
7
+ _version.py
8
+
9
+ # Build artifacts
10
+ dist/
11
+ build/
12
+ *.egg-info/
13
+
14
+ # Frontend (fletchr-studio canvas)
15
+ node_modules/
16
+ fletchr-studio/src/fletchr_studio/static/
17
+
18
+ # Generated studio demo (fletchr-studio/examples/make_demo.py)
19
+ studio-demo/
20
+
21
+ # Coverage
22
+ .coverage
23
+ htmlcov/
24
+ coverage.xml
25
+
26
+ # Lint / type-check caches
27
+ .ruff_cache/
28
+ .mypy_cache/
@@ -0,0 +1,70 @@
1
+ # Changelog
2
+
3
+ All notable changes to `fletchr-studio` are documented here.
4
+
5
+ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
6
+ this project follows [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.1.0] - 2026-07-27
9
+
10
+ Initial release: a visual flowgraph editor for fletchr pipelines —
11
+ sources → transformers → sinks on a canvas, compiled to plain Python.
12
+
13
+ ### Added
14
+
15
+ - **Canvas editor** (React Flow) served by a local FastAPI host
16
+ (`fletchr-studio` CLI): drag blocks from a palette grouped by
17
+ category (frame/mux/packet/clock/dataframe lanes, color-coded type
18
+ chips and port handles), connect typed ports with live
19
+ reject-on-connect, configure params in an inspector with
20
+ type-resolved widgets (`Literal` → dropdowns, `bool | None` →
21
+ tri-state, mappings → key/value row editors, paths → file browser).
22
+ Undo/redo, copy/paste, Ctrl+S, auto-layout with crossing reduction,
23
+ nameable nodes, recent files, resizable inspector.
24
+ - **Palette from the live registries** — every registered transformer
25
+ (plugin-contributed ones included, via the normal entry-point
26
+ discovery), file source/sink blocks with per-extension format
27
+ options introspected from reader/writer signatures, and merge
28
+ blocks wrapping `stack` / `merge`.
29
+ - **Codegen artifact** — graphs compile to a runnable script
30
+ depending only on the fletchr packages: linear runs become `|`
31
+ pipelines, imports group by each block's defining module (plugin
32
+ blocks import from their own package), source/sink paths lift into
33
+ `main()` kwargs and an argparse CLI (`python flow.py --capture
34
+ other.bits`), and sink paths support `{stem}`/`{name}` tokens
35
+ derived from a single upstream `read_file`. The graph document
36
+ embeds as `__fletchr_graph__` with a drift-detecting hash;
37
+ reopening is byte-identical.
38
+ - **Runs with observability** — sampled preview runs (sinks skipped,
39
+ sources sliced) and full runs execute in a background thread with
40
+ live per-node status, cooperative cancellation at node boundaries,
41
+ per-node capture of stdout/stderr/warnings/logging shown in the
42
+ inspector, previews of completed nodes mid-run, and a toggle to
43
+ disable result caching (intermediates freed eagerly) for large
44
+ runs.
45
+ - **Previews** — schema, row/null counts, and paginated table windows
46
+ for any cached node (containers, polars, pandas), as JSON or Arrow
47
+ IPC; internal underscore-prefixed columns hidden.
48
+ - **Sessions API** — graph CRUD with live validation, run control,
49
+ previews, generate/save/open (drift flagged), and a file-browser
50
+ endpoint, all under `/api`; `Branding` lets a downstream
51
+ meta-package present the studio under its own name and favicon.
52
+ - **Validation** — structural (cycles, ports, params), subclass-aware
53
+ port types mirroring `Pipeline`'s rules, and extension-aware I/O
54
+ checks (missing required format options like `.bits`
55
+ `start_time`, unregistered extensions) surfaced live as node
56
+ badges.
57
+ - **Demo generator** — `python fletchr-studio/examples/make_demo.py`
58
+ writes a factory serial-bus scenario (packet lane, `PacketSelect`
59
+ lane change, measurand decoding) with data, an openable flow, and a
60
+ self-verifying run.
61
+ - **Packaging guard** — `hatch-jupyter-builder` ensures dists always
62
+ contain the canvas bundle: prebuilt bundles need no Node, missing
63
+ bundles are npm-built when Node is present, and a headless dist
64
+ fails the build instead of shipping silently.
65
+
66
+ ## Links
67
+
68
+ - [Source](https://github.com/fletchr-labs/fletchr/tree/main/fletchr-studio)
69
+ - [PyPI](https://pypi.org/project/fletchr-studio/)
70
+ - [Design record](https://github.com/fletchr-labs/fletchr/blob/main/docs/design/studio.md)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Olsten
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: fletchr-studio
3
+ Version: 0.1.0
4
+ Summary: Flowgraph engine for the fletchr studio GUI: graph model, palette introspection, validation, and Python code generation.
5
+ Project-URL: Homepage, https://github.com/fletchr-labs/fletchr
6
+ Project-URL: Repository, https://github.com/fletchr-labs/fletchr
7
+ Project-URL: Issues, https://github.com/fletchr-labs/fletchr/issues
8
+ Project-URL: Changelog, https://github.com/fletchr-labs/fletchr/blob/main/fletchr-studio/CHANGELOG.md
9
+ Author-email: Jonathan Olsten <jolsten@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: arrow,codegen,flowgraph,gui,pipeline,pyarrow
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: attrs>=22.1.0
28
+ Requires-Dist: fastapi>=0.110
29
+ Requires-Dist: fletchr-core>=0.1.0
30
+ Requires-Dist: uvicorn>=0.29
31
+ Description-Content-Type: text/markdown
32
+
33
+ # fletchr-studio
34
+
35
+ The flowgraph engine behind the fletchr studio GUI: a graph document
36
+ model for source → transformer → sink flowgraphs, a block palette built
37
+ by introspecting the live fletchr registries, graph validation, and
38
+ Python code generation with round-trip reopen.
39
+
40
+ The package also ships the studio itself: a FastAPI host
41
+ (`fletchr_studio.server`) serving a React Flow canvas UI, with
42
+ sessions, sampled/full graph runs, and per-node table previews. See
43
+ [`docs/design/studio.md`](../docs/design/studio.md) at the workspace
44
+ root for the design record and phased plan.
45
+
46
+ ## Running the GUI
47
+
48
+ ```bash
49
+ fletchr-studio # serves http://127.0.0.1:8410
50
+ ```
51
+
52
+ Drag blocks from the palette, connect them (incompatible ports are
53
+ rejected), configure params in the inspector, then **Preview Run** —
54
+ sources are sampled and sinks are skipped, so a preview never
55
+ overwrites real outputs. Click any node to see its table preview.
56
+ **Full Run** executes everything, sinks included. **Generate** shows
57
+ the emitted script; **Save** writes it; **Open** reopens a generated
58
+ `.py` (hand edits are flagged as drift).
59
+
60
+ ## Rebranding for meta-packages
61
+
62
+ A downstream protocol wrapper can present the studio as its own tool.
63
+ Ship a console script that launches it with a `Branding`:
64
+
65
+ ```python
66
+ # acme_decoder/studio.py
67
+ import acme_decoder.plugins # noqa: F401 - registers blocks via entry points
68
+ from fletchr_studio import Branding
69
+ from fletchr_studio.server import main
70
+
71
+
72
+ def cli() -> None:
73
+ main(branding=Branding(name="ACME Decoder Studio", favicon="path/to/icon.svg"))
74
+ ```
75
+
76
+ ```toml
77
+ [project.scripts]
78
+ acme-studio = "acme_decoder.studio:cli"
79
+ ```
80
+
81
+ The name flows into the browser tab, the top bar, and the FastAPI
82
+ title; the favicon replaces the default. Generated scripts still
83
+ record `fletchr-studio <version>` as their generator — that's
84
+ provenance, not presentation. The wrapper's transformers, readers,
85
+ and writers appear in the palette automatically via the normal
86
+ entry-point plugin discovery.
87
+
88
+ The canvas bundle builds into `src/fletchr_studio/static/` and ships
89
+ in both the wheel and the sdist. Packaging is guarded by
90
+ `hatch-jupyter-builder`: building a dist with the bundle already
91
+ present needs no Node (`skip-if-exists`); with the bundle missing it
92
+ runs `npm install` + `npm run build` itself; and with neither bundle
93
+ nor Node the build **fails** rather than producing a headless dist
94
+ (`ensured-targets`). Editable installs only warn, so Python-only
95
+ contributors without Node get a working headless dev server. Escape
96
+ hatch for deliberate headless builds: `SKIP_JUPYTER_BUILDER=1`.
97
+
98
+ To rebuild the bundle by hand you need Node 18+:
99
+
100
+ ```bash
101
+ cd frontend
102
+ npm install
103
+ npm run build # type-checks and outputs to ../src/fletchr_studio/static
104
+ npm run dev # dev server with /api proxied to a running fletchr-studio
105
+ ```
106
+
107
+ ## What it does
108
+
109
+ ```python
110
+ from fletchr_studio import GraphDoc, generate_code, load_graph
111
+
112
+ doc = GraphDoc()
113
+ doc.add_node("capture", "source", "read_file", path="capture.arrow")
114
+ doc.add_node("sel", "transform", "Subframe", columns="1-32")
115
+ doc.add_node("inv", "transform", "Invert")
116
+ doc.add_node("out", "sink", "write_file", path="frames.parquet")
117
+ doc.add_edge("capture", "sel")
118
+ doc.add_edge("sel", "inv")
119
+ doc.add_edge("inv", "out")
120
+
121
+ code = generate_code(doc)
122
+ ```
123
+
124
+ `generate_code` validates the graph (unknown blocks, port type
125
+ mismatches, cycles, missing params — reusing the same subclass-aware
126
+ compatibility rules `Pipeline` enforces) and emits a plain Python
127
+ script depending only on the fletchr packages:
128
+
129
+ ```python
130
+ """Flowgraph generated by fletchr-studio 0.1.0."""
131
+
132
+ import argparse
133
+
134
+ from fletchr_core import read_file, write_file
135
+ from fletchr_core.transform import Invert, Subframe
136
+
137
+
138
+ def main(capture_path='capture.arrow', out_path='frames.parquet'):
139
+ capture = read_file(capture_path)
140
+ inv = (Subframe(columns='1-32') | Invert())(capture)
141
+ write_file(out_path, inv)
142
+
143
+
144
+ def _cli():
145
+ parser = argparse.ArgumentParser(description="Flowgraph generated by fletchr-studio.")
146
+ parser.add_argument("--capture", dest="capture_path", default='capture.arrow', help="source 'capture' path")
147
+ parser.add_argument("--out", dest="out_path", default='frames.parquet', help="sink 'out' path")
148
+ main(**vars(parser.parse_args()))
149
+
150
+
151
+ if __name__ == "__main__":
152
+ _cli()
153
+
154
+
155
+ __fletchr_graph__ = {...}
156
+ ```
157
+
158
+ Source and sink paths are lifted into `main()`'s signature and an
159
+ argparse CLI, so the artifact is parameterizable without editing:
160
+ `python flow.py --capture other.bits`, or import it and call
161
+ `main(capture_path=...)` from a loop.
162
+
163
+ The embedded `__fletchr_graph__` literal is the graph document —
164
+ `load_graph("flow.py")` reopens it, and a stored SHA-256 of the code
165
+ section detects hand edits (drift) without parsing arbitrary Python.
166
+ Linear chains compile to `|` pipelines; fan-out becomes named
167
+ intermediates; merge blocks (`stack`, `merge`) compile to plain calls.
168
+
169
+ The palette (`build_palette()`) discovers every registered transformer
170
+ — including plugin-contributed ones — plus file source/sink blocks and
171
+ the merge blocks, with parameter schemas introspected from attrs fields
172
+ or `__init__` signatures and port types from `apply` annotations.
@@ -0,0 +1,140 @@
1
+ # fletchr-studio
2
+
3
+ The flowgraph engine behind the fletchr studio GUI: a graph document
4
+ model for source → transformer → sink flowgraphs, a block palette built
5
+ by introspecting the live fletchr registries, graph validation, and
6
+ Python code generation with round-trip reopen.
7
+
8
+ The package also ships the studio itself: a FastAPI host
9
+ (`fletchr_studio.server`) serving a React Flow canvas UI, with
10
+ sessions, sampled/full graph runs, and per-node table previews. See
11
+ [`docs/design/studio.md`](../docs/design/studio.md) at the workspace
12
+ root for the design record and phased plan.
13
+
14
+ ## Running the GUI
15
+
16
+ ```bash
17
+ fletchr-studio # serves http://127.0.0.1:8410
18
+ ```
19
+
20
+ Drag blocks from the palette, connect them (incompatible ports are
21
+ rejected), configure params in the inspector, then **Preview Run** —
22
+ sources are sampled and sinks are skipped, so a preview never
23
+ overwrites real outputs. Click any node to see its table preview.
24
+ **Full Run** executes everything, sinks included. **Generate** shows
25
+ the emitted script; **Save** writes it; **Open** reopens a generated
26
+ `.py` (hand edits are flagged as drift).
27
+
28
+ ## Rebranding for meta-packages
29
+
30
+ A downstream protocol wrapper can present the studio as its own tool.
31
+ Ship a console script that launches it with a `Branding`:
32
+
33
+ ```python
34
+ # acme_decoder/studio.py
35
+ import acme_decoder.plugins # noqa: F401 - registers blocks via entry points
36
+ from fletchr_studio import Branding
37
+ from fletchr_studio.server import main
38
+
39
+
40
+ def cli() -> None:
41
+ main(branding=Branding(name="ACME Decoder Studio", favicon="path/to/icon.svg"))
42
+ ```
43
+
44
+ ```toml
45
+ [project.scripts]
46
+ acme-studio = "acme_decoder.studio:cli"
47
+ ```
48
+
49
+ The name flows into the browser tab, the top bar, and the FastAPI
50
+ title; the favicon replaces the default. Generated scripts still
51
+ record `fletchr-studio <version>` as their generator — that's
52
+ provenance, not presentation. The wrapper's transformers, readers,
53
+ and writers appear in the palette automatically via the normal
54
+ entry-point plugin discovery.
55
+
56
+ The canvas bundle builds into `src/fletchr_studio/static/` and ships
57
+ in both the wheel and the sdist. Packaging is guarded by
58
+ `hatch-jupyter-builder`: building a dist with the bundle already
59
+ present needs no Node (`skip-if-exists`); with the bundle missing it
60
+ runs `npm install` + `npm run build` itself; and with neither bundle
61
+ nor Node the build **fails** rather than producing a headless dist
62
+ (`ensured-targets`). Editable installs only warn, so Python-only
63
+ contributors without Node get a working headless dev server. Escape
64
+ hatch for deliberate headless builds: `SKIP_JUPYTER_BUILDER=1`.
65
+
66
+ To rebuild the bundle by hand you need Node 18+:
67
+
68
+ ```bash
69
+ cd frontend
70
+ npm install
71
+ npm run build # type-checks and outputs to ../src/fletchr_studio/static
72
+ npm run dev # dev server with /api proxied to a running fletchr-studio
73
+ ```
74
+
75
+ ## What it does
76
+
77
+ ```python
78
+ from fletchr_studio import GraphDoc, generate_code, load_graph
79
+
80
+ doc = GraphDoc()
81
+ doc.add_node("capture", "source", "read_file", path="capture.arrow")
82
+ doc.add_node("sel", "transform", "Subframe", columns="1-32")
83
+ doc.add_node("inv", "transform", "Invert")
84
+ doc.add_node("out", "sink", "write_file", path="frames.parquet")
85
+ doc.add_edge("capture", "sel")
86
+ doc.add_edge("sel", "inv")
87
+ doc.add_edge("inv", "out")
88
+
89
+ code = generate_code(doc)
90
+ ```
91
+
92
+ `generate_code` validates the graph (unknown blocks, port type
93
+ mismatches, cycles, missing params — reusing the same subclass-aware
94
+ compatibility rules `Pipeline` enforces) and emits a plain Python
95
+ script depending only on the fletchr packages:
96
+
97
+ ```python
98
+ """Flowgraph generated by fletchr-studio 0.1.0."""
99
+
100
+ import argparse
101
+
102
+ from fletchr_core import read_file, write_file
103
+ from fletchr_core.transform import Invert, Subframe
104
+
105
+
106
+ def main(capture_path='capture.arrow', out_path='frames.parquet'):
107
+ capture = read_file(capture_path)
108
+ inv = (Subframe(columns='1-32') | Invert())(capture)
109
+ write_file(out_path, inv)
110
+
111
+
112
+ def _cli():
113
+ parser = argparse.ArgumentParser(description="Flowgraph generated by fletchr-studio.")
114
+ parser.add_argument("--capture", dest="capture_path", default='capture.arrow', help="source 'capture' path")
115
+ parser.add_argument("--out", dest="out_path", default='frames.parquet', help="sink 'out' path")
116
+ main(**vars(parser.parse_args()))
117
+
118
+
119
+ if __name__ == "__main__":
120
+ _cli()
121
+
122
+
123
+ __fletchr_graph__ = {...}
124
+ ```
125
+
126
+ Source and sink paths are lifted into `main()`'s signature and an
127
+ argparse CLI, so the artifact is parameterizable without editing:
128
+ `python flow.py --capture other.bits`, or import it and call
129
+ `main(capture_path=...)` from a loop.
130
+
131
+ The embedded `__fletchr_graph__` literal is the graph document —
132
+ `load_graph("flow.py")` reopens it, and a stored SHA-256 of the code
133
+ section detects hand edits (drift) without parsing arbitrary Python.
134
+ Linear chains compile to `|` pipelines; fan-out becomes named
135
+ intermediates; merge blocks (`stack`, `merge`) compile to plain calls.
136
+
137
+ The palette (`build_palette()`) discovers every registered transformer
138
+ — including plugin-contributed ones — plus file source/sink blocks and
139
+ the merge blocks, with parameter schemas introspected from attrs fields
140
+ or `__init__` signatures and port types from `apply` annotations.
@@ -0,0 +1,156 @@
1
+ """Generate a self-contained studio demo: test data + an openable flowgraph.
2
+
3
+ The scenario is a factory-floor serial bus log: machines share a bus
4
+ and emit variable-length status messages, captured to a CSV in the
5
+ delimited packets layout (`time, ctime, pid, pdl, w1..wN`, ragged
6
+ rows). Machine IDs: 1 = stamping press, 2 = conveyor, 3 = spot welder.
7
+
8
+ Message payloads (8-bit words):
9
+
10
+ press (pid=1, 4 words) cycle_hi, cycle_lo, motor_temp, vibration
11
+ conveyor (pid=2, 4 words) cycle_hi, cycle_lo, motor_temp, vibration
12
+ welder (pid=3, 6 words) cycle_hi, cycle_lo, motor_temp, vibration,
13
+ current_hi, current_lo
14
+
15
+ The generated flow exercises the packet lane end to end:
16
+
17
+ bus_log (.csv, packets mode) ─┬→ welder (PacketFilter pid=3) → welder_log.csv
18
+ └→ press (PacketSelect pid=1) → decode → press_health.parquet
19
+
20
+ `PacketSelect` stacks the fixed-length press messages into a
21
+ FrameArray (packet → frame lane change), and `MeasurandGroup` decodes
22
+ cycle count (16-bit across two words), motor temperature, and
23
+ vibration into the dataframe lane.
24
+
25
+ Usage:
26
+
27
+ uv run python fletchr-studio/examples/make_demo.py [out_dir]
28
+
29
+ then `cd <out_dir>` and run `fletchr-studio` — File > Open >
30
+ demo_flow.py. Paths in the flow are relative, so the server must be
31
+ started from the demo directory.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import os
37
+ import sys
38
+ from pathlib import Path
39
+
40
+ import numpy as np
41
+
42
+ from fletchr_studio import (
43
+ KIND_SINK,
44
+ KIND_SOURCE,
45
+ KIND_TRANSFORM,
46
+ GraphDoc,
47
+ generate_code,
48
+ run_graph,
49
+ )
50
+
51
+ N_MESSAGES = 400
52
+ MACHINES = {1: "press", 2: "conveyor", 3: "welder"}
53
+
54
+
55
+ def make_bus_log(rng: np.random.Generator) -> str:
56
+ start = np.datetime64("2025-03-10T09:00:00", "ns")
57
+ cycles = dict.fromkeys(MACHINES, 0)
58
+ rows = ["time,ctime,pid,pdl,w1,w2,w3,w4,w5,w6"]
59
+ clock = start
60
+ for _ in range(N_MESSAGES):
61
+ clock = clock + np.timedelta64(int(rng.integers(80, 400)), "ms")
62
+ pid = int(rng.choice([1, 1, 2, 2, 3])) # welder chattier than nothing, rarer than the rest
63
+ cycles[pid] += int(rng.integers(1, 4))
64
+ cycle = cycles[pid] % 65536
65
+ motor_temp = int(np.clip(rng.normal(70 + 12 * pid, 6), 0, 255))
66
+ vibration = int(np.clip(rng.normal(28, 9), 0, 255))
67
+ words = [cycle >> 8, cycle & 0xFF, motor_temp, vibration]
68
+ if pid == 3: # welder appends weld current, 16-bit
69
+ current = int(np.clip(rng.normal(11_500, 900), 0, 65_535))
70
+ words += [current >> 8, current & 0xFF]
71
+ stamp = str(clock)
72
+ cells = [stamp, stamp, str(pid), str(len(words)), *map(str, words)]
73
+ rows.append(",".join(cells))
74
+ return "\n".join(rows) + "\n"
75
+
76
+
77
+ def make_flow() -> GraphDoc:
78
+ doc = GraphDoc()
79
+ doc.add_node(
80
+ "bus_log",
81
+ KIND_SOURCE,
82
+ "read_file",
83
+ path="bus_log.csv",
84
+ mode="packets",
85
+ bits=8,
86
+ )
87
+ doc.add_node("welder", KIND_TRANSFORM, "PacketFilter", pid=3)
88
+ doc.add_node("press", KIND_TRANSFORM, "PacketSelect", pid=1, apdl=4)
89
+ doc.add_node(
90
+ "decode",
91
+ KIND_TRANSFORM,
92
+ "MeasurandGroup",
93
+ measurands={
94
+ "cycle_count": "[1+2];u",
95
+ "motor_temp": "[3];u",
96
+ "vibration": "[4];u",
97
+ },
98
+ )
99
+ doc.add_node("welder_out", KIND_SINK, "write_file", path="welder_log.csv", header=True)
100
+ doc.add_node("health_out", KIND_SINK, "write_file", path="press_health.parquet")
101
+ doc.add_edge("bus_log", "welder")
102
+ doc.add_edge("welder", "welder_out")
103
+ doc.add_edge("bus_log", "press")
104
+ doc.add_edge("press", "decode")
105
+ doc.add_edge("decode", "health_out")
106
+ doc.layout.update(
107
+ {
108
+ "bus_log": [40, 160],
109
+ "welder": [300, 60],
110
+ "welder_out": [560, 60],
111
+ "press": [300, 260],
112
+ "decode": [560, 260],
113
+ "health_out": [820, 260],
114
+ }
115
+ )
116
+ return doc
117
+
118
+
119
+ def main() -> None:
120
+ out_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "studio-demo").resolve()
121
+ out_dir.mkdir(parents=True, exist_ok=True)
122
+
123
+ rng = np.random.default_rng(1972)
124
+ with open(out_dir / "bus_log.csv", "w", encoding="utf-8", newline="") as data_file:
125
+ data_file.write(make_bus_log(rng))
126
+
127
+ doc = make_flow()
128
+ with open(out_dir / "demo_flow.py", "w", encoding="utf-8", newline="") as flow_file:
129
+ flow_file.write(generate_code(doc))
130
+
131
+ # Self-verify: run the flow (full mode, so the sinks write) from
132
+ # the demo directory, since the flow uses relative paths.
133
+ previous_cwd = os.getcwd()
134
+ os.chdir(out_dir)
135
+ try:
136
+ result = run_graph(doc, mode="full")
137
+ finally:
138
+ os.chdir(previous_cwd)
139
+
140
+ print(f"demo written to {out_dir}")
141
+ for node_id, node in result.nodes.items():
142
+ rows = f" {node.rows} rows" if node.rows is not None else ""
143
+ print(f" {node_id:12} {node.status}{rows}")
144
+ for line in node.logs:
145
+ print(f" | {line}")
146
+ if not result.ok:
147
+ raise SystemExit("demo flow did not run cleanly")
148
+ print()
149
+ print("Open it in the studio:")
150
+ print(f" cd {out_dir}")
151
+ print(" fletchr-studio")
152
+ print(" -> File > Open... > demo_flow.py, then hit the play button.")
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <title>fletchr studio</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>