processforge 0.3.9__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.9/src/processforge.egg-info → processforge-0.3.10}/PKG-INFO +2 -1
  2. {processforge-0.3.9 → processforge-0.3.10}/pyproject.toml +2 -1
  3. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/api/serve.py +22 -0
  4. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/run.py +4 -0
  5. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/container_client.py +2 -2
  6. processforge-0.3.10/src/processforge/utils/s3_upload.py +69 -0
  7. {processforge-0.3.9 → processforge-0.3.10/src/processforge.egg-info}/PKG-INFO +2 -1
  8. {processforge-0.3.9 → processforge-0.3.10}/src/processforge.egg-info/SOURCES.txt +2 -0
  9. {processforge-0.3.9 → processforge-0.3.10}/src/processforge.egg-info/requires.txt +1 -0
  10. processforge-0.3.10/tests/test_s3_upload.py +81 -0
  11. {processforge-0.3.9 → processforge-0.3.10}/LICENSE +0 -0
  12. {processforge-0.3.9 → processforge-0.3.10}/MANIFEST.in +0 -0
  13. {processforge-0.3.9 → processforge-0.3.10}/README.md +0 -0
  14. {processforge-0.3.9 → processforge-0.3.10}/flowsheets/closed-loop-chain.json +0 -0
  15. {processforge-0.3.9 → processforge-0.3.10}/flowsheets/custom/crimson/MUWS_HB.json +0 -0
  16. {processforge-0.3.9 → processforge-0.3.10}/flowsheets/custom/simple_hyrdaulic_scheme.json +0 -0
  17. {processforge-0.3.9 → processforge-0.3.10}/flowsheets/festim/hydrogen_transport_1d.json +0 -0
  18. {processforge-0.3.9 → processforge-0.3.10}/flowsheets/hydraulic-chain.json +0 -0
  19. {processforge-0.3.9 → processforge-0.3.10}/flowsheets/openmc/msre_eigenvalue.json +0 -0
  20. {processforge-0.3.9 → processforge-0.3.10}/setup.cfg +0 -0
  21. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/__init__.py +0 -0
  22. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/__main__.py +0 -0
  23. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/_schema.py +0 -0
  24. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/analysis/__init__.py +0 -0
  25. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/analysis/dof.py +0 -0
  26. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/api/__init__.py +0 -0
  27. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/__init__.py +0 -0
  28. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/apply.py +0 -0
  29. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/common.py +0 -0
  30. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/diagram.py +0 -0
  31. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/display.py +0 -0
  32. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/export_fmu.py +0 -0
  33. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/export_modelica.py +0 -0
  34. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/init.py +0 -0
  35. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/plan.py +0 -0
  36. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/cli/validate.py +0 -0
  37. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/compose.py +0 -0
  38. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/__init__.py +0 -0
  39. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/backends/__init__.py +0 -0
  40. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/backends/base.py +0 -0
  41. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/backends/casadi_backend.py +0 -0
  42. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/backends/pyomo_backend.py +0 -0
  43. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/backends/scipy_backend.py +0 -0
  44. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/flowsheet.py +0 -0
  45. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/jacobian.py +0 -0
  46. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/mixin.py +0 -0
  47. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/solver.py +0 -0
  48. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/stream_var.py +0 -0
  49. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/units/__init__.py +0 -0
  50. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/units/flash_eo.py +0 -0
  51. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/units/heater_eo.py +0 -0
  52. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/units/pipes_eo.py +0 -0
  53. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/units/pump_eo.py +0 -0
  54. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/units/strainer_eo.py +0 -0
  55. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/eo/units/valve_eo.py +0 -0
  56. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/flowsheet.py +0 -0
  57. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/fmu/__init__.py +0 -0
  58. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/fmu/_fmi_vars.py +0 -0
  59. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/fmu/builder.py +0 -0
  60. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/fmu/slave_template.py +0 -0
  61. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/lock.py +0 -0
  62. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/modelica/__init__.py +0 -0
  63. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/modelica/mo_writer.py +0 -0
  64. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/modelica/omc_runner.py +0 -0
  65. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/modelica/transpiler.py +0 -0
  66. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/modelica/unit_equations.py +0 -0
  67. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/pcl/__init__.py +0 -0
  68. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/pcl/compiler.py +0 -0
  69. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/pcl/namespace.py +0 -0
  70. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/provenance.py +0 -0
  71. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/__init__.py +0 -0
  72. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/base.py +0 -0
  73. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/base_jacobian_mixin.py +0 -0
  74. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/cantera_jacobian.py +0 -0
  75. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/cantera_provider.py +0 -0
  76. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/coolprop_provider.py +0 -0
  77. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/festim_provider.py +0 -0
  78. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/jacobian_contributor.py +0 -0
  79. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/manager.py +0 -0
  80. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/modelica_jacobian.py +0 -0
  81. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/modelica_provider.py +0 -0
  82. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/openmc_provider.py +0 -0
  83. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/reference_state_registry.py +0 -0
  84. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/providers/registry.py +0 -0
  85. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/result.py +0 -0
  86. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/result_schema.py +0 -0
  87. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/schemas/__init__.py +0 -0
  88. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/schemas/flowsheet_schema.json +0 -0
  89. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/schemas/openmc/openmc_model.py +0 -0
  90. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/simulate.py +0 -0
  91. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/solver.py +0 -0
  92. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/state.py +0 -0
  93. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/thermo.py +0 -0
  94. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/types.py +0 -0
  95. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/__init__.py +0 -0
  96. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/base.py +0 -0
  97. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/cstr.py +0 -0
  98. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/flash.py +0 -0
  99. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/heater.py +0 -0
  100. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/pfr.py +0 -0
  101. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/pipes.py +0 -0
  102. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/pump.py +0 -0
  103. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/registry.py +0 -0
  104. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/solver_unit.py +0 -0
  105. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/strainer.py +0 -0
  106. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/tank.py +0 -0
  107. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/units/valve.py +0 -0
  108. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/utils/__init__.py +0 -0
  109. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/utils/flowsheet_diagram.py +0 -0
  110. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/utils/mermaid_diagram.py +0 -0
  111. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/utils/topology.py +0 -0
  112. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/utils/unit_consistency.py +0 -0
  113. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/utils/validate_flowsheet.py +0 -0
  114. {processforge-0.3.9 → processforge-0.3.10}/src/processforge/utils/validation.py +0 -0
  115. {processforge-0.3.9 → processforge-0.3.10}/src/processforge.egg-info/dependency_links.txt +0 -0
  116. {processforge-0.3.9 → processforge-0.3.10}/src/processforge.egg-info/entry_points.txt +0 -0
  117. {processforge-0.3.9 → processforge-0.3.10}/src/processforge.egg-info/top_level.txt +0 -0
  118. {processforge-0.3.9 → processforge-0.3.10}/tests/test_cli.py +0 -0
  119. {processforge-0.3.9 → processforge-0.3.10}/tests/test_festim_provider.py +0 -0
  120. {processforge-0.3.9 → processforge-0.3.10}/tests/test_init.py +0 -0
  121. {processforge-0.3.9 → processforge-0.3.10}/tests/test_providers.py +0 -0
  122. {processforge-0.3.9 → processforge-0.3.10}/tests/test_state.py +0 -0
  123. {processforge-0.3.9 → 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.9
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
@@ -33,6 +33,7 @@ Requires-Dist: typer>=0.12
33
33
  Provides-Extra: api
34
34
  Requires-Dist: fastapi>=0.110; extra == "api"
35
35
  Requires-Dist: uvicorn>=0.29; extra == "api"
36
+ Requires-Dist: s3fs>=2024.0; extra == "api"
36
37
  Provides-Extra: eo
37
38
  Requires-Dist: pyomo>=6.7; extra == "eo"
38
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.9"
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"
@@ -42,6 +42,7 @@ dependencies = [
42
42
  api = [
43
43
  "fastapi>=0.110",
44
44
  "uvicorn>=0.29",
45
+ "s3fs>=2024.0",
45
46
  ]
46
47
  eo = [
47
48
  "pyomo>=6.7",
@@ -15,6 +15,7 @@ import os
15
15
  import traceback
16
16
 
17
17
  from fastapi import FastAPI, HTTPException
18
+ from loguru import logger
18
19
 
19
20
  app = FastAPI(title="Processforge Provider")
20
21
 
@@ -76,6 +77,7 @@ def run(body: dict):
76
77
  try:
77
78
  provider.initialize(provider_config, flowsheet_config)
78
79
  result = provider.run_simulation(unit_cfg, body.get("inlet", {}))
80
+ _maybe_upload_run_outputs(result)
79
81
  return result.as_dict() | {"metadata": result.metadata}
80
82
  except HTTPException:
81
83
  raise
@@ -88,6 +90,26 @@ def run(body: dict):
88
90
  provider.teardown()
89
91
 
90
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
+
91
113
  if __name__ == "__main__":
92
114
  import uvicorn
93
115
 
@@ -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"}``.
@@ -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.9
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
@@ -33,6 +33,7 @@ Requires-Dist: typer>=0.12
33
33
  Provides-Extra: api
34
34
  Requires-Dist: fastapi>=0.110; extra == "api"
35
35
  Requires-Dist: uvicorn>=0.29; extra == "api"
36
+ Requires-Dist: s3fs>=2024.0; extra == "api"
36
37
  Provides-Extra: eo
37
38
  Requires-Dist: pyomo>=6.7; extra == "eo"
38
39
  Provides-Extra: eo-casadi
@@ -107,6 +107,7 @@ src/processforge/units/valve.py
107
107
  src/processforge/utils/__init__.py
108
108
  src/processforge/utils/flowsheet_diagram.py
109
109
  src/processforge/utils/mermaid_diagram.py
110
+ src/processforge/utils/s3_upload.py
110
111
  src/processforge/utils/topology.py
111
112
  src/processforge/utils/unit_consistency.py
112
113
  src/processforge/utils/validate_flowsheet.py
@@ -115,5 +116,6 @@ tests/test_cli.py
115
116
  tests/test_festim_provider.py
116
117
  tests/test_init.py
117
118
  tests/test_providers.py
119
+ tests/test_s3_upload.py
118
120
  tests/test_state.py
119
121
  tests/test_types.py
@@ -13,6 +13,7 @@ typer>=0.12
13
13
  [api]
14
14
  fastapi>=0.110
15
15
  uvicorn>=0.29
16
+ s3fs>=2024.0
16
17
 
17
18
  [cantera]
18
19
  cantera>=3.0
@@ -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