processforge 0.3.21__tar.gz → 0.3.25__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {processforge-0.3.21/src/processforge.egg-info → processforge-0.3.25}/PKG-INFO +2 -1
- {processforge-0.3.21 → processforge-0.3.25}/flowsheets/hydraulic-chain.json +1 -1
- {processforge-0.3.21 → processforge-0.3.25}/pyproject.toml +2 -1
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/__init__.py +1 -1
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/apply.py +3 -7
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/common.py +106 -35
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/display.py +32 -4
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/init.py +115 -69
- processforge-0.3.25/src/processforge/cli/plan.py +198 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/run.py +38 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/runs.py +13 -21
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/output_collector.py +17 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/container_client.py +13 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/coolprop_provider.py +12 -3
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/state.py +61 -27
- {processforge-0.3.21 → processforge-0.3.25/src/processforge.egg-info}/PKG-INFO +2 -1
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge.egg-info/SOURCES.txt +1 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge.egg-info/requires.txt +1 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_cli.py +53 -0
- processforge-0.3.25/tests/test_display.py +36 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_runs_command.py +24 -1
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_state.py +95 -0
- processforge-0.3.21/src/processforge/cli/plan.py +0 -141
- {processforge-0.3.21 → processforge-0.3.25}/LICENSE +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/MANIFEST.in +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/README.md +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/flowsheets/coupled/msre_coupled.json +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/flowsheets/festim/tds_tungsten.json +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/flowsheets/openmc/msre_eigenvalue.json +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/setup.cfg +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/__main__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/_schema.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/api/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/api/serve.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/export_fmu.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/compose.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/coupling.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/flowsheet.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/solver.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/fmu/_fmi_vars.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/fmu/builder.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/fmu/slave_template.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/lock.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/persistence/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/persistence/archive.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/persistence/artifact_store.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/provenance.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/errors.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/quantity.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/result.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/schemas/festim/festim_model.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/simulate.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/solver.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/thermo.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/types.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/base.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/utils/s3_upload.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_container_client.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_coupling.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_festim_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_festim_schema.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_init.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_openmc_provider.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_provider_errors.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_providers.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_result_zarr.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_s3_upload.py +0 -0
- {processforge-0.3.21 → processforge-0.3.25}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: processforge
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.25
|
|
4
4
|
Summary: A Python-based process simulation framework for chemical engineering applications.
|
|
5
5
|
Author-email: Processforge Team <team@processforge.dev>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -31,6 +31,7 @@ Requires-Dist: zarr>=3.0
|
|
|
31
31
|
Requires-Dist: pint<1.0,>=0.24
|
|
32
32
|
Requires-Dist: coolprop>=7.2.0
|
|
33
33
|
Requires-Dist: typer>=0.12
|
|
34
|
+
Requires-Dist: arrow>=1.3
|
|
34
35
|
Provides-Extra: api
|
|
35
36
|
Requires-Dist: fastapi>=0.110; extra == "api"
|
|
36
37
|
Requires-Dist: uvicorn>=0.29; extra == "api"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "processforge"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.25"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -37,6 +37,7 @@ dependencies = [
|
|
|
37
37
|
"pint>=0.24,<1.0",
|
|
38
38
|
"coolprop>=7.2.0",
|
|
39
39
|
"typer>=0.12",
|
|
40
|
+
"arrow>=1.3",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.optional-dependencies]
|
|
@@ -63,7 +63,7 @@ from .common import (
|
|
|
63
63
|
write_divergence_report,
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
from .display import print_structural_diff
|
|
66
|
+
from .display import print_param_drift, print_structural_diff
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def apply(
|
|
@@ -131,13 +131,9 @@ def apply(
|
|
|
131
131
|
if not drifted:
|
|
132
132
|
logger.info("No drift detected. System is already at the desired state.")
|
|
133
133
|
return
|
|
134
|
-
stream_drifts = [d for d in drifted if d.startswith("streams.")]
|
|
135
|
-
unit_drifts = [d for d in drifted if d.startswith("units.")]
|
|
136
134
|
logger.warning("Drift detected:")
|
|
137
|
-
if
|
|
138
|
-
|
|
139
|
-
if unit_drifts:
|
|
140
|
-
logger.warning(f" Unit drift : {unit_drifts}")
|
|
135
|
+
old_config = state.config if hasattr(state, "config") else state.get("config", {})
|
|
136
|
+
print_param_drift(drifted, old_config, config)
|
|
141
137
|
|
|
142
138
|
# Build flowsheet; attach saved state for warm-start unless topology changed
|
|
143
139
|
fs = EOFlowsheet(config, backend=backend)
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
6
|
import hashlib
|
|
7
|
-
import importlib
|
|
7
|
+
import importlib.util
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
10
|
import urllib.error
|
|
@@ -145,6 +145,13 @@ def load_state_manager(outputs_dir: str, base_name: str) -> tuple[ProcessStateAr
|
|
|
145
145
|
# Provider checking
|
|
146
146
|
# ---------------------------------------------------------------------------
|
|
147
147
|
|
|
148
|
+
# Retry/poll settings for containerized provider health checks. A container
|
|
149
|
+
# spun up by ``pf init`` may take a few seconds to become ready; poll rather
|
|
150
|
+
# than failing immediately so ``pf run`` waits for readiness with visible text.
|
|
151
|
+
HEALTH_MAX_ATTEMPTS = 6
|
|
152
|
+
HEALTH_RETRY_DELAY = 5 # seconds between attempts
|
|
153
|
+
|
|
154
|
+
|
|
148
155
|
def is_local_provider_url(url: str | None) -> bool:
|
|
149
156
|
"""Return True if *url* points at a locally-managed provider.
|
|
150
157
|
|
|
@@ -184,6 +191,85 @@ def _ping_provider_health(url: str, timeout: int = 5) -> tuple[bool, "dict | str
|
|
|
184
191
|
return False, str(exc)
|
|
185
192
|
|
|
186
193
|
|
|
194
|
+
def _check_container_provider(
|
|
195
|
+
name: str,
|
|
196
|
+
ptype: str,
|
|
197
|
+
url: str,
|
|
198
|
+
flowsheet_path: str,
|
|
199
|
+
*,
|
|
200
|
+
fail_fast: bool,
|
|
201
|
+
errors: list[str],
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Probe a containerized provider's ``/health`` endpoint with a retry/poll.
|
|
204
|
+
|
|
205
|
+
Polls up to ``HEALTH_MAX_ATTEMPTS`` times (``HEALTH_RETRY_DELAY`` apart) so a
|
|
206
|
+
container that is still booting after ``pf init`` gets a chance to become
|
|
207
|
+
ready before we give up. Logs a visible ``[OK]``/``[ERR]`` line (matching the
|
|
208
|
+
``pf plan`` convention) so ``pf run`` never silently waits.
|
|
209
|
+
"""
|
|
210
|
+
import time
|
|
211
|
+
|
|
212
|
+
ok, info = False, "not probed"
|
|
213
|
+
for attempt in range(1, HEALTH_MAX_ATTEMPTS + 1):
|
|
214
|
+
ok, info = _ping_provider_health(url, timeout=5)
|
|
215
|
+
if ok:
|
|
216
|
+
break
|
|
217
|
+
if attempt < HEALTH_MAX_ATTEMPTS:
|
|
218
|
+
logger.info(
|
|
219
|
+
f" Waiting for '{name}' container to become healthy at {url}… "
|
|
220
|
+
f"(attempt {attempt}/{HEALTH_MAX_ATTEMPTS})"
|
|
221
|
+
)
|
|
222
|
+
time.sleep(HEALTH_RETRY_DELAY)
|
|
223
|
+
|
|
224
|
+
if ok:
|
|
225
|
+
payload = info if isinstance(info, dict) else {}
|
|
226
|
+
status = payload.get("status", "?")
|
|
227
|
+
provider_type = payload.get("provider_type", "?")
|
|
228
|
+
logger.info(
|
|
229
|
+
f" [OK] {name} [{ptype}] {url} — status={status} provider_type={provider_type}"
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
msg = (
|
|
234
|
+
f" [ERR] {name} [{ptype}] {url} — unreachable after "
|
|
235
|
+
f"{HEALTH_MAX_ATTEMPTS} attempt(s): {info}. "
|
|
236
|
+
f"Run: pf init {flowsheet_path}"
|
|
237
|
+
)
|
|
238
|
+
logger.error(msg)
|
|
239
|
+
if fail_fast:
|
|
240
|
+
raise SystemExit(1)
|
|
241
|
+
errors.append(f"Provider '{name}' unreachable at {url}: {info}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _check_pip_provider(
|
|
245
|
+
name: str,
|
|
246
|
+
ptype: str,
|
|
247
|
+
flowsheet_path: str,
|
|
248
|
+
*,
|
|
249
|
+
fail_fast: bool,
|
|
250
|
+
errors: list[str],
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Verify a pip-installable provider's module is importable."""
|
|
253
|
+
from ..providers.registry import _PROVIDER_CATALOG
|
|
254
|
+
|
|
255
|
+
catalog = _PROVIDER_CATALOG.get(ptype, {})
|
|
256
|
+
module = catalog.get("module", "")
|
|
257
|
+
try:
|
|
258
|
+
importlib.util.find_spec(module)
|
|
259
|
+
logger.info(f" [OK] {name} [{ptype}] (pip — importable)")
|
|
260
|
+
except (ModuleNotFoundError, ValueError):
|
|
261
|
+
dep = catalog.get("optional_dep")
|
|
262
|
+
hint = f"pip install 'processforge[{dep}]'" if dep else "built-in"
|
|
263
|
+
msg = (
|
|
264
|
+
f" [WARN] {name} [{ptype}] — not installed. "
|
|
265
|
+
f"Run: pf init {flowsheet_path} (install with: {hint})"
|
|
266
|
+
)
|
|
267
|
+
logger.warning(msg)
|
|
268
|
+
if fail_fast:
|
|
269
|
+
raise SystemExit(1)
|
|
270
|
+
errors.append(f"Provider '{name}' not installed ({hint})")
|
|
271
|
+
|
|
272
|
+
|
|
187
273
|
def check_providers(
|
|
188
274
|
config: dict,
|
|
189
275
|
flowsheet_path: str,
|
|
@@ -193,56 +279,41 @@ def check_providers(
|
|
|
193
279
|
) -> None:
|
|
194
280
|
"""Verify all declared providers are reachable or importable.
|
|
195
281
|
|
|
282
|
+
For containerized providers this pings the container's ``/health`` endpoint
|
|
283
|
+
(with a short retry/poll so a still-booting container can become ready) and
|
|
284
|
+
logs a visible ``[OK]``/``[ERR]`` line — so ``pf run`` shows a clear health
|
|
285
|
+
check instead of silently waiting. Pip-installable providers are checked for
|
|
286
|
+
importability and reported the same way.
|
|
287
|
+
|
|
196
288
|
Parameters
|
|
197
289
|
----------
|
|
198
290
|
fail_fast:
|
|
199
291
|
If ``True`` (default), raise ``SystemExit`` on the first failure.
|
|
200
292
|
If ``False``, accumulate errors and raise once at the end.
|
|
201
293
|
verbose:
|
|
202
|
-
|
|
294
|
+
Accepted for compatibility; the health check is always logged.
|
|
203
295
|
"""
|
|
204
|
-
from ..providers.registry import is_containerized
|
|
296
|
+
from ..providers.registry import is_containerized
|
|
205
297
|
|
|
206
298
|
providers = config.get("providers", {})
|
|
207
299
|
errors: list[str] = []
|
|
208
300
|
|
|
301
|
+
if providers:
|
|
302
|
+
logger.info("=== Provider / Container Health ===")
|
|
303
|
+
|
|
209
304
|
for name, cfg in providers.items():
|
|
210
305
|
ptype = cfg.get("type", "")
|
|
211
306
|
if is_containerized(ptype):
|
|
212
307
|
url = _resolve_provider_url(cfg, ptype)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
except (urllib.error.URLError, OSError, TimeoutError) as exc:
|
|
218
|
-
msg = (
|
|
219
|
-
f"Provider '{name}' unreachable at {url}. "
|
|
220
|
-
f"Run: pf init {flowsheet_path}"
|
|
221
|
-
)
|
|
222
|
-
if fail_fast:
|
|
223
|
-
logger.error(msg)
|
|
224
|
-
raise SystemExit(1)
|
|
225
|
-
logger.error(msg)
|
|
226
|
-
errors.append(msg)
|
|
308
|
+
_check_container_provider(
|
|
309
|
+
name, ptype, url, flowsheet_path,
|
|
310
|
+
fail_fast=fail_fast, errors=errors,
|
|
311
|
+
)
|
|
227
312
|
else:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if verbose:
|
|
233
|
-
logger.info(f"Provider '{name}' — importable (pip)")
|
|
234
|
-
except (ModuleNotFoundError, ValueError):
|
|
235
|
-
dep = catalog.get("optional_dep")
|
|
236
|
-
hint = f"pip install 'processforge[{dep}]'" if dep else "built-in"
|
|
237
|
-
msg = (
|
|
238
|
-
f"Provider '{name}' not installed. "
|
|
239
|
-
f"Run: pf init {flowsheet_path} (install with: {hint})"
|
|
240
|
-
)
|
|
241
|
-
if fail_fast:
|
|
242
|
-
logger.error(msg)
|
|
243
|
-
raise SystemExit(1)
|
|
244
|
-
logger.error(msg)
|
|
245
|
-
errors.append(msg)
|
|
313
|
+
_check_pip_provider(
|
|
314
|
+
name, ptype, flowsheet_path,
|
|
315
|
+
fail_fast=fail_fast, errors=errors,
|
|
316
|
+
)
|
|
246
317
|
|
|
247
318
|
if errors:
|
|
248
319
|
raise SystemExit(1)
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
import importlib.util
|
|
8
|
+
|
|
7
9
|
from loguru import logger
|
|
8
10
|
|
|
9
11
|
|
|
@@ -53,7 +55,7 @@ def print_unit_mismatches(mismatches: list[Any]) -> None:
|
|
|
53
55
|
logger.warning(f"Unit annotation — stream '{m.stream_name}'.{m.property_name}: {m.message}")
|
|
54
56
|
|
|
55
57
|
|
|
56
|
-
def print_provider_health(config: dict) -> list[str]:
|
|
58
|
+
def print_provider_health(config: dict, strict: bool = False) -> list[str]:
|
|
57
59
|
"""Log per-provider reachability for flowsheets using Docker containers.
|
|
58
60
|
|
|
59
61
|
For each provider declared in *config*:
|
|
@@ -63,10 +65,9 @@ def print_provider_health(config: dict) -> list[str]:
|
|
|
63
65
|
* pip-installable providers are reported as importable or not.
|
|
64
66
|
|
|
65
67
|
Returns a list of failure messages — one per unreachable containerized
|
|
66
|
-
provider (empty means every provider is healthy/available).
|
|
68
|
+
provider (empty means every provider is healthy/available). When *strict*
|
|
69
|
+
is True, a missing pip-installable provider is also appended to failures.
|
|
67
70
|
"""
|
|
68
|
-
import importlib
|
|
69
|
-
|
|
70
71
|
from ..providers.registry import is_containerized, _PROVIDER_CATALOG
|
|
71
72
|
|
|
72
73
|
providers = config.get("providers", {})
|
|
@@ -104,6 +105,10 @@ def print_provider_health(config: dict) -> list[str]:
|
|
|
104
105
|
dep = catalog.get("optional_dep")
|
|
105
106
|
hint = f"pip install 'processforge[{dep}]'" if dep else "built-in"
|
|
106
107
|
logger.warning(f" [WARN] {name} [{ptype}] — not installed ({hint})")
|
|
108
|
+
if strict:
|
|
109
|
+
failures.append(
|
|
110
|
+
f"Provider '{name}' not installed ({hint})"
|
|
111
|
+
)
|
|
107
112
|
|
|
108
113
|
return failures
|
|
109
114
|
|
|
@@ -122,3 +127,26 @@ def print_structural_diff(diff: dict) -> None:
|
|
|
122
127
|
logger.info(f" - {name:<20} [{unit_type}] (removed)")
|
|
123
128
|
if not any(diff.get(k) for k in ("added", "modified", "removed")):
|
|
124
129
|
logger.info(" (no structural changes)")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _resolve_path(cfg: dict, path: str) -> object:
|
|
133
|
+
"""Resolve a dotted config path (e.g. ``simulation.tf``) to its value."""
|
|
134
|
+
cur: object = cfg
|
|
135
|
+
for part in path.split("."):
|
|
136
|
+
if isinstance(cur, dict) and part in cur:
|
|
137
|
+
cur = cur[part]
|
|
138
|
+
else:
|
|
139
|
+
return "<missing>"
|
|
140
|
+
return cur
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def print_param_drift(drifted: list[str], old_config: dict, new_config: dict) -> None:
|
|
144
|
+
"""Log parameter drift paths with their old → new values."""
|
|
145
|
+
logger.info("=== Parameter Drift vs. Saved State ===")
|
|
146
|
+
if not drifted:
|
|
147
|
+
logger.info(" (no parameter changes)")
|
|
148
|
+
return
|
|
149
|
+
for path in drifted:
|
|
150
|
+
old_val = _resolve_path(old_config, path)
|
|
151
|
+
new_val = _resolve_path(new_config, path)
|
|
152
|
+
logger.info(f" ~ {path}: {old_val!r} → {new_val!r}")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import importlib
|
|
5
|
+
import importlib.util
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import shutil
|
|
@@ -11,8 +11,18 @@ import subprocess
|
|
|
11
11
|
import typer
|
|
12
12
|
from loguru import logger
|
|
13
13
|
|
|
14
|
-
from .common import
|
|
15
|
-
|
|
14
|
+
from .common import (
|
|
15
|
+
extract_providers,
|
|
16
|
+
is_local_provider_url,
|
|
17
|
+
)
|
|
18
|
+
from ..lock import flowsheet_env_dir, read_lock, write_lock
|
|
19
|
+
from ..providers.registry import (
|
|
20
|
+
get_provider_docker_image,
|
|
21
|
+
get_provider_default_port,
|
|
22
|
+
is_containerized,
|
|
23
|
+
_PROVIDER_CATALOG,
|
|
24
|
+
)
|
|
25
|
+
from ..utils.validate_flowsheet import validate_flowsheet
|
|
16
26
|
|
|
17
27
|
|
|
18
28
|
def _migrate_legacy_env(pf_dir: str) -> None:
|
|
@@ -21,7 +31,9 @@ def _migrate_legacy_env(pf_dir: str) -> None:
|
|
|
21
31
|
Older processforge stored a single environment at ``.processforge/lock.json``
|
|
22
32
|
and ``.processforge/docker-compose.yml``. This relocates those into the hashed
|
|
23
33
|
env dir derived from the flowsheet recorded in the legacy lock, so an existing
|
|
24
|
-
repo keeps its provider environment after upgrading.
|
|
34
|
+
repo keeps its provider environment after upgrading. The target always uses the
|
|
35
|
+
same hashed env dir that ``pf init``/``read_lock`` locate, so the migrated env
|
|
36
|
+
is never orphaned.
|
|
25
37
|
"""
|
|
26
38
|
legacy_lock = os.path.join(pf_dir, "lock.json")
|
|
27
39
|
legacy_compose = os.path.join(pf_dir, "docker-compose.yml")
|
|
@@ -35,15 +47,19 @@ def _migrate_legacy_env(pf_dir: str) -> None:
|
|
|
35
47
|
except Exception:
|
|
36
48
|
recorded = None
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
# Always target the hashed env dir keyed by the recorded flowsheet path so it
|
|
51
|
+
# matches what `pf init <flowsheet>` / `read_lock` expect on subsequent runs.
|
|
52
|
+
env_dir = flowsheet_env_dir(pf_dir, recorded or "legacy")
|
|
53
|
+
if recorded:
|
|
54
|
+
logger.info(
|
|
55
|
+
f"Migrating legacy .processforge/lock.json + docker-compose.yml into "
|
|
56
|
+
f"'{os.path.relpath(env_dir, pf_dir)}/'."
|
|
57
|
+
)
|
|
40
58
|
else:
|
|
41
|
-
base = os.path.splitext(os.path.basename(recorded or "legacy"))[0] or "legacy"
|
|
42
|
-
env_dir = os.path.join(pf_dir, base)
|
|
43
59
|
logger.warning(
|
|
44
60
|
"Migrating legacy .processforge/lock.json + docker-compose.yml into "
|
|
45
|
-
f"'{os.path.relpath(env_dir, pf_dir)}/' (recorded flowsheet
|
|
46
|
-
"
|
|
61
|
+
f"'{os.path.relpath(env_dir, pf_dir)}/' (no recorded flowsheet found — "
|
|
62
|
+
"re-run `pf init <flowsheet.json>` to restore the correct env dir)."
|
|
47
63
|
)
|
|
48
64
|
|
|
49
65
|
if os.path.exists(env_dir):
|
|
@@ -73,15 +89,19 @@ def init(
|
|
|
73
89
|
"--path",
|
|
74
90
|
help="Root directory to initialise in (default: current directory)",
|
|
75
91
|
),
|
|
92
|
+
no_pull: bool = typer.Option(
|
|
93
|
+
False,
|
|
94
|
+
"--no-pull",
|
|
95
|
+
help="Generate docker-compose.yml but skip pulling provider images",
|
|
96
|
+
),
|
|
97
|
+
force: bool = typer.Option(
|
|
98
|
+
False,
|
|
99
|
+
"--force",
|
|
100
|
+
"-f",
|
|
101
|
+
help="Re-initialise even if the environment already exists",
|
|
102
|
+
),
|
|
76
103
|
) -> None:
|
|
77
104
|
"""Initialise the .processforge/ project directory."""
|
|
78
|
-
from ..providers.registry import (
|
|
79
|
-
is_containerized,
|
|
80
|
-
get_provider_docker_image,
|
|
81
|
-
get_provider_default_port,
|
|
82
|
-
_PROVIDER_CATALOG,
|
|
83
|
-
)
|
|
84
|
-
from ..lock import write_lock
|
|
85
105
|
from ..compose import generate_compose
|
|
86
106
|
|
|
87
107
|
root = path or "."
|
|
@@ -96,17 +116,6 @@ def init(
|
|
|
96
116
|
# dir so existing repos aren't silently broken by the new structure.
|
|
97
117
|
_migrate_legacy_env(pf_dir)
|
|
98
118
|
|
|
99
|
-
# Remove stale .pfstate snapshot directories from outputs/
|
|
100
|
-
stale_count = 0
|
|
101
|
-
for entry in os.listdir(outputs_dir):
|
|
102
|
-
if entry.endswith(".pfstate"):
|
|
103
|
-
stale = os.path.join(outputs_dir, entry)
|
|
104
|
-
if os.path.isdir(stale):
|
|
105
|
-
shutil.rmtree(stale)
|
|
106
|
-
stale_count += 1
|
|
107
|
-
if stale_count:
|
|
108
|
-
logger.info(f"Removed {stale_count} stale snapshot(s) from {outputs_dir}/.")
|
|
109
|
-
|
|
110
119
|
# Write config.json (always)
|
|
111
120
|
config_path = os.path.join(pf_dir, "config.json")
|
|
112
121
|
if not os.path.exists(config_path):
|
|
@@ -121,6 +130,26 @@ def init(
|
|
|
121
130
|
else:
|
|
122
131
|
logger.info(f"{config_path} already exists — skipped.")
|
|
123
132
|
|
|
133
|
+
# Honour a configured outputs_dir for the rest of init.
|
|
134
|
+
outputs_dir_name = "outputs"
|
|
135
|
+
try:
|
|
136
|
+
with open(config_path, encoding="utf-8") as f:
|
|
137
|
+
outputs_dir_name = json.load(f).get("outputs_dir", "outputs")
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
outputs_dir = os.path.join(root, outputs_dir_name)
|
|
141
|
+
|
|
142
|
+
# Remove stale .pfstate snapshot directories from outputs/
|
|
143
|
+
stale_count = 0
|
|
144
|
+
for entry in os.listdir(outputs_dir):
|
|
145
|
+
if entry.endswith(".pfstate"):
|
|
146
|
+
stale = os.path.join(outputs_dir, entry)
|
|
147
|
+
if os.path.isdir(stale):
|
|
148
|
+
shutil.rmtree(stale)
|
|
149
|
+
stale_count += 1
|
|
150
|
+
if stale_count:
|
|
151
|
+
logger.info(f"Removed {stale_count} stale snapshot(s) from {outputs_dir}/.")
|
|
152
|
+
|
|
124
153
|
# No flowsheet → scaffold only
|
|
125
154
|
if not flowsheet:
|
|
126
155
|
logger.info(".processforge/ initialised successfully.")
|
|
@@ -131,15 +160,21 @@ def init(
|
|
|
131
160
|
flowsheet_path = flowsheet
|
|
132
161
|
if not os.path.exists(flowsheet_path):
|
|
133
162
|
logger.error(f"Flowsheet '{flowsheet_path}' not found.")
|
|
134
|
-
raise
|
|
163
|
+
raise typer.Exit(code=1)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
validate_flowsheet(flowsheet_path)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Failed to validate flowsheet '{flowsheet_path}': {e}")
|
|
169
|
+
raise typer.Exit(code=1)
|
|
135
170
|
|
|
136
171
|
providers = extract_providers(flowsheet_path)
|
|
137
172
|
logger.info(f"Reading providers from {flowsheet_path}...")
|
|
138
173
|
|
|
139
|
-
# Categorize providers
|
|
140
|
-
pip_providers: dict[str, dict] = {}
|
|
174
|
+
# Categorize providers, reusing the shared local/remote/pip classification.
|
|
141
175
|
local_docker_providers: dict[str, dict] = {}
|
|
142
176
|
remote_docker_providers: dict[str, dict] = {}
|
|
177
|
+
pip_providers: dict[str, dict] = {}
|
|
143
178
|
for name, cfg in providers.items():
|
|
144
179
|
ptype = cfg.get("type", "")
|
|
145
180
|
if is_containerized(ptype):
|
|
@@ -151,7 +186,8 @@ def init(
|
|
|
151
186
|
local_docker_providers[name] = {
|
|
152
187
|
"type": ptype,
|
|
153
188
|
"url": url,
|
|
154
|
-
"docker_image": cfg.get("docker_image")
|
|
189
|
+
"docker_image": cfg.get("docker_image")
|
|
190
|
+
or get_provider_docker_image(ptype),
|
|
155
191
|
"port": port,
|
|
156
192
|
}
|
|
157
193
|
logger.info(f" {name}: type={ptype}, url={url} (Docker, local)")
|
|
@@ -159,7 +195,8 @@ def init(
|
|
|
159
195
|
remote_docker_providers[name] = {
|
|
160
196
|
"type": ptype,
|
|
161
197
|
"url": url,
|
|
162
|
-
"docker_image": cfg.get("docker_image")
|
|
198
|
+
"docker_image": cfg.get("docker_image")
|
|
199
|
+
or get_provider_docker_image(ptype),
|
|
163
200
|
}
|
|
164
201
|
logger.info(
|
|
165
202
|
f" {name}: type={ptype}, url={url} (Docker, remote — skipping compose)"
|
|
@@ -185,45 +222,48 @@ def init(
|
|
|
185
222
|
# Providers with an explicit remote URL are assumed to be running elsewhere
|
|
186
223
|
# (e.g. a cloud deployment of the ghcr.io image) and are not touched here.
|
|
187
224
|
if local_docker_providers:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
if os.path.exists(compose_path):
|
|
225
|
+
env_dir = flowsheet_env_dir(pf_dir, flowsheet_path)
|
|
226
|
+
compose_path = os.path.join(env_dir, "docker-compose.yml")
|
|
227
|
+
if os.path.exists(compose_path) and not force:
|
|
192
228
|
logger.warning(
|
|
193
229
|
f"Environment already initialized — reinitializing from {flowsheet_path}"
|
|
194
230
|
)
|
|
195
231
|
|
|
196
|
-
generate_compose(
|
|
232
|
+
generate_compose(
|
|
233
|
+
pf_dir, local_docker_providers, outputs_dir, flowsheet=flowsheet_path
|
|
234
|
+
)
|
|
197
235
|
logger.info(f"Generated {compose_path}")
|
|
198
236
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
237
|
+
if no_pull:
|
|
238
|
+
logger.info("Skipping Docker image pull (--no-pull).")
|
|
239
|
+
else:
|
|
240
|
+
# Attempt docker compose pull. Capture output and stream it via the
|
|
241
|
+
# logger; on timeout the child is reaped (subprocess.run kills it)
|
|
242
|
+
# so no orphaned/blocked process is left behind.
|
|
243
|
+
try:
|
|
244
|
+
logger.info("Pulling Docker images (this may take a while)...")
|
|
245
|
+
result = subprocess.run(
|
|
246
|
+
["docker", "compose", "-f", compose_path, "pull"],
|
|
247
|
+
stdout=subprocess.PIPE,
|
|
248
|
+
stderr=subprocess.STDOUT,
|
|
249
|
+
text=True,
|
|
250
|
+
timeout=600,
|
|
251
|
+
)
|
|
252
|
+
for line in result.stdout.splitlines():
|
|
253
|
+
if line.strip():
|
|
254
|
+
logger.info(line)
|
|
255
|
+
if result.returncode == 0:
|
|
256
|
+
logger.info("Pulled Docker images.")
|
|
257
|
+
else:
|
|
258
|
+
logger.warning(
|
|
259
|
+
f"docker compose pull failed with exit code {result.returncode}."
|
|
260
|
+
)
|
|
261
|
+
except FileNotFoundError:
|
|
218
262
|
logger.warning(
|
|
219
|
-
|
|
263
|
+
"Docker not found. Install Docker to use containerized providers."
|
|
220
264
|
)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"Docker not found. Install Docker to use containerized providers."
|
|
224
|
-
)
|
|
225
|
-
except subprocess.TimeoutExpired:
|
|
226
|
-
logger.warning("docker compose pull timed out after 600s.")
|
|
265
|
+
except subprocess.TimeoutExpired:
|
|
266
|
+
logger.warning("docker compose pull timed out after 600s.")
|
|
227
267
|
|
|
228
268
|
logger.info(
|
|
229
269
|
"To start the containerized provider(s), run:\n"
|
|
@@ -242,15 +282,19 @@ def init(
|
|
|
242
282
|
else:
|
|
243
283
|
logger.info("No containerized providers — skipping Docker setup.")
|
|
244
284
|
|
|
245
|
-
# Write lock file
|
|
285
|
+
# Write lock file, reusing the shared URL resolution for the recorded url.
|
|
246
286
|
lock_providers: dict[str, dict] = {}
|
|
247
287
|
for name, cfg in providers.items():
|
|
248
288
|
ptype = cfg.get("type", "")
|
|
249
289
|
if is_containerized(ptype):
|
|
290
|
+
url = cfg.get("url")
|
|
291
|
+
if not url:
|
|
292
|
+
port = get_provider_default_port(ptype) or 9000
|
|
293
|
+
url = f"http://localhost:{port}"
|
|
250
294
|
lock_providers[name] = {
|
|
251
|
-
"docker_image": cfg.get("docker_image")
|
|
252
|
-
|
|
253
|
-
|
|
295
|
+
"docker_image": cfg.get("docker_image")
|
|
296
|
+
or get_provider_docker_image(ptype),
|
|
297
|
+
"url": url,
|
|
254
298
|
}
|
|
255
299
|
else:
|
|
256
300
|
lock_providers[name] = {
|
|
@@ -261,5 +305,7 @@ def init(
|
|
|
261
305
|
from .. import __version__ as pf_version
|
|
262
306
|
|
|
263
307
|
write_lock(pf_dir, flowsheet_path, lock_providers, pf_version)
|
|
264
|
-
logger.info(
|
|
308
|
+
logger.info(
|
|
309
|
+
f"Wrote {os.path.join(flowsheet_env_dir(pf_dir, flowsheet_path), 'lock.json')}"
|
|
310
|
+
)
|
|
265
311
|
logger.info(".processforge/ initialised successfully.")
|