renderflow 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: renderflow
3
+ Version: 0.1.1
4
+ Summary: Generic workflow app runtime and Streamlit renderer with provider plugins
5
+ Author: Brian Day
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: streamlit>=1.31.0
10
+ Requires-Dist: pandas>=2.0.0
11
+ Requires-Dist: plotly>=5.18.0
12
+ Requires-Dist: kaleido>=0.2.1
13
+
14
+ # renderflow
15
+
16
+ Workflow runtime and rendering API for:
17
+ - Streamlit UI
18
+ - CLI execution
19
+ - HTML report export
20
+ - Individual figure export
21
+
22
+ ## Core Idea
23
+
24
+ `renderflow` owns the interface contract and rendering behavior.
25
+ Provider packages should mostly define workflows (`run_workflow` + params), not custom UI/CLI plumbing.
26
+
27
+ ## Demo App
28
+
29
+ Live Streamlit demo:
30
+
31
+ https://demo-for-renderflow.streamlit.app/
32
+
33
+ ## CLI
34
+
35
+ List installed providers:
36
+
37
+ ```bash
38
+ renderflow list-providers
39
+ ```
40
+
41
+ List provider workflows:
42
+
43
+ ```bash
44
+ renderflow list-workflows --provider crsd-inspector
45
+ ```
46
+
47
+ Show interpreted workflow parameters:
48
+
49
+ ```bash
50
+ renderflow show-params --provider crsd-inspector --workflow signal_analysis
51
+ ```
52
+
53
+ Execute a workflow in terminal mode:
54
+
55
+ ```bash
56
+ renderflow execute \
57
+ --provider crsd-inspector \
58
+ --workflow signal_analysis \
59
+ --param crsd_directory=examples \
60
+ --param prf_hz=1000 \
61
+ --output terminal
62
+ ```
63
+
64
+ Execute and export both:
65
+ - one combined report file (`--html`)
66
+ - per-figure files (`--save-figures-dir` + `--figure-format`)
67
+
68
+ ```bash
69
+ renderflow execute \
70
+ --provider crsd-inspector \
71
+ --workflow signal_analysis \
72
+ --param crsd_directory=examples \
73
+ --html output/report.html \
74
+ --save-figures-dir output/figures \
75
+ --figure-format html
76
+ ```
77
+
78
+ Add per-figure JSON in the same run:
79
+
80
+ ```bash
81
+ renderflow execute \
82
+ --provider crsd-inspector \
83
+ --workflow signal_analysis \
84
+ --param crsd_directory=examples \
85
+ --html output/report.html \
86
+ --save-figures-dir output/figures \
87
+ --figure-format html \
88
+ --figure-format json
89
+ ```
90
+
91
+ Export multiple figure formats in a single run:
92
+
93
+ ```bash
94
+ renderflow execute \
95
+ --provider crsd-inspector \
96
+ --workflow signal_analysis \
97
+ --param crsd_directory=examples \
98
+ --save-figures-dir output/figures \
99
+ --figure-format html \
100
+ --figure-format json
101
+ ```
102
+
103
+ Comma-separated format lists are also accepted:
104
+
105
+ ```bash
106
+ renderflow execute \
107
+ --provider crsd-inspector \
108
+ --workflow signal_analysis \
109
+ --param crsd_directory=examples \
110
+ --html output/report.html \
111
+ --save-figures-dir output/figures \
112
+ --figure-format html,json
113
+ ```
114
+
115
+ If `--figure-format` is omitted, per-figure export defaults to `html`.
116
+ Image formats (`png`, `jpg`, `jpeg`, `svg`, `pdf`) require Kaleido. `renderflow` includes `kaleido` as a dependency.
117
+
118
+ Launch Streamlit:
119
+
120
+ ```bash
121
+ renderflow run --provider crsd-inspector
122
+ ```
123
+
124
+ ## Workflow Result Contract
125
+
126
+ Use `renderflow.workflow.Workflow` inside provider workflows:
127
+
128
+ ```python
129
+ from renderflow.workflow import Workflow
130
+
131
+ workflow = Workflow(name="My Workflow", description="...")
132
+ workflow.params = {
133
+ "threshold": {"type": "number", "default": 0.5, "label": "Threshold"},
134
+ }
135
+
136
+ workflow.add_text("Summary text")
137
+ workflow.add_table("Metrics", {"name": ["a"], "value": [1]})
138
+ workflow.add_plot(fig, title="Spectrum", figure_id="spectrum", save=True)
139
+ workflow.add_code("print('debug')", language="python")
140
+ return workflow.build()
141
+ ```
142
+
143
+ `add_plot(..., save=False)` marks a plot as not exportable when using figure-save operations.
144
+
145
+ Minimum return contract from `run_workflow(...)`:
146
+ - must return a `dict`
147
+ - either:
148
+ - modern shape: `{"results": [ ... ]}`
149
+ - legacy shape: `{"text": [...], "tables": [...], "plots": [...]}`
150
+ - for modern shape, each item in `results` must be a dict with:
151
+ - `type` in `text | table | plot | code`
152
+ - if `type == "plot"`, item must include `figure`
153
+
154
+ ## Provider Contract Options
155
+
156
+ ### 1) Explicit `AppSpec` (fully explicit)
157
+
158
+ Entry point:
159
+
160
+ ```toml
161
+ [project.entry-points."renderflow.providers"]
162
+ my-provider = "my_provider.app_definition:get_app_spec"
163
+ ```
164
+
165
+ `get_app_spec()` returns `renderflow.contracts.AppSpec`.
166
+
167
+ ### 2) Auto-Defined Provider (minimal)
168
+
169
+ If no `app_definition` exists, `renderflow` auto-builds from:
170
+ - `<provider>.workflows.*` modules with `run_workflow(...)`
171
+ - optional `<provider>.renderflow` module:
172
+ - `APP_NAME = "..."`
173
+ - `WORKFLOWS_PACKAGE = "provider.custom_workflows"` (optional)
174
+ - optional custom metadata constants for provider setup
175
+
176
+ Workflow parameters are pulled from:
177
+ 1. `workflow.params` if a `workflow` object exists in the module
178
+ 2. `PARAMS` module global
179
+ 3. inferred function signature defaults
180
+
181
+ This lets packages like `crsd-inspector` keep only workflow definitions and optional init logic, while `renderflow` handles CLI + Streamlit parameter interpretation and rendering.
@@ -0,0 +1,168 @@
1
+ # renderflow
2
+
3
+ Workflow runtime and rendering API for:
4
+ - Streamlit UI
5
+ - CLI execution
6
+ - HTML report export
7
+ - Individual figure export
8
+
9
+ ## Core Idea
10
+
11
+ `renderflow` owns the interface contract and rendering behavior.
12
+ Provider packages should mostly define workflows (`run_workflow` + params), not custom UI/CLI plumbing.
13
+
14
+ ## Demo App
15
+
16
+ Live Streamlit demo:
17
+
18
+ https://demo-for-renderflow.streamlit.app/
19
+
20
+ ## CLI
21
+
22
+ List installed providers:
23
+
24
+ ```bash
25
+ renderflow list-providers
26
+ ```
27
+
28
+ List provider workflows:
29
+
30
+ ```bash
31
+ renderflow list-workflows --provider crsd-inspector
32
+ ```
33
+
34
+ Show interpreted workflow parameters:
35
+
36
+ ```bash
37
+ renderflow show-params --provider crsd-inspector --workflow signal_analysis
38
+ ```
39
+
40
+ Execute a workflow in terminal mode:
41
+
42
+ ```bash
43
+ renderflow execute \
44
+ --provider crsd-inspector \
45
+ --workflow signal_analysis \
46
+ --param crsd_directory=examples \
47
+ --param prf_hz=1000 \
48
+ --output terminal
49
+ ```
50
+
51
+ Execute and export both:
52
+ - one combined report file (`--html`)
53
+ - per-figure files (`--save-figures-dir` + `--figure-format`)
54
+
55
+ ```bash
56
+ renderflow execute \
57
+ --provider crsd-inspector \
58
+ --workflow signal_analysis \
59
+ --param crsd_directory=examples \
60
+ --html output/report.html \
61
+ --save-figures-dir output/figures \
62
+ --figure-format html
63
+ ```
64
+
65
+ Add per-figure JSON in the same run:
66
+
67
+ ```bash
68
+ renderflow execute \
69
+ --provider crsd-inspector \
70
+ --workflow signal_analysis \
71
+ --param crsd_directory=examples \
72
+ --html output/report.html \
73
+ --save-figures-dir output/figures \
74
+ --figure-format html \
75
+ --figure-format json
76
+ ```
77
+
78
+ Export multiple figure formats in a single run:
79
+
80
+ ```bash
81
+ renderflow execute \
82
+ --provider crsd-inspector \
83
+ --workflow signal_analysis \
84
+ --param crsd_directory=examples \
85
+ --save-figures-dir output/figures \
86
+ --figure-format html \
87
+ --figure-format json
88
+ ```
89
+
90
+ Comma-separated format lists are also accepted:
91
+
92
+ ```bash
93
+ renderflow execute \
94
+ --provider crsd-inspector \
95
+ --workflow signal_analysis \
96
+ --param crsd_directory=examples \
97
+ --html output/report.html \
98
+ --save-figures-dir output/figures \
99
+ --figure-format html,json
100
+ ```
101
+
102
+ If `--figure-format` is omitted, per-figure export defaults to `html`.
103
+ Image formats (`png`, `jpg`, `jpeg`, `svg`, `pdf`) require Kaleido. `renderflow` includes `kaleido` as a dependency.
104
+
105
+ Launch Streamlit:
106
+
107
+ ```bash
108
+ renderflow run --provider crsd-inspector
109
+ ```
110
+
111
+ ## Workflow Result Contract
112
+
113
+ Use `renderflow.workflow.Workflow` inside provider workflows:
114
+
115
+ ```python
116
+ from renderflow.workflow import Workflow
117
+
118
+ workflow = Workflow(name="My Workflow", description="...")
119
+ workflow.params = {
120
+ "threshold": {"type": "number", "default": 0.5, "label": "Threshold"},
121
+ }
122
+
123
+ workflow.add_text("Summary text")
124
+ workflow.add_table("Metrics", {"name": ["a"], "value": [1]})
125
+ workflow.add_plot(fig, title="Spectrum", figure_id="spectrum", save=True)
126
+ workflow.add_code("print('debug')", language="python")
127
+ return workflow.build()
128
+ ```
129
+
130
+ `add_plot(..., save=False)` marks a plot as not exportable when using figure-save operations.
131
+
132
+ Minimum return contract from `run_workflow(...)`:
133
+ - must return a `dict`
134
+ - either:
135
+ - modern shape: `{"results": [ ... ]}`
136
+ - legacy shape: `{"text": [...], "tables": [...], "plots": [...]}`
137
+ - for modern shape, each item in `results` must be a dict with:
138
+ - `type` in `text | table | plot | code`
139
+ - if `type == "plot"`, item must include `figure`
140
+
141
+ ## Provider Contract Options
142
+
143
+ ### 1) Explicit `AppSpec` (fully explicit)
144
+
145
+ Entry point:
146
+
147
+ ```toml
148
+ [project.entry-points."renderflow.providers"]
149
+ my-provider = "my_provider.app_definition:get_app_spec"
150
+ ```
151
+
152
+ `get_app_spec()` returns `renderflow.contracts.AppSpec`.
153
+
154
+ ### 2) Auto-Defined Provider (minimal)
155
+
156
+ If no `app_definition` exists, `renderflow` auto-builds from:
157
+ - `<provider>.workflows.*` modules with `run_workflow(...)`
158
+ - optional `<provider>.renderflow` module:
159
+ - `APP_NAME = "..."`
160
+ - `WORKFLOWS_PACKAGE = "provider.custom_workflows"` (optional)
161
+ - optional custom metadata constants for provider setup
162
+
163
+ Workflow parameters are pulled from:
164
+ 1. `workflow.params` if a `workflow` object exists in the module
165
+ 2. `PARAMS` module global
166
+ 3. inferred function signature defaults
167
+
168
+ This lets packages like `crsd-inspector` keep only workflow definitions and optional init logic, while `renderflow` handles CLI + Streamlit parameter interpretation and rendering.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "renderflow"
7
+ version = "0.1.1"
8
+ description = "Generic workflow app runtime and Streamlit renderer with provider plugins"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Brian Day" }
14
+ ]
15
+ dependencies = [
16
+ "streamlit>=1.31.0",
17
+ "pandas>=2.0.0",
18
+ "plotly>=5.18.0",
19
+ "kaleido>=0.2.1"
20
+ ]
21
+
22
+ [project.scripts]
23
+ renderflow = "renderflow.cli:main"
24
+ workflow-renderer-streamlit = "renderflow.cli:main"
25
+
26
+ [tool.setuptools.packages.find]
27
+ include = ["renderflow*"]
@@ -0,0 +1,7 @@
1
+ """Renderflow core package."""
2
+
3
+ from renderflow.cli import main
4
+ from renderflow.progress import emit_progress, wrap_with_timing
5
+ from renderflow.workflow import Workflow
6
+
7
+ __all__ = ["main", "Workflow", "emit_progress", "wrap_with_timing"]
@@ -0,0 +1,6 @@
1
+ """Backward-compatible module for the Streamlit renderer entrypoint."""
2
+
3
+ from renderflow.cli import main
4
+ from renderflow.streamlit_renderer import launch_streamlit_renderer, run_renderer
5
+
6
+ __all__ = ["launch_streamlit_renderer", "main", "run_renderer"]
@@ -0,0 +1,227 @@
1
+ """Automatic provider definition from workflow modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import inspect
7
+ import pkgutil
8
+ from dataclasses import asdict, is_dataclass
9
+ from typing import Any, Callable
10
+
11
+ from renderflow.contracts import AppSpec, InitializerSpec, ParamSpec, WorkflowSpec
12
+
13
+
14
+ def _param_specs_from_mapping(params: dict[str, Any] | None) -> list[ParamSpec]:
15
+ specs: list[ParamSpec] = []
16
+ for key, cfg in (params or {}).items():
17
+ if isinstance(cfg, ParamSpec):
18
+ specs.append(cfg)
19
+ continue
20
+ cfg = cfg or {}
21
+ specs.append(
22
+ ParamSpec(
23
+ key=key,
24
+ label=cfg.get("label", key),
25
+ type=cfg.get("type", "text"),
26
+ default=cfg.get("default"),
27
+ min=cfg.get("min"),
28
+ max=cfg.get("max"),
29
+ step=cfg.get("step"),
30
+ options=cfg.get("options", []),
31
+ help=cfg.get("help", ""),
32
+ )
33
+ )
34
+ return specs
35
+
36
+
37
+ def _coerce_param_specs(value: Any) -> list[ParamSpec]:
38
+ if value is None:
39
+ return []
40
+ if isinstance(value, dict):
41
+ return _param_specs_from_mapping(value)
42
+ specs: list[ParamSpec] = []
43
+ for item in value:
44
+ if isinstance(item, ParamSpec):
45
+ specs.append(item)
46
+ elif isinstance(item, dict):
47
+ specs.append(ParamSpec(**item))
48
+ return specs
49
+
50
+
51
+ def _infer_params_from_signature(func: Callable[..., Any]) -> list[ParamSpec]:
52
+ reserved = {"signal_data", "metadata", "context", "kwargs", "args", "self"}
53
+ specs: list[ParamSpec] = []
54
+ for name, param in inspect.signature(func).parameters.items():
55
+ if name in reserved or param.kind in {inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD}:
56
+ continue
57
+ default = None if param.default is inspect.Parameter.empty else param.default
58
+ param_type = "text"
59
+ if isinstance(default, bool):
60
+ param_type = "checkbox"
61
+ elif isinstance(default, (int, float)):
62
+ param_type = "number"
63
+ specs.append(ParamSpec(key=name, label=name.replace("_", " ").title(), type=param_type, default=default))
64
+ return specs
65
+
66
+
67
+ def _invoke_workflow(func: Callable[..., dict[str, Any]], context: dict[str, Any], params: dict[str, Any]):
68
+ if not isinstance(context, dict):
69
+ raise TypeError(f"Workflow context must be a dict, got {type(context).__name__}")
70
+ if not isinstance(params, dict):
71
+ raise TypeError(f"Workflow params must be a dict, got {type(params).__name__}")
72
+
73
+ sig = inspect.signature(func)
74
+ kwargs: dict[str, Any] = {}
75
+ merged_metadata: dict[str, Any] = {}
76
+ base_metadata = context.get("metadata")
77
+ if isinstance(base_metadata, dict):
78
+ merged_metadata.update(base_metadata)
79
+ for key, value in context.items():
80
+ if key != "metadata":
81
+ merged_metadata[key] = value
82
+ merged_metadata.update(params)
83
+
84
+ if "signal_data" in sig.parameters and "signal_data" in context:
85
+ kwargs["signal_data"] = context["signal_data"]
86
+ if "metadata" in sig.parameters:
87
+ kwargs["metadata"] = merged_metadata
88
+ if "context" in sig.parameters:
89
+ kwargs["context"] = context
90
+
91
+ for key, value in params.items():
92
+ if key in sig.parameters and key not in kwargs:
93
+ kwargs[key] = value
94
+
95
+ result = func(**kwargs)
96
+ if not isinstance(result, dict):
97
+ raise TypeError(
98
+ f"Workflow '{func.__module__}.{func.__name__}' must return a dict, got {type(result).__name__}"
99
+ )
100
+ return result
101
+
102
+
103
+ def _discover_workflows(provider_name: str, package_name: str) -> list[WorkflowSpec]:
104
+ pkg = importlib.import_module(package_name)
105
+ specs: list[WorkflowSpec] = []
106
+ for module_info in sorted(pkgutil.iter_modules(pkg.__path__), key=lambda m: m.name):
107
+ if module_info.name.startswith("_"):
108
+ continue
109
+ module = importlib.import_module(f"{package_name}.{module_info.name}")
110
+ run_workflow = getattr(module, "run_workflow", None)
111
+ if not callable(run_workflow):
112
+ continue
113
+
114
+ workflow_obj = getattr(module, "workflow", None)
115
+ wf_id = module_info.name
116
+ wf_name = getattr(workflow_obj, "name", None) or getattr(module, "WORKFLOW_NAME", wf_id)
117
+ wf_desc = getattr(workflow_obj, "description", None) or getattr(module, "WORKFLOW_DESCRIPTION", "")
118
+ raw_params = getattr(workflow_obj, "params", None)
119
+ if raw_params is None:
120
+ raw_params = getattr(module, "PARAMS", None)
121
+ if raw_params is None:
122
+ param_specs = _infer_params_from_signature(run_workflow)
123
+ else:
124
+ param_specs = _coerce_param_specs(raw_params)
125
+
126
+ def _make_run(func):
127
+ def _run(context: dict[str, Any], params: dict[str, Any]) -> dict[str, Any]:
128
+ return _invoke_workflow(func, context, params)
129
+
130
+ return _run
131
+
132
+ specs.append(
133
+ WorkflowSpec(
134
+ id=wf_id,
135
+ name=wf_name,
136
+ description=wf_desc,
137
+ params=param_specs,
138
+ run=_make_run(run_workflow),
139
+ )
140
+ )
141
+ if not specs:
142
+ raise RuntimeError(f"No workflow modules with run_workflow() found under {package_name}")
143
+ return specs
144
+
145
+
146
+ def _dict_to_app_spec(raw: dict[str, Any]) -> AppSpec:
147
+ if {"app_name", "initializers", "workflows"} - set(raw.keys()):
148
+ raise TypeError("Dictionary provider spec missing one of: app_name, initializers, workflows")
149
+ initializers: list[InitializerSpec] = []
150
+ for item in raw["initializers"]:
151
+ if isinstance(item, InitializerSpec):
152
+ initializers.append(item)
153
+ else:
154
+ initializers.append(
155
+ InitializerSpec(
156
+ id=item["id"],
157
+ name=item["name"],
158
+ description=item.get("description", ""),
159
+ params=_coerce_param_specs(item.get("params")),
160
+ initialize=item["initialize"],
161
+ )
162
+ )
163
+
164
+ workflows: list[WorkflowSpec] = []
165
+ for item in raw["workflows"]:
166
+ if isinstance(item, WorkflowSpec):
167
+ workflows.append(item)
168
+ else:
169
+ workflows.append(
170
+ WorkflowSpec(
171
+ id=item["id"],
172
+ name=item["name"],
173
+ description=item.get("description", ""),
174
+ params=_coerce_param_specs(item.get("params")),
175
+ run=item["run"],
176
+ )
177
+ )
178
+ return AppSpec(app_name=raw["app_name"], initializers=initializers, workflows=workflows)
179
+
180
+
181
+ def coerce_to_app_spec(obj: Any, provider_name: str | None = None) -> AppSpec:
182
+ if callable(obj) and not isinstance(obj, type):
183
+ obj = obj()
184
+ if isinstance(obj, AppSpec):
185
+ return obj
186
+ if is_dataclass(obj):
187
+ return _dict_to_app_spec(asdict(obj))
188
+ if isinstance(obj, dict):
189
+ return _dict_to_app_spec(obj)
190
+ raise TypeError(f"Unsupported provider definition object for {provider_name or 'provider'}: {type(obj)}")
191
+
192
+
193
+ def auto_build_app_spec(provider_name: str) -> AppSpec:
194
+ provider_pkg = importlib.import_module(provider_name)
195
+
196
+ cfg_module = None
197
+ try:
198
+ cfg_module = importlib.import_module(f"{provider_name}.renderflow")
199
+ except ModuleNotFoundError:
200
+ cfg_module = None
201
+
202
+ app_name = getattr(cfg_module, "APP_NAME", None) or getattr(provider_pkg, "APP_NAME", None) or provider_name
203
+ workflow_package = (
204
+ getattr(cfg_module, "WORKFLOWS_PACKAGE", None) or getattr(provider_pkg, "WORKFLOWS_PACKAGE", None)
205
+ )
206
+ if not workflow_package:
207
+ workflow_package = f"{provider_name}.workflows"
208
+
209
+ initializers: list[InitializerSpec] = []
210
+ init_func = getattr(cfg_module, "initialize", None) or getattr(provider_pkg, "initialize", None)
211
+ init_params = getattr(cfg_module, "INIT_PARAMS", None) or getattr(provider_pkg, "INIT_PARAMS", None)
212
+ if callable(init_func):
213
+ initializers.append(
214
+ InitializerSpec(
215
+ id="default_initializer",
216
+ name="Initialization",
217
+ description="Provider initialization context",
218
+ params=_coerce_param_specs(init_params),
219
+ initialize=init_func,
220
+ )
221
+ )
222
+
223
+ return AppSpec(
224
+ app_name=app_name,
225
+ initializers=initializers,
226
+ workflows=_discover_workflows(provider_name, workflow_package),
227
+ )