Skip to content

Commit dba087e

Browse files
committed
cli update
1 parent b338c03 commit dba087e

File tree

5 files changed

+400
-43
lines changed

5 files changed

+400
-43
lines changed

docs/reference/api-full.md

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ Quick links:
1212
- [CLI Reference](cli.md)
1313
- [DSL Reference](dsl.md)
1414

15-
Generated from source code on: August 13, 2025 at 20:50 UTC
15+
Generated from source code on: August 13, 2025 at 21:26 UTC
1616

17-
Modules auto-discovered: 67
17+
Modules auto-discovered: 68
1818

1919
---
2020

@@ -3712,6 +3712,94 @@ Returns:
37123712

37133713
---
37143714

3715+
## ngraph.utils.output_paths
3716+
3717+
Utilities for building CLI artifact output paths.
3718+
3719+
This module centralizes logic for composing file and directory paths for
3720+
artifacts produced by the NetGraph CLI. Paths are built from an optional
3721+
output directory, a prefix (usually derived from the scenario file or
3722+
results file), and a per-artifact suffix.
3723+
3724+
### build_artifact_path(output_dir: 'Optional[Path]', prefix: 'str', suffix: 'str') -> 'Path'
3725+
3726+
Compose an artifact path as output_dir / (prefix + suffix).
3727+
3728+
If ``output_dir`` is None, the path is created relative to the current
3729+
working directory.
3730+
3731+
Args:
3732+
output_dir: Base directory for outputs; if None, use CWD.
3733+
prefix: Filename prefix; usually derived from scenario or results stem.
3734+
suffix: Per-artifact suffix including the dot (e.g. ".results.json").
3735+
3736+
Returns:
3737+
The composed path.
3738+
3739+
### ensure_parent_dir(path: 'Path') -> 'None'
3740+
3741+
Ensure the parent directory exists for a file path.
3742+
3743+
### profiles_dir_for_run(scenario_path: 'Path', output_dir: 'Optional[Path]') -> 'Path'
3744+
3745+
Return the directory for child worker profiles for ``run --profile``.
3746+
3747+
Args:
3748+
scenario_path: The scenario YAML path.
3749+
output_dir: Optional base output directory.
3750+
3751+
Returns:
3752+
Directory path where worker profiles should be stored.
3753+
3754+
### resolve_override_path(override: 'Optional[Path]', output_dir: 'Optional[Path]') -> 'Optional[Path]'
3755+
3756+
Resolve an override path with respect to an optional output directory.
3757+
3758+
- Absolute override paths are returned as-is.
3759+
- Relative override paths are interpreted as relative to ``output_dir``
3760+
3761+
when provided; otherwise relative to the current working directory.
3762+
3763+
Args:
3764+
override: Path provided by the user to override the default.
3765+
output_dir: Optional base directory for relative overrides.
3766+
3767+
Returns:
3768+
The resolved path or None if no override was provided.
3769+
3770+
### results_path_for_run(scenario_path: 'Path', output_dir: 'Optional[Path]', results_override: 'Optional[Path]') -> 'Path'
3771+
3772+
Determine the results JSON path for the ``run`` command.
3773+
3774+
Behavior:
3775+
3776+
- If ``results_override`` is provided, return it (resolved relative to
3777+
3778+
``output_dir`` when that is specified, otherwise as-is).
3779+
3780+
- Else if ``output_dir`` is provided, return ``output_dir/<prefix>.results.json``.
3781+
- Else, return ``<scenario_stem>.results.json`` in the current working directory.
3782+
3783+
Args:
3784+
scenario_path: The scenario YAML file path.
3785+
output_dir: Optional base output directory.
3786+
results_override: Optional explicit results file path.
3787+
3788+
Returns:
3789+
The path where results should be written.
3790+
3791+
### scenario_prefix_from_path(scenario_path: 'Path') -> 'str'
3792+
3793+
Return a safe prefix derived from a scenario file path.
3794+
3795+
Args:
3796+
scenario_path: The scenario YAML file path.
3797+
3798+
Returns:
3799+
The scenario filename stem, trimmed of extensions.
3800+
3801+
---
3802+
37153803

37163804
## Error Handling
37173805

docs/reference/cli.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ The CLI provides three primary commands:
3636
# Inspect a scenario to understand its structure
3737
python -m ngraph inspect my_scenario.yaml
3838

39-
# Run a scenario (generates my_scenario.json by default)
39+
# Run a scenario (generates my_scenario.results.json by default)
4040
python -m ngraph run my_scenario.yaml
4141

4242
# Generate analysis report from results
@@ -115,7 +115,7 @@ python -m ngraph [--verbose|--quiet] run <scenario_file> [options]
115115

116116
**Options:**
117117

118-
- `--results`, `-r`: Path to export results as JSON (default: `<scenario_name>.json`)
118+
- `--results`, `-r`: Path to export results as JSON (default: `<scenario_name>.results.json`)
119119
- `--no-results`: Disable results file generation (for edge cases)
120120
- `--stdout`: Print results to stdout in addition to saving file
121121
- `--keys`, `-k`: Space-separated list of workflow step names to include in output
@@ -186,7 +186,7 @@ python -m ngraph report results.json --html --include-code
186186
### Basic Execution
187187

