hepyy-workflows 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.
- hepyy_workflows-0.1.0/PKG-INFO +113 -0
- hepyy_workflows-0.1.0/README.md +83 -0
- hepyy_workflows-0.1.0/pyproject.toml +47 -0
- hepyy_workflows-0.1.0/setup.cfg +4 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/__init__.py +3 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/agent/__init__.py +5 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/agent/service.py +80 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/agent/stdio.py +54 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/analysis/__init__.py +5 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/analysis/expressions.py +184 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/analysis/histograms.py +166 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/analysis/runner.py +738 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/analysis/views.py +142 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/cli.py +196 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/core/__init__.py +28 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/core/config.py +152 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/core/contracts.py +188 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/core/source.py +110 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/data/particles.parquet +0 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/examples.py +53 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/export/__init__.py +5 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/export/standalone.py +156 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/processing.py +65 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/project.py +38 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/readers/__init__.py +17 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/readers/formats.py +666 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/readers/mapped.py +608 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/readers/scaffold.py +68 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/readers/service.py +271 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/reporting/__init__.py +5 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/reporting/report.py +77 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/schemas/reader-draft.schema.json +45 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/schemas/reader.schema.json +31 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/schemas/recipe.schema.json +87 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/starter/README.md +22 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/starter/reader.yaml +20 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/starter/recipe.yaml +25 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/ui/__init__.py +5 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/ui/service.py +65 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/ui/tui.py +23 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows/ui/web.py +103 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows.egg-info/PKG-INFO +113 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows.egg-info/SOURCES.txt +50 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows.egg-info/dependency_links.txt +1 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows.egg-info/entry_points.txt +2 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows.egg-info/requires.txt +31 -0
- hepyy_workflows-0.1.0/src/hepyy_workflows.egg-info/top_level.txt +1 -0
- hepyy_workflows-0.1.0/tests/test_agent.py +56 -0
- hepyy_workflows-0.1.0/tests/test_analysis.py +642 -0
- hepyy_workflows-0.1.0/tests/test_foundation.py +164 -0
- hepyy_workflows-0.1.0/tests/test_processing.py +62 -0
- hepyy_workflows-0.1.0/tests/test_readers.py +668 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hepyy-workflows
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Explicit, portable HEP data workflows
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: hepyy>=0.2.18
|
|
8
|
+
Requires-Dist: jsonschema>=4.23
|
|
9
|
+
Requires-Dist: numpy>=1.26
|
|
10
|
+
Requires-Dist: pyarrow>=18
|
|
11
|
+
Requires-Dist: PyYAML>=6
|
|
12
|
+
Provides-Extra: parquet
|
|
13
|
+
Provides-Extra: root
|
|
14
|
+
Requires-Dist: uproot>=5; extra == "root"
|
|
15
|
+
Requires-Dist: awkward>=2; extra == "root"
|
|
16
|
+
Provides-Extra: hdf5
|
|
17
|
+
Requires-Dist: h5py>=3; extra == "hdf5"
|
|
18
|
+
Provides-Extra: hepmc
|
|
19
|
+
Requires-Dist: hepyy>=0.2.18; extra == "hepmc"
|
|
20
|
+
Provides-Extra: pythia
|
|
21
|
+
Requires-Dist: hepyy>=0.2.18; extra == "pythia"
|
|
22
|
+
Provides-Extra: web
|
|
23
|
+
Requires-Dist: fastapi>=0.115; extra == "web"
|
|
24
|
+
Requires-Dist: uvicorn>=0.30; extra == "web"
|
|
25
|
+
Provides-Extra: tui
|
|
26
|
+
Requires-Dist: textual>=0.80; extra == "tui"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.9; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# hepyy-workflows
|
|
32
|
+
|
|
33
|
+
An explicit HEP workflow package. [SPECS.md](SPECS.md) defines the full project;
|
|
34
|
+
this checkout currently implements **milestones 1 through 6**. The package name is
|
|
35
|
+
`hepyy_workflows` and the CLI is `hwf`. Python 3.11+ is required.
|
|
36
|
+
|
|
37
|
+
The package provides Arrow dataset/source contracts, strict public reader and
|
|
38
|
+
recipe schemas, a starter project, and bounded inspect/draft/validate/preview
|
|
39
|
+
workflows for Parquet, ROOT TTrees, explicit HDF5 layouts, and HepMC3 ASCII.
|
|
40
|
+
Recipes now run generic views, selections, histograms and cutflows and export a
|
|
41
|
+
standalone file-analysis runner, self-contained reports, and local web/terminal
|
|
42
|
+
authoring tools.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
henv -n YOUR_ENV -x python -m pip install -e '.[dev]'
|
|
46
|
+
henv -n YOUR_ENV -x python -m pytest
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Create a starter project **from any directory** (choose a destination that does
|
|
50
|
+
not yet exist):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
henv -n YOUR_ENV -x hwf project init /path/to/my-analysis
|
|
54
|
+
cd /path/to/my-analysis
|
|
55
|
+
henv -n YOUR_ENV -x hwf schema validate reader reader.yaml --json
|
|
56
|
+
henv -n YOUR_ENV -x hwf schema validate recipe recipe.yaml --json
|
|
57
|
+
henv -n YOUR_ENV -x hwf reader inspect data/particles.parquet --json
|
|
58
|
+
henv -n YOUR_ENV -x hwf reader validate reader.yaml --input data/particles.parquet --json
|
|
59
|
+
henv -n YOUR_ENV -x hwf reader preview reader.yaml --input data/particles.parquet --events 2 --json
|
|
60
|
+
henv -n YOUR_ENV -x hwf recipe run recipe.yaml --input data/particles.parquet --output results --json
|
|
61
|
+
henv -n YOUR_ENV -x hwf recipe export recipe.yaml --input data/particles.parquet --output standalone --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The last command reads the starter project's sample file. See
|
|
65
|
+
[working-in-a-project.md](docs/working-in-a-project.md) for what the starter
|
|
66
|
+
files mean and [examples/readers](examples/readers/README.md) for each format.
|
|
67
|
+
For choosing event boundaries, collection paths, units and labels in an unfamiliar
|
|
68
|
+
file, see [FILE_FORMAT_README.md](FILE_FORMAT_README.md).
|
|
69
|
+
See [analysis.md](docs/analysis.md) for recipe operations, histogram products,
|
|
70
|
+
and standalone execution; the [Lund](examples/lund/README.md) and
|
|
71
|
+
[dilepton](examples/dilepton/README.md) examples are complete workflows.
|
|
72
|
+
|
|
73
|
+
Build an offline report from completed results, or inspect them remotely in a
|
|
74
|
+
terminal:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
henv -n YOUR_ENV -x hwf report results --output results.html --json
|
|
78
|
+
henv -n YOUR_ENV -x hwf tui results
|
|
79
|
+
henv -n YOUR_ENV -x hwf web recipe.yaml --port 8765
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The report is one HTML file with embedded data and scripts; it works from
|
|
83
|
+
`file://` without a server or network connection. The web editor listens on
|
|
84
|
+
localhost and validates edits through the same service as the CLI.
|
|
85
|
+
|
|
86
|
+
Agents can use the same operations through line-delimited JSON:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
printf '%s\n' '{"id":1,"method":"inspect_input","params":{"input":"data/particles.parquet"}}' \
|
|
90
|
+
| henv -n YOUR_ENV -x hwf agent serve
|
|
91
|
+
henv -n YOUR_ENV -x hwf reader scaffold my_reader --output src/my_reader
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [agent-workflow.md](docs/agent-workflow.md) for the typed method contract
|
|
95
|
+
and the recommended inspect → draft → validate → preview → run → export flow.
|
|
96
|
+
|
|
97
|
+
Recipes may declare a visible Python preprocessing module. It receives the
|
|
98
|
+
ordered input paths and a Parquet output path, and its source and dependencies
|
|
99
|
+
are bundled by standalone export. See [processing.md](docs/processing.md).
|
|
100
|
+
|
|
101
|
+
`hwf schema validate` accepts `reader-draft`, `reader`, or `recipe`; failed
|
|
102
|
+
validation exits nonzero and reports stable diagnostic codes and YAML paths.
|
|
103
|
+
Paths passed to the CLI are resolved from your current directory; use absolute
|
|
104
|
+
paths when invoking it from elsewhere. A missing file reports `FILE_NOT_FOUND`.
|
|
105
|
+
An omitted momentum-field unit resolves to GeV/c and appears in `assumed_units`
|
|
106
|
+
in JSON output. Declare a different source unit explicitly when needed.
|
|
107
|
+
Schema files are available under `schemas/` and ship inside the wheel.
|
|
108
|
+
|
|
109
|
+
The package requires NumPy, PyArrow, JSON Schema and PyYAML. Pure histogramming does not import hepyy,
|
|
110
|
+
FastJet, PYTHIA, HepMC, uproot, h5py or a web server. Optional extras in
|
|
111
|
+
`pyproject.toml` reserve these dependency boundaries. For all milestone 2
|
|
112
|
+
readers, install `.[root,hdf5,hepmc]` in the analysis environment. See
|
|
113
|
+
[architecture.md](docs/architecture.md) and [provenance.md](docs/provenance.md).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# hepyy-workflows
|
|
2
|
+
|
|
3
|
+
An explicit HEP workflow package. [SPECS.md](SPECS.md) defines the full project;
|
|
4
|
+
this checkout currently implements **milestones 1 through 6**. The package name is
|
|
5
|
+
`hepyy_workflows` and the CLI is `hwf`. Python 3.11+ is required.
|
|
6
|
+
|
|
7
|
+
The package provides Arrow dataset/source contracts, strict public reader and
|
|
8
|
+
recipe schemas, a starter project, and bounded inspect/draft/validate/preview
|
|
9
|
+
workflows for Parquet, ROOT TTrees, explicit HDF5 layouts, and HepMC3 ASCII.
|
|
10
|
+
Recipes now run generic views, selections, histograms and cutflows and export a
|
|
11
|
+
standalone file-analysis runner, self-contained reports, and local web/terminal
|
|
12
|
+
authoring tools.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
henv -n YOUR_ENV -x python -m pip install -e '.[dev]'
|
|
16
|
+
henv -n YOUR_ENV -x python -m pytest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Create a starter project **from any directory** (choose a destination that does
|
|
20
|
+
not yet exist):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
henv -n YOUR_ENV -x hwf project init /path/to/my-analysis
|
|
24
|
+
cd /path/to/my-analysis
|
|
25
|
+
henv -n YOUR_ENV -x hwf schema validate reader reader.yaml --json
|
|
26
|
+
henv -n YOUR_ENV -x hwf schema validate recipe recipe.yaml --json
|
|
27
|
+
henv -n YOUR_ENV -x hwf reader inspect data/particles.parquet --json
|
|
28
|
+
henv -n YOUR_ENV -x hwf reader validate reader.yaml --input data/particles.parquet --json
|
|
29
|
+
henv -n YOUR_ENV -x hwf reader preview reader.yaml --input data/particles.parquet --events 2 --json
|
|
30
|
+
henv -n YOUR_ENV -x hwf recipe run recipe.yaml --input data/particles.parquet --output results --json
|
|
31
|
+
henv -n YOUR_ENV -x hwf recipe export recipe.yaml --input data/particles.parquet --output standalone --json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The last command reads the starter project's sample file. See
|
|
35
|
+
[working-in-a-project.md](docs/working-in-a-project.md) for what the starter
|
|
36
|
+
files mean and [examples/readers](examples/readers/README.md) for each format.
|
|
37
|
+
For choosing event boundaries, collection paths, units and labels in an unfamiliar
|
|
38
|
+
file, see [FILE_FORMAT_README.md](FILE_FORMAT_README.md).
|
|
39
|
+
See [analysis.md](docs/analysis.md) for recipe operations, histogram products,
|
|
40
|
+
and standalone execution; the [Lund](examples/lund/README.md) and
|
|
41
|
+
[dilepton](examples/dilepton/README.md) examples are complete workflows.
|
|
42
|
+
|
|
43
|
+
Build an offline report from completed results, or inspect them remotely in a
|
|
44
|
+
terminal:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
henv -n YOUR_ENV -x hwf report results --output results.html --json
|
|
48
|
+
henv -n YOUR_ENV -x hwf tui results
|
|
49
|
+
henv -n YOUR_ENV -x hwf web recipe.yaml --port 8765
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The report is one HTML file with embedded data and scripts; it works from
|
|
53
|
+
`file://` without a server or network connection. The web editor listens on
|
|
54
|
+
localhost and validates edits through the same service as the CLI.
|
|
55
|
+
|
|
56
|
+
Agents can use the same operations through line-delimited JSON:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
printf '%s\n' '{"id":1,"method":"inspect_input","params":{"input":"data/particles.parquet"}}' \
|
|
60
|
+
| henv -n YOUR_ENV -x hwf agent serve
|
|
61
|
+
henv -n YOUR_ENV -x hwf reader scaffold my_reader --output src/my_reader
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
See [agent-workflow.md](docs/agent-workflow.md) for the typed method contract
|
|
65
|
+
and the recommended inspect → draft → validate → preview → run → export flow.
|
|
66
|
+
|
|
67
|
+
Recipes may declare a visible Python preprocessing module. It receives the
|
|
68
|
+
ordered input paths and a Parquet output path, and its source and dependencies
|
|
69
|
+
are bundled by standalone export. See [processing.md](docs/processing.md).
|
|
70
|
+
|
|
71
|
+
`hwf schema validate` accepts `reader-draft`, `reader`, or `recipe`; failed
|
|
72
|
+
validation exits nonzero and reports stable diagnostic codes and YAML paths.
|
|
73
|
+
Paths passed to the CLI are resolved from your current directory; use absolute
|
|
74
|
+
paths when invoking it from elsewhere. A missing file reports `FILE_NOT_FOUND`.
|
|
75
|
+
An omitted momentum-field unit resolves to GeV/c and appears in `assumed_units`
|
|
76
|
+
in JSON output. Declare a different source unit explicitly when needed.
|
|
77
|
+
Schema files are available under `schemas/` and ship inside the wheel.
|
|
78
|
+
|
|
79
|
+
The package requires NumPy, PyArrow, JSON Schema and PyYAML. Pure histogramming does not import hepyy,
|
|
80
|
+
FastJet, PYTHIA, HepMC, uproot, h5py or a web server. Optional extras in
|
|
81
|
+
`pyproject.toml` reserve these dependency boundaries. For all milestone 2
|
|
82
|
+
readers, install `.[root,hdf5,hepmc]` in the analysis environment. See
|
|
83
|
+
[architecture.md](docs/architecture.md) and [provenance.md](docs/provenance.md).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hepyy-workflows"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Explicit, portable HEP data workflows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = ["hepyy>=0.2.18", "jsonschema>=4.23", "numpy>=1.26", "pyarrow>=18", "PyYAML>=6"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
parquet = []
|
|
15
|
+
root = ["uproot>=5", "awkward>=2"]
|
|
16
|
+
hdf5 = ["h5py>=3"]
|
|
17
|
+
hepmc = ["hepyy>=0.2.18"]
|
|
18
|
+
pythia = ["hepyy>=0.2.18"]
|
|
19
|
+
web = ["fastapi>=0.115", "uvicorn>=0.30"]
|
|
20
|
+
tui = ["textual>=0.80"]
|
|
21
|
+
dev = ["pytest>=8", "ruff>=0.9"]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
hwf = "hepyy_workflows.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
hepyy_workflows = ["schemas/*.json", "data/*.parquet", "starter/*.yaml", "starter/*.md"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
target-version = "py311"
|
|
37
|
+
line-length = 100
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint.per-file-ignores]
|
|
43
|
+
"src/hepyy_workflows/reporting/report.py" = ["E501"]
|
|
44
|
+
"src/hepyy_workflows/ui/web.py" = ["E501"]
|
|
45
|
+
"src/hepyy_workflows/agent/service.py" = ["E501"]
|
|
46
|
+
"src/hepyy_workflows/readers/scaffold.py" = ["E501"]
|
|
47
|
+
"tests/test_agent.py" = ["E501"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Stable JSON operations for agents and ordinary automation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from hepyy_workflows.analysis.runner import prepare_recipe, run_recipe
|
|
9
|
+
from hepyy_workflows.export.standalone import export_recipe
|
|
10
|
+
from hepyy_workflows.readers.scaffold import scaffold_reader
|
|
11
|
+
from hepyy_workflows.readers.service import (
|
|
12
|
+
draft_reader,
|
|
13
|
+
inspect_input,
|
|
14
|
+
preview_reader,
|
|
15
|
+
validate_reader,
|
|
16
|
+
write_draft,
|
|
17
|
+
)
|
|
18
|
+
from hepyy_workflows.reporting.report import build_report
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _paths(values: list[str] | None) -> list[Path]:
|
|
22
|
+
return [Path(value) for value in values or []]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _error(error: Exception, code: str = "AGENT_FAILED") -> dict[str, Any]:
|
|
26
|
+
return {
|
|
27
|
+
"valid": False,
|
|
28
|
+
"diagnostics": [
|
|
29
|
+
{
|
|
30
|
+
"code": getattr(error, "code", code),
|
|
31
|
+
"path": getattr(error, "path", "/"),
|
|
32
|
+
"message": str(error),
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dispatch(method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
39
|
+
try:
|
|
40
|
+
if method == "inspect_input":
|
|
41
|
+
return inspect_input(Path(params["input"]))
|
|
42
|
+
if method == "draft_reader":
|
|
43
|
+
if "output" in params:
|
|
44
|
+
return write_draft(Path(params["input"]), Path(params["output"]))
|
|
45
|
+
draft, inspection = draft_reader(Path(params["input"]))
|
|
46
|
+
return {**inspection, "draft": draft}
|
|
47
|
+
if method == "validate_reader":
|
|
48
|
+
return validate_reader(Path(params["reader"]), _paths(params.get("inputs")))
|
|
49
|
+
if method == "preview_reader":
|
|
50
|
+
return preview_reader(
|
|
51
|
+
Path(params["reader"]), _paths(params.get("inputs")), int(params.get("events", 10))
|
|
52
|
+
)
|
|
53
|
+
if method == "validate_recipe":
|
|
54
|
+
_, _, prepared = prepare_recipe(Path(params["recipe"]), params.get("inputs"))
|
|
55
|
+
return {
|
|
56
|
+
"valid": True,
|
|
57
|
+
"inputs": {k: [str(p) for p in v] for k, v in prepared["paths"].items()},
|
|
58
|
+
}
|
|
59
|
+
if method == "run_analysis":
|
|
60
|
+
return run_recipe(
|
|
61
|
+
Path(params["recipe"]),
|
|
62
|
+
Path(params["output"]),
|
|
63
|
+
params.get("inputs"),
|
|
64
|
+
params.get("max_events"),
|
|
65
|
+
)
|
|
66
|
+
if method == "export_standalone":
|
|
67
|
+
return export_recipe(
|
|
68
|
+
Path(params["recipe"]), Path(params["output"]), params.get("inputs")
|
|
69
|
+
)
|
|
70
|
+
if method == "build_report":
|
|
71
|
+
return build_report(Path(params["results"]), Path(params["output"]))
|
|
72
|
+
if method == "scaffold_reader":
|
|
73
|
+
return scaffold_reader(params["name"], Path(params["output"]))
|
|
74
|
+
if method == "get_status":
|
|
75
|
+
return {"valid": True, "status": "idle", "diagnostics": []}
|
|
76
|
+
if method == "cancel_run":
|
|
77
|
+
return _error(ValueError("no asynchronous run is active"), "NO_ACTIVE_RUN")
|
|
78
|
+
return _error(ValueError(f"unknown method: {method}"), "METHOD_UNKNOWN")
|
|
79
|
+
except Exception as error:
|
|
80
|
+
return _error(error)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Line-delimited JSON agent interface; no shell or arbitrary code endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .service import dispatch
|
|
9
|
+
|
|
10
|
+
_METHODS = (
|
|
11
|
+
"inspect_input",
|
|
12
|
+
"draft_reader",
|
|
13
|
+
"validate_reader",
|
|
14
|
+
"preview_reader",
|
|
15
|
+
"validate_recipe",
|
|
16
|
+
"run_analysis",
|
|
17
|
+
"export_standalone",
|
|
18
|
+
"build_report",
|
|
19
|
+
"scaffold_reader",
|
|
20
|
+
"get_status",
|
|
21
|
+
"cancel_run",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def serve_stdio() -> int:
|
|
26
|
+
for line in sys.stdin:
|
|
27
|
+
if not line.strip():
|
|
28
|
+
continue
|
|
29
|
+
try:
|
|
30
|
+
request = json.loads(line)
|
|
31
|
+
if not isinstance(request, dict) or not isinstance(request.get("method"), str):
|
|
32
|
+
raise ValueError("request needs a method")
|
|
33
|
+
method = request["method"]
|
|
34
|
+
if method == "initialize":
|
|
35
|
+
result = {
|
|
36
|
+
"protocolVersion": "2025-03-26",
|
|
37
|
+
"serverInfo": {"name": "hwf", "version": "1"},
|
|
38
|
+
}
|
|
39
|
+
elif method == "tools/list":
|
|
40
|
+
result = {
|
|
41
|
+
"tools": [
|
|
42
|
+
{"name": name, "description": f"HWF {name} operation"} for name in _METHODS
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
elif method == "tools/call":
|
|
46
|
+
params = request.get("params", {})
|
|
47
|
+
result = dispatch(params["name"], params.get("arguments", {}))
|
|
48
|
+
else:
|
|
49
|
+
result = dispatch(method, request.get("params", {}))
|
|
50
|
+
response = {"id": request.get("id"), "result": result}
|
|
51
|
+
except Exception as error:
|
|
52
|
+
response = {"id": None, "error": {"code": "REQUEST_INVALID", "message": str(error)}}
|
|
53
|
+
print(json.dumps(response, default=str), flush=True)
|
|
54
|
+
return 0
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Small, side-effect-free expression evaluator for recipe values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import math
|
|
7
|
+
import operator
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExpressionError(ValueError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def wrapped_angle(value: float) -> float:
|
|
17
|
+
return math.atan2(math.sin(value), math.cos(value))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_FUNCTIONS = {
|
|
21
|
+
"abs": abs,
|
|
22
|
+
"sqrt": math.sqrt,
|
|
23
|
+
"log": math.log,
|
|
24
|
+
"log10": math.log10,
|
|
25
|
+
"sin": math.sin,
|
|
26
|
+
"cos": math.cos,
|
|
27
|
+
"tan": math.tan,
|
|
28
|
+
"atan2": math.atan2,
|
|
29
|
+
"hypot": math.hypot,
|
|
30
|
+
"wrapped_angle": wrapped_angle,
|
|
31
|
+
"isfinite": math.isfinite,
|
|
32
|
+
"min": min,
|
|
33
|
+
"max": max,
|
|
34
|
+
}
|
|
35
|
+
_BINARY = {
|
|
36
|
+
ast.Add: operator.add,
|
|
37
|
+
ast.Sub: operator.sub,
|
|
38
|
+
ast.Mult: operator.mul,
|
|
39
|
+
ast.Div: operator.truediv,
|
|
40
|
+
ast.Pow: math.pow,
|
|
41
|
+
ast.Mod: operator.mod,
|
|
42
|
+
}
|
|
43
|
+
_COMPARE = {
|
|
44
|
+
ast.Eq: operator.eq,
|
|
45
|
+
ast.NotEq: operator.ne,
|
|
46
|
+
ast.Lt: operator.lt,
|
|
47
|
+
ast.LtE: operator.le,
|
|
48
|
+
ast.Gt: operator.gt,
|
|
49
|
+
ast.GtE: operator.ge,
|
|
50
|
+
ast.Is: operator.is_,
|
|
51
|
+
ast.IsNot: operator.is_not,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@lru_cache(maxsize=1024)
|
|
56
|
+
def compile_expression(source: str) -> ast.Expression:
|
|
57
|
+
try:
|
|
58
|
+
tree = ast.parse(source, mode="eval")
|
|
59
|
+
except SyntaxError as exc:
|
|
60
|
+
raise ExpressionError(f"invalid expression {source!r}: {exc.msg}") from exc
|
|
61
|
+
_check(tree.body)
|
|
62
|
+
return tree
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def referenced_names(source: str) -> set[str]:
|
|
66
|
+
"""Return field/parameter roots, excluding whitelisted function names."""
|
|
67
|
+
|
|
68
|
+
tree = compile_expression(source)
|
|
69
|
+
names = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
|
|
70
|
+
calls = {
|
|
71
|
+
node.func.id
|
|
72
|
+
for node in ast.walk(tree)
|
|
73
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
|
74
|
+
}
|
|
75
|
+
return names - calls
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _path(node: ast.AST) -> str | None:
|
|
79
|
+
if isinstance(node, ast.Name):
|
|
80
|
+
return node.id
|
|
81
|
+
if isinstance(node, ast.Attribute):
|
|
82
|
+
parent = _path(node.value)
|
|
83
|
+
return None if parent is None else parent + "." + node.attr
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _check(node: ast.AST) -> None:
|
|
88
|
+
if isinstance(node, ast.Constant):
|
|
89
|
+
if not isinstance(node.value, (int, float, str, bool, type(None))):
|
|
90
|
+
raise ExpressionError("unsupported literal")
|
|
91
|
+
elif isinstance(node, (ast.Name, ast.Attribute)):
|
|
92
|
+
if _path(node) is None or any(part.startswith("_") for part in _path(node).split(".")):
|
|
93
|
+
raise ExpressionError("private or dynamic field access is forbidden")
|
|
94
|
+
elif isinstance(node, ast.BinOp) and type(node.op) in _BINARY:
|
|
95
|
+
_check(node.left)
|
|
96
|
+
_check(node.right)
|
|
97
|
+
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd, ast.Not)):
|
|
98
|
+
_check(node.operand)
|
|
99
|
+
elif isinstance(node, ast.BoolOp) and isinstance(node.op, (ast.And, ast.Or)):
|
|
100
|
+
for value in node.values:
|
|
101
|
+
_check(value)
|
|
102
|
+
elif isinstance(node, ast.Compare) and all(type(op) in _COMPARE for op in node.ops):
|
|
103
|
+
_check(node.left)
|
|
104
|
+
for item in node.comparators:
|
|
105
|
+
_check(item)
|
|
106
|
+
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
107
|
+
if node.func.id not in _FUNCTIONS or node.keywords:
|
|
108
|
+
raise ExpressionError("function is not allowed")
|
|
109
|
+
for item in node.args:
|
|
110
|
+
_check(item)
|
|
111
|
+
else:
|
|
112
|
+
raise ExpressionError(f"unsupported expression element: {type(node).__name__}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def field(context: dict, path: str) -> Any:
|
|
116
|
+
if path in context:
|
|
117
|
+
return context[path]
|
|
118
|
+
parts = path.split(".")
|
|
119
|
+
for start in range(len(parts), 0, -1):
|
|
120
|
+
prefix = ".".join(parts[:start])
|
|
121
|
+
if prefix in context:
|
|
122
|
+
value = context[prefix]
|
|
123
|
+
for part in parts[start:]:
|
|
124
|
+
if not isinstance(value, dict):
|
|
125
|
+
return None
|
|
126
|
+
value = value.get(part)
|
|
127
|
+
return value
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def evaluate(source: str, context: dict) -> Any:
|
|
132
|
+
tree = compile_expression(source)
|
|
133
|
+
|
|
134
|
+
def visit(node: ast.AST) -> Any:
|
|
135
|
+
if isinstance(node, ast.Constant):
|
|
136
|
+
return node.value
|
|
137
|
+
if isinstance(node, (ast.Name, ast.Attribute)):
|
|
138
|
+
return field(context, _path(node))
|
|
139
|
+
if isinstance(node, ast.BinOp):
|
|
140
|
+
left, right = visit(node.left), visit(node.right)
|
|
141
|
+
if left is None or right is None:
|
|
142
|
+
return None
|
|
143
|
+
if not isinstance(left, (int, float)) or not isinstance(right, (int, float)):
|
|
144
|
+
return None
|
|
145
|
+
try:
|
|
146
|
+
value = _BINARY[type(node.op)](left, right)
|
|
147
|
+
except (ArithmeticError, OverflowError, TypeError, ValueError):
|
|
148
|
+
return None
|
|
149
|
+
return value if not isinstance(value, float) or math.isfinite(value) else None
|
|
150
|
+
if isinstance(node, ast.UnaryOp):
|
|
151
|
+
value = visit(node.operand)
|
|
152
|
+
if isinstance(node.op, ast.Not):
|
|
153
|
+
return not bool(value)
|
|
154
|
+
return None if value is None else (-value if isinstance(node.op, ast.USub) else +value)
|
|
155
|
+
if isinstance(node, ast.BoolOp):
|
|
156
|
+
if isinstance(node.op, ast.And):
|
|
157
|
+
return all(bool(visit(item)) for item in node.values)
|
|
158
|
+
return any(bool(visit(item)) for item in node.values)
|
|
159
|
+
if isinstance(node, ast.Compare):
|
|
160
|
+
left = visit(node.left)
|
|
161
|
+
for op, right_node in zip(node.ops, node.comparators, strict=True):
|
|
162
|
+
right = visit(right_node)
|
|
163
|
+
if left is None or right is None:
|
|
164
|
+
if not isinstance(op, (ast.Is, ast.IsNot, ast.Eq, ast.NotEq)):
|
|
165
|
+
return False
|
|
166
|
+
try:
|
|
167
|
+
if not _COMPARE[type(op)](left, right):
|
|
168
|
+
return False
|
|
169
|
+
except TypeError:
|
|
170
|
+
return False
|
|
171
|
+
left = right
|
|
172
|
+
return True
|
|
173
|
+
if isinstance(node, ast.Call):
|
|
174
|
+
args = [visit(item) for item in node.args]
|
|
175
|
+
if any(item is None for item in args):
|
|
176
|
+
return None
|
|
177
|
+
try:
|
|
178
|
+
value = _FUNCTIONS[node.func.id](*args)
|
|
179
|
+
except (ArithmeticError, OverflowError, TypeError, ValueError):
|
|
180
|
+
return None
|
|
181
|
+
return value if not isinstance(value, float) or math.isfinite(value) else None
|
|
182
|
+
raise AssertionError("expression was not checked")
|
|
183
|
+
|
|
184
|
+
return visit(tree.body)
|