processforge 0.3.5__tar.gz → 0.3.8__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.5/src/processforge.egg-info → processforge-0.3.8}/PKG-INFO +1 -1
- {processforge-0.3.5 → processforge-0.3.8}/flowsheets/openmc/msre_eigenvalue.json +5 -5
- {processforge-0.3.5 → processforge-0.3.8}/pyproject.toml +1 -1
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/__init__.py +1 -1
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/apply.py +2 -1
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/common.py +45 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/display.py +55 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/init.py +89 -16
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/plan.py +11 -3
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/run.py +2 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/compose.py +20 -8
- processforge-0.3.8/src/processforge/lock.py +74 -0
- {processforge-0.3.5 → processforge-0.3.8/src/processforge.egg-info}/PKG-INFO +1 -1
- {processforge-0.3.5 → processforge-0.3.8}/tests/test_cli.py +42 -0
- {processforge-0.3.5 → processforge-0.3.8}/tests/test_init.py +32 -14
- processforge-0.3.5/src/processforge/lock.py +0 -41
- {processforge-0.3.5 → processforge-0.3.8}/LICENSE +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/MANIFEST.in +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/README.md +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/flowsheets/closed-loop-chain.json +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/flowsheets/hydraulic-chain.json +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/setup.cfg +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/__main__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/_schema.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/analysis/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/analysis/dof.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/diagram.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/export_fmu.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/export_modelica.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/cli/validate.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/backends/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/backends/base.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/backends/casadi_backend.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/backends/pyomo_backend.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/backends/scipy_backend.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/flowsheet.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/jacobian.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/mixin.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/solver.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/stream_var.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/units/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/units/flash_eo.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/units/heater_eo.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/units/pipes_eo.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/units/pump_eo.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/units/strainer_eo.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/eo/units/valve_eo.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/flowsheet.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/fmu/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/fmu/_fmi_vars.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/fmu/builder.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/fmu/slave_template.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/modelica/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/modelica/mo_writer.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/modelica/omc_runner.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/modelica/transpiler.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/modelica/unit_equations.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/pcl/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/pcl/compiler.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/pcl/namespace.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/provenance.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/base.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/base_jacobian_mixin.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/cantera_jacobian.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/cantera_provider.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/container_client.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/coolprop_provider.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/festim_provider.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/jacobian_contributor.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/manager.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/modelica_jacobian.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/modelica_provider.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/openmc_provider.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/reference_state_registry.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/registry.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/result.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/result_schema.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/schemas/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/schemas/flowsheet_schema.json +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/schemas/openmc/openmc_model.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/simulate.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/solver.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/state.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/thermo.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/types.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/base.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/cstr.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/flash.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/heater.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/pfr.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/pipes.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/pump.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/registry.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/solver_unit.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/strainer.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/tank.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/units/valve.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/utils/__init__.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/utils/flowsheet_diagram.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/utils/mermaid_diagram.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/utils/topology.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/utils/unit_consistency.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/utils/validate_flowsheet.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge/utils/validation.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge.egg-info/SOURCES.txt +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge.egg-info/dependency_links.txt +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge.egg-info/entry_points.txt +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge.egg-info/requires.txt +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/src/processforge.egg-info/top_level.txt +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/tests/test_festim_provider.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/tests/test_providers.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/tests/test_state.py +0 -0
- {processforge-0.3.5 → processforge-0.3.8}/tests/test_types.py +0 -0
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"id": 1,
|
|
18
18
|
"density": 2.26,
|
|
19
19
|
"density_units": "g/cm3",
|
|
20
|
-
"temperature":
|
|
20
|
+
"temperature": 300.0,
|
|
21
21
|
"elements": [
|
|
22
22
|
{ "element": "C", "percent": 1.0, "percent_type": "ao" }
|
|
23
23
|
]
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"id": 2,
|
|
27
27
|
"density": 9.0,
|
|
28
28
|
"density_units": "g/cm3",
|
|
29
|
-
"temperature":
|
|
29
|
+
"temperature": 400.0,
|
|
30
30
|
"elements": [
|
|
31
31
|
{ "element": "Ni", "percent": 0.72, "percent_type": "wo" },
|
|
32
32
|
{ "element": "Mo", "percent": 0.16, "percent_type": "wo" },
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"id": 3,
|
|
39
39
|
"density": 2.2,
|
|
40
40
|
"density_units": "g/cm3",
|
|
41
|
-
"temperature":
|
|
41
|
+
"temperature": 300.0,
|
|
42
42
|
"depletable": true,
|
|
43
43
|
"nuclides": [
|
|
44
44
|
{ "name": "Li7", "percent": 0.335000, "percent_type": "ao" },
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"id": 4,
|
|
56
56
|
"density": 8.9,
|
|
57
57
|
"density_units": "g/cm3",
|
|
58
|
-
"temperature":
|
|
58
|
+
"temperature": 300.0,
|
|
59
59
|
"elements": [
|
|
60
60
|
{ "element": "Ni", "percent": 0.72, "percent_type": "ao" },
|
|
61
61
|
{ "element": "Cr", "percent": 0.20, "percent_type": "ao" },
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"id": 5,
|
|
67
67
|
"density": 1.0e-4,
|
|
68
68
|
"density_units": "g/cm3",
|
|
69
|
-
"temperature":
|
|
69
|
+
"temperature": 300.0,
|
|
70
70
|
"nuclides": [
|
|
71
71
|
{ "name": "He4", "percent": 1.0, "percent_type": "ao" }
|
|
72
72
|
]
|
|
@@ -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.8"
|
|
8
8
|
description = "A Python-based process simulation framework for chemical engineering applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -31,7 +31,7 @@ def register_commands(app: typer.Typer) -> None:
|
|
|
31
31
|
)(apply)
|
|
32
32
|
app.command(
|
|
33
33
|
"plan",
|
|
34
|
-
help="Validate a flowsheet, run DOF analysis, structural diff, and generate a Mermaid diagram",
|
|
34
|
+
help="Validate a flowsheet, run DOF analysis, structural diff, container health check, and generate a Mermaid diagram",
|
|
35
35
|
)(plan)
|
|
36
36
|
app.command("diagram", help="Generate a flowsheet diagram")(diagram)
|
|
37
37
|
app.command(
|
|
@@ -17,6 +17,7 @@ from .common import (
|
|
|
17
17
|
build_divergence_report,
|
|
18
18
|
build_run_metadata,
|
|
19
19
|
check_providers,
|
|
20
|
+
display_backend,
|
|
20
21
|
flowsheet_basename,
|
|
21
22
|
load_state_manager,
|
|
22
23
|
log_residual_breakdown,
|
|
@@ -132,7 +133,7 @@ def apply(
|
|
|
132
133
|
logger.info(" Status : CONVERGED")
|
|
133
134
|
logger.info(f" Final ||F|| : {fs.solver_stats.get('final_norm', '?'):.3e}")
|
|
134
135
|
logger.info(f" Iterations : {fs.solver_stats.get('iterations', '?')}")
|
|
135
|
-
logger.info(f" Backend : {fs.backend}")
|
|
136
|
+
logger.info(f" Backend : {display_backend(config, fs.backend)}")
|
|
136
137
|
logger.info(f" Snapshot ID : {snapshot_id}")
|
|
137
138
|
logger.info(f" Results zarr : {zarr_path}")
|
|
138
139
|
logger.info(f" Elapsed (s) : {elapsed:.2f}")
|
|
@@ -98,6 +98,23 @@ def build_run_metadata(config: dict, solver_tol: float, solver_max_iter: int, ba
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def display_backend(config: dict, eo_backend: str) -> str:
|
|
102
|
+
"""Resolve the backend label shown in run/apply summaries.
|
|
103
|
+
|
|
104
|
+
When a flowsheet delegates its computation to one or more providers
|
|
105
|
+
(e.g. ``openmc``, ``cantera``), the provider type is the engine that
|
|
106
|
+
actually ran the simulation — report that rather than the EO solver
|
|
107
|
+
backend (which is a no-op for provider-driven flowsheets). Falls back
|
|
108
|
+
to the EO solver backend when no providers are declared.
|
|
109
|
+
"""
|
|
110
|
+
provider_types = sorted(
|
|
111
|
+
{p.get("type") for p in config.get("providers", {}).values() if p.get("type")}
|
|
112
|
+
)
|
|
113
|
+
if provider_types:
|
|
114
|
+
return "+".join(provider_types)
|
|
115
|
+
return eo_backend
|
|
116
|
+
|
|
117
|
+
|
|
101
118
|
def extract_providers(flowsheet_path: str) -> dict:
|
|
102
119
|
"""Read flowsheet JSON and return the raw providers dict."""
|
|
103
120
|
try:
|
|
@@ -128,6 +145,21 @@ def load_state_manager(outputs_dir: str, base_name: str) -> tuple[StateManager,
|
|
|
128
145
|
# Provider checking
|
|
129
146
|
# ---------------------------------------------------------------------------
|
|
130
147
|
|
|
148
|
+
def is_local_provider_url(url: str | None) -> bool:
|
|
149
|
+
"""Return True if *url* points at a locally-managed provider.
|
|
150
|
+
|
|
151
|
+
A provider is considered local — and thus a candidate for ``pf init`` to
|
|
152
|
+
generate docker-compose and pull its image — when it has no explicit URL
|
|
153
|
+
or its URL points at localhost / 127.0.0.1 / 0.0.0.0. Any other URL is
|
|
154
|
+
treated as an externally-managed (cloud / remote) provider that
|
|
155
|
+
``pf init`` must not try to spin up or pull an image for.
|
|
156
|
+
"""
|
|
157
|
+
if not url:
|
|
158
|
+
return True
|
|
159
|
+
lowered = url.lower()
|
|
160
|
+
return any(token in lowered for token in ("localhost", "127.0.0.1", "0.0.0.0"))
|
|
161
|
+
|
|
162
|
+
|
|
131
163
|
def _resolve_provider_url(cfg: dict, ptype: str) -> str:
|
|
132
164
|
"""Build the provider URL, falling back to the default port."""
|
|
133
165
|
from ..providers.registry import get_provider_default_port
|
|
@@ -139,6 +171,19 @@ def _resolve_provider_url(cfg: dict, ptype: str) -> str:
|
|
|
139
171
|
return url
|
|
140
172
|
|
|
141
173
|
|
|
174
|
+
def _ping_provider_health(url: str, timeout: int = 5) -> tuple[bool, "dict | str"]:
|
|
175
|
+
"""GET ``{url}/health`` and return ``(ok, payload_or_error)``.
|
|
176
|
+
|
|
177
|
+
Returns the parsed JSON health payload on success, or the error string on
|
|
178
|
+
failure (unreachable service, timeout, or non-JSON response).
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
with urllib.request.urlopen(f"{url.rstrip('/')}/health", timeout=timeout) as resp:
|
|
182
|
+
return True, json.loads(resp.read().decode())
|
|
183
|
+
except (urllib.error.URLError, OSError, TimeoutError, ValueError) as exc:
|
|
184
|
+
return False, str(exc)
|
|
185
|
+
|
|
186
|
+
|
|
142
187
|
def check_providers(
|
|
143
188
|
config: dict,
|
|
144
189
|
flowsheet_path: str,
|
|
@@ -53,6 +53,61 @@ def print_unit_mismatches(mismatches: list[Any]) -> None:
|
|
|
53
53
|
logger.warning(f"Unit annotation — stream '{m.stream_name}'.{m.property_name}: {m.message}")
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def print_provider_health(config: dict) -> list[str]:
|
|
57
|
+
"""Log per-provider reachability for flowsheets using Docker containers.
|
|
58
|
+
|
|
59
|
+
For each provider declared in *config*:
|
|
60
|
+
|
|
61
|
+
* containerized providers are probed via ``GET /health`` and reported with
|
|
62
|
+
their URL, ``status``, and ``provider_type`` from the health payload;
|
|
63
|
+
* pip-installable providers are reported as importable or not.
|
|
64
|
+
|
|
65
|
+
Returns a list of failure messages — one per unreachable containerized
|
|
66
|
+
provider (empty means every provider is healthy/available).
|
|
67
|
+
"""
|
|
68
|
+
import importlib
|
|
69
|
+
|
|
70
|
+
from ..providers.registry import is_containerized, _PROVIDER_CATALOG
|
|
71
|
+
|
|
72
|
+
providers = config.get("providers", {})
|
|
73
|
+
logger.info("=== Provider / Container Health ===")
|
|
74
|
+
failures: list[str] = []
|
|
75
|
+
if not providers:
|
|
76
|
+
logger.info(" No providers declared.")
|
|
77
|
+
return failures
|
|
78
|
+
|
|
79
|
+
for name, cfg in providers.items():
|
|
80
|
+
ptype = cfg.get("type", "")
|
|
81
|
+
if is_containerized(ptype):
|
|
82
|
+
from .common import _resolve_provider_url, _ping_provider_health
|
|
83
|
+
|
|
84
|
+
url = _resolve_provider_url(cfg, ptype)
|
|
85
|
+
ok, info = _ping_provider_health(url, timeout=5)
|
|
86
|
+
if ok:
|
|
87
|
+
payload = info if isinstance(info, dict) else {}
|
|
88
|
+
status = payload.get("status", "?")
|
|
89
|
+
provider_type = payload.get("provider_type", "?")
|
|
90
|
+
logger.info(
|
|
91
|
+
f" [OK] {name} [{ptype}] {url} — status={status} provider_type={provider_type}"
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
msg = f" [ERR] {name} [{ptype}] {url} — unreachable: {info}"
|
|
95
|
+
logger.error(msg)
|
|
96
|
+
failures.append(f"Provider '{name}' unreachable at {url}: {info}")
|
|
97
|
+
else:
|
|
98
|
+
catalog = _PROVIDER_CATALOG.get(ptype, {})
|
|
99
|
+
module = catalog.get("module", "")
|
|
100
|
+
try:
|
|
101
|
+
importlib.util.find_spec(module)
|
|
102
|
+
logger.info(f" [OK] {name} [{ptype}] (pip — importable)")
|
|
103
|
+
except (ModuleNotFoundError, ValueError):
|
|
104
|
+
dep = catalog.get("optional_dep")
|
|
105
|
+
hint = f"pip install 'processforge[{dep}]'" if dep else "built-in"
|
|
106
|
+
logger.warning(f" [WARN] {name} [{ptype}] — not installed ({hint})")
|
|
107
|
+
|
|
108
|
+
return failures
|
|
109
|
+
|
|
110
|
+
|
|
56
111
|
def print_structural_diff(diff: dict) -> None:
|
|
57
112
|
"""Print a +/~/- structural diff of units."""
|
|
58
113
|
logger.info("=== Structural Diff vs. Saved State ===")
|
|
@@ -11,7 +11,56 @@ import subprocess
|
|
|
11
11
|
import typer
|
|
12
12
|
from loguru import logger
|
|
13
13
|
|
|
14
|
-
from .common import extract_providers
|
|
14
|
+
from .common import extract_providers, is_local_provider_url
|
|
15
|
+
from ..lock import flowsheet_env_dir, read_lock
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _migrate_legacy_env(pf_dir: str) -> None:
|
|
19
|
+
"""Move a legacy root-level lock.json / docker-compose.yml into a per-flowsheet dir.
|
|
20
|
+
|
|
21
|
+
Older processforge stored a single environment at ``.processforge/lock.json``
|
|
22
|
+
and ``.processforge/docker-compose.yml``. This relocates those into the hashed
|
|
23
|
+
env dir derived from the flowsheet recorded in the legacy lock, so an existing
|
|
24
|
+
repo keeps its provider environment after upgrading.
|
|
25
|
+
"""
|
|
26
|
+
legacy_lock = os.path.join(pf_dir, "lock.json")
|
|
27
|
+
legacy_compose = os.path.join(pf_dir, "docker-compose.yml")
|
|
28
|
+
if not (os.path.exists(legacy_lock) or os.path.exists(legacy_compose)):
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
recorded = None
|
|
32
|
+
if os.path.exists(legacy_lock):
|
|
33
|
+
try:
|
|
34
|
+
recorded = read_lock(pf_dir).get("flowsheet")
|
|
35
|
+
except Exception:
|
|
36
|
+
recorded = None
|
|
37
|
+
|
|
38
|
+
if recorded and os.path.exists(recorded):
|
|
39
|
+
env_dir = flowsheet_env_dir(pf_dir, recorded)
|
|
40
|
+
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
|
+
logger.warning(
|
|
44
|
+
"Migrating legacy .processforge/lock.json + docker-compose.yml into "
|
|
45
|
+
f"'{os.path.relpath(env_dir, pf_dir)}/' (recorded flowsheet not found on "
|
|
46
|
+
"disk — re-run `pf init <flowsheet.json>` to restore the correct env dir)."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if os.path.exists(env_dir):
|
|
50
|
+
logger.warning(
|
|
51
|
+
f"Migration target {os.path.relpath(env_dir, pf_dir)}/ already exists — "
|
|
52
|
+
"skipping legacy migration."
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
os.makedirs(env_dir, exist_ok=True)
|
|
57
|
+
for src in (legacy_lock, legacy_compose):
|
|
58
|
+
if os.path.exists(src):
|
|
59
|
+
shutil.move(src, os.path.join(env_dir, os.path.basename(src)))
|
|
60
|
+
logger.info(
|
|
61
|
+
"Migrated legacy .processforge/ environment into per-flowsheet dir "
|
|
62
|
+
f"'{os.path.relpath(env_dir, pf_dir)}/'."
|
|
63
|
+
)
|
|
15
64
|
|
|
16
65
|
|
|
17
66
|
def init(
|
|
@@ -42,6 +91,11 @@ def init(
|
|
|
42
91
|
os.makedirs(pf_dir, exist_ok=True)
|
|
43
92
|
os.makedirs(outputs_dir, exist_ok=True)
|
|
44
93
|
|
|
94
|
+
# Migrate a legacy single-environment layout (.processforge/lock.json and
|
|
95
|
+
# .processforge/docker-compose.yml at the root) into a per-flowsheet env
|
|
96
|
+
# dir so existing repos aren't silently broken by the new structure.
|
|
97
|
+
_migrate_legacy_env(pf_dir)
|
|
98
|
+
|
|
45
99
|
# Remove stale .pfstate snapshot directories from outputs/
|
|
46
100
|
stale_count = 0
|
|
47
101
|
for entry in os.listdir(outputs_dir):
|
|
@@ -84,21 +138,32 @@ def init(
|
|
|
84
138
|
|
|
85
139
|
# Categorize providers
|
|
86
140
|
pip_providers: dict[str, dict] = {}
|
|
87
|
-
|
|
141
|
+
local_docker_providers: dict[str, dict] = {}
|
|
142
|
+
remote_docker_providers: dict[str, dict] = {}
|
|
88
143
|
for name, cfg in providers.items():
|
|
89
144
|
ptype = cfg.get("type", "")
|
|
90
145
|
if is_containerized(ptype):
|
|
91
146
|
url = cfg.get("url")
|
|
92
|
-
if
|
|
147
|
+
if is_local_provider_url(url):
|
|
93
148
|
port = get_provider_default_port(ptype) or 9000
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
149
|
+
if not url:
|
|
150
|
+
url = f"http://localhost:{port}"
|
|
151
|
+
local_docker_providers[name] = {
|
|
152
|
+
"type": ptype,
|
|
153
|
+
"url": url,
|
|
154
|
+
"docker_image": cfg.get("docker_image") or get_provider_docker_image(ptype),
|
|
155
|
+
"port": port,
|
|
156
|
+
}
|
|
157
|
+
logger.info(f" {name}: type={ptype}, url={url} (Docker, local)")
|
|
158
|
+
else:
|
|
159
|
+
remote_docker_providers[name] = {
|
|
160
|
+
"type": ptype,
|
|
161
|
+
"url": url,
|
|
162
|
+
"docker_image": cfg.get("docker_image") or get_provider_docker_image(ptype),
|
|
163
|
+
}
|
|
164
|
+
logger.info(
|
|
165
|
+
f" {name}: type={ptype}, url={url} (Docker, remote — skipping compose)"
|
|
166
|
+
)
|
|
102
167
|
else:
|
|
103
168
|
pip_providers[name] = {"type": ptype}
|
|
104
169
|
logger.info(f" {name}: type={ptype} (pip)")
|
|
@@ -116,15 +181,19 @@ def init(
|
|
|
116
181
|
hint = f"pip install 'processforge[{dep}]'" if dep else "built-in"
|
|
117
182
|
logger.warning(f" {name} — not installed. Install with: {hint}")
|
|
118
183
|
|
|
119
|
-
# Generate compose for Docker providers
|
|
120
|
-
|
|
121
|
-
|
|
184
|
+
# Generate compose + pull images only for locally-managed Docker providers.
|
|
185
|
+
# Providers with an explicit remote URL are assumed to be running elsewhere
|
|
186
|
+
# (e.g. a cloud deployment of the ghcr.io image) and are not touched here.
|
|
187
|
+
if local_docker_providers:
|
|
188
|
+
compose_path = os.path.join(
|
|
189
|
+
flowsheet_env_dir(pf_dir, flowsheet_path), "docker-compose.yml"
|
|
190
|
+
)
|
|
122
191
|
if os.path.exists(compose_path):
|
|
123
192
|
logger.warning(
|
|
124
193
|
f"Environment already initialized — reinitializing from {flowsheet_path}"
|
|
125
194
|
)
|
|
126
195
|
|
|
127
|
-
generate_compose(pf_dir,
|
|
196
|
+
generate_compose(pf_dir, local_docker_providers, outputs_dir, flowsheet=flowsheet_path)
|
|
128
197
|
logger.info(f"Generated {compose_path}")
|
|
129
198
|
|
|
130
199
|
# Attempt docker compose pull — stream progress live
|
|
@@ -155,6 +224,10 @@ def init(
|
|
|
155
224
|
)
|
|
156
225
|
except subprocess.TimeoutExpired:
|
|
157
226
|
logger.warning("docker compose pull timed out after 600s.")
|
|
227
|
+
elif remote_docker_providers:
|
|
228
|
+
logger.info(
|
|
229
|
+
"All containerized providers use remote URLs — skipping Docker setup."
|
|
230
|
+
)
|
|
158
231
|
else:
|
|
159
232
|
logger.info("No containerized providers — skipping Docker setup.")
|
|
160
233
|
|
|
@@ -177,5 +250,5 @@ def init(
|
|
|
177
250
|
from .. import __version__ as pf_version
|
|
178
251
|
|
|
179
252
|
write_lock(pf_dir, flowsheet_path, lock_providers, pf_version)
|
|
180
|
-
logger.info(f"Wrote {os.path.join(pf_dir, 'lock.json')}")
|
|
253
|
+
logger.info(f"Wrote {os.path.join(flowsheet_env_dir(pf_dir, flowsheet_path), 'lock.json')}")
|
|
181
254
|
logger.info(".processforge/ initialised successfully.")
|
|
@@ -18,7 +18,12 @@ from .common import (
|
|
|
18
18
|
require_existing_file,
|
|
19
19
|
validate_snapshot_config,
|
|
20
20
|
)
|
|
21
|
-
from .display import
|
|
21
|
+
from .display import (
|
|
22
|
+
print_dof_report,
|
|
23
|
+
print_provider_health,
|
|
24
|
+
print_structural_diff,
|
|
25
|
+
print_unit_mismatches,
|
|
26
|
+
)
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
def plan(
|
|
@@ -116,7 +121,10 @@ def plan(
|
|
|
116
121
|
logger.info(" Warm-start available : No snapshot found")
|
|
117
122
|
logger.info(" Homotopy eligible : No (cold start)")
|
|
118
123
|
|
|
119
|
-
# Step 7:
|
|
124
|
+
# Step 7: Provider / container health check
|
|
125
|
+
health_failures = print_provider_health(config)
|
|
126
|
+
|
|
127
|
+
# Step 8: Mermaid diagram
|
|
120
128
|
if not no_diagram:
|
|
121
129
|
diagram_output_dir = output_dir or "diagrams"
|
|
122
130
|
os.makedirs(diagram_output_dir, exist_ok=True)
|
|
@@ -129,5 +137,5 @@ def plan(
|
|
|
129
137
|
|
|
130
138
|
# Exit non-zero on hard errors
|
|
131
139
|
hard_errors = [m for m in mismatches if not m.compatible]
|
|
132
|
-
if hard_errors or report.system_dof < 0:
|
|
140
|
+
if hard_errors or report.system_dof < 0 or health_failures:
|
|
133
141
|
raise SystemExit(1)
|
|
@@ -14,6 +14,7 @@ from ..result import plot_results, plot_timeseries, save_results_zarr
|
|
|
14
14
|
from ..state import StateManager
|
|
15
15
|
from .common import (
|
|
16
16
|
check_providers,
|
|
17
|
+
display_backend,
|
|
17
18
|
flowsheet_basename,
|
|
18
19
|
output_root,
|
|
19
20
|
require_existing_file,
|
|
@@ -96,6 +97,7 @@ def run(
|
|
|
96
97
|
os.makedirs(outputs_dir, exist_ok=True)
|
|
97
98
|
zarr_path = os.path.join(outputs_dir, f"{base_name}_results.zarr")
|
|
98
99
|
save_results_zarr(results, zarr_path, run_info=run_info)
|
|
100
|
+
logger.info(f" Backend : {display_backend(config, getattr(fs, 'backend', 'dynamic'))}")
|
|
99
101
|
logger.info(f"Results saved to {zarr_path}")
|
|
100
102
|
|
|
101
103
|
if export_images:
|
|
@@ -5,6 +5,8 @@ import os
|
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
7
|
|
|
8
|
+
from .lock import flowsheet_env_dir
|
|
9
|
+
|
|
8
10
|
COMPOSE_FILENAME = "docker-compose.yml"
|
|
9
11
|
COMPOSE_HEADER = (
|
|
10
12
|
"# Auto-generated by pf init — do not edit manually.\n"
|
|
@@ -12,7 +14,9 @@ COMPOSE_HEADER = (
|
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
def _compose_path(pf_dir: str) -> str:
|
|
17
|
+
def _compose_path(pf_dir: str, flowsheet: str | None = None) -> str:
|
|
18
|
+
if flowsheet is not None:
|
|
19
|
+
return os.path.join(flowsheet_env_dir(pf_dir, flowsheet), COMPOSE_FILENAME)
|
|
16
20
|
return os.path.join(pf_dir, COMPOSE_FILENAME)
|
|
17
21
|
|
|
18
22
|
|
|
@@ -20,6 +24,7 @@ def generate_compose(
|
|
|
20
24
|
pf_dir: str,
|
|
21
25
|
containerized_providers: dict[str, dict],
|
|
22
26
|
outputs_dir: str = "outputs",
|
|
27
|
+
flowsheet: str | None = None,
|
|
23
28
|
) -> None:
|
|
24
29
|
"""Generate docker-compose.yml for providers that need Docker.
|
|
25
30
|
|
|
@@ -27,6 +32,9 @@ def generate_compose(
|
|
|
27
32
|
pf_dir: Path to .processforge/ directory.
|
|
28
33
|
containerized_providers: Dict of provider_name -> {"url": ..., "docker_image": ..., "port": ...}.
|
|
29
34
|
outputs_dir: Host directory to mount as /data in containers.
|
|
35
|
+
flowsheet: Flowsheet path. When given, the compose file is written into
|
|
36
|
+
that flowsheet's per-flowsheet environment directory; otherwise it
|
|
37
|
+
is written to the legacy root location.
|
|
30
38
|
"""
|
|
31
39
|
if not containerized_providers:
|
|
32
40
|
return
|
|
@@ -37,16 +45,20 @@ def generate_compose(
|
|
|
37
45
|
port = info.get("port") or 9000
|
|
38
46
|
lines.append(f" {name}:")
|
|
39
47
|
lines.append(f" image: {image}")
|
|
40
|
-
#
|
|
41
|
-
# http://localhost:<port
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
lines.append(f"
|
|
48
|
+
# Publish the provider's API-contract port (host:container) so the
|
|
49
|
+
# service is reachable at http://localhost:<port>, matching the
|
|
50
|
+
# PORT/EXPOSE declared in the provider Dockerfile.
|
|
51
|
+
lines.append(f" ports:")
|
|
52
|
+
lines.append(f' - "{port}:{port}"')
|
|
45
53
|
lines.append(f" volumes:")
|
|
46
54
|
lines.append(f' - "${{OPENMC_DATA_ROOT:-{outputs_dir}}}:/data"')
|
|
47
55
|
lines.append("")
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
if flowsheet is not None:
|
|
58
|
+
target_dir = flowsheet_env_dir(pf_dir, flowsheet)
|
|
59
|
+
else:
|
|
60
|
+
target_dir = pf_dir
|
|
61
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
62
|
+
path = os.path.join(target_dir, COMPOSE_FILENAME)
|
|
51
63
|
with open(path, "w", encoding="utf-8") as f:
|
|
52
64
|
f.write("\n".join(lines))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Lock file read/write for .processforge/<env-key>/lock.json.
|
|
2
|
+
|
|
3
|
+
Each flowsheet gets its own environment directory under ``.processforge/``,
|
|
4
|
+
keyed by a stable hash of the flowsheet's absolute path. This lets multiple
|
|
5
|
+
flowsheets keep separate provider environments (lock + compose) without
|
|
6
|
+
clobbering each other. The lock file is provenance metadata only.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
LOCK_VERSION = 1
|
|
16
|
+
LOCK_FILENAME = "lock.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def flowsheet_env_key(flowsheet_path: str) -> str:
|
|
20
|
+
"""Stable key for a flowsheet's environment dir.
|
|
21
|
+
|
|
22
|
+
Derived from the sha1 of the flowsheet's real (absolute) path so it is
|
|
23
|
+
collision-proof across folders and stable across re-inits.
|
|
24
|
+
"""
|
|
25
|
+
abs_path = os.path.realpath(flowsheet_path)
|
|
26
|
+
return hashlib.sha1(abs_path.encode("utf-8")).hexdigest()[:12]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def flowsheet_env_dir(pf_dir: str, flowsheet_path: str) -> str:
|
|
30
|
+
"""Directory holding a flowsheet's lock.json and docker-compose.yml."""
|
|
31
|
+
return os.path.join(pf_dir, flowsheet_env_key(flowsheet_path))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _lock_path(pf_dir: str, flowsheet: str | None = None) -> str:
|
|
35
|
+
if flowsheet is not None:
|
|
36
|
+
return os.path.join(flowsheet_env_dir(pf_dir, flowsheet), LOCK_FILENAME)
|
|
37
|
+
return os.path.join(pf_dir, LOCK_FILENAME)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_lock(pf_dir: str, flowsheet: str | None = None) -> dict[str, Any] | None:
|
|
41
|
+
"""Read lock.json.
|
|
42
|
+
|
|
43
|
+
When *flowsheet* is given, reads that flowsheet's env dir. Otherwise falls
|
|
44
|
+
back to the legacy root ``.processforge/lock.json`` (used during migration).
|
|
45
|
+
Returns None if not found.
|
|
46
|
+
"""
|
|
47
|
+
path = _lock_path(pf_dir, flowsheet)
|
|
48
|
+
if not os.path.exists(path):
|
|
49
|
+
if flowsheet is None and os.path.exists(os.path.join(pf_dir, LOCK_FILENAME)):
|
|
50
|
+
path = os.path.join(pf_dir, LOCK_FILENAME)
|
|
51
|
+
else:
|
|
52
|
+
return None
|
|
53
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
54
|
+
return json.load(f)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def write_lock(
|
|
58
|
+
pf_dir: str,
|
|
59
|
+
flowsheet: str,
|
|
60
|
+
providers: dict[str, dict],
|
|
61
|
+
processforge_version: str,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Write lock.json into the flowsheet's environment directory."""
|
|
64
|
+
lock = {
|
|
65
|
+
"version": LOCK_VERSION,
|
|
66
|
+
"processforge_version": processforge_version,
|
|
67
|
+
"flowsheet": flowsheet,
|
|
68
|
+
"providers": providers,
|
|
69
|
+
}
|
|
70
|
+
env_dir = flowsheet_env_dir(pf_dir, flowsheet)
|
|
71
|
+
os.makedirs(env_dir, exist_ok=True)
|
|
72
|
+
path = os.path.join(env_dir, LOCK_FILENAME)
|
|
73
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
74
|
+
json.dump(lock, f, indent=2)
|
|
@@ -442,6 +442,48 @@ class TestCmdRun:
|
|
|
442
442
|
run(flowsheet=str(flowsheet), export_images=False)
|
|
443
443
|
|
|
444
444
|
|
|
445
|
+
class TestCmdPlan:
|
|
446
|
+
@pytest.fixture
|
|
447
|
+
def container_flowsheet(self, tmp_path):
|
|
448
|
+
"""Write a minimal valid flowsheet using a Docker-containerized provider."""
|
|
449
|
+
flowsheet = {
|
|
450
|
+
"providers": {"openmc": {"type": "openmc"}},
|
|
451
|
+
"streams": {},
|
|
452
|
+
"units": {},
|
|
453
|
+
"simulation": {"mode": "steady"},
|
|
454
|
+
}
|
|
455
|
+
path = tmp_path / "container.json"
|
|
456
|
+
with open(path, "w") as f:
|
|
457
|
+
json.dump(flowsheet, f)
|
|
458
|
+
return path
|
|
459
|
+
|
|
460
|
+
def test_container_provider_health_ok(self, container_flowsheet, log_output):
|
|
461
|
+
import processforge.cli.common as common_mod
|
|
462
|
+
from processforge.cli.plan import plan
|
|
463
|
+
|
|
464
|
+
health = {"status": "ready", "provider_type": "openmc"}
|
|
465
|
+
with patch.object(
|
|
466
|
+
common_mod, "_ping_provider_health", return_value=(True, health)
|
|
467
|
+
), patch.object(common_mod, "_resolve_provider_url", return_value="http://localhost:9001"):
|
|
468
|
+
plan(flowsheet=str(container_flowsheet), no_diagram=True)
|
|
469
|
+
|
|
470
|
+
assert any("Provider / Container Health" in m for m in log_output)
|
|
471
|
+
assert any("[OK] openmc" in m and "provider_type=openmc" in m for m in log_output)
|
|
472
|
+
|
|
473
|
+
def test_container_provider_health_failure_exits(self, container_flowsheet, log_output):
|
|
474
|
+
import processforge.cli.common as common_mod
|
|
475
|
+
from processforge.cli.plan import plan
|
|
476
|
+
|
|
477
|
+
with patch.object(
|
|
478
|
+
common_mod, "_ping_provider_health", return_value=(False, "Connection refused")
|
|
479
|
+
), patch.object(common_mod, "_resolve_provider_url", return_value="http://localhost:9001"), \
|
|
480
|
+
pytest.raises(SystemExit) as exc_info:
|
|
481
|
+
plan(flowsheet=str(container_flowsheet), no_diagram=True)
|
|
482
|
+
|
|
483
|
+
assert exc_info.value.code == 1
|
|
484
|
+
assert any("[ERR] openmc" in m and "unreachable" in m for m in log_output)
|
|
485
|
+
|
|
486
|
+
|
|
445
487
|
class TestCmdApply:
|
|
446
488
|
def _containerized_config(self, flowsheet_path):
|
|
447
489
|
return {
|
|
@@ -8,7 +8,7 @@ from unittest.mock import patch
|
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
|
-
from processforge.lock import read_lock, write_lock, LOCK_VERSION
|
|
11
|
+
from processforge.lock import read_lock, write_lock, LOCK_VERSION, flowsheet_env_dir
|
|
12
12
|
from processforge.compose import generate_compose, COMPOSE_FILENAME
|
|
13
13
|
from processforge.providers.registry import (
|
|
14
14
|
_PROVIDER_CATALOG,
|
|
@@ -153,7 +153,7 @@ class TestLockFile:
|
|
|
153
153
|
"coolprop": {"docker_image": None, "url": None},
|
|
154
154
|
}
|
|
155
155
|
write_lock(str(tmp_pf_dir), "test.json", providers, "0.2.34")
|
|
156
|
-
lock = read_lock(str(tmp_pf_dir))
|
|
156
|
+
lock = read_lock(str(tmp_pf_dir), "test.json")
|
|
157
157
|
|
|
158
158
|
assert lock is not None
|
|
159
159
|
assert lock["version"] == LOCK_VERSION
|
|
@@ -163,11 +163,27 @@ class TestLockFile:
|
|
|
163
163
|
assert "coolprop" in lock["providers"]
|
|
164
164
|
assert lock["providers"]["openmc"]["url"] == "http://localhost:9001"
|
|
165
165
|
|
|
166
|
+
def test_lock_written_to_per_flowsheet_dir(self, tmp_pf_dir):
|
|
167
|
+
write_lock(str(tmp_pf_dir), "a/test.json", {}, "0.2.34")
|
|
168
|
+
# No legacy root lock.json is written.
|
|
169
|
+
assert not (tmp_pf_dir / "lock.json").exists()
|
|
170
|
+
# Instead it lives in the hashed env dir.
|
|
171
|
+
assert (Path(flowsheet_env_dir(str(tmp_pf_dir), "a/test.json")) / "lock.json").exists()
|
|
172
|
+
|
|
173
|
+
def test_two_flowsheets_separate_dirs(self, tmp_pf_dir):
|
|
174
|
+
write_lock(str(tmp_pf_dir), "flows/a.json", {}, "0.2.34")
|
|
175
|
+
write_lock(str(tmp_pf_dir), "flows/b.json", {}, "0.2.34")
|
|
176
|
+
dir_a = Path(flowsheet_env_dir(str(tmp_pf_dir), "flows/a.json"))
|
|
177
|
+
dir_b = Path(flowsheet_env_dir(str(tmp_pf_dir), "flows/b.json"))
|
|
178
|
+
assert dir_a != dir_b
|
|
179
|
+
assert (dir_a / "lock.json").exists()
|
|
180
|
+
assert (dir_b / "lock.json").exists()
|
|
181
|
+
|
|
166
182
|
def test_lock_creates_directory(self, tmp_path):
|
|
167
183
|
pf_dir = tmp_path / ".processforge"
|
|
168
184
|
providers = {"coolprop": {"docker_image": None, "url": None}}
|
|
169
185
|
write_lock(str(pf_dir), "test.json", providers, "0.2.34")
|
|
170
|
-
assert read_lock(str(pf_dir)) is not None
|
|
186
|
+
assert read_lock(str(pf_dir), "test.json") is not None
|
|
171
187
|
|
|
172
188
|
|
|
173
189
|
# ---------------------------------------------------------------------------
|
|
@@ -183,28 +199,30 @@ class TestComposeGeneration:
|
|
|
183
199
|
"port": 9001,
|
|
184
200
|
},
|
|
185
201
|
}
|
|
186
|
-
generate_compose(str(tmp_pf_dir), docker_providers)
|
|
187
|
-
compose_path = tmp_pf_dir
|
|
188
|
-
assert
|
|
202
|
+
generate_compose(str(tmp_pf_dir), docker_providers, flowsheet="a.json")
|
|
203
|
+
compose_path = os.path.join(flowsheet_env_dir(str(tmp_pf_dir), "a.json"), COMPOSE_FILENAME)
|
|
204
|
+
assert os.path.exists(compose_path)
|
|
189
205
|
|
|
190
|
-
content = compose_path.read_text()
|
|
206
|
+
content = Path(compose_path).read_text()
|
|
191
207
|
assert "services:" in content
|
|
192
208
|
assert "openmc:" in content
|
|
193
209
|
assert "ghcr.io/urjanova/processforge-openmc:latest" in content
|
|
194
210
|
assert "9001:9001" in content
|
|
211
|
+
assert "ports:" in content
|
|
212
|
+
assert "network_mode" not in content
|
|
195
213
|
|
|
196
214
|
def test_generate_compose_empty_when_no_docker(self, tmp_pf_dir):
|
|
197
|
-
generate_compose(str(tmp_pf_dir), {})
|
|
198
|
-
compose_path = tmp_pf_dir
|
|
199
|
-
assert not
|
|
215
|
+
generate_compose(str(tmp_pf_dir), {}, flowsheet="a.json")
|
|
216
|
+
compose_path = os.path.join(flowsheet_env_dir(str(tmp_pf_dir), "a.json"), COMPOSE_FILENAME)
|
|
217
|
+
assert not os.path.exists(compose_path)
|
|
200
218
|
|
|
201
219
|
def test_generate_compose_multiple_providers(self, tmp_pf_dir):
|
|
202
220
|
docker_providers = {
|
|
203
221
|
"openmc": {"docker_image": "ghcr.io/test/openmc:latest", "port": 9001},
|
|
204
222
|
"festim": {"docker_image": "ghcr.io/test/festim:latest", "port": 9002},
|
|
205
223
|
}
|
|
206
|
-
generate_compose(str(tmp_pf_dir), docker_providers)
|
|
207
|
-
content = (tmp_pf_dir
|
|
224
|
+
generate_compose(str(tmp_pf_dir), docker_providers, flowsheet="a.json")
|
|
225
|
+
content = Path(os.path.join(flowsheet_env_dir(str(tmp_pf_dir), "a.json"), COMPOSE_FILENAME)).read_text()
|
|
208
226
|
assert "openmc:" in content
|
|
209
227
|
assert "festim:" in content
|
|
210
228
|
|
|
@@ -215,8 +233,8 @@ class TestComposeGeneration:
|
|
|
215
233
|
"port": 9001,
|
|
216
234
|
},
|
|
217
235
|
}
|
|
218
|
-
generate_compose(str(tmp_pf_dir), docker_providers)
|
|
219
|
-
content = (tmp_pf_dir
|
|
236
|
+
generate_compose(str(tmp_pf_dir), docker_providers, flowsheet="a.json")
|
|
237
|
+
content = Path(os.path.join(flowsheet_env_dir(str(tmp_pf_dir), "a.json"), COMPOSE_FILENAME)).read_text()
|
|
220
238
|
assert "my-org/custom-openmc:v2" in content
|
|
221
239
|
assert "ghcr.io/urjanova/processforge-openmc" not in content
|
|
222
240
|
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""Lock file read/write for .processforge/lock.json."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
LOCK_VERSION = 1
|
|
9
|
-
LOCK_FILENAME = "lock.json"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _lock_path(pf_dir: str) -> str:
|
|
13
|
-
return os.path.join(pf_dir, LOCK_FILENAME)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def read_lock(pf_dir: str) -> dict[str, Any] | None:
|
|
17
|
-
"""Read lock.json from .processforge/. Returns None if not found."""
|
|
18
|
-
path = _lock_path(pf_dir)
|
|
19
|
-
if not os.path.exists(path):
|
|
20
|
-
return None
|
|
21
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
22
|
-
return json.load(f)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def write_lock(
|
|
26
|
-
pf_dir: str,
|
|
27
|
-
flowsheet: str,
|
|
28
|
-
providers: dict[str, dict],
|
|
29
|
-
processforge_version: str,
|
|
30
|
-
) -> None:
|
|
31
|
-
"""Write lock.json to .processforge/."""
|
|
32
|
-
lock = {
|
|
33
|
-
"version": LOCK_VERSION,
|
|
34
|
-
"processforge_version": processforge_version,
|
|
35
|
-
"flowsheet": flowsheet,
|
|
36
|
-
"providers": providers,
|
|
37
|
-
}
|
|
38
|
-
os.makedirs(pf_dir, exist_ok=True)
|
|
39
|
-
path = _lock_path(pf_dir)
|
|
40
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
41
|
-
json.dump(lock, f, indent=2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/jacobian_contributor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{processforge-0.3.5 → processforge-0.3.8}/src/processforge/providers/reference_state_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|