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__ = [ ...@@ -32,26 +32,5 @@ __all__ = [
def main() -> None: def main() -> None:
# TODO: Use argparse? e.g. for GUI or templates from .cli import app
# TODO: return SimStadt result when ran with params? Display KPIS, df, and files app()
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)")
"""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: ...@@ -200,6 +200,7 @@ def get_simstadt_output(path: Path) -> str:
def run_simstadt(workflow_path: Path, name: str) -> str: def run_simstadt(workflow_path: Path, name: str) -> str:
"""Invoke the SimStadt CLI for the given workflow. Returns stdout.""" """Invoke the SimStadt CLI for the given workflow. Returns stdout."""
workflow_path = workflow_path.resolve()
with chdir(get_simstadt_folder()): with chdir(get_simstadt_folder()):
log.info("Launching %s:", name) log.info("Launching %s:", name)
result = subprocess.run( result = subprocess.run(
...@@ -209,8 +210,8 @@ def run_simstadt(workflow_path: Path, name: str) -> str: ...@@ -209,8 +210,8 @@ def run_simstadt(workflow_path: Path, name: str) -> str:
check=False, check=False,
) )
if result.returncode != 0: if result.returncode != 0:
log.warning(" Workflow failed!\n%s", result.stdout) output = (result.stdout + result.stderr).strip()
raise ValueError("Workflow failed!") raise ValueError(f"Workflow failed!\n{output}")
log.debug(result.stdout) log.debug(result.stdout)
log.info(" Workflow finished successfully!\n") log.info(" Workflow finished successfully!\n")
return result.stdout 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