salpa-cli 0.1.2__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.
Files changed (35) hide show
  1. bocoflow_cli/__init__.py +8 -0
  2. bocoflow_cli/cli.py +248 -0
  3. bocoflow_cli/docs/README.md +13 -0
  4. bocoflow_cli/docs/multi-node-packages.md +46 -0
  5. bocoflow_cli/docs/node-package-structure.md +75 -0
  6. bocoflow_cli/docs/testing-and-loading-your-node.md +36 -0
  7. bocoflow_cli/docs.py +44 -0
  8. bocoflow_cli/scaffold.py +184 -0
  9. bocoflow_cli/templates/individual-node/README.md +37 -0
  10. bocoflow_cli/templates/individual-node/_TEMPLATE_USAGE.md +73 -0
  11. bocoflow_cli/templates/individual-node/core.py +49 -0
  12. bocoflow_cli/templates/individual-node/meta.toml +48 -0
  13. bocoflow_cli/templates/individual-node/node.py +136 -0
  14. bocoflow_cli/templates/individual-node/pixi.toml +44 -0
  15. bocoflow_cli/templates/individual-node/tests/test_node.py +86 -0
  16. bocoflow_cli/templates/multi-node-package/README.md +32 -0
  17. bocoflow_cli/templates/multi-node-package/_TEMPLATE_USAGE.md +74 -0
  18. bocoflow_cli/templates/multi-node-package/node_one/README.md +14 -0
  19. bocoflow_cli/templates/multi-node-package/node_one/core.py +37 -0
  20. bocoflow_cli/templates/multi-node-package/node_one/meta.toml +43 -0
  21. bocoflow_cli/templates/multi-node-package/node_one/node.py +97 -0
  22. bocoflow_cli/templates/multi-node-package/node_one/tests/test_node.py +64 -0
  23. bocoflow_cli/templates/multi-node-package/node_two/README.md +15 -0
  24. bocoflow_cli/templates/multi-node-package/node_two/core.py +20 -0
  25. bocoflow_cli/templates/multi-node-package/node_two/meta.toml +36 -0
  26. bocoflow_cli/templates/multi-node-package/node_two/node.py +101 -0
  27. bocoflow_cli/templates/multi-node-package/node_two/tests/test_node.py +72 -0
  28. bocoflow_cli/templates/multi-node-package/package.toml +63 -0
  29. bocoflow_cli/templates/multi-node-package/pixi.toml +46 -0
  30. bocoflow_cli/templates/multi-node-package/workflows/{{PACKAGE_NAME}}-pipeline.json +26 -0
  31. salpa_cli-0.1.2.dist-info/METADATA +79 -0
  32. salpa_cli-0.1.2.dist-info/RECORD +35 -0
  33. salpa_cli-0.1.2.dist-info/WHEEL +4 -0
  34. salpa_cli-0.1.2.dist-info/entry_points.txt +3 -0
  35. salpa_cli-0.1.2.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,97 @@
