processforge 0.3.8__tar.gz → 0.3.10__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.
Files changed (123) hide show
  1. {processforge-0.3.8/src/processforge.egg-info → processforge-0.3.10}/PKG-INFO +5 -1
  2. {processforge-0.3.8 → processforge-0.3.10}/pyproject.toml +6 -1
  3. processforge-0.3.10/src/processforge/api/serve.py +116 -0
  4. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/init.py +11 -0
  5. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/run.py +4 -0
  6. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/container_client.py +2 -2
  7. processforge-0.3.10/src/processforge/utils/__init__.py +0 -0
  8. processforge-0.3.10/src/processforge/utils/s3_upload.py +69 -0
  9. {processforge-0.3.8 → processforge-0.3.10/src/processforge.egg-info}/PKG-INFO +5 -1
  10. {processforge-0.3.8 → processforge-0.3.10}/src/processforge.egg-info/SOURCES.txt +4 -0
  11. {processforge-0.3.8 → processforge-0.3.10}/src/processforge.egg-info/requires.txt +5 -0
  12. processforge-0.3.10/tests/test_s3_upload.py +81 -0
  13. {processforge-0.3.8 → processforge-0.3.10}/LICENSE +0 -0
  14. {processforge-0.3.8 → processforge-0.3.10}/MANIFEST.in +0 -0
  15. {processforge-0.3.8 → processforge-0.3.10}/README.md +0 -0
  16. {processforge-0.3.8 → processforge-0.3.10}/flowsheets/closed-loop-chain.json +0 -0
  17. {processforge-0.3.8 → processforge-0.3.10}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
  18. {processforge-0.3.8 → processforge-0.3.10}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
  19. {processforge-0.3.8 → processforge-0.3.10}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
  20. {processforge-0.3.8 → processforge-0.3.10}/flowsheets/hydraulic-chain.json +0 -0
  21. {processforge-0.3.8 → processforge-0.3.10}/flowsheets/openmc/msre_eigenvalue.json +0 -0
  22. {processforge-0.3.8 → processforge-0.3.10}/setup.cfg +0 -0
  23. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/__init__.py +0 -0
  24. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/__main__.py +0 -0
  25. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/_schema.py +0 -0
  26. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/analysis/__init__.py +0 -0
  27. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/analysis/dof.py +0 -0
  28. {processforge-0.3.8/src/processforge/utils → processforge-0.3.10/src/processforge/api}/__init__.py +0 -0
  29. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/__init__.py +0 -0
  30. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/apply.py +0 -0
  31. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/common.py +0 -0
  32. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/diagram.py +0 -0
  33. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/display.py +0 -0
  34. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/export_fmu.py +0 -0
  35. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/export_modelica.py +0 -0
  36. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/plan.py +0 -0
  37. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/cli/validate.py +0 -0
  38. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/compose.py +0 -0
  39. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/__init__.py +0 -0
  40. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/backends/__init__.py +0 -0
  41. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/backends/base.py +0 -0
  42. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/backends/casadi_backend.py +0 -0
  43. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/backends/pyomo_backend.py +0 -0
  44. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/backends/scipy_backend.py +0 -0
  45. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/flowsheet.py +0 -0
  46. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/jacobian.py +0 -0
  47. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/mixin.py +0 -0
  48. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/solver.py +0 -0
  49. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/stream_var.py +0 -0
  50. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/units/__init__.py +0 -0
  51. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/units/flash_eo.py +0 -0
  52. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/units/heater_eo.py +0 -0
  53. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/units/pipes_eo.py +0 -0
  54. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/units/pump_eo.py +0 -0
  55. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/units/strainer_eo.py +0 -0
  56. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/eo/units/valve_eo.py +0 -0
  57. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/flowsheet.py +0 -0
  58. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/fmu/__init__.py +0 -0
  59. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/fmu/_fmi_vars.py +0 -0
  60. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/fmu/builder.py +0 -0
  61. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/fmu/slave_template.py +0 -0
  62. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/lock.py +0 -0
  63. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/modelica/__init__.py +0 -0
  64. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/modelica/mo_writer.py +0 -0
  65. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/modelica/omc_runner.py +0 -0
  66. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/modelica/transpiler.py +0 -0
  67. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/modelica/unit_equations.py +0 -0
  68. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/pcl/__init__.py +0 -0
  69. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/pcl/compiler.py +0 -0
  70. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/pcl/namespace.py +0 -0
  71. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/provenance.py +0 -0
  72. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/__init__.py +0 -0
  73. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/base.py +0 -0
  74. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/base_jacobian_mixin.py +0 -0
  75. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/cantera_jacobian.py +0 -0
  76. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/cantera_provider.py +0 -0
  77. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/coolprop_provider.py +0 -0
  78. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/festim_provider.py +0 -0
  79. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/jacobian_contributor.py +0 -0
  80. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/manager.py +0 -0
  81. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/modelica_jacobian.py +0 -0
  82. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/modelica_provider.py +0 -0
  83. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/openmc_provider.py +0 -0
  84. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/reference_state_registry.py +0 -0
  85. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/providers/registry.py +0 -0
  86. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/result.py +0 -0
  87. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/result_schema.py +0 -0
  88. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/schemas/__init__.py +0 -0
  89. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/schemas/flowsheet_schema.json +0 -0
  90. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/schemas/openmc/openmc_model.py +0 -0
  91. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/simulate.py +0 -0
  92. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/solver.py +0 -0
  93. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/state.py +0 -0
  94. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/thermo.py +0 -0
  95. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/types.py +0 -0
  96. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/__init__.py +0 -0
  97. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/base.py +0 -0
  98. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/cstr.py +0 -0
  99. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/flash.py +0 -0
  100. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/heater.py +0 -0
  101. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/pfr.py +0 -0
  102. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/pipes.py +0 -0
  103. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/pump.py +0 -0
  104. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/registry.py +0 -0
  105. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/solver_unit.py +0 -0
  106. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/strainer.py +0 -0
  107. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/tank.py +0 -0
  108. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/units/valve.py +0 -0
  109. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/utils/flowsheet_diagram.py +0 -0
  110. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/utils/mermaid_diagram.py +0 -0
  111. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/utils/topology.py +0 -0
  112. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/utils/unit_consistency.py +0 -0
  113. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/utils/validate_flowsheet.py +0 -0
  114. {processforge-0.3.8 → processforge-0.3.10}/src/processforge/utils/validation.py +0 -0
  115. {processforge-0.3.8 → processforge-0.3.10}/src/processforge.egg-info/dependency_links.txt +0 -0
  116. {processforge-0.3.8 → processforge-0.3.10}/src/processforge.egg-info/entry_points.txt +0 -0
  117. {processforge-0.3.8 → processforge-0.3.10}/src/processforge.egg-info/top_level.txt +0 -0
  118. {processforge-0.3.8 → processforge-0.3.10}/tests/test_cli.py +0 -0
  119. {processforge-0.3.8 → processforge-0.3.10}/tests/test_festim_provider.py +0 -0
  120. {processforge-0.3.8 → processforge-0.3.10}/tests/test_init.py +0 -0
  121. {processforge-0.3.8 → processforge-0.3.10}/tests/test_providers.py +0 -0
  122. {processforge-0.3.8 → processforge-0.3.10}/tests/test_state.py +0 -0
  123. {processforge-0.3.8 → processforge-0.3.10}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.3.8
