Commit eef5b8c9 authored by Eric Duminil's avatar Eric Duminil
Browse files

Adding a possible CLI for simstadt

parent d93a5557
......@@ -32,26 +32,5 @@ __all__ = [
def main() -> None:
# TODO: Use argparse? e.g. for GUI or templates
# TODO: return SimStadt result when ran with params? Display KPIS, df, and files
import re
from .runner import get_simstadt_folder, get_simstadt_output
print("simstadt - Python library for SimStadt workflows.")
print("See https://simstadt.hft-stuttgart.de/ for SimStadt documentation.")
print()
try:
folder = get_simstadt_folder()
print(f"SimStadt folder : {folder}")
except ValueError as e:
print(f"SimStadt folder : NOT FOUND\n{e}")
return
output = get_simstadt_output(folder)
m = re.search(r"Launching SimStadt (\S+) \((\w+), rev\. (\w+), (\d\d\d\d)(\d\d)(\d\d)\)", output)
if m:
version, branch, commit, yyyy, mm, dd = m.groups()
print(f"SimStadt version: {version} (branch: {branch}, rev: {commit}, date: {yyyy}-{mm}-{dd})")
else:
print("SimStadt version: unknown (could not parse version string)")
from .cli import app
app()
"""CLI for the simstadt library."""
import argparse
import logging
import re
import subprocess
import sys
from pathlib import Path
def cmd_info(args: argparse.Namespace) -> None:
from .runner import get_simstadt_folder, get_simstadt_output, _simstadt_script
try:
folder = get_simstadt_folder()
except ValueError as e:
print(f"SimStadt folder : NOT FOUND\n{e}", file=sys.stderr)
raise SystemExit(1)
if args.gui:
print(f"Launching SimStadt GUI from {folder} ...", file=sys.stderr)
subprocess.Popen([_simstadt_script()], cwd=folder)
return
print("simstadt - Python library for SimStadt workflows.")
print("See https://simstadt.hft-stuttgart.de/ for SimStadt documentation.")
print()
print(f"SimStadt folder : {folder}")
output = get_simstadt_output(folder)
m = re.search(r"Launching SimStadt (\S+) \((\w+), rev\. (\w+), (\d\d\d\d)(\d\d)(\d\d)\)", output)
if m:
version, branch, commit, yyyy, mm, dd = m.groups()
print(f"SimStadt version: {version} (branch: {branch}, rev: {commit}, date: {yyyy}-{mm}-{dd})")
else:
print("SimStadt version: unknown (could not parse version string)")
def cmd_run(args: argparse.Namespace) -> None:
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING)
from .runner import run_workflow_with_citygml
p = Path(args.template)
template = p if (p.exists() or p.with_suffix(".flow").exists()) else args.template
try:
results = run_workflow_with_citygml(
template=template,
citygml_path=args.citygml,
description=args.description,
destination=args.destination,
project_path=args.project_path,
)
except (FileNotFoundError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
raise SystemExit(1)
print(f"\n{results.description}\n")
for kpi in results.kpis:
print(f" {kpi}")
if args.files:
print("\nOutput files:")
for f in results.output_files:
print(f" {f}")
if args.save:
if args.save.suffix == ".json":
results.dataframe.to_json(args.save, orient="records", indent=2)
else:
results.dataframe.to_csv(args.save)
print(f"\nSaved to {args.save}", file=sys.stderr)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="simstadt",
description="simstadt - Python library for SimStadt workflows.",
)
parser.add_argument("--gui", action="store_true", help="Launch the SimStadt GUI.")
parser.add_argument("template", nargs="?", help="Template name (from SIMSTADT_TEMPLATE_PATH) or path to a .flow directory.")
parser.add_argument("citygml", nargs="?", type=Path, help="Path to the CityGML input file.")
parser.add_argument("-d", "--description", help="Human-readable label for the result.")
parser.add_argument("--destination", help="Workflow folder name (default: timestamped random id).")
parser.add_argument("-p", "--project-path", dest="project_path", type=Path, help="Directory where the workflow folder is created.")
parser.add_argument("-f", "--files", action="store_true", help="Show output files after the run.")
parser.add_argument("-s", "--save", type=Path, metavar="PATH", help="Save result DataFrame to a file (.csv or .json).")
parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging.")
return parser
def app() -> None:
parser = build_parser()
args = parser.parse_args()
if args.template and args.citygml:
cmd_run(args)
else:
cmd_info(args)
......@@ -200,6 +200,7 @@ def get_simstadt_output(path: Path) -> str:
def run_simstadt(workflow_path: Path, name: str) -> str:
"""Invoke the SimStadt CLI for the given workflow. Returns stdout."""
workflow_path = workflow_path.resolve()
with chdir(get_simstadt_folder()):
log.info("Launching %s:", name)
result = subprocess.run(
......@@ -209,8 +210,8 @@ def run_simstadt(workflow_path: Path, name: str) -> str:
check=False,
)
if result.returncode != 0:
log.warning(" Workflow failed!\n%s", result.stdout)
raise ValueError("Workflow failed!")
output = (result.stdout + result.stderr).strip()
raise ValueError(f"Workflow failed!\n{output}")
log.debug(result.stdout)
log.info(" Workflow finished successfully!\n")
return result.stdout
......
"""Tests for the simstadt CLI."""
import sys
from pathlib import Path
import pytest
from simstadt.cli import app, build_parser
from .test_runner import CITYGML, HEAT_TEMPLATE, PV_TEMPLATE
@pytest.fixture
def run_cli(monkeypatch):
"""Set sys.argv and invoke app(). Returns (stdout, stderr) via capsys."""
def _run(capsys, *args):
monkeypatch.setattr(sys, "argv", ["simstadt", *[str(a) for a in args]])
app()
return capsys.readouterr()
return _run
# ---------------------------------------------------------------------------
# Parser
# ---------------------------------------------------------------------------
def test_parser_defaults():
args = build_parser().parse_args(["mytemplate", "city.gml"])
assert args.template == "mytemplate"
assert args.citygml == Path("city.gml")
assert args.files is False
assert args.verbose is False
assert args.description is None
assert args.destination is None
assert args.project_path is None
assert args.save is None
assert args.gui is False
def test_parser_all_flags():
args = build_parser().parse_args(
[
"t",
"c.gml",
"-f",
"-v",
"-d",
"my desc",
"--destination",
"myrun",
"-p",
"/some/path",
"-s",
"out.csv",
]
)
assert args.files is True
assert args.verbose is True
assert args.description == "my desc"
assert args.destination == "myrun"
assert args.project_path == Path("/some/path")
assert args.save == Path("out.csv")
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
def test_no_args_calls_info(monkeypatch, capsys):
monkeypatch.setattr(sys, "argv", ["simstadt"])
monkeypatch.setattr("simstadt.cli.cmd_info", lambda args: print("info called"))
app()
assert "info called" in capsys.readouterr().out
def test_template_and_citygml_calls_run(monkeypatch, capsys):
monkeypatch.setattr(sys, "argv", ["simstadt", "tmpl", "city.gml"])
monkeypatch.setattr("simstadt.cli.cmd_run", lambda args: print("run called"))
app()
assert "run called" in capsys.readouterr().out
# ---------------------------------------------------------------------------
# cmd_run
# ---------------------------------------------------------------------------
def test_run_kpis_on_stdout(mock_simstadt, run_cli, capsys, tmp_path):
proj = tmp_path / "Test.proj"
proj.mkdir()
out, err = run_cli(capsys, HEAT_TEMPLATE, CITYGML, "-p", proj)
assert "Heated area" in out
assert "Year of construction" in out
assert err == "" # silent on success
def test_run_files_flag(mock_simstadt, run_cli, capsys, tmp_path):
proj = tmp_path / "Test.proj"
proj.mkdir()
out, _ = run_cli(capsys, HEAT_TEMPLATE, CITYGML, "-p", proj, "--files")
assert "Output files:" in out
assert "MiniBuchwald_hourly_demand.csv" in out
assert "Specific Heating Demand" in out
assert "Mean Uvalue" in out
def test_run_no_files_flag(mock_simstadt, run_cli, capsys, tmp_path):
proj = tmp_path / "Test.proj"
proj.mkdir()
out, _ = run_cli(capsys, HEAT_TEMPLATE, CITYGML, "-p", proj)
assert "Output files:" not in out
def test_run_error_on_stderr(monkeypatch, capsys, tmp_path):
monkeypatch.setattr(
"simstadt.runner.run_simstadt",
lambda *a, **kw: (_ for _ in ()).throw(ValueError("boom")),
)
monkeypatch.setattr(
sys,
"argv",
[
"simstadt",
str(HEAT_TEMPLATE),
str(CITYGML),
"-p",
str(tmp_path / "Test.proj"),
],
)
(tmp_path / "Test.proj").mkdir()
with pytest.raises(SystemExit):
app()
out, err = capsys.readouterr()
assert "boom" in err
assert out == ""
def test_run_save_csv(mock_simstadt, run_cli, capsys, tmp_path):
proj = tmp_path / "Test.proj"
proj.mkdir()
out_file = tmp_path / "results.csv"
run_cli(capsys, HEAT_TEMPLATE, CITYGML, "-p", proj, "-s", out_file)
assert out_file.exists()
import pandas as pd
df = pd.read_csv(out_file)
assert len(df) > 0
assert "November Heating demand" in df
def test_run_save_json(mock_simstadt, run_cli, capsys, tmp_path):
proj = tmp_path / "Test.proj"
proj.mkdir()
out_file = tmp_path / "results.json"
run_cli(capsys, PV_TEMPLATE, CITYGML, "-p", proj, "-s", out_file)
assert out_file.exists()
import json
data = json.loads(out_file.read_text())
assert isinstance(data, list)
assert len(data) > 0
assert "PV potential nominal power" in data[0]
def test_run_save_goes_to_stderr(mock_simstadt, run_cli, capsys, tmp_path):
proj = tmp_path / "Test.proj"
proj.mkdir()
out_file = tmp_path / "results.csv"
out, err = run_cli(capsys, HEAT_TEMPLATE, CITYGML, "-p", proj, "-s", out_file)
assert "Saved to" in err
assert "Saved to" not in out
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment