Persist material efficiency snapshots

This commit is contained in:
2026-09-06 08:23:28 +02:00
parent c411581a1a
commit ed0e20a04b
10 changed files with 851 additions and 32 deletions
+96 -18
View File
@@ -37,10 +37,18 @@ def _parser() -> argparse.ArgumentParser:
raw.add_argument("--query", action="append", type=_query_item, default=[], metavar="KEY=VALUE")
raw.add_argument("--pretty", action="store_true", help="Pretty-print sanitized JSON")
raw.add_argument("--verbose", action="store_true", help="Show safe request progress on stderr")
raw.add_argument("--save-fixture", metavar="NAME", help="Save sanitized JSON under fixtures/enlyze/")
raw.add_argument("--save-raw", metavar="NAME", help="Save unmodified response under ignored data/raw/enlyze/")
raw.add_argument("--fixture-dir", type=Path, default=Path("fixtures/enlyze"), help=argparse.SUPPRESS)
raw.add_argument("--raw-dir", type=Path, default=Path("data/raw/enlyze"), help=argparse.SUPPRESS)
raw.add_argument(
"--save-fixture", metavar="NAME", help="Save sanitized JSON under fixtures/enlyze/"
)
raw.add_argument(
"--save-raw", metavar="NAME", help="Save unmodified response under ignored data/raw/enlyze/"
)
raw.add_argument(
"--fixture-dir", type=Path, default=Path("fixtures/enlyze"), help=argparse.SUPPRESS
)
raw.add_argument(
"--raw-dir", type=Path, default=Path("data/raw/enlyze"), help=argparse.SUPPRESS
)
raw.add_argument(
"--secrets-file",
type=Path,
@@ -52,18 +60,44 @@ def _parser() -> argparse.ArgumentParser:
timeseries.add_argument("--start", required=True, help="ISO 8601 datetime with timezone")
timeseries.add_argument("--end", required=True, help="ISO 8601 datetime with timezone")
timeseries.add_argument("--variable", required=True, help="Variable UUID")
timeseries.add_argument("--resampling-interval", type=int, help="Seconds; schema range is 10..604800")
timeseries.add_argument(
"--resampling-interval", type=int, help="Seconds; schema range is 10..604800"
)
timeseries.add_argument(
"--resampling-method",
choices=["first", "last", "max", "min", "count", "sum", "avg", "median", "std", "q5", "q25", "q75", "q95"],
choices=[
"first",
"last",
"max",
"min",
"count",
"sum",
"avg",
"median",
"std",
"q5",
"q25",
"q75",
"q95",
],
help="Optional schema-defined method for this variable",
)
timeseries.add_argument("--pretty", action="store_true", help="Pretty-print sanitized JSON")
timeseries.add_argument("--verbose", action="store_true", help="Show safe request progress on stderr")
timeseries.add_argument("--save-fixture", metavar="NAME", help="Save sanitized JSON under fixtures/enlyze/")
timeseries.add_argument("--save-raw", metavar="NAME", help="Save unmodified response under ignored data/raw/enlyze/")
timeseries.add_argument("--fixture-dir", type=Path, default=Path("fixtures/enlyze"), help=argparse.SUPPRESS)
timeseries.add_argument("--raw-dir", type=Path, default=Path("data/raw/enlyze"), help=argparse.SUPPRESS)
timeseries.add_argument(
"--verbose", action="store_true", help="Show safe request progress on stderr"
)
timeseries.add_argument(
"--save-fixture", metavar="NAME", help="Save sanitized JSON under fixtures/enlyze/"
)
timeseries.add_argument(
"--save-raw", metavar="NAME", help="Save unmodified response under ignored data/raw/enlyze/"
)
timeseries.add_argument(
"--fixture-dir", type=Path, default=Path("fixtures/enlyze"), help=argparse.SUPPRESS
)
timeseries.add_argument(
"--raw-dir", type=Path, default=Path("data/raw/enlyze"), help=argparse.SUPPRESS
)
timeseries.add_argument(
"--secrets-file",
type=Path,
@@ -78,6 +112,10 @@ def _parser() -> argparse.ArgumentParser:
material.add_argument("--poll-interval-seconds", type=float, default=10.0)
material.add_argument("--state-directory", type=Path, default=Path("data/state/material"))
material.add_argument("--secrets-file", type=Path, default=Path("secrets/enlyze.env"))
efficiency = runners.add_parser("material-efficiency", help="Persist ERP feedback KPI points")
efficiency.add_argument("--config", type=Path, required=True)
efficiency.add_argument("--secrets-file", type=Path, default=Path("secrets/enlyze.env"))
efficiency.add_argument("--erp-secrets-file", type=Path, default=Path("secrets/erp.env"))
return parser
@@ -102,8 +140,10 @@ def _run_material(args: argparse.Namespace) -> int:
try:
calculation = load_material_calculation(args.config, args.calculation_id)
runner = build_material_runner(
calculation, poll_interval_seconds=args.poll_interval_seconds,
state_directory=args.state_directory, secrets_file=args.secrets_file,
calculation,
poll_interval_seconds=args.poll_interval_seconds,
state_directory=args.state_directory,
secrets_file=args.secrets_file,
)
runner.run()
except (CalculationConfigError, ConfigurationError) as exc:
@@ -117,9 +157,35 @@ def _run_material(args: argparse.Namespace) -> int:
return 0
def _run_material_efficiency(args: argparse.Namespace) -> int:
from production_analytics.calculations.config import CalculationConfigError
from production_analytics.enlyze.exploration import ConfigurationError
from production_analytics.service.material_efficiency_runtime import (
build_material_efficiency_runner,
)
try:
build_material_efficiency_runner(
args.config,
secrets_file=args.secrets_file,
erp_secrets_file=args.erp_secrets_file,
).run()
except (CalculationConfigError, ConfigurationError) as exc:
print(f"Config/startup error: {exc}", file=sys.stderr)
return 2
except Exception as exc:
print(f"Startup/runtime error: {type(exc).__name__}", file=sys.stderr)
return 1
except KeyboardInterrupt:
return 0
return 0
def main(argv: Sequence[str] | None = None) -> int:
args = _parser().parse_args(argv)
if args.namespace == "run":
if args.command == "material-efficiency":
return _run_material_efficiency(args)
return _run_material(args)
if args.namespace != "enlyze" or args.command not in {"raw", "timeseries"}:
return 2
@@ -133,7 +199,8 @@ def main(argv: Sequence[str] | None = None) -> int:
operation = "GET" if args.command == "raw" else "POST"
target = args.path if args.command == "raw" else "/v2/timeseries"
print(
f"Requesting {operation} {target} (timeout={settings.timeout_seconds:g}s; authentication {authentication}).",
f"Requesting {operation} {target} (timeout={settings.timeout_seconds:g}s; "
f"authentication {authentication}).",
file=sys.stderr,
)
client = ExplorationClient(settings)
@@ -141,8 +208,13 @@ def main(argv: Sequence[str] | None = None) -> int:
response = client.get(args.path, dict(args.query))
request = {"method": "GET", "path": response.path}
else:
if args.resampling_interval is not None and not 10 <= args.resampling_interval <= 604800:
raise ExplorationError("--resampling-interval must be between 10 and 604800 seconds.")
if (
args.resampling_interval is not None
and not 10 <= args.resampling_interval <= 604800
):
raise ExplorationError(
"--resampling-interval must be between 10 and 604800 seconds."
)
variable: dict[str, str] = {"uuid": args.variable}
if args.resampling_method:
variable["resampling_method"] = args.resampling_method
@@ -170,9 +242,15 @@ def main(argv: Sequence[str] | None = None) -> int:
indent = 2 if args.pretty or args.save_fixture else None
print(json.dumps(payload, indent=indent, sort_keys=True))
if args.save_fixture:
print(f"Sanitized fixture written to {_write_fixture(args.fixture_dir, args.save_fixture, payload)}")
print(
"Sanitized fixture written to "
f"{_write_fixture(args.fixture_dir, args.save_fixture, payload)}"
)
if args.save_raw:
print(f"Raw response written to {_write_fixture(args.raw_dir, args.save_raw, raw_payload)}")
print(
"Raw response written to "
f"{_write_fixture(args.raw_dir, args.save_raw, raw_payload)}"
)
except ExplorationError as error:
print(f"ENLYZE exploration error: {error}")
return 1