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.
- fletchr_studio-0.1.0/.gitignore +28 -0
- fletchr_studio-0.1.0/CHANGELOG.md +70 -0
- fletchr_studio-0.1.0/LICENSE +21 -0
- fletchr_studio-0.1.0/PKG-INFO +172 -0
- fletchr_studio-0.1.0/README.md +140 -0
- fletchr_studio-0.1.0/examples/make_demo.py +156 -0
- fletchr_studio-0.1.0/frontend/index.html +13 -0
- fletchr_studio-0.1.0/frontend/package-lock.json +1982 -0
- fletchr_studio-0.1.0/frontend/package.json +23 -0
- fletchr_studio-0.1.0/frontend/public/favicon.svg +7 -0
- fletchr_studio-0.1.0/frontend/src/App.tsx +824 -0
- fletchr_studio-0.1.0/frontend/src/BlockNode.tsx +79 -0
- fletchr_studio-0.1.0/frontend/src/FileDialog.tsx +130 -0
- fletchr_studio-0.1.0/frontend/src/Inspector.tsx +516 -0
- fletchr_studio-0.1.0/frontend/src/PaletteSidebar.tsx +102 -0
- fletchr_studio-0.1.0/frontend/src/PreviewPanel.tsx +101 -0
- fletchr_studio-0.1.0/frontend/src/TopBar.tsx +207 -0
- fletchr_studio-0.1.0/frontend/src/api.ts +102 -0
- fletchr_studio-0.1.0/frontend/src/layout.ts +85 -0
- fletchr_studio-0.1.0/frontend/src/main.tsx +6 -0
- fletchr_studio-0.1.0/frontend/src/styles.css +988 -0
- fletchr_studio-0.1.0/frontend/src/types.ts +114 -0
- fletchr_studio-0.1.0/frontend/tsconfig.json +16 -0
- fletchr_studio-0.1.0/frontend/vite.config.ts +15 -0
- fletchr_studio-0.1.0/pyproject.toml +102 -0
- fletchr_studio-0.1.0/src/fletchr_studio/__init__.py +108 -0
- fletchr_studio-0.1.0/src/fletchr_studio/_pkgversion.py +9 -0
- fletchr_studio-0.1.0/src/fletchr_studio/_version.py +34 -0
- fletchr_studio-0.1.0/src/fletchr_studio/branding.py +42 -0
- fletchr_studio-0.1.0/src/fletchr_studio/codegen.py +401 -0
- fletchr_studio-0.1.0/src/fletchr_studio/exceptions.py +38 -0
- fletchr_studio-0.1.0/src/fletchr_studio/graph.py +246 -0
- fletchr_studio-0.1.0/src/fletchr_studio/palette.py +479 -0
- fletchr_studio-0.1.0/src/fletchr_studio/preview.py +74 -0
- fletchr_studio-0.1.0/src/fletchr_studio/runner.py +318 -0
- fletchr_studio-0.1.0/src/fletchr_studio/server.py +245 -0
- fletchr_studio-0.1.0/src/fletchr_studio/sessions.py +220 -0
- fletchr_studio-0.1.0/src/fletchr_studio/static/assets/index-BNeWOtLI.css +1 -0
- fletchr_studio-0.1.0/src/fletchr_studio/static/assets/index-CL3dBVkr.js +170 -0
- fletchr_studio-0.1.0/src/fletchr_studio/static/favicon.svg +7 -0
- fletchr_studio-0.1.0/src/fletchr_studio/static/index.html +14 -0
- fletchr_studio-0.1.0/src/fletchr_studio/validate.py +268 -0
- fletchr_studio-0.1.0/tests/conftest.py +18 -0
- fletchr_studio-0.1.0/tests/test_codegen.py +302 -0
- fletchr_studio-0.1.0/tests/test_graph.py +84 -0
- fletchr_studio-0.1.0/tests/test_palette.py +148 -0
- fletchr_studio-0.1.0/tests/test_preview.py +35 -0
- fletchr_studio-0.1.0/tests/test_runner.py +249 -0
- fletchr_studio-0.1.0/tests/test_server.py +198 -0
- 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>
|