stageflow-framework 0.1.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.
- stageflow/__init__.py +25 -0
- stageflow/builtins/__init__.py +34 -0
- stageflow/builtins/dicts.py +59 -0
- stageflow/builtins/lists.py +71 -0
- stageflow/builtins/lists_extra.py +110 -0
- stageflow/builtins/logic.py +84 -0
- stageflow/builtins/strings.py +81 -0
- stageflow/builtins/vars.py +118 -0
- stageflow/core/__init__.py +7 -0
- stageflow/core/context.py +86 -0
- stageflow/core/event.py +71 -0
- stageflow/core/jsonlogic.py +43 -0
- stageflow/core/node.py +256 -0
- stageflow/core/pipeline.py +108 -0
- stageflow/core/session.py +398 -0
- stageflow/core/stage.py +152 -0
- stageflow/core/utils.py +62 -0
- stageflow/docs/__init__.py +4 -0
- stageflow/docs/html.py +479 -0
- stageflow/docs/schema.py +38 -0
- stageflow/docs/schemas/pipeline.json +134 -0
- stageflow/py.typed +1 -0
- stageflow/testing.py +59 -0
- stageflow_framework-0.1.0.dist-info/METADATA +167 -0
- stageflow_framework-0.1.0.dist-info/RECORD +27 -0
- stageflow_framework-0.1.0.dist-info/WHEEL +5 -0
- stageflow_framework-0.1.0.dist-info/top_level.txt +1 -0
stageflow/docs/html.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate StageFlow HTML docs and related JSON assets in code (no file writes).
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from stageflow.docs.html import generate_docs_assets
|
|
6
|
+
html_page, schema, stages_json = generate_docs_assets()
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import html
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, Iterable
|
|
14
|
+
|
|
15
|
+
import stageflow.builtins # noqa: F401 - ensure builtins are registered
|
|
16
|
+
from stageflow.core.stage import get_stages, get_stages_by_category
|
|
17
|
+
from stageflow.docs.schema import generate_pipeline_schema, generate_stages_json
|
|
18
|
+
|
|
19
|
+
TYPE_HINTS = {"string", "str", "number", "int", "float", "bool", "any", "object", "list"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _render_json(data: Any) -> str:
|
|
23
|
+
def fmt(val: Any, indent: int = 0) -> str:
|
|
24
|
+
pad = " " * indent
|
|
25
|
+
if isinstance(val, dict):
|
|
26
|
+
if not val:
|
|
27
|
+
return "{}"
|
|
28
|
+
inner = []
|
|
29
|
+
for k, v in val.items():
|
|
30
|
+
inner.append(f'{pad} "{k}": {fmt(v, indent + 1)}')
|
|
31
|
+
return "{\n" + "\n".join(inner) + f"\n{pad}" + "}"
|
|
32
|
+
if isinstance(val, list):
|
|
33
|
+
if not val:
|
|
34
|
+
return "[]"
|
|
35
|
+
inner = [fmt(v, indent + 1) for v in val]
|
|
36
|
+
return "[ " + ", ".join(inner) + " ]"
|
|
37
|
+
if isinstance(val, str):
|
|
38
|
+
if val.lower() in TYPE_HINTS:
|
|
39
|
+
return val
|
|
40
|
+
return f"\"{val}\""
|
|
41
|
+
if isinstance(val, bool):
|
|
42
|
+
return "true" if val else "false"
|
|
43
|
+
return str(val)
|
|
44
|
+
|
|
45
|
+
return html.escape(fmt(data))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _fields_to_placeholder_map(fields: list[dict[str, Any]] | dict[str, Any] | None, builder) -> dict[str, Any]:
|
|
49
|
+
mapping: dict[str, Any] = {}
|
|
50
|
+
if not fields:
|
|
51
|
+
return mapping
|
|
52
|
+
iterable = fields.items() if isinstance(fields, dict) else fields
|
|
53
|
+
for item in iterable:
|
|
54
|
+
if isinstance(item, tuple):
|
|
55
|
+
name, spec = item
|
|
56
|
+
field = {"name": name}
|
|
57
|
+
if isinstance(spec, dict):
|
|
58
|
+
field.update(spec)
|
|
59
|
+
else:
|
|
60
|
+
field["type"] = spec
|
|
61
|
+
else:
|
|
62
|
+
field = item or {}
|
|
63
|
+
name = field.get("name")
|
|
64
|
+
if not name:
|
|
65
|
+
continue
|
|
66
|
+
mapping[name] = builder(field)
|
|
67
|
+
return mapping
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _render_stage_field_table(fields: list[dict[str, Any]], empty_label: str) -> str:
|
|
71
|
+
if not fields:
|
|
72
|
+
return f"<div class='muted'>{html.escape(empty_label)}</div>"
|
|
73
|
+
rows = []
|
|
74
|
+
for field in fields:
|
|
75
|
+
name = field.get("name", "")
|
|
76
|
+
typ = field.get("type", "any")
|
|
77
|
+
description = field.get("description", "")
|
|
78
|
+
default = field.get("default", "")
|
|
79
|
+
optional = field.get("optional", False)
|
|
80
|
+
parts = [f"<code>{html.escape(str(typ))}</code>"]
|
|
81
|
+
if default not in (None, ""):
|
|
82
|
+
parts.append(f"default: <code>{html.escape(str(default))}</code>")
|
|
83
|
+
if optional:
|
|
84
|
+
parts.append("<span class='badge opt'>optional</span>")
|
|
85
|
+
desc_part = html.escape(str(description)) if description else ""
|
|
86
|
+
rows.append(
|
|
87
|
+
f"<tr><td><code>{html.escape(str(name))}</code></td>"
|
|
88
|
+
f"<td>{' '.join(parts)}</td>"
|
|
89
|
+
f"<td>{desc_part}</td></tr>"
|
|
90
|
+
)
|
|
91
|
+
return (
|
|
92
|
+
"<table class='fields'>"
|
|
93
|
+
"<thead><tr><th>Field</th><th>Info</th><th></th></tr></thead>"
|
|
94
|
+
f"<tbody>{''.join(rows)}</tbody>"
|
|
95
|
+
"</table>"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _render_field_list(props: dict[str, Any], required: Iterable[str]) -> str:
|
|
100
|
+
req_set = set(required or [])
|
|
101
|
+
rows = []
|
|
102
|
+
for name, spec in props.items():
|
|
103
|
+
typ = spec.get("type", "any")
|
|
104
|
+
description = spec.get("description", "")
|
|
105
|
+
default = spec.get("default", "")
|
|
106
|
+
enum = spec.get("enum", [])
|
|
107
|
+
parts = [f"<code>{html.escape(str(typ))}</code>"]
|
|
108
|
+
if default not in (None, ""):
|
|
109
|
+
parts.append(f"default: <code>{html.escape(str(default))}</code>")
|
|
110
|
+
if enum:
|
|
111
|
+
parts.append("enum: " + ", ".join(html.escape(str(e)) for e in enum))
|
|
112
|
+
if description:
|
|
113
|
+
parts.append(html.escape(description))
|
|
114
|
+
req_badge = "<span class='badge req'>required</span>" if name in req_set else ""
|
|
115
|
+
rows.append(
|
|
116
|
+
f"<tr><td><code>{html.escape(name)}</code></td>"
|
|
117
|
+
f"<td>{' '.join(parts)}</td>"
|
|
118
|
+
f"<td>{req_badge}</td></tr>"
|
|
119
|
+
)
|
|
120
|
+
return "\n".join(rows)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_nodes_section(schema: dict[str, Any]) -> str:
|
|
124
|
+
defs = schema.get("$defs", {})
|
|
125
|
+
blocks = []
|
|
126
|
+
for key in ("stage_node", "condition_node", "parallel_node", "map_node", "subpipeline_node", "terminal_node"):
|
|
127
|
+
node = defs.get(key)
|
|
128
|
+
if not node:
|
|
129
|
+
continue
|
|
130
|
+
title = key.replace("_", " ").title()
|
|
131
|
+
props = node.get("properties", {})
|
|
132
|
+
required = node.get("required", [])
|
|
133
|
+
rows = _render_field_list(props, required)
|
|
134
|
+
example = {**{k: v.get("const") for k, v in props.items() if "const" in (v or {})}}
|
|
135
|
+
blocks.append(
|
|
136
|
+
f"""
|
|
137
|
+
<details class="node-block">
|
|
138
|
+
<summary>{html.escape(title)}</summary>
|
|
139
|
+
<div class="card">
|
|
140
|
+
<table class="fields">
|
|
141
|
+
<thead><tr><th>Field</th><th>Info</th><th></th></tr></thead>
|
|
142
|
+
<tbody>{rows}</tbody>
|
|
143
|
+
</table>
|
|
144
|
+
<div class="example">
|
|
145
|
+
<div class="label">Minimal fragment</div>
|
|
146
|
+
<pre>{_render_json(example)}</pre>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</details>
|
|
150
|
+
"""
|
|
151
|
+
)
|
|
152
|
+
return "\n".join(blocks)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def build_stages_section() -> str:
|
|
156
|
+
by_cat = get_stages_by_category()
|
|
157
|
+
sections = []
|
|
158
|
+
for category in sorted(by_cat.keys()):
|
|
159
|
+
cards = []
|
|
160
|
+
for stage_cls in sorted(by_cat[category], key=lambda c: c.stage_name):
|
|
161
|
+
specs = stage_cls.get_specs()
|
|
162
|
+
arguments = specs.get("arguments") or []
|
|
163
|
+
config = specs.get("config") or []
|
|
164
|
+
outputs = specs.get("outputs") or []
|
|
165
|
+
allowed_events = specs.get("allowed_events") or []
|
|
166
|
+
allowed_inputs = specs.get("allowed_inputs") or []
|
|
167
|
+
stage_name = specs.get("stage_name", stage_cls.__name__)
|
|
168
|
+
args_placeholder = _fields_to_placeholder_map(arguments, lambda f: _placeholder_from_hint(f.get("type")))
|
|
169
|
+
config_placeholder = _fields_to_placeholder_map(
|
|
170
|
+
config, lambda f: f.get("default") if "default" in f else _placeholder_from_hint(f.get("type"))
|
|
171
|
+
)
|
|
172
|
+
outputs_placeholder = _fields_to_placeholder_map(outputs, lambda f: f.get("name"))
|
|
173
|
+
search_blob = " ".join(
|
|
174
|
+
str(x)
|
|
175
|
+
for x in [
|
|
176
|
+
stage_name,
|
|
177
|
+
specs.get("description", ""),
|
|
178
|
+
category,
|
|
179
|
+
" ".join(f.get("name", "") for f in arguments),
|
|
180
|
+
" ".join(f.get("name", "") for f in config),
|
|
181
|
+
" ".join(f.get("name", "") for f in outputs),
|
|
182
|
+
]
|
|
183
|
+
).lower()
|
|
184
|
+
example_node = {
|
|
185
|
+
"id": stage_name.lower(),
|
|
186
|
+
"type": "stage",
|
|
187
|
+
"stage": stage_name,
|
|
188
|
+
**({"config": config_placeholder} if config_placeholder else {}),
|
|
189
|
+
**({"arguments": args_placeholder} if args_placeholder else {}),
|
|
190
|
+
**({"outputs": outputs_placeholder} if outputs_placeholder else {}),
|
|
191
|
+
"next": "next_node",
|
|
192
|
+
}
|
|
193
|
+
cards.append(
|
|
194
|
+
f"""
|
|
195
|
+
<div class="stage-card" data-search="{html.escape(search_blob)}">
|
|
196
|
+
<div class="stage-title">{html.escape(stage_name)}</div>
|
|
197
|
+
<div class="muted">{html.escape(specs.get("description", '') or 'No description')}</div>
|
|
198
|
+
<div class="chip-row">
|
|
199
|
+
<span class="chip">category: {html.escape(category)}</span>
|
|
200
|
+
<span class="chip">{'skipable' if specs.get('skipable') else 'not skipable'}</span>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="block">
|
|
203
|
+
<div class="label">arguments</div>
|
|
204
|
+
{_render_stage_field_table(arguments, "No arguments")}
|
|
205
|
+
</div>
|
|
206
|
+
<div class="block">
|
|
207
|
+
<div class="label">config</div>
|
|
208
|
+
{_render_stage_field_table(config, "No config")}
|
|
209
|
+
</div>
|
|
210
|
+
<div class="block">
|
|
211
|
+
<div class="label">outputs</div>
|
|
212
|
+
{_render_stage_field_table(outputs, "No outputs")}
|
|
213
|
+
</div>
|
|
214
|
+
<div class="block two-cols">
|
|
215
|
+
<div>
|
|
216
|
+
<div class="label">allowed events</div>
|
|
217
|
+
<pre>{_render_json(allowed_events)}</pre>
|
|
218
|
+
</div>
|
|
219
|
+
<div>
|
|
220
|
+
<div class="label">allowed inputs</div>
|
|
221
|
+
<pre>{_render_json(allowed_inputs)}</pre>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="block">
|
|
225
|
+
<div class="label">Example usage</div>
|
|
226
|
+
<pre>{_render_json(example_node)}</pre>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
"""
|
|
230
|
+
)
|
|
231
|
+
sections.append(
|
|
232
|
+
f"""
|
|
233
|
+
<details class="category" open data-category="{html.escape(category.lower())}">
|
|
234
|
+
<summary>Category: {html.escape(category)}</summary>
|
|
235
|
+
<div class="stage-grid">
|
|
236
|
+
{''.join(cards)}
|
|
237
|
+
</div>
|
|
238
|
+
</details>
|
|
239
|
+
"""
|
|
240
|
+
)
|
|
241
|
+
return "\n".join(sections)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def build_example_block(schema: dict[str, Any]) -> str:
|
|
245
|
+
example = {
|
|
246
|
+
"api_version": schema.get("properties", {}).get("api_version", {}).get("default"),
|
|
247
|
+
"entry": "start",
|
|
248
|
+
"nodes": [
|
|
249
|
+
{
|
|
250
|
+
"id": "start",
|
|
251
|
+
"type": "stage",
|
|
252
|
+
"stage": "InitStage",
|
|
253
|
+
"outputs": {"value": "value"},
|
|
254
|
+
"next": "finish",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"id": "finish",
|
|
258
|
+
"type": "terminal",
|
|
259
|
+
"result": {"status": "ok"},
|
|
260
|
+
"artifacts": ["value"],
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
}
|
|
264
|
+
return f"""
|
|
265
|
+
<section>
|
|
266
|
+
<h2>Minimal pipeline example</h2>
|
|
267
|
+
<p>Copy and plug in your own stages and context paths.</p>
|
|
268
|
+
<pre>{_render_json(example)}</pre>
|
|
269
|
+
</section>
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _placeholder_from_hint(val: Any) -> Any:
|
|
274
|
+
if isinstance(val, dict):
|
|
275
|
+
return {k: _placeholder_from_hint(v) for k, v in val.items()}
|
|
276
|
+
if isinstance(val, list):
|
|
277
|
+
if val:
|
|
278
|
+
return [_placeholder_from_hint(val[0])]
|
|
279
|
+
return []
|
|
280
|
+
if isinstance(val, str):
|
|
281
|
+
low = val.lower()
|
|
282
|
+
if low in {"string", "str"}:
|
|
283
|
+
return "<string>"
|
|
284
|
+
if low in {"number", "int", "float"}:
|
|
285
|
+
return 0
|
|
286
|
+
if low in {"bool", "boolean"}:
|
|
287
|
+
return True
|
|
288
|
+
if low == "object":
|
|
289
|
+
return {}
|
|
290
|
+
if low == "list":
|
|
291
|
+
return []
|
|
292
|
+
return val or "<value>"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def build_html(schema: dict[str, Any], stages_json: str) -> str:
|
|
296
|
+
schema_json = json.dumps(schema, indent=2, ensure_ascii=False)
|
|
297
|
+
stages_obj_literal = stages_json
|
|
298
|
+
try:
|
|
299
|
+
stages_obj_literal = json.dumps(json.loads(stages_json), ensure_ascii=False, indent=2)
|
|
300
|
+
except Exception:
|
|
301
|
+
# If parsing fails, fall back to raw string (still escaped in HTML/pre).
|
|
302
|
+
stages_obj_literal = json.dumps(stages_json, ensure_ascii=False)
|
|
303
|
+
return f"""<!DOCTYPE html>
|
|
304
|
+
<html lang="ru">
|
|
305
|
+
<head>
|
|
306
|
+
<meta charset="UTF-8">
|
|
307
|
+
<title>StageFlow Docs</title>
|
|
308
|
+
<style>
|
|
309
|
+
:root {{
|
|
310
|
+
--bg: #0f1115;
|
|
311
|
+
--card: #181b21;
|
|
312
|
+
--text: #e8ecf2;
|
|
313
|
+
--muted: #9aa3b5;
|
|
314
|
+
--accent: #4ea1ff;
|
|
315
|
+
--border: #262b33;
|
|
316
|
+
--chip: #1f252d;
|
|
317
|
+
}}
|
|
318
|
+
* {{ box-sizing: border-box; }}
|
|
319
|
+
body {{ margin:0; font-family: "Segoe UI", sans-serif; background: var(--bg); color: var(--text); padding: 40px 56px; min-height: 100vh; }}
|
|
320
|
+
.content {{ max-width: 1280px; margin: 0 auto; }}
|
|
321
|
+
h1, h2, h3 {{ margin: 0 0 12px; }}
|
|
322
|
+
p {{ margin: 0 0 12px; color: var(--muted); }}
|
|
323
|
+
section {{ margin-bottom: 32px; }}
|
|
324
|
+
details {{ margin-bottom: 12px; }}
|
|
325
|
+
summary {{ cursor: pointer; color: var(--accent); font-weight: 600; }}
|
|
326
|
+
.card {{ background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }}
|
|
327
|
+
.fields {{ width: 100%; border-collapse: collapse; margin-top: 8px; table-layout: fixed; }}
|
|
328
|
+
.fields th, .fields td {{ border-bottom: 1px solid var(--border); padding: 8px; text-align: left; vertical-align: top; }}
|
|
329
|
+
.fields th {{ color: var(--muted); font-weight: 600; }}
|
|
330
|
+
.fields td:first-child {{ width: 32%; }}
|
|
331
|
+
pre {{ background: #0a0c10; border: 1px solid var(--border); border-radius: 8px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }}
|
|
332
|
+
.label {{ color: var(--muted); font-size: 12px; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.04em; }}
|
|
333
|
+
.example {{ margin-top: 12px; }}
|
|
334
|
+
.stage-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: 24px; }}
|
|
335
|
+
.stage-card {{ background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; display: flex; flex-direction: column; gap: 12px; }}
|
|
336
|
+
.stage-title {{ font-weight: 700; font-size: 17px; }}
|
|
337
|
+
.muted {{ color: var(--muted); line-height: 1.5; }}
|
|
338
|
+
.chip-row {{ display: flex; gap: 8px; flex-wrap: wrap; }}
|
|
339
|
+
.chip {{ background: var(--chip); padding: 4px 8px; border-radius: 999px; color: var(--muted); font-size: 12px; border: 1px solid var(--border); }}
|
|
340
|
+
.badge.req {{ background: #2f3; color: #0a0; padding: 2px 6px; border-radius: 6px; font-size: 12px; }}
|
|
341
|
+
.badge.opt {{ background: #f8c146; color: #4b3200; padding: 2px 6px; border-radius: 6px; font-size: 12px; }}
|
|
342
|
+
.block {{ display: flex; flex-direction: column; gap: 4px; }}
|
|
343
|
+
.two-cols {{ display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }}
|
|
344
|
+
.topbar {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; gap: 12px; flex-wrap: wrap; }}
|
|
345
|
+
.topbar-actions {{ display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }}
|
|
346
|
+
.search {{ background: var(--card); border: 1px solid var(--border); color: var(--text); padding: 10px 12px; border-radius: 10px; min-width: 260px; }}
|
|
347
|
+
.search:focus {{ outline: 1px solid var(--accent); }}
|
|
348
|
+
.cta {{ background: var(--accent); color: #0b0f16; padding: 10px 14px; border-radius: 10px; text-decoration: none; font-weight: 700; border: 1px solid transparent; cursor: pointer; }}
|
|
349
|
+
.cta.secondary {{ background: transparent; color: var(--text); border-color: var(--border); }}
|
|
350
|
+
.category summary {{ font-size: 16px; margin-bottom: 8px; }}
|
|
351
|
+
.category[hidden], .node-block[hidden] {{ display: none; }}
|
|
352
|
+
.node-grid details {{ margin-bottom: 8px; }}
|
|
353
|
+
</style>
|
|
354
|
+
</head>
|
|
355
|
+
<body>
|
|
356
|
+
<div class="content">
|
|
357
|
+
<div class="topbar">
|
|
358
|
+
<div>
|
|
359
|
+
<h1>StageFlow</h1>
|
|
360
|
+
<p>Cheat sheet for pipelines, node types, and available stages.</p>
|
|
361
|
+
</div>
|
|
362
|
+
<div class="topbar-actions">
|
|
363
|
+
<input id="search" class="search" type="search" placeholder="Search stages and categories..." />
|
|
364
|
+
<button id="toggle-all" class="cta secondary" type="button">Collapse all</button>
|
|
365
|
+
<button id="download-stages" class="cta secondary" type="button">Download stages JSON</button>
|
|
366
|
+
<button id="download-schema" class="cta secondary" type="button">Download pipeline schema</button>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<section>
|
|
371
|
+
<h2>Node types</h2>
|
|
372
|
+
<p>Quick overview of fields and required properties for each node type.</p>
|
|
373
|
+
<div class="node-grid">{build_nodes_section(schema)}</div>
|
|
374
|
+
</section>
|
|
375
|
+
|
|
376
|
+
<section>
|
|
377
|
+
<h2>Stages (registry)</h2>
|
|
378
|
+
<p>All registered stages with arguments, config, outputs, and allowed events.</p>
|
|
379
|
+
{build_stages_section()}
|
|
380
|
+
</section>
|
|
381
|
+
|
|
382
|
+
{build_example_block(schema)}
|
|
383
|
+
<section id="stages-json">
|
|
384
|
+
<h2>Stages JSON</h2>
|
|
385
|
+
<p>Raw stage specs used to build this page.</p>
|
|
386
|
+
<pre>{html.escape(stages_json)}</pre>
|
|
387
|
+
</section>
|
|
388
|
+
<section id="pipeline-schema">
|
|
389
|
+
<h2>Pipeline JSON Schema</h2>
|
|
390
|
+
<p>Schema used for validation (stage enum injected).</p>
|
|
391
|
+
<pre>{_render_json(schema)}</pre>
|
|
392
|
+
</section>
|
|
393
|
+
</div>
|
|
394
|
+
<script>
|
|
395
|
+
const pipelineSchema = {schema_json};
|
|
396
|
+
const stagesSpec = {stages_obj_literal};
|
|
397
|
+
const searchInput = document.getElementById('search');
|
|
398
|
+
const categories = Array.from(document.querySelectorAll('details.category'));
|
|
399
|
+
const nodeBlocks = Array.from(document.querySelectorAll('details.node-block'));
|
|
400
|
+
const toggleAllBtn = document.getElementById('toggle-all');
|
|
401
|
+
const downloadStagesBtn = document.getElementById('download-stages');
|
|
402
|
+
const downloadSchemaBtn = document.getElementById('download-schema');
|
|
403
|
+
|
|
404
|
+
function applyFilter() {{
|
|
405
|
+
const term = (searchInput.value || '').toLowerCase().trim();
|
|
406
|
+
categories.forEach((cat) => {{
|
|
407
|
+
let hasVisible = false;
|
|
408
|
+
const cards = Array.from(cat.querySelectorAll('.stage-card'));
|
|
409
|
+
cards.forEach((card) => {{
|
|
410
|
+
const haystack = (card.dataset.search || '').toLowerCase();
|
|
411
|
+
const match = !term || haystack.includes(term);
|
|
412
|
+
card.style.display = match ? '' : 'none';
|
|
413
|
+
if (match) hasVisible = true;
|
|
414
|
+
}});
|
|
415
|
+
cat.hidden = !hasVisible;
|
|
416
|
+
if (hasVisible && term) {{
|
|
417
|
+
cat.open = true;
|
|
418
|
+
}}
|
|
419
|
+
}});
|
|
420
|
+
nodeBlocks.forEach((block) => {{
|
|
421
|
+
const haystack = (block.querySelector('summary')?.textContent || '').toLowerCase();
|
|
422
|
+
const match = !term || haystack.includes(term);
|
|
423
|
+
block.hidden = !match;
|
|
424
|
+
}});
|
|
425
|
+
}}
|
|
426
|
+
|
|
427
|
+
if (searchInput) {{
|
|
428
|
+
searchInput.addEventListener('input', applyFilter);
|
|
429
|
+
applyFilter();
|
|
430
|
+
}}
|
|
431
|
+
|
|
432
|
+
function setAll(open) {{
|
|
433
|
+
[...categories, ...nodeBlocks].forEach((elem) => {{
|
|
434
|
+
if (!elem.hidden) {{
|
|
435
|
+
elem.open = open;
|
|
436
|
+
}}
|
|
437
|
+
}});
|
|
438
|
+
toggleAllBtn.textContent = open ? 'Collapse all' : 'Expand all';
|
|
439
|
+
}}
|
|
440
|
+
|
|
441
|
+
if (toggleAllBtn) {{
|
|
442
|
+
let allOpen = true;
|
|
443
|
+
toggleAllBtn.addEventListener('click', () => {{
|
|
444
|
+
allOpen = !allOpen;
|
|
445
|
+
setAll(allOpen);
|
|
446
|
+
}});
|
|
447
|
+
}}
|
|
448
|
+
|
|
449
|
+
function downloadJson(data, filename) {{
|
|
450
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], {{ type: 'application/json' }});
|
|
451
|
+
const url = URL.createObjectURL(blob);
|
|
452
|
+
const link = document.createElement('a');
|
|
453
|
+
link.href = url;
|
|
454
|
+
link.download = filename;
|
|
455
|
+
link.click();
|
|
456
|
+
URL.revokeObjectURL(url);
|
|
457
|
+
}}
|
|
458
|
+
|
|
459
|
+
if (downloadStagesBtn) {{
|
|
460
|
+
downloadStagesBtn.addEventListener('click', () => downloadJson(stagesSpec, 'stages.json'));
|
|
461
|
+
}}
|
|
462
|
+
if (downloadSchemaBtn) {{
|
|
463
|
+
downloadSchemaBtn.addEventListener('click', () => downloadJson(pipelineSchema, 'pipeline.json'));
|
|
464
|
+
}}
|
|
465
|
+
</script>
|
|
466
|
+
</body>
|
|
467
|
+
</html>
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def generate_docs_assets() -> tuple[str, dict, str]:
|
|
472
|
+
"""
|
|
473
|
+
Return (html_page, pipeline_schema, stages_json) using registered stages, without writing any files.
|
|
474
|
+
"""
|
|
475
|
+
stages = get_stages()
|
|
476
|
+
schema = generate_pipeline_schema(stages)
|
|
477
|
+
stages_json = generate_stages_json(stages)
|
|
478
|
+
html_page = build_html(schema, stages_json)
|
|
479
|
+
return html_page, schema, stages_json
|
stageflow/docs/schema.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import json
|
|
3
|
+
from importlib import resources
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_stages_yaml(stage_registry: dict) -> str:
|
|
7
|
+
stages_doc = {}
|
|
8
|
+
for name, cls in stage_registry.items():
|
|
9
|
+
specs = cls.get_specs()
|
|
10
|
+
stages_doc[name] = specs
|
|
11
|
+
return yaml.dump(stages_doc, allow_unicode=True, sort_keys=False, indent=2)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_stages_json(stage_registry: dict) -> str:
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
stages_doc = {}
|
|
18
|
+
for name, cls in stage_registry.items():
|
|
19
|
+
specs = cls.get_specs()
|
|
20
|
+
stages_doc[name] = specs
|
|
21
|
+
return json.dumps(stages_doc, indent=2)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_pipeline_schema() -> dict:
|
|
25
|
+
with resources.files("stageflow.docs.schemas").joinpath("pipeline.json").open("r", encoding="utf-8") as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def generate_pipeline_schema(stage_registry: dict) -> dict:
|
|
30
|
+
"""Return pipeline JSON Schema with stage names enum-injected."""
|
|
31
|
+
schema = load_pipeline_schema()
|
|
32
|
+
stage_names = list(stage_registry.keys())
|
|
33
|
+
# Inject enum into stage node definition.
|
|
34
|
+
try:
|
|
35
|
+
schema["$defs"]["stage_node"]["properties"]["stage"]["enum"] = stage_names
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
return schema
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "StageFlow Pipeline",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["entry", "nodes"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"api_version": { "type": ["string", "null"] },
|
|
8
|
+
"entry": { "type": "string" },
|
|
9
|
+
"metadata": { "type": "object" },
|
|
10
|
+
"nodes": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": { "$ref": "#/$defs/node" },
|
|
13
|
+
"minItems": 1
|
|
14
|
+
},
|
|
15
|
+
"subpipelines": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": { "$ref": "#/$defs/pipeline" },
|
|
18
|
+
"default": {}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"$defs": {
|
|
23
|
+
"pipeline": { "$ref": "#" },
|
|
24
|
+
"node": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"required": ["id", "type"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"id": { "type": "string" },
|
|
29
|
+
"type": { "type": "string" },
|
|
30
|
+
"metadata": { "type": "object" }
|
|
31
|
+
},
|
|
32
|
+
"discriminator": {
|
|
33
|
+
"propertyName": "type",
|
|
34
|
+
"mapping": {
|
|
35
|
+
"stage": { "$ref": "#/$defs/stage_node" },
|
|
36
|
+
"condition": { "$ref": "#/$defs/condition_node" },
|
|
37
|
+
"parallel": { "$ref": "#/$defs/parallel_node" },
|
|
38
|
+
"terminal": { "$ref": "#/$defs/terminal_node" },
|
|
39
|
+
"subpipeline": { "$ref": "#/$defs/subpipeline_node" }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"stage_node": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"required": ["id", "type", "stage"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"id": { "type": "string" },
|
|
48
|
+
"type": { "const": "stage" },
|
|
49
|
+
"stage": { "type": "string" },
|
|
50
|
+
"config": { "type": "object" },
|
|
51
|
+
"arguments": { "type": "object" },
|
|
52
|
+
"outputs": { "type": "object" },
|
|
53
|
+
"next": { "type": ["string", "null"] },
|
|
54
|
+
"fallback": { "type": ["string", "null"] },
|
|
55
|
+
"metadata": { "type": "object" }
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
},
|
|
59
|
+
"condition": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"required": ["if", "then"],
|
|
62
|
+
"properties": {
|
|
63
|
+
"if": { "type": "object" },
|
|
64
|
+
"then": { "type": "string" }
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
},
|
|
68
|
+
"condition_node": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"required": ["id", "type", "conditions"],
|
|
71
|
+
"properties": {
|
|
72
|
+
"id": { "type": "string" },
|
|
73
|
+
"type": { "const": "condition" },
|
|
74
|
+
"conditions": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": { "$ref": "#/$defs/condition" },
|
|
77
|
+
"minItems": 1
|
|
78
|
+
},
|
|
79
|
+
"else": { "type": ["string", "null"] },
|
|
80
|
+
"metadata": { "type": "object" }
|
|
81
|
+
},
|
|
82
|
+
"additionalProperties": false
|
|
83
|
+
},
|
|
84
|
+
"parallel_node": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"required": ["id", "type", "children"],
|
|
87
|
+
"properties": {
|
|
88
|
+
"id": { "type": "string" },
|
|
89
|
+
"type": { "const": "parallel" },
|
|
90
|
+
"children": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": { "type": "string" },
|
|
93
|
+
"minItems": 1
|
|
94
|
+
},
|
|
95
|
+
"policy": { "type": "string", "enum": ["all", "any"], "default": "all" },
|
|
96
|
+
"cancel_on_error": { "type": "boolean", "default": true },
|
|
97
|
+
"next": { "type": ["string", "null"] },
|
|
98
|
+
"metadata": { "type": "object" }
|
|
99
|
+
},
|
|
100
|
+
"additionalProperties": false
|
|
101
|
+
},
|
|
102
|
+
"terminal_node": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"required": ["id", "type"],
|
|
105
|
+
"properties": {
|
|
106
|
+
"id": { "type": "string" },
|
|
107
|
+
"type": { "const": "terminal" },
|
|
108
|
+
"artifacts": {
|
|
109
|
+
"type": "array",
|
|
110
|
+
"items": { "type": "string" },
|
|
111
|
+
"default": []
|
|
112
|
+
},
|
|
113
|
+
"result": { "type": ["object", "null"] },
|
|
114
|
+
"metadata": { "type": "object" }
|
|
115
|
+
},
|
|
116
|
+
"additionalProperties": false
|
|
117
|
+
},
|
|
118
|
+
"subpipeline_node": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"required": ["id", "type", "subpipeline_id"],
|
|
121
|
+
"properties": {
|
|
122
|
+
"id": { "type": "string" },
|
|
123
|
+
"type": { "const": "subpipeline" },
|
|
124
|
+
"subpipeline_id": { "type": "string" },
|
|
125
|
+
"inputs": { "type": "object" },
|
|
126
|
+
"artifact_outputs": { "type": "object" },
|
|
127
|
+
"result_output": { "type": ["string", "null"] },
|
|
128
|
+
"next": { "type": ["string", "null"] },
|
|
129
|
+
"metadata": { "type": "object" }
|
|
130
|
+
},
|
|
131
|
+
"additionalProperties": false
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
stageflow/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|