3
+ Version: 0.3.10
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
@@ -30,6 +30,10 @@ Requires-Dist: pandas>=2.2
30
30
  Requires-Dist: zarr>=3.0
31
31
  Requires-Dist: coolprop>=7.2.0
32
32
  Requires-Dist: typer>=0.12
33
+ Provides-Extra: api
34
+ Requires-Dist: fastapi>=0.110; extra == "api"
35
+ Requires-Dist: uvicorn>=0.29; extra == "api"
36
+ Requires-Dist: s3fs>=2024.0; extra == "api"
33
37
  Provides-Extra: eo
34
38
  Requires-Dist: pyomo>=6.7; extra == "eo"
35
39
  Provides-Extra: eo-casadi
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "processforge"
7
- version = "0.3.8"
7
+ version = "0.3.10"
8
8
  description = "A Python-based process simulation framework for chemical engineering applications."
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -39,6 +39,11 @@ dependencies = [
39
39
  ]
40
40
 
41
41
  [project.optional-dependencies]
42
+ api = [
43
+ "fastapi>=0.110",
44
+ "uvicorn>=0.29",
45
+ "s3fs>=2024.0",
46
+ ]
42
47
  eo = [
43
48
  "pyomo>=6.7",
44
49
  ]
@@ -0,0 +1,116 @@
1
+ """Processforge provider HTTP API.
2
+
3
+ Run as a module so deployment platforms that execute
4
+ ``python -m processforge.api.serve`` can boot the server directly::
5
+
6
+ python -m processforge.api.serve
7
+
8
+ The listen port is taken from the ``PORT`` environment variable
9
+ (default ``9000``), and the provider type from ``PROVIDER_TYPE``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import traceback
16
+
17
+ from fastapi import FastAPI, HTTPException
18
+ from loguru import logger
19
+
20
+ app = FastAPI(title="Processforge Provider")
21
+
22
+ PROVIDER_TYPE = os.environ.get("PROVIDER_TYPE", "")
23
+ DEFAULT_PORT = int(os.environ.get("PORT", "9000"))
24
+
25
+
26
+ @app.get("/health")
27
+ def health():
28
+ return {
29
+ "status": "ready",
30
+ "provider_type": PROVIDER_TYPE,
31
+ }
32
+
33
+
34
+ @app.post("/run")
35
+ def run(body: dict):
36
+ from processforge.types import UnitConfig, MaterialDef
37
+
38
+ try:
39
+ unit_cfg = UnitConfig.from_dict(body["unit_config"])
40
+ except Exception as exc:
41
+ raise HTTPException(status_code=400, detail=f"Invalid unit_config: {exc}") from exc
42
+
43
+ materials_raw = body.get("materials", {})
44
+ materials = {
45
+ name: MaterialDef.from_dict(mat)
46
+ for name, mat in materials_raw.items()
47
+ }
48
+
49
+ from processforge.providers.registry import get_provider_class
50
+
51
+ try:
52
+ provider_cls = get_provider_class(PROVIDER_TYPE)
53
+ except ValueError as exc:
54
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
55
+
56
+ provider = provider_cls()
57
+
58
+ # Build the provider config for initialize(). Prefer the real config the
59
+ # CLI serialised into the request body (carries cross_sections, output_dir,
60
+ # url, …); fall back to a minimal container-default config otherwise.
61
+ provider_config_raw = body.get("provider_config")
62
+ if provider_config_raw:
63
+ from processforge.types import provider_config_from_dict
64
+
65
+ provider_config = provider_config_from_dict(provider_config_raw)
66
+ else:
67
+ provider_config = type("Cfg", (), {
68
+ "url": None,
69
+ "output_dir": os.environ.get("PROCESSFORGE_OUTPUT_DIR", "/data"),
70
+ "cross_sections": None,
71
+ "type": PROVIDER_TYPE,
72
+ })()
73
+
74
+ # Build a minimal flowsheet config holding materials.
75
+ flowsheet_config = type("FS", (), {"materials": materials})()
76
+
77
+ try:
78
+ provider.initialize(provider_config, flowsheet_config)
79
+ result = provider.run_simulation(unit_cfg, body.get("inlet", {}))
80
+ _maybe_upload_run_outputs(result)
81
+ return result.as_dict() | {"metadata": result.metadata}
82
+ except HTTPException:
83
+ raise
84
+ except Exception as exc:
85
+ raise HTTPException(
86
+ status_code=500,
87
+ detail=f"Simulation failed: {exc}\n{traceback.format_exc()}",
88
+ ) from exc
89
+ finally:
90
+ provider.teardown()
91
+
92
+
93
+ def _maybe_upload_run_outputs(result) -> None:
94
+ """Upload the provider's run_dir to S3 when configured.
95
+
96
+ No-op unless ``S3_BUCKET`` is set in the container environment and the
97
+ provider recorded a ``run_dir`` in its result metadata. Upload failures are
98
+ logged and never fail the run.
99
+ """
100
+ run_dir = result.metadata.get("run_dir")
101
+ if not run_dir:
102
+ return
103
+ try:
104
+ from processforge.utils.s3_upload import upload_directory_to_s3
105
+
106
+ s3_uri = upload_directory_to_s3(run_dir)
107
+ if s3_uri:
108
+ result.metadata["s3_uri"] = s3_uri
109
+ except Exception as exc: # noqa: BLE001
110
+ logger.warning(f"S3 upload of {run_dir} failed: {exc}")
111
+
112
+
113
+ if __name__ == "__main__":
114
+ import uvicorn
115
+
116
+ uvicorn.run(app, host="0.0.0.0", port=DEFAULT_PORT)
@@ -224,6 +224,17 @@ def init(
224
224
  )
225
225
  except subprocess.TimeoutExpired:
226
226
  logger.warning("docker compose pull timed out after 600s.")
227
+
228
+ logger.info(
229
+ "To start the containerized provider(s), run:\n"
230
+ f" docker compose -f {compose_path} up -d"
231
+ )
232
+ if any(info["type"] == "openmc" for info in local_docker_providers.values()):
233
+ logger.info(
234
+ "Running OpenMC — set OPENMC_DATA_ROOT to mount your OpenMC "
235
+ "data/workspace into the container (defaults to 'outputs'):\n"
236
+ f" OPENMC_DATA_ROOT=/path/to/data docker compose -f {compose_path} up -d"
237
+ )
227
238
  elif remote_docker_providers:
228
239
  logger.info(
229
240
  "All containerized providers use remote URLs — skipping Docker setup."
@@ -100,6 +100,10 @@ def run(
100
100
  logger.info(f" Backend : {display_backend(config, getattr(fs, 'backend', 'dynamic'))}")
101
101
  logger.info(f"Results saved to {zarr_path}")
102
102
 
103
+ for name, res in results.items():
104
+ if isinstance(res, dict) and (s3_uri := res.get("metadata", {}).get("s3_uri")):
105
+ logger.info(f" Run outputs (S3): {s3_uri}")
106
+
103
107
  if export_images:
104
108
  try:
105
109
  plot_results(results, fname=f"{base_name}_results.png")
@@ -1,13 +1,13 @@
1
1
  """CLI-side HTTP client for containerized providers (OpenMC, FESTIM, …).