1
+ """{{PACKAGE_NAME}} — node one: {{NODE1_DESCRIPTION}}
2
+
3
+ Stage 1 of the pipeline. Reads input and writes prepared records, which node two
4
+ picks up from `predecessor_data`.
5
+ """
6
+
7
+ import os
8
+
9
+ from bocoflow_core.node import Node, NodeException, NodeResult
10
+ from bocoflow_core.parameters import (
11
+ FileParameterEdit,
12
+ FolderParameter,
13
+ TextParameter,
14
+ )
15
+ from bocoflow_core.stream_logger import stream_log
16
+
17
+ # Standard 3-stage import. Stage 3 is what lets the SERVER import node.py to read
18
+ # OPTIONS for the UI without this package's dependencies installed.
19
+ try:
20
+ from .core import prepare, read_records # package context (pytest/direct)
21
+ except ImportError:
22
+ try:
23
+ from core import prepare, read_records # node_runner injects node_dir on path
24
+ except ImportError:
25
+ prepare = read_records = None # server env: heavy deps absent
26
+
27
+
28
+ class {{NODE1_CLASS_NAME}}(Node):
29
+ """{{NODE1_DESCRIPTION}}"""
30
+
31
+ category = "{{CATEGORY}}"
32
+ tags = [{{HASHTAGS}}]
33
+
34
+ OPTIONS = {
35
+ "text_input": TextParameter(
36
+ "Input (one record per line)", default="",
37
+ docstring="Records to prepare, one per line. Leave empty to use a file.",
38
+ ),
39
+ "input_file": FileParameterEdit(
40
+ "Input File (optional)",
41
+ docstring="Optional input file, one record per line.",
42
+ ),
43
+ "output_dir": FolderParameter(
44
+ "Output Directory",
45
+ docstring="Directory where prepared records are written.",
46
+ ),
47
+ }
48
+
49
+ def execute(self, predecessor_data, flow_vars):
50
+ stream_log("Preparing records...", node_id=self.node_id, progress=0)
51
+ try:
52
+ if prepare is None:
53
+ raise NodeException(
54
+ "setup",
55
+ "core.py could not be imported. Check that this package's shared "
56
+ "pixi environment installed its dependencies (`pixi install`).",
57
+ )
58
+
59
+ result = NodeResult()
60
+
61
+ text = flow_vars["text_input"].get_value() or ""
62
+ file_val = flow_vars["input_file"].get_value()
63
+ file_path = self.resolve_path(file_val) if file_val else ""
64
+
65
+ records = read_records(text, file_path)
66
+ if not records:
67
+ raise NodeException("input", "No input provided (via text or file).")
68
+
69
+ stream_log(
70
+ f"Preparing {len(records)} record(s)...",
71
+ node_id=self.node_id, progress=40,
72
+ )
73
+ out = prepare(records)
74
+
75
+ out_dir = self.resolve_path(flow_vars["output_dir"].get_value())
76
+ os.makedirs(out_dir, exist_ok=True)
77
+ out_file = os.path.join(out_dir, "prepared.txt")
78
+ with open(out_file, "w", encoding="utf-8") as fh:
79
+ for record in out["prepared"]:
80
+ fh.write(f"{record}\n")
81
+
82
+ # Keys placed in result.data are what node two reads from
83
+ # predecessor_data — this dict IS the contract between the two nodes.
84
+ result.data = {
85
+ **out,
86
+ "prepared_file": self.format_output_path(out_file),
87
+ }
88
+ result.files["output"] = {"prepared_file": result.data["prepared_file"]}
89
+ result.success = True
90
+ result.message = f"Prepared {out['n_prepared']} record(s)"
91
+ stream_log(result.message, node_id=self.node_id, progress=100)
92
+ return result.to_json()
93
+
94
+ except NodeException:
95
+ raise # keep the original stage tag
96
+ except Exception as e:
97
+ raise NodeException("prepare", str(e))
@@ -0,0 +1,64 @@
1
+ """Tests for {{PACKAGE_NAME}} node one.
2
+
3
+ core.py is pure Python and imports fine here, so its tests ALWAYS run.
4
+ node.py imports bocoflow_core, which the RUNTIME provides and which is not a
5
+ pixi dependency — so its tests carry @needs_runtime and skip. Validate node.py
6
+ by loading the node into Salpa and running it.
7
+
8
+ Guard per-test with @needs_runtime, never with a module-level `pytestmark` —
9
+ a module-level mark skips the core tests too, and `pixi run test` then exits 0
10
+ having validated nothing.
11
+ """
12
+
13
+ import json
14
+ import os
15
+
16
+ import pytest
17
+
18
+ from core import prepare, read_records # pure — always importable here
19
+
20
+ try:
21
+ from node import {{NODE1_CLASS_NAME}}
22
+ _NODE_OK = True
23
+ except Exception: # node.py imports bocoflow_core (runtime-provided)
24
+ _NODE_OK = False
25
+
26
+ needs_runtime = pytest.mark.skipif(
27
+ not _NODE_OK,
28
+ reason="node.py needs the bocoflow_core runtime; load the node into Salpa to validate it",
29
+ )
30
+
31
+
32
+ # ---- pure core (these RUN in the bare pixi env) ----------------------------
33
+
34
+ def test_read_records_from_text():
35
+ assert read_records("Alpha\nBeta\n\n Gamma ") == ["Alpha", "Beta", "Gamma"]
36
+
37
+
38
+ def test_prepare_normalizes():
39
+ out = prepare(["Alpha", "Beta"])
40
+ assert out["prepared"] == ["alpha", "beta"]
41
+ assert out["n_prepared"] == 2
42
+
43
+
44
+ # ---- node wiring (needs the bocoflow_core runtime) -------------------------
45
+
46
+ @needs_runtime
47
+ def test_execute_writes_prepared_file(tmp_path):
48
+ class _P: # minimal Parameter stub — build flow_vars from these rather than
49
+ def __init__(self, v): # from node.OPTIONS, whose Parameter objects are
50
+ self._v = v # class attributes shared across every instance.
51
+
52
+ def get_value(self):
53
+ return self._v
54
+
55
+ node = {{NODE1_CLASS_NAME}}({"node_id": "test"}) # node_info dict; node_id feeds stream_log(node_id=...)
56
+ flow_vars = {
57
+ "text_input": _P("Alpha\nBeta"),
58
+ "input_file": _P(""),
59
+ "output_dir": _P(str(tmp_path)),
60
+ }
61
+ result = json.loads(node.execute(None, flow_vars))
62
+ assert result["success"] is True
63
+ assert result["data"]["n_prepared"] == 2
64
+ assert os.path.exists(tmp_path / "prepared.txt")
@@ -0,0 +1,15 @@
1
+ # {{NODE2_DISPLAY_NAME}}
2
+
3
+ {{NODE2_DESCRIPTION}}
4
+
5
+ Part of the **{{DISPLAY_NAME}}** package. Stage 2 of the pipeline — reads what
6
+ {{NODE1_DISPLAY_NAME}} produced from `predecessor_data`.
7
+
8
+ ## Inputs
9
+
10
+ - The `prepared` records from an upstream {{NODE1_DISPLAY_NAME}} node.
11
+
12
+ ## Output
13
+
14
+ - `data["longest"]` / `data["n_summarized"]`
15
+ - `summary.txt` in the output directory
@@ -0,0 +1,20 @@
1
+ """Pure-Python core for {{PACKAGE_NAME}} node two.
2
+
3
+ No BoCoFlow imports here — pure Python. See node_one/core.py for why the
4
+ core/node split matters.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Dict, List
10
+
11
+
12
+ def summarize(records: List[str]) -> Dict:
13
+ """TODO: replace with this node's real second-stage computation."""
14
+ # --- Replace from here ------------------------------------------------
15
+ longest = max(records, key=len) if records else ""
16
+ # --- to here ----------------------------------------------------------
17
+ return {
18
+ "n_summarized": len(records),
19
+ "longest": longest,
20
+ }
@@ -0,0 +1,36 @@
1
+ # Per-node metadata. One of these per node directory listed in package.toml.
2
+ #
3
+ # Note there is NO [dependencies] block: in a shared-environment package the deps
4
+ # live in the package's root pixi.toml, not here.
5
+
6
+ [package]
7
+ # kebab-case, and GLOBALLY unique — the registry keys on it and a collision
8
+ # silently overwrites the other node. Keep this line comment-free.
9
+ name = "{{PACKAGE_NAME}}-node-two"
10
+ version = "0.1.0"
11
+ description = "{{NODE2_DESCRIPTION}}"
12
+ author = "{{AUTHOR}}"
13
+ license = "MIT" # REQUIRED — see node_one/meta.toml
14
+
15
+ [node]
16
+ class_name = "{{NODE2_CLASS_NAME}}" # must match the class in node.py exactly
17
+ display_name = "{{NODE2_DISPLAY_NAME}}"
18
+ shared_environment = "{{SHARED_ENV}}" # must match [package.environment] in package.toml
19
+ num_in = 1 # consumes node_one's output
20
+ num_out = 1
21
+ category = "{{CATEGORY}}"
22
+
23
+ hashtags = [{{HASHTAGS}}]
24
+
25
+ [node.metadata]
26
+ icon = "📤"
27
+ tooltip = "{{NODE2_DESCRIPTION}}"
28
+
29
+ [node.features]
30
+ supports_resume = false
31
+ requires_gpu = false
32
+ parallel_capable = false
33
+ long_running = false
34
+
35
+ [documentation]
36
+ readme = "README.md"
@@ -0,0 +1,101 @@
1
+ """{{PACKAGE_NAME}} — node two: {{NODE2_DESCRIPTION}}
2
+
3
+ Stage 2 of the pipeline. Reads what node one placed in its `result.data` via
4
+ `predecessor_data`, and demonstrates how nodes in a package chain together.
5
+ """
6
+
7
+ import os
8
+
9
+ from bocoflow_core.node import Node, NodeException, NodeResult
10
+ from bocoflow_core.parameters import FolderParameter
11
+ from bocoflow_core.stream_logger import stream_log
12
+
13
+ try:
14
+ from .core import summarize # package context (pytest/direct)
15
+ except ImportError:
16
+ try:
17
+ from core import summarize # node_runner injects node_dir on path
18
+ except ImportError:
19
+ summarize = None # server env: heavy deps absent
20
+
21
+
22
+ def _merge_predecessors(predecessor_data):
23
+ """Flatten upstream node `data` dicts into one carry-forward dict.
24
+
25
+ This is how a node reads what its upstream neighbour produced: whatever the
26
+ previous node put in `result.data` arrives here.
27
+ """
28
+ merged = {}
29
+ for pred in (predecessor_data or []):
30
+ if not isinstance(pred, dict):
31
+ continue
32
+ scope = pred.get("data") if isinstance(pred.get("data"), dict) else pred
33
+ for k, v in scope.items():
34
+ if k not in ("success", "message", "metadata", "files"):
35
+ merged[k] = v
36
+ return merged
37
+
38
+
39
+ class {{NODE2_CLASS_NAME}}(Node):
40
+ """{{NODE2_DESCRIPTION}}"""
41
+
42
+ category = "{{CATEGORY}}"
43
+ tags = [{{HASHTAGS}}]
44
+
45
+ OPTIONS = {
46
+ "output_dir": FolderParameter(
47
+ "Output Directory",
48
+ docstring="Directory where the summary is written.",
49
+ ),
50
+ }
51
+
52
+ def execute(self, predecessor_data, flow_vars):
53
+ stream_log("Summarizing...", node_id=self.node_id, progress=0)
54
+ try:
55
+ if summarize is None:
56
+ raise NodeException(
57
+ "setup",
58
+ "core.py could not be imported. Check that this package's shared "
59
+ "pixi environment installed its dependencies (`pixi install`).",
60
+ )
61
+
62
+ carried = _merge_predecessors(predecessor_data)
63
+ result = NodeResult()
64
+
65
+ # node_one put "prepared" in its result.data — that is the contract.
66
+ records = carried.get("prepared")
67
+ if not records:
68
+ raise NodeException(
69
+ "input",
70
+ "No 'prepared' records from the upstream node. Connect this "
71
+ "node's input to {{NODE1_DISPLAY_NAME}} and run that first.",
72
+ )
73
+
74
+ stream_log(
75
+ f"Summarizing {len(records)} record(s)...",
76
+ node_id=self.node_id, progress=40,
77
+ )
78
+ out = summarize(records)
79
+
80
+ out_dir = self.resolve_path(flow_vars["output_dir"].get_value())
81
+ os.makedirs(out_dir, exist_ok=True)
82
+ out_file = os.path.join(out_dir, "summary.txt")
83
+ with open(out_file, "w", encoding="utf-8") as fh:
84
+ fh.write(f"records: {out['n_summarized']}\n")
85
+ fh.write(f"longest: {out['longest']}\n")
86
+
87
+ result.data = {
88
+ **carried, # carry upstream keys forward
89
+ **out,
90
+ "summary_file": self.format_output_path(out_file),
91
+ }
92
+ result.files["output"] = {"summary_file": result.data["summary_file"]}
93
+ result.success = True
94
+ result.message = f"Summarized {out['n_summarized']} record(s)"
95
+ stream_log(result.message, node_id=self.node_id, progress=100)
96
+ return result.to_json()
97
+
98
+ except NodeException:
99
+ raise # keep the original stage tag
100
+ except Exception as e:
101
+ raise NodeException("summarize", str(e))
@@ -0,0 +1,72 @@
1
+ """Tests for {{PACKAGE_NAME}} node two.
2
+
3
+ See node_one/tests/test_node.py for why the node import is guarded per-test with
4
+ @needs_runtime rather than a module-level pytestmark.
5
+ """
6
+
7
+ import json
8
+ import os
9
+
10
+ import pytest
11
+
12
+ from core import summarize # pure — always importable here
13
+
14
+ try:
15
+ from node import {{NODE2_CLASS_NAME}}
16
+ _NODE_OK = True
17
+ except Exception: # node.py imports bocoflow_core (runtime-provided)
18
+ _NODE_OK = False
19
+
20
+ needs_runtime = pytest.mark.skipif(
21
+ not _NODE_OK,
22
+ reason="node.py needs the bocoflow_core runtime; load the node into Salpa to validate it",
23
+ )
24
+
25
+
26
+ # ---- pure core (these RUN in the bare pixi env) ----------------------------
27
+
28
+ def test_summarize_counts_and_longest():
29
+ out = summarize(["alpha", "be", "gamma-longest"])
30
+ assert out["n_summarized"] == 3
31
+ assert out["longest"] == "gamma-longest"
32
+
33
+
34
+ def test_summarize_handles_empty():
35
+ assert summarize([])["n_summarized"] == 0
36
+
37
+
38
+ # ---- node wiring (needs the bocoflow_core runtime) -------------------------
39
+
40
+ @needs_runtime
41
+ def test_execute_reads_predecessor_data(tmp_path):
42
+ class _P:
43
+ def __init__(self, v):
44
+ self._v = v
45
+
46
+ def get_value(self):
47
+ return self._v
48
+
49
+ node = {{NODE2_CLASS_NAME}}({"node_id": "test"})
50
+ # Shaped exactly like what node one returns: the upstream result.data dict.
51
+ predecessor_data = [{"data": {"prepared": ["alpha", "beta"], "n_prepared": 2}}]
52
+ flow_vars = {"output_dir": _P(str(tmp_path))}
53
+
54
+ result = json.loads(node.execute(predecessor_data, flow_vars))
55
+ assert result["success"] is True
56
+ assert result["data"]["n_summarized"] == 2
57
+ assert result["data"]["n_prepared"] == 2 # upstream keys carried forward
58
+ assert os.path.exists(tmp_path / "summary.txt")
59
+
60
+
61
+ @needs_runtime
62
+ def test_execute_without_predecessor_raises():
63
+ class _P:
64
+ def __init__(self, v):
65
+ self._v = v
66
+
67
+ def get_value(self):
68
+ return self._v
69
+
70
+ node = {{NODE2_CLASS_NAME}}({"node_id": "test"})
71
+ with pytest.raises(Exception):
72
+ node.execute(None, {"output_dir": _P("/tmp")})
@@ -0,0 +1,63 @@
1
+ # Multi-node package manifest.
2
+ #
3
+ # The PRESENCE of this file is what makes this a multi-node package — it is how
4
+ # the registry and the installer tell one from a single-node package. Replace
5
+ # every double-brace placeholder below.
6
+ #
7
+ # Guide: https://salpa.app/docs/custom-nodes
8
+ # (also bundled — run `salpa docs multi-node-packages`)
9
+
10
+ [package]
11
+ # kebab-case; the DIRECTORY uses underscores. Keep this line comment-free.
12
+ name = "{{PACKAGE_NAME}}"
13
+ version = "0.1.0"
14
+ description = "{{PACKAGE_DESCRIPTION}}"
15
+ author = "{{AUTHOR}}"
16
+ license = "MIT" # use the real license of the tools you wrap
17
+
18
+ [package.nodes]
19
+ # REQUIRED. These are DIRECTORY names (underscores) — not the kebab-case
20
+ # [package].name inside each node's meta.toml. This list drives both registry
21
+ # discovery and what the installer copies: a node missing here does not ship.
22
+ nodes = [
23
+ "node_one",
24
+ "node_two",
25
+ ]
26
+
27
+ [package.environment]
28
+ # ONE environment shared by every node in this package — the main reason to use a
29
+ # multi-node package rather than N single-node ones. Put ALL nodes' deps here.
30
+ #
31
+ # Normally this name also appears as `shared_environment` in EVERY node's meta.toml,
32
+ # and the runtime reads that per-node copy — keep them in sync (the easiest thing to
33
+ # get wrong here).
34
+ #
35
+ # OPT-OUT — a node whose deps CONFLICT with the shared env can run in its OWN env:
36
+ # omit `shared_environment` in that node's meta.toml AND give it its own pixi.toml
37
+ # (declaring all its deps + `redis-py`, but NOT `bocoflow_core` — the runtime installs
38
+ # that). The runtime builds that env lazily on first run; the rest keep sharing.
39
+ # A node that omits BOTH falls to the server env, where its imports fail.
40
+ #
41
+ # The shared env itself is the pixi.toml beside this file. The installer stages it
42
+ # into shared_environments/<name>/ for you — never create that by hand.
43
+ #
44
+ # Stdlib-only nodes need no environment: delete this section AND the root
45
+ # pixi.toml, and the nodes run IN_PROCESS.
46
+ shared_environment = "{{SHARED_ENV}}"
47
+
48
+ [package.metadata]
49
+ category = "{{CATEGORY}}" # e.g. "Molecular Dynamics", "Metal Docking"
50
+ hashtags = [{{HASHTAGS}}]
51
+ # platform_note = "Requires X, which has no Windows conda package."
52
+
53
+ [package.documentation]
54
+ readme = "README.md"
55
+
56
+ [package.workflows]
57
+ # A bundled demo workflow. To be registrable, the JSON must carry a top-level
58
+ # `template_info` object with a `name` — a plain saved .bcflow is skipped with a
59
+ # "No template_info" warning and nothing is registered.
60
+ #
61
+ # The path below is only used to FIND the file. The published entry is always
62
+ # packages/<pkg>/workflows/<basename>, so the file must live in workflows/.
63
+ examples = ["workflows/{{PACKAGE_NAME}}-pipeline.json"]
@@ -0,0 +1,46 @@
1
+ # THE shared environment for every node in this package.
2
+ #
3
+ # There is exactly one of these per package — by default every member node shares
4
+ # it and ships no pixi.toml of its own. On install, this file is staged to
5
+ # shared_environments/{{SHARED_ENV}}/pixi.toml, which is what the registry
6
+ # resolves for each node that declares `shared_environment` in its meta.toml.
7
+ # This file is the source of truth; the staged copy is generated.
8
+ #
9
+ # Exception — a member whose deps CONFLICT with this shared set can opt out and run
10
+ # in its own env: see the OPT-OUT note in package.toml's [package.environment].
11
+ #
12
+ # Delete this file (and [package.environment] in package.toml) if every node is
13
+ # stdlib-only — they will run IN_PROCESS instead.
14
+
15
+ [workspace]
16
+ # must match [package.environment].shared_environment (keep this line comment-free)
17
+ name = "{{SHARED_ENV}}"
18
+ version = "0.1.0"
19
+ description = "Shared environment for {{PACKAGE_NAME}} nodes"
20
+ channels = ["conda-forge"]
21
+ # Declare ONLY platforms your deps actually build for — pixi solves ALL declared
22
+ # platforms, so one dep with no win-64 build fails the whole solve. Many
23
+ # bioconda / specialized toolchains have no win-64 and sometimes no osx-arm64.
24
+ platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"]
25
+
26
+ [dependencies]
27
+ python = ">=3.10,<3.13"
28
+
29
+ # Redis client for stream_log() — real-time node logs and progress in the UI.
30
+ redis-py = ">=5.0.0"
31
+
32
+ # pytest must be in the DEFAULT env (not a [feature.dev] table) or `pixi run test`
33
+ # reports "pytest: command not found".
34
+ pytest = ">=7"
35
+
36
+ # --- Dependencies for ALL nodes in this package go here, not per-node ---
37
+ # numpy = ">=1.24"
38
+
39
+ [tasks]
40
+ # Run each node's tests from inside its own directory so `from core import ...`
41
+ # resolves — node dirs are not importable packages from the package root.
42
+ # `-rs` prints skip reasons: node.py tests skip in this bare env (they need the
43
+ # bocoflow_core runtime) — expected, not a failure. Add one line per node as it grows.
44
+ test-node-one = { cmd = "python -m pytest tests/ -v -rs", cwd = "node_one" }
45
+ test-node-two = { cmd = "python -m pytest tests/ -v -rs", cwd = "node_two" }
46
+ test = { depends-on = ["test-node-one", "test-node-two"] }
@@ -0,0 +1,26 @@
1
+ {
2
+ "_comment": "Bundled demo workflow. This stub carries only the template_info block the registry needs. The real graph/react/settings/flow_vars are produced by BUILDING the pipeline on the canvas and exporting it (Workflow -> Export). Replace this whole file with that export, keeping template_info. A graph authored by hand rarely loads — node_id UUIDs, links, and react layout must be internally consistent.",
3
+ "version": "2.0",
4
+ "template_info": {
5
+ "id": "{{PACKAGE_NAME}}-pipeline",
6
+ "name": "{{DISPLAY_NAME}} Pipeline",
7
+ "description": "{{PACKAGE_DESCRIPTION}}",
8
+ "required_packages": ["{{PACKAGE_NAME}}"],
9
+ "category": "{{CATEGORY}}",
10
+ "difficulty": "beginner",
11
+ "estimated_time": "2 minutes",
12
+ "tags": [{{HASHTAGS}}, "pipeline", "workflow-template"],
13
+ "author": "{{AUTHOR}}"
14
+ },
15
+ "metadata": {},
16
+ "react": { "nodes": [], "edges": [] },
17
+ "settings": {},
18
+ "graph": {
19
+ "directed": true,
20
+ "multigraph": false,
21
+ "graph": {},
22
+ "nodes": [],
23
+ "links": []
24
+ },
25
+ "flow_vars": {}
26
+ }
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: salpa-cli
3
+ Version: 0.1.2
4
+ Summary: Salpa node authoring CLI — scaffold node packages that match the shipped template contract.
5
+ Project-URL: Homepage, https://bocores.com/salpa-cli
6
+ Project-URL: Documentation, https://salpa.app/docs/custom-nodes
7
+ Author: BoundaryComputing
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: cli,computational-science,node-authoring,salpa,scaffold,workflow
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Software Development :: Code Generators
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: rich>=13
23
+ Requires-Dist: typer>=0.12
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # salpa-cli
29
+
30
+ The Salpa node authoring CLI. Scaffolds node packages that match the shipped
31
+ template contract — the deterministic path to a new node.
32
+
33
+ ```bash
34
+ python3 -m pip install salpa-cli
35
+ salpa new my-analyzer # interactive
36
+ salpa new my-suite -t multi-node-package --yes
37
+ ```
38
+
39
+ Installing adds the `salpa` command. If your shell can't find `salpa` afterward,
40
+ your Python scripts directory isn't on your `PATH` —
41
+ installing into a virtual environment is the simplest fix (it puts `salpa` on `PATH`).
42
+
43
+ `salpa new` creates `./<under_scored_name>/` from a bundled template, with every
44
+ placeholder substituted and the directory underscored (the dir name becomes the
45
+ Python package name, so a hyphen would break `from .core import ...`; meta.toml's
46
+ `name` stays kebab-case).
47
+
48
+ ## Templates
49
+
50
+ Templates ship inside the wheel and are the single source of truth for the node
51
+ contract, so there is no second copy to drift.
52
+
53
+ | Template | For |
54
+ |----------|-----|
55
+ | `individual-node` | one node with its own pixi env (default) |
56
+ | `multi-node-package` | several related nodes sharing one pixi env, chained via `predecessor_data` |
57
+
58
+ ## Options
59
+
60
+ ```
61
+ salpa new [NAME]
62
+ -t, --template individual-node | multi-node-package
63
+ -d, --description one-line description
64
+ --author author name
65
+ --category UI category (e.g. "Cheminformatics")
66
+ -o, --output directory to create the package in (default: .)
67
+ -y, --yes non-interactive: accept defaults, no prompts
68
+ --install run `pixi install` in the new package afterwards
69
+ ```
70
+
71
+ ## Authoring guide
72
+
73
+ `salpa docs` prints the bundled node-authoring guide. Full documentation:
74
+ https://salpa.app/docs/custom-nodes
75
+
76
+ ## Reserved
77
+
78
+ `salpa publish` / `install` / `run` are reserved for later and not implemented
79
+ in v1. v1 is `salpa new` (plus `salpa docs`).
@@ -0,0 +1,35 @@
1
+ bocoflow_cli/__init__.py,sha256=SnQMExFig29NtzL6ml6lKCXoQAFxpM_amfVQAlxC1CQ,257
2
+ bocoflow_cli/cli.py,sha256=yDXMurakQOT1yGzCGunZraWyAuknqNEIMe3R942BAZw,9504
3
+ bocoflow_cli/docs.py,sha256=GkBL9uppFFOy_hbYLS06G5CvxzORqskkcdpkmgm-fvo,1607
4
+ bocoflow_cli/scaffold.py,sha256=h2kmpVkmg87jnWKo4TDEtuDdqmwu1DL3O4MRHOtvdb4,7135
5
+ bocoflow_cli/docs/README.md,sha256=jjKTiaI7HaCcKqnWRbd4FVsGvo1kV_IlMYvpSjYp1FQ,804
6
+ bocoflow_cli/docs/multi-node-packages.md,sha256=NR_2dL260IA8yx1elBrXQriWMBXB82IATmmWAhgeT-M,1731
7
+ bocoflow_cli/docs/node-package-structure.md,sha256=uKNK7j_Ug_FxIHk8E1tPD5qFX6XbIp0DaG6MH9tEkzQ,3516
8
+ bocoflow_cli/docs/testing-and-loading-your-node.md,sha256=xsozYebKdI6lsc-LHeetRPHV4uMwE5fIeig2UrYx5kk,1244
9
+ bocoflow_cli/templates/individual-node/README.md,sha256=9VBzXcy5aAW9g8CUf5V6Roba1naTCYdbYnRU0gMNaPo,1130
10
+ bocoflow_cli/templates/individual-node/_TEMPLATE_USAGE.md,sha256=MVYWxoRs2ysdPXwRCptY5S_S71gZWz05salMdkTob60,2978
11
+ bocoflow_cli/templates/individual-node/core.py,sha256=Rwnxw4hRuEsdF5ekvkezIIZYp6UVeMpX6EYoTCIXoK0,1885
12
+ bocoflow_cli/templates/individual-node/meta.toml,sha256=z_iFX6FFc-Mx7Uu1mFCB-u18SHduQ0MfsW_zNfWjzDc,2136
13
+ bocoflow_cli/templates/individual-node/node.py,sha256=3xyAJgOSTeoO0x1h5RHqZ2PUdvxHGJGbK0gzpHkFd64,5441
14
+ bocoflow_cli/templates/individual-node/pixi.toml,sha256=EctsjeA1LIP9WZjD5xntqcP8lDlcfPdeCLMID6jggZc,1687
15
+ bocoflow_cli/templates/individual-node/tests/test_node.py,sha256=1awhi1BXf8X-m8esfjdqJdzKPEOB2oUHEnMM9VaW5eg,2910
16
+ bocoflow_cli/templates/multi-node-package/README.md,sha256=b17FimZIE6X4UddbTDuO37OpzFg5yvyt-qg9_M2XX9c,851
17
+ bocoflow_cli/templates/multi-node-package/_TEMPLATE_USAGE.md,sha256=RCS9OJIIINn0ClnCwipWelJIV112I18u3QcDlTNkpu8,2920
18
+ bocoflow_cli/templates/multi-node-package/package.toml,sha256=OWP2w6n1viBCFFD5su8hv4Ir2Y2gsOSXB02RTlt0IDk,2708
19
+ bocoflow_cli/templates/multi-node-package/pixi.toml,sha256=kYvURCZLrQ1SVT5vPcnOEcIGviafUvqJ4Nvw9WGqwmc,2182
20
+ bocoflow_cli/templates/multi-node-package/node_one/README.md,sha256=blMdEey02NerIsU1A6rfaNHlRxYPN-xymtZ1XQoEk4c,339
21
+ bocoflow_cli/templates/multi-node-package/node_one/core.py,sha256=JlG9paitNlbncVxDvDn5SVUydMhQF8YIrLMSq4GzVcM,1470
22
+ bocoflow_cli/templates/multi-node-package/node_one/meta.toml,sha256=Vb9s2QY9vznEVazPkZEeLtrbnxL07v7OY_bMu1Npym8,1632
23
+ bocoflow_cli/templates/multi-node-package/node_one/node.py,sha256=Wu492KxqKo40zLmZuVVKU2lXu074pe-HvecTOaS1KvQ,3651
24
+ bocoflow_cli/templates/multi-node-package/node_one/tests/test_node.py,sha256=_HGuABb4x3POwS4DQb_uFhjghhPsJmHCc18xLw5hXuo,2174
25
+ bocoflow_cli/templates/multi-node-package/node_two/README.md,sha256=KoUcgmyPD7BOKubiheFPt3p9SBF6d7Tfk3I-dJlP-9U,367
26
+ bocoflow_cli/templates/multi-node-package/node_two/core.py,sha256=LYERqJKVFzJ7bbCBcLdiVYFbR5p-RR9b4mtG8-m8OAU,634
27
+ bocoflow_cli/templates/multi-node-package/node_two/meta.toml,sha256=yQxjlSxSWJJqXHaXmZT-zbiDuE5Z65_CvmvtWOqoTDA,1120
28
+ bocoflow_cli/templates/multi-node-package/node_two/node.py,sha256=WtDfMYTjaAF0Go-4vhjMevFWJYnASkiK6ER8tSAfc4w,3789
29
+ bocoflow_cli/templates/multi-node-package/node_two/tests/test_node.py,sha256=7UdAKcHq0YVmy_iazaahDJB72pYZbi5_IWnqNJrQGdE,2117
30
+ bocoflow_cli/templates/multi-node-package/workflows/{{PACKAGE_NAME}}-pipeline.json,sha256=sJMD2EcV-I-sCSddpBNpAY3kth69cqidkJGBVX25k2E,1033
31
+ salpa_cli-0.1.2.dist-info/METADATA,sha256=cHkq2KTTFsCt_N6AQjFuUe2PVQgBB4AHqzDUBYK-tPQ,2916
32
+ salpa_cli-0.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
33
+ salpa_cli-0.1.2.dist-info/entry_points.txt,sha256=WTdnthEebNs7iECOOw6UFtaQw7-oBkpdzJGQRZ_NNSs,79
34
+ salpa_cli-0.1.2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
35
+ salpa_cli-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ bocoflow = bocoflow_cli.cli:app
3
+ salpa = bocoflow_cli.cli:app