188188
```bash
189-
# Run a scenario (creates my_network.json by default)
189+
# Run a scenario (creates my_network.results.json by default)
190190
python -m ngraph run my_network.yaml
191191

192192
# Run a scenario and save results to custom file
@@ -402,8 +402,8 @@ The CLI executes the complete workflow defined in your scenario file, running al
402402
```bash
403403
# Development workflow
404404
python -m ngraph inspect my_scenario.yaml --detail # Validate and debug
405-
python -m ngraph run my_scenario.yaml # Execute (creates my_scenario.json)
406-
python -m ngraph report my_scenario.json --html # Generate my_scenario.ipynb/html
405+
python -m ngraph run my_scenario.yaml # Execute (creates my_scenario.results.json)
406+
python -m ngraph report my_scenario.results.json --html # Generate my_scenario.ipynb/html
407407
```
408408

409409
### Debugging Scenarios

ngraph/cli.py

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
from ngraph.profiling import PerformanceProfiler, PerformanceReporter
1717
from ngraph.report import ReportGenerator
1818
from ngraph.scenario import Scenario
19+
from ngraph.utils.output_paths import (
20+
build_artifact_path,
21+
ensure_parent_dir,
22+
profiles_dir_for_run,
23+
resolve_override_path,
24+
results_path_for_run,
25+
)
1926

2027
logger = get_logger(__name__)
2128

@@ -1143,19 +1150,21 @@ def _summarize_node_matches(
11431150

11441151
def _run_scenario(
11451152
path: Path,
1146-
output: Optional[Path],
1153+
results_override: Optional[Path],
11471154
no_results: bool,
11481155
stdout: bool,
11491156
keys: Optional[list[str]] = None,
11501157
profile: bool = False,
11511158
profile_memory: bool = False,
1159+
output_dir: Optional[Path] = None,
11521160
) -> None:
11531161
"""Run a scenario file and export results as JSON by default.
11541162
11551163
Args:
11561164
path: Scenario YAML file.
11571165
output: Optional explicit path where JSON results should be written. When
1158-
``None``, defaults to ``<scenario_name>.json`` in the current directory.
1166+
``None``, defaults to ``<scenario_name>.results.json`` in the current directory,
1167+
or under ``--output`` if provided.
11591168
no_results: Whether to disable results file generation.
11601169
stdout: Whether to also print results to stdout.
11611170
keys: Optional list of workflow step names to include. When ``None`` all steps are
@@ -1179,8 +1188,8 @@ def _run_scenario(
11791188
logger.info("Starting scenario execution with profiling")
11801189

11811190
# Enable child-process profiling for parallel workflows
1182-
child_profile_dir = Path("worker_profiles")
1183-
child_profile_dir.mkdir(exist_ok=True)
1191+
child_profile_dir = profiles_dir_for_run(path, output_dir)
1192+
child_profile_dir.mkdir(parents=True, exist_ok=True)
11841193
os.environ["NGRAPH_PROFILE_DIR"] = str(child_profile_dir.resolve())
11851194
logger.info(f"Worker profiles will be saved to: {child_profile_dir}")
11861195

@@ -1214,10 +1223,13 @@ def _run_scenario(
12141223
f.unlink()
12151224
except Exception:
12161225
pass
1217-
try:
1218-
child_profile_dir.rmdir() # Remove dir if empty
1219-
except Exception:
1220-
pass
1226+
# Keep the profiles directory when an explicit output dir is used
1227+
# to make artifact paths consistent and discoverable.
1228+
if output_dir is None:
1229+
try:
1230+
child_profile_dir.rmdir() # Remove dir if empty
1231+
except Exception:
1232+
pass
12211233

12221234
# Generate and display performance report
12231235
reporter = PerformanceReporter(profiler.results)
@@ -1244,9 +1256,14 @@ def _run_scenario(
12441256

12451257
json_str = json.dumps(results_dict, indent=2, default=str)
12461258

1247-
# Derive default results file name from scenario when not provided
1248-
effective_output = output or Path(f"{path.stem}.json")
1259+
# Derive default results file path using output directory policy
1260+
effective_output = results_path_for_run(
1261+
scenario_path=path,
1262+
output_dir=output_dir,
1263+
results_override=results_override,
1264+
)
12491265

1266+
ensure_parent_dir(effective_output)
12501267
logger.info(f"Writing results to: {effective_output}")
12511268
effective_output.write_text(json_str)
12521269
logger.info("Results written successfully")
@@ -1313,7 +1330,8 @@ def main(argv: Optional[List[str]] = None) -> None:
13131330
type=Path,
13141331
default=None,
13151332
help=(
1316-
"Export results to JSON file (default: <scenario_name>.json in current directory)"
1333+
"Export results to JSON file (default: <scenario_name>.results.json;"
1334+
" placed under --output when provided)"
13171335
),
13181336
)
13191337
run_parser.add_argument(
@@ -1371,7 +1389,8 @@ def main(argv: Optional[List[str]] = None) -> None:
13711389
"-n",
13721390
type=Path,
13731391
help=(
1374-
"Output path for Jupyter notebook (default: <results_name>.ipynb in current directory)"
1392+
"Output path for Jupyter notebook (default: <results_name>.ipynb;"
1393+
" placed under --output when provided)"
13751394
),
13761395
)
13771396
report_parser.add_argument(
@@ -1380,7 +1399,8 @@ def main(argv: Optional[List[str]] = None) -> None:
13801399
nargs="?",
13811400
const=Path("analysis.html"),
13821401
help=(
1383-
"Generate HTML report (default: <results_name>.html in current directory if no path specified)"
1402+
"Generate HTML report (default: <results_name>.html if no path specified;"
1403+
" placed under --output when provided)"
13841404
),
13851405
)
13861406
report_parser.add_argument(
@@ -1389,6 +1409,20 @@ def main(argv: Optional[List[str]] = None) -> None:
13891409
help="Include code cells in HTML output (default: report without code)",
13901410
)
13911411

1412+
# Global output directory for all commands
1413+
for p in (run_parser, inspect_parser, report_parser):
1414+
p.add_argument(
1415+
"--output",
1416+
"-o",
1417+
type=Path,
1418+
default=None,
1419+
help=(
1420+
"Output directory for generated artifacts. When provided,"
1421+
" all files will be written under this folder using a"
1422+
" consistent '<prefix>.<suffix>' naming convention."
1423+
),
1424+
)
1425+
13921426
# Determine effective arguments (support both direct calls and module entrypoint)
13931427
effective_args = sys.argv[1:] if argv is None else argv
13941428

@@ -1410,22 +1444,24 @@ def main(argv: Optional[List[str]] = None) -> None:
14101444

14111445
if args.command == "run":
14121446
_run_scenario(
1413-
args.scenario,
1414-
args.results,
1415-
args.no_results,
1416-
args.stdout,
1417-
args.keys,
1418-
args.profile,
1419-
args.profile_memory,
1447+
path=args.scenario,
1448+
results_override=args.results,
1449+
no_results=args.no_results,
1450+
stdout=args.stdout,
1451+
keys=args.keys,
1452+
profile=args.profile,
1453+
profile_memory=args.profile_memory,
1454+
output_dir=args.output,
14201455
)
14211456
elif args.command == "inspect":
14221457
_inspect_scenario(args.scenario, args.detail)
14231458
elif args.command == "report":
14241459
_generate_report(
1425-
args.results,
1426-
args.notebook,
1427-
args.html,
1428-
args.include_code,
1460+
results_path=args.results,
1461+
notebook_path=args.notebook,
1462+
html_path=args.html,
1463+
include_code=args.include_code,
1464+
output_dir=args.output,
14291465
)
14301466

14311467

@@ -1434,6 +1470,7 @@ def _generate_report(
14341470
notebook_path: Optional[Path],
14351471
html_path: Optional[Path],
14361472
include_code: bool,
1473+
output_dir: Optional[Path] = None,
14371474
) -> None:
14381475
"""Generate analysis reports from results file.
14391476
@@ -1450,20 +1487,28 @@ def _generate_report(
14501487
generator = ReportGenerator(results_path)
14511488
generator.load_results()
14521489

1453-
# Generate notebook (default derives from results file name)
1454-
notebook_output = notebook_path or Path(f"{results_path.stem}.ipynb")
1455-
generated_notebook = generator.generate_notebook(notebook_output)
1490+
# Determine notebook output path
1491+
nb_out = resolve_override_path(notebook_path, output_dir)
1492+
if nb_out is None:
1493+
nb_out = build_artifact_path(output_dir, results_path.stem, ".ipynb")
1494+
1495+
ensure_parent_dir(nb_out)
1496+
generated_notebook = generator.generate_notebook(nb_out)
14561497
print(f"✅ Notebook generated: {generated_notebook}")
14571498

14581499
# Generate HTML if requested
1459-
if html_path:
1460-
# If --html was passed without an explicit path, argparse provides the const
1461-
# value. In that case, derive the HTML name from the results file stem.
1500+
html_out: Optional[Path] = None
1501+
if html_path is not None:
14621502
if html_path == Path("analysis.html"):
1463-
html_path = Path(f"{results_path.stem}.html")
1503+
html_out = build_artifact_path(output_dir, results_path.stem, ".html")
1504+
else:
1505+
html_out = resolve_override_path(html_path, output_dir)
1506+
1507+
if html_out is not None:
1508+
ensure_parent_dir(html_out)
14641509
generated_html = generator.generate_html_report(
14651510
notebook_path=generated_notebook,
1466-
html_path=html_path,
1511+
html_path=html_out,
14671512
include_code=include_code,
14681513
)
14691514
print(f"✅ HTML report generated: {generated_html}")

0 commit comments

Comments
 (0)