2
2
 
3
3
  A provider that runs in a Docker container does its real work inside the
4
- container (in ``docker/provider_server.py``). The CLI side must NOT import the
4
+ container (in ``processforge.api.serve``). The CLI side must NOT import the
5
5
  backend library (OpenMC, FESTIM, …) locally — instead it speaks HTTP to the
6
6
  container's ``provider_server.py`` instance. :class:`ContainerProviderClient`
7
7
  implements the :class:`~processforge.providers.base.AbstractProvider` contract
8
8
  as a thin HTTP wrapper around that server.
9
9
 
10
- Request/response contract (matches ``docker/provider_server.py``)
10
+ Request/response contract (matches ``processforge.api.serve``)
11
11
  ----------------------------------------------------------------
12
12
  * ``POST /run`` with a JSON body
13
13
  ``{"unit_config", "materials", "inlet", "output_dir", "provider_config"}``.
File without changes
@@ -0,0 +1,69 @@
1
+ """S3 upload helper for provider containers.
2
+
3
+ Uploads a provider's run-output directory (``run_dir``) to S3 so ephemeral
4
+ container-local artifacts (``statepoint.h5``, mesh tallies, ``*.h5m``) survive
5
+ after the container stops. The container opts in by setting ``S3_BUCKET`` in its
6
+ environment; credential env vars reuse the convention from
7
+ :func:`processforge.result.save_results_zarr_s3`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from datetime import datetime, timezone
14
+
15
+
16
+ def s3_storage_options() -> dict:
17
+ """Build s3fs storage options from environment variables.
18
+
19
+ Returns an empty dict when no credentials are configured so s3fs falls
20
+ back to its default credential chain (e.g. instance/role-based auth).
21
+ """
22
+ opts: dict = {}
23
+ key = os.environ.get("S3_ACCESS_KEY")
24
+ secret = os.environ.get("S3_SECRET_KEY")
25
+ if key:
26
+ opts["key"] = key
27
+ if secret:
28
+ opts["secret"] = secret
29
+ client_kwargs = {
30
+ k: v
31
+ for k, v in {
32
+ "endpoint_url": os.environ.get("S3_ENDPOINT_URL"),
33
+ "region_name": os.environ.get("S3_REGION_NAME", "ams3"),
34
+ }.items()
35
+ if v
36
+ }
37
+ if client_kwargs:
38
+ opts["client_kwargs"] = client_kwargs
39
+ return opts
40
+
41
+
42
+ def upload_directory_to_s3(run_dir: str) -> str | None:
43
+ """Upload *run_dir* recursively to S3.
44
+
45
+ Reads the destination from the container environment:
46
+
47
+ * ``S3_BUCKET`` — required; enables the upload. When unset, returns ``None``
48
+ and the caller keeps its local artifacts.
49
+ * ``S3_PREFIX`` — optional key prefix (e.g. ``openmc-runs``).
50
+
51
+ The object key is ``<S3_PREFIX>/<run_dir_basename>-<utc_stamp>/`` so
52
+ repeated runs do not clobber each other.
53
+
54
+ Returns the uploaded ``s3://`` URI, or ``None`` when disabled.
55
+ """
56
+ bucket = os.environ.get("S3_BUCKET")
57
+ if not bucket:
58
+ return None
59
+
60
+ prefix = os.environ.get("S3_PREFIX", "").strip("/")
61
+ tag = f"{os.path.basename(run_dir.rstrip('/'))}-{datetime.now(timezone.utc):%Y%m%dT%H%M%SZ}"
62
+ s3_dir = f"{prefix + '/' if prefix else ''}{tag}"
63
+
64
+ import s3fs
65
+
66
+ s3fs.S3FileSystem(**s3_storage_options()).put(
67
+ run_dir, f"s3://{bucket}/{s3_dir}", recursive=True
68
+ )
69
+ return f"s3://{bucket}/{s3_dir}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: processforge
3
- Version: 0.3.8
3
+ Version: 0.3.10
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
@@ -30,6 +30,10 @@ Requires-Dist: pandas>=2.2
30
30
  Requires-Dist: zarr>=3.0
31
31
  Requires-Dist: coolprop>=7.2.0
32
32
  Requires-Dist: typer>=0.12
33
+ Provides-Extra: api
34
+ Requires-Dist: fastapi>=0.110; extra == "api"
35
+ Requires-Dist: uvicorn>=0.29; extra == "api"
36
+ Requires-Dist: s3fs>=2024.0; extra == "api"
33
37
  Provides-Extra: eo
34
38
  Requires-Dist: pyomo>=6.7; extra == "eo"
35
39
  Provides-Extra: eo-casadi
@@ -30,6 +30,8 @@ src/processforge.egg-info/requires.txt
30
30
  src/processforge.egg-info/top_level.txt
31
31
  src/processforge/analysis/__init__.py
32
32
  src/processforge/analysis/dof.py
33
+ src/processforge/api/__init__.py
34
+ src/processforge/api/serve.py
33
35
  src/processforge/cli/__init__.py
34
36
  src/processforge/cli/apply.py
35
37
  src/processforge/cli/common.py
@@ -105,6 +107,7 @@ src/processforge/units/valve.py
105
107
  src/processforge/utils/__init__.py
106
108
  src/processforge/utils/flowsheet_diagram.py
107
109
  src/processforge/utils/mermaid_diagram.py
110
+ src/processforge/utils/s3_upload.py
108
111
  src/processforge/utils/topology.py
109
112
  src/processforge/utils/unit_consistency.py
110
113
  src/processforge/utils/validate_flowsheet.py
@@ -113,5 +116,6 @@ tests/test_cli.py
113
116
  tests/test_festim_provider.py
114
117
  tests/test_init.py
115
118
  tests/test_providers.py
119
+ tests/test_s3_upload.py
116
120
  tests/test_state.py
117
121
  tests/test_types.py
@@ -10,6 +10,11 @@ zarr>=3.0
10
10
  coolprop>=7.2.0
11
11
  typer>=0.12
12
12
 
13
+ [api]
14
+ fastapi>=0.110
15
+ uvicorn>=0.29
16
+ s3fs>=2024.0
17
+
13
18
  [cantera]
14
19
  cantera>=3.0
15
20
 
@@ -0,0 +1,81 @@
1
+ """Tests for the S3 run-output upload helper."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sys
6
+ from datetime import datetime, timezone
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+
11
+ from processforge.utils import s3_upload
12
+
13
+
14
+ @pytest.fixture
15
+ def clean_env(monkeypatch):
16
+ for var in (
17
+ "S3_BUCKET",
18
+ "S3_PREFIX",
19
+ "S3_ACCESS_KEY",
20
+ "S3_SECRET_KEY",
21
+ "S3_ENDPOINT_URL",
22
+ "S3_REGION_NAME",
23
+ ):
24
+ monkeypatch.delenv(var, raising=False)
25
+ return monkeypatch
26
+
27
+
28
+ def test_s3_storage_options_defaults_to_ams3(clean_env):
29
+ opts = s3_upload.s3_storage_options()
30
+ assert opts["client_kwargs"]["region_name"] == "ams3"
31
+ # No creds configured -> not present, s3fs uses default chain.
32
+ assert "key" not in opts
33
+ assert "secret" not in opts
34
+
35
+
36
+ def test_s3_storage_options_includes_credentials(clean_env):
37
+ clean_env.setenv("S3_ACCESS_KEY", "ak")
38
+ clean_env.setenv("S3_SECRET_KEY", "sk")
39
+ clean_env.setenv("S3_ENDPOINT_URL", "https://s3.example.com")
40
+ opts = s3_upload.s3_storage_options()
41
+ assert opts["key"] == "ak"
42
+ assert opts["secret"] == "sk"
43
+ assert opts["client_kwargs"]["endpoint_url"] == "https://s3.example.com"
44
+
45
+
46
+ def test_upload_directory_to_s3_disabled_without_bucket(clean_env):
47
+ assert s3_upload.upload_directory_to_s3("/tmp/run") is None
48
+
49
+
50
+ def test_upload_directory_to_s3_uploads_recursively(clean_env):
51
+ clean_env.setenv("S3_BUCKET", "my-bucket")
52
+ clean_env.setenv("S3_PREFIX", "openmc-runs")
53
+
54
+ mock_fs = MagicMock()
55
+ mock_s3fs = MagicMock()
56
+ mock_s3fs.S3FileSystem.return_value = mock_fs
57
+
58
+ with patch.dict(sys.modules, {"s3fs": mock_s3fs}):
59
+ uri = s3_upload.upload_directory_to_s3("/tmp/processforge/openmc/msre_run")
60
+
61
+ assert uri.startswith("s3://my-bucket/openmc-runs/msre_run-")
62
+ assert uri.endswith(datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")[:8]) or "-" in uri
63
+ mock_fs.put.assert_called_once()
64
+ args, kwargs = mock_fs.put.call_args
65
+ assert args[0] == "/tmp/processforge/openmc/msre_run"
66
+ assert args[1].startswith("s3://my-bucket/openmc-runs/msre_run-")
67
+ assert kwargs.get("recursive") is True
68
+
69
+
70
+ def test_upload_directory_to_s3_no_prefix(clean_env):
71
+ clean_env.setenv("S3_BUCKET", "b")
72
+
73
+ mock_fs = MagicMock()
74
+ mock_s3fs = MagicMock()
75
+ mock_s3fs.S3FileSystem.return_value = mock_fs
76
+
77
+ with patch.dict(sys.modules, {"s3fs": mock_s3fs}):
78
+ uri = s3_upload.upload_directory_to_s3("/data/festim")
79
+
80
+ assert uri == f"s3://b/festim-{datetime.now(timezone.utc):%Y%m%dT%H%M%SZ}"
81
+ assert mock_fs.put.call_args[1].get("recursive") is True
File without changes
File without changes
File without changes
File without changes