Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ngraph/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""NetGraph package."""

from __future__ import annotations

from . import cli

__all__ = ["cli"]
6 changes: 6 additions & 0 deletions ngraph/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from __future__ import annotations

from .cli import main

if __name__ == "__main__":
main()
47 changes: 47 additions & 0 deletions ngraph/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any, Dict, List, Optional

from ngraph.scenario import Scenario


def _run_scenario(path: Path, output: Optional[Path]) -> None:
"""Run a scenario file and store results as JSON."""
yaml_text = path.read_text()
scenario = Scenario.from_yaml(yaml_text)
scenario.run()

results_dict: Dict[str, Dict[str, Any]] = scenario.results.to_dict()
json_str = json.dumps(results_dict, indent=2, default=str)
if output:
output.write_text(json_str)
Copy link

Copilot AI May 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Writing JSON without a trailing newline can break POSIX conventions; consider appending "\n" to json_str when calling write_text.

Suggested change
output.write_text(json_str)
output.write_text(json_str + "\n")

Copilot uses AI. Check for mistakes.
else:
print(json_str)


def main(argv: Optional[List[str]] = None) -> None:
"""Entry point for the ``ngraph`` command."""
parser = argparse.ArgumentParser(prog="ngraph")
subparsers = parser.add_subparsers(dest="command", required=True)

run_parser = subparsers.add_parser("run", help="Run a scenario")
run_parser.add_argument("scenario", type=Path, help="Path to scenario YAML")
run_parser.add_argument(
"--results",
"-r",
type=Path,
default=None,
help="Write JSON results to this file instead of stdout",
)

args = parser.parse_args(argv)

if args.command == "run":
_run_scenario(args.scenario, args.results)


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions ngraph/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ def get_all(self, key: str) -> Dict[str, Any]:
if key in data:
result[step_name] = data[key]
return result

def to_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a dictionary representation of all stored results."""
return {step: data.copy() for step, data in self._store.items()}
Copy link

Copilot AI May 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The to_dict method performs a shallow copy of each inner result dict. If nested mutable values are present, consider using copy.deepcopy or documenting that nested structures remain shared.

Suggested change
return {step: data.copy() for step, data in self._store.items()}
return {step: copy.deepcopy(data) for step, data in self._store.items()}

Copilot uses AI. Check for mistakes.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ dev = [
"black",
"isort",
]
[project.scripts]
ngraph = "ngraph.cli:main"


# ---------------------------------------------------------------------
# Pytest flags
Expand Down
22 changes: 22 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import json
from pathlib import Path

from ngraph import cli


def test_cli_run_file(tmp_path: Path) -> None:
scenario = Path("tests/scenarios/scenario_1.yaml")
Copy link

Copilot AI May 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding the scenario path assumes the current working directory; consider deriving it with Path(__file__).parent / "scenarios" / "scenario_1.yaml" for more robust test location resolution.

Suggested change
scenario = Path("tests/scenarios/scenario_1.yaml")
scenario = Path(__file__).parent / "scenarios" / "scenario_1.yaml"

Copilot uses AI. Check for mistakes.
out_file = tmp_path / "res.json"
cli.main(["run", str(scenario), "--results", str(out_file)])
assert out_file.is_file()
data = json.loads(out_file.read_text())
assert "build_graph" in data
assert "graph" in data["build_graph"]


def test_cli_run_stdout(capsys) -> None:
scenario = Path("tests/scenarios/scenario_1.yaml")
cli.main(["run", str(scenario)])
captured = capsys.readouterr()
data = json.loads(captured.out)
assert "build_graph" in data