ert 19.0.1__py3-none-any.whl → 20.0.0b1__py3-none-any.whl

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 (87) hide show
  1. ert/__main__.py +94 -63
  2. ert/analysis/_es_update.py +11 -14
  3. ert/cli/main.py +1 -1
  4. ert/config/__init__.py +3 -2
  5. ert/config/_create_observation_dataframes.py +52 -375
  6. ert/config/_observations.py +527 -200
  7. ert/config/_read_summary.py +4 -5
  8. ert/config/ert_config.py +52 -117
  9. ert/config/everest_control.py +40 -39
  10. ert/config/everest_response.py +3 -15
  11. ert/config/field.py +4 -76
  12. ert/config/forward_model_step.py +17 -1
  13. ert/config/gen_data_config.py +14 -17
  14. ert/config/observation_config_migrations.py +821 -0
  15. ert/config/parameter_config.py +18 -28
  16. ert/config/parsing/__init__.py +0 -1
  17. ert/config/parsing/_parse_zonemap.py +45 -0
  18. ert/config/parsing/config_keywords.py +1 -0
  19. ert/config/parsing/config_schema.py +2 -0
  20. ert/config/parsing/observations_parser.py +2 -0
  21. ert/config/response_config.py +5 -23
  22. ert/config/rft_config.py +129 -31
  23. ert/config/summary_config.py +1 -13
  24. ert/config/surface_config.py +0 -57
  25. ert/dark_storage/compute/misfits.py +0 -42
  26. ert/dark_storage/endpoints/__init__.py +0 -2
  27. ert/dark_storage/endpoints/experiments.py +2 -5
  28. ert/dark_storage/json_schema/experiment.py +1 -2
  29. ert/field_utils/__init__.py +0 -2
  30. ert/field_utils/field_utils.py +1 -117
  31. ert/gui/ertwidgets/listeditbox.py +9 -1
  32. ert/gui/ertwidgets/models/ertsummary.py +20 -6
  33. ert/gui/ertwidgets/pathchooser.py +9 -1
  34. ert/gui/ertwidgets/stringbox.py +11 -3
  35. ert/gui/ertwidgets/textbox.py +10 -3
  36. ert/gui/ertwidgets/validationsupport.py +19 -1
  37. ert/gui/main_window.py +11 -6
  38. ert/gui/simulation/experiment_panel.py +1 -1
  39. ert/gui/simulation/run_dialog.py +11 -1
  40. ert/gui/tools/manage_experiments/export_dialog.py +4 -0
  41. ert/gui/tools/manage_experiments/manage_experiments_panel.py +1 -0
  42. ert/gui/tools/manage_experiments/storage_info_widget.py +1 -1
  43. ert/gui/tools/manage_experiments/storage_widget.py +21 -4
  44. ert/gui/tools/plot/data_type_proxy_model.py +1 -1
  45. ert/gui/tools/plot/plot_api.py +35 -27
  46. ert/gui/tools/plot/plot_widget.py +5 -0
  47. ert/gui/tools/plot/plot_window.py +4 -7
  48. ert/run_models/ensemble_experiment.py +2 -9
  49. ert/run_models/ensemble_smoother.py +1 -9
  50. ert/run_models/everest_run_model.py +31 -23
  51. ert/run_models/initial_ensemble_run_model.py +19 -22
  52. ert/run_models/manual_update.py +11 -5
  53. ert/run_models/model_factory.py +7 -7
  54. ert/run_models/multiple_data_assimilation.py +3 -16
  55. ert/sample_prior.py +12 -14
  56. ert/scheduler/job.py +24 -4
  57. ert/services/__init__.py +7 -3
  58. ert/services/_storage_main.py +59 -22
  59. ert/services/ert_server.py +186 -24
  60. ert/shared/version.py +3 -3
  61. ert/storage/local_ensemble.py +50 -116
  62. ert/storage/local_experiment.py +94 -109
  63. ert/storage/local_storage.py +10 -12
  64. ert/storage/migration/to24.py +26 -0
  65. ert/storage/migration/to25.py +91 -0
  66. ert/utils/__init__.py +20 -0
  67. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/METADATA +4 -51
  68. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/RECORD +80 -83
  69. everest/bin/everest_script.py +5 -5
  70. everest/bin/kill_script.py +2 -2
  71. everest/bin/monitor_script.py +2 -2
  72. everest/bin/utils.py +4 -4
  73. everest/detached/everserver.py +6 -6
  74. everest/gui/everest_client.py +0 -6
  75. everest/gui/main_window.py +2 -2
  76. everest/util/__init__.py +1 -19
  77. ert/dark_storage/compute/__init__.py +0 -0
  78. ert/dark_storage/endpoints/compute/__init__.py +0 -0
  79. ert/dark_storage/endpoints/compute/misfits.py +0 -95
  80. ert/services/_base_service.py +0 -387
  81. ert/services/webviz_ert_service.py +0 -20
  82. ert/shared/storage/command.py +0 -38
  83. ert/shared/storage/extraction.py +0 -42
  84. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/WHEEL +0 -0
  85. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/entry_points.txt +0 -0
  86. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/licenses/COPYING +0 -0
  87. {ert-19.0.1.dist-info → ert-20.0.0b1.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,7 @@ from functools import partial
12
12
  from pathlib import Path
13
13
  from typing import Any
14
14
 
15
- from ert.services import ErtServer
15
+ from ert.services import create_ertserver_client
16
16
  from everest.bin.utils import setup_logging
17
17
  from everest.config import EverestConfig, ServerConfig
18
18
  from everest.detached import stop_server, wait_for_server_to_stop
@@ -74,7 +74,7 @@ def _handle_keyboard_interrupt(signal: int, _: Any, after: bool = False) -> None
74
74
 
75
75
  def kill_everest(options: argparse.Namespace) -> None:
76
76
  try:
77
- client = ErtServer.session(
77
+ client = create_ertserver_client(
78
78
  Path(ServerConfig.get_session_dir(options.config.output_dir)), timeout=1
79
79
  )
80
80
  server_context = ServerConfig.get_server_context_from_conn_info(
@@ -7,7 +7,7 @@ from functools import partial
7
7
  from pathlib import Path
8
8
  from textwrap import dedent
9
9
 
10
- from ert.services import ErtServer
10
+ from ert.services import create_ertserver_client
11
11
  from ert.storage import ErtStorageException, ExperimentState
12
12
  from everest.config import EverestConfig, ServerConfig
13
13
  from everest.everest_storage import EverestStorage
@@ -82,7 +82,7 @@ def _build_args_parser() -> argparse.ArgumentParser:
82
82
  def monitor_everest(options: argparse.Namespace) -> None:
83
83
  config: EverestConfig = options.config
84
84
  try:
85
- with ErtServer.session(
85
+ with create_ertserver_client(
86
86
  Path(ServerConfig.get_session_dir(config.output_dir)), timeout=1
87
87
  ) as client:
88
88
  server_context = ServerConfig.get_server_context_from_conn_info(
everest/bin/utils.py CHANGED
@@ -26,11 +26,12 @@ from ert.ensemble_evaluator import (
26
26
  from ert.ensemble_evaluator.event import EndEvent
27
27
  from ert.logging import LOGGING_CONFIG
28
28
  from ert.plugins.plugin_manager import ErtPluginManager
29
- from ert.services import ErtServer
29
+ from ert.services import create_ertserver_client
30
30
  from ert.storage import (
31
31
  ExperimentStatus,
32
32
  open_storage,
33
33
  )
34
+ from ert.utils import makedirs_if_needed
34
35
  from everest.config import EverestConfig
35
36
  from everest.config.server_config import ServerConfig
36
37
  from everest.detached import (
@@ -40,7 +41,6 @@ from everest.detached import (
40
41
  wait_for_server_to_stop,
41
42
  )
42
43
  from everest.strings import EVEREST, OPT_PROGRESS_ID, SIM_PROGRESS_ID
43
- from everest.util import makedirs_if_needed
44
44
 
45
45
  JOB_SUCCESS = "Finished"
46
46
  JOB_RUNNING = "Running"
@@ -54,7 +54,7 @@ def cleanup_logging() -> None:
54
54
  @contextmanager
55
55
  def setup_logging(options: argparse.Namespace) -> Generator[None, None, None]:
56
56
  if isinstance(options.config, EverestConfig):
57
- makedirs_if_needed(options.config.output_dir, roll_if_exists=False)
57
+ makedirs_if_needed(Path(options.config.output_dir), roll_if_exists=False)
58
58
  log_dir = Path(options.config.output_dir) / "logs"
59
59
  else:
60
60
  # `everest branch` gives a tuple object here.
@@ -106,7 +106,7 @@ def handle_keyboard_interrupt(signum: int, _: Any, options: argparse.Namespace)
106
106
  "The optimization will be stopped and the program will exit..."
107
107
  )
108
108
  try:
109
- client = ErtServer.session(
109
+ client = create_ertserver_client(
110
110
  Path(ServerConfig.get_session_dir(options.config.output_dir))
111
111
  )
112
112
  server_context = ServerConfig.get_server_context_from_conn_info(
@@ -12,11 +12,11 @@ import yaml
12
12
  from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
13
13
 
14
14
  from ert.plugins.plugin_manager import ErtPluginManager
15
- from ert.services import ErtServer
16
- from ert.services._base_service import BaseServiceExit
15
+ from ert.services import ErtServer, ErtServerExit, create_ertserver_client
17
16
  from ert.storage import ExperimentStatus
18
17
  from ert.storage.local_experiment import ExperimentState
19
18
  from ert.trace import tracer
19
+ from ert.utils import makedirs_if_needed
20
20
  from everest.config import ServerConfig
21
21
  from everest.strings import (
22
22
  DEFAULT_LOGGING_FORMAT,
@@ -25,7 +25,7 @@ from everest.strings import (
25
25
  EXPERIMENT_SERVER,
26
26
  OPTIMIZATION_LOG_DIR,
27
27
  )
28
- from everest.util import makedirs_if_needed, version_info
28
+ from everest.util import version_info
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
@@ -34,7 +34,7 @@ def _configure_loggers(
34
34
  log_dir: Path, logging_level: int, output_file: str | None
35
35
  ) -> None:
36
36
  def make_handler_config(path: Path, log_level: int) -> dict[str, Any]:
37
- makedirs_if_needed(str(path.parent))
37
+ makedirs_if_needed(path.parent)
38
38
  return {
39
39
  "class": "logging.FileHandler",
40
40
  "formatter": "default",
@@ -166,7 +166,7 @@ def main() -> None:
166
166
  timeout=240, project=Path(server_path), logging_config=log_file.name
167
167
  ) as server:
168
168
  server.fetch_connection_info()
169
- with ErtServer.session(project=Path(server_path)) as client:
169
+ with create_ertserver_client(Path(server_path)) as client:
170
170
  done = False
171
171
  while not done:
172
172
  response = client.get(
@@ -178,7 +178,7 @@ def main() -> None:
178
178
  ExperimentState.running,
179
179
  }
180
180
  time.sleep(0.5)
181
- except BaseServiceExit:
181
+ except ErtServerExit:
182
182
  # Server exit, happens on normal shutdown and keyboard interrupt
183
183
  logging.getLogger(EVERSERVER).info("Everserver stopped by user")
184
184
  except Exception as e:
@@ -62,12 +62,6 @@ class EverestClient:
62
62
  def config(self) -> dict[str, str]:
63
63
  return self._http_get(EverEndpoints.config_path).json()
64
64
 
65
- def get_runtime(self) -> int:
66
- if self._start_time is None:
67
- self._start_time = int(self._http_get(EverEndpoints.start_time).text)
68
-
69
- return int(time.time()) - self._start_time
70
-
71
65
  @property
72
66
  def credentials(self) -> str:
73
67
  return b64encode(f"{self._username}:{self._password}".encode()).decode()
@@ -13,7 +13,7 @@ from PyQt6.QtWidgets import (
13
13
  from ert.gui.ertnotifier import ErtNotifier
14
14
  from ert.gui.simulation.run_dialog import RunDialog
15
15
  from ert.plugins import ErtPluginManager
16
- from ert.services import ErtServer
16
+ from ert.services import create_ertserver_client
17
17
  from everest.config import ServerConfig
18
18
  from everest.detached import wait_for_server
19
19
  from everest.gui.everest_client import EverestClient
@@ -43,7 +43,7 @@ class EverestMainWindow(QMainWindow):
43
43
  self.setCentralWidget(self.central_widget)
44
44
 
45
45
  def run(self) -> None:
46
- storage_client = ErtServer.session(
46
+ storage_client = create_ertserver_client(
47
47
  Path(ServerConfig.get_session_dir(self.output_dir))
48
48
  )
49
49
  wait_for_server(storage_client, 60)
everest/util/__init__.py CHANGED
@@ -1,6 +1,4 @@
1
1
  import logging
2
- import os
3
- from datetime import UTC, datetime
4
2
 
5
3
  from ropt.version import version as ropt_version
6
4
 
@@ -8,7 +6,7 @@ try:
8
6
  from ert.shared.version import version as ert_version
9
7
  except ImportError:
10
8
  ert_version = "0.0.0"
11
- from _ert.utils import file_safe_timestamp
9
+
12
10
  from everest.strings import EVEREST
13
11
 
14
12
 
@@ -16,14 +14,6 @@ def version_info() -> str:
16
14
  return f"everest:{ert_version}, ropt:{ropt_version}, ert:{ert_version}"
17
15
 
18
16
 
19
- def makedirs_if_needed(path: str, roll_if_exists: bool = False) -> None:
20
- if os.path.isdir(path):
21
- if not roll_if_exists:
22
- return
23
- _roll_dir(path) # exists and should be rolled
24
- os.makedirs(path)
25
-
26
-
27
17
  def warn_user_that_runpath_is_nonempty() -> None:
28
18
  print(
29
19
  "Everest is running in an existing runpath.\n\n"
@@ -34,11 +24,3 @@ def warn_user_that_runpath_is_nonempty() -> None:
34
24
  "be used if not configured correctly.\n"
35
25
  )
36
26
  logging.getLogger(EVEREST).warning("Everest is running in an existing runpath")
37
-
38
-
39
- def _roll_dir(old_name: str) -> None:
40
- old_name = os.path.realpath(old_name)
41
- timestamp = file_safe_timestamp(datetime.now(UTC).isoformat())
42
- new_name = f"{old_name}__{timestamp}"
43
- os.rename(old_name, new_name)
44
- logging.getLogger(EVEREST).info(f"renamed {old_name} to {new_name}")
File without changes
File without changes
@@ -1,95 +0,0 @@
1
- import json
2
- from datetime import datetime
3
- from typing import Annotated, Any
4
- from uuid import UUID
5
-
6
- import pandas as pd
7
- from dateutil.parser import parse
8
- from fastapi import APIRouter, Depends, Query, status
9
- from fastapi.responses import Response
10
-
11
- from ert.dark_storage import exceptions as exc
12
- from ert.dark_storage.common import get_storage
13
- from ert.dark_storage.compute.misfits import calculate_signed_chi_squared_misfits
14
- from ert.dark_storage.endpoints.observations import (
15
- _get_observations,
16
- )
17
- from ert.dark_storage.endpoints.responses import data_for_response
18
- from ert.storage import Storage
19
-
20
- router = APIRouter(tags=["misfits"])
21
- DEFAULT_STORAGEREADER = Depends(get_storage)
22
-
23
-
24
- @router.get(
25
- "/compute/misfits",
26
- responses={
27
- status.HTTP_200_OK: {
28
- "content": {"text/csv": {}},
29
- }
30
- },
31
- )
32
- async def get_response_misfits(
33
- *,
34
- storage: Storage = DEFAULT_STORAGEREADER,
35
- ensemble_id: UUID,
36
- response_name: str,
37
- realization_index: int | None = None,
38
- summary_misfits: bool = False,
39
- filter_on: Annotated[
40
- str | None, Query(description="JSON string with filters")
41
- ] = None,
42
- ) -> Response:
43
- ensemble = storage.get_ensemble(ensemble_id)
44
- dataframe = data_for_response(
45
- ensemble,
46
- response_name,
47
- json.loads(filter_on) if filter_on is not None else None,
48
- )
49
- if realization_index is not None:
50
- dataframe = pd.DataFrame(dataframe.loc[realization_index]).T
51
-
52
- response_dict = {}
53
- for index, data in dataframe.iterrows():
54
- data_df = pd.DataFrame(data).T
55
- response_dict[index] = data_df
56
-
57
- experiment = ensemble.experiment
58
- response_type = experiment.response_key_to_response_type[response_name]
59
- obs_keys = experiment.response_key_to_observation_key[response_type].get(
60
- response_name, []
61
- )
62
- obs = _get_observations(
63
- ensemble.experiment,
64
- obs_keys,
65
- json.loads(filter_on) if filter_on is not None else None,
66
- )
67
-
68
- if not obs_keys:
69
- raise ValueError(f"No observations for key {response_name}")
70
- if not obs:
71
- raise ValueError(f"Cant fetch observations for key {response_name}")
72
- o = obs[0]
73
-
74
- def parse_index(x: Any) -> int | datetime:
75
- try:
76
- return int(x)
77
- except ValueError:
78
- return parse(x)
79
-
80
- observation_df = pd.DataFrame(
81
- data={"values": o["values"], "errors": o["errors"]},
82
- index=[parse_index(x) for x in o["x_axis"]],
83
- )
84
- try:
85
- result_df = calculate_signed_chi_squared_misfits(
86
- response_dict, observation_df, summary_misfits
87
- )
88
- except Exception as misfits_exc:
89
- raise exc.UnprocessableError(
90
- f"Unable to compute misfits: {misfits_exc}"
91
- ) from misfits_exc
92
- return Response(
93
- content=result_df.to_csv().encode(),
94
- media_type="text/csv",
95
- )
@@ -1,387 +0,0 @@
1
- """
2
- This file contains a more generic version of "ert services", and
3
- is scheduled for removal when WebvizErt is removed.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import contextlib
9
- import io
10
- import json
11
- import os
12
- import signal
13
- import sys
14
- import threading
15
- import types
16
- from collections.abc import Callable, Mapping, Sequence
17
- from logging import Logger, getLogger
18
- from pathlib import Path
19
- from select import PIPE_BUF, select
20
- from subprocess import Popen, TimeoutExpired
21
- from tempfile import NamedTemporaryFile
22
- from time import sleep
23
- from types import FrameType
24
- from typing import TYPE_CHECKING, Any, Generic, Self, TypedDict, TypeVar
25
-
26
- if TYPE_CHECKING:
27
- pass
28
-
29
- T = TypeVar("T", bound="BaseService")
30
-
31
-
32
- class ErtServerConnectionInfo(TypedDict):
33
- urls: list[str]
34
- authtoken: str
35
- host: str
36
- port: str
37
- cert: str
38
- auth: str
39
-
40
-
41
- SERVICE_CONF_PATHS: set[str] = set()
42
-
43
-
44
- class BaseServiceExit(OSError):
45
- pass
46
-
47
-
48
- def cleanup_service_files(signum: int, frame: FrameType | None) -> None:
49
- for file_path in SERVICE_CONF_PATHS:
50
- file = Path(file_path)
51
- if file.exists():
52
- file.unlink()
53
- raise BaseServiceExit(f"Signal {signum} received.")
54
-
55
-
56
- if threading.current_thread() is threading.main_thread():
57
- signal.signal(signal.SIGTERM, cleanup_service_files)
58
- signal.signal(signal.SIGINT, cleanup_service_files)
59
-
60
-
61
- def local_exec_args(script_args: str | list[str]) -> list[str]:
62
- """
63
- Convenience function that returns the exec_args for executing a Python
64
- script in the directory of '_base_service.py'.
65
-
66
- This is done instead of using 'python -m [module path]' due to the '-m' flag
67
- adding the user's current working directory to sys.path. Executing a Python
68
- script by itself will add the directory of the script rather than the
69
- current working directory, thus we avoid accidentally importing user's
70
- directories that just happen to have the same names as the ones we use.
71
- """
72
- if isinstance(script_args, str):
73
- script = script_args
74
- rest: list[str] = []
75
- else:
76
- script = script_args[0]
77
- rest = script_args[1:]
78
- script = f"_{script}_main.py"
79
- return [sys.executable, str(Path(__file__).parent / script), *rest]
80
-
81
-
82
- class _Context(Generic[T]):
83
- def __init__(self, service: T) -> None:
84
- self._service = service
85
-
86
- def __enter__(self) -> T:
87
- return self._service
88
-
89
- def __exit__(
90
- self,
91
- exc_type: type[BaseException] | None,
92
- exc_value: BaseException | None,
93
- traceback: types.TracebackType | None,
94
- ) -> bool:
95
- self._service.shutdown()
96
- return exc_type is None
97
-
98
-
99
- class _Proc(threading.Thread):
100
- def __init__(
101
- self,
102
- service_name: str,
103
- exec_args: Sequence[str],
104
- timeout: int,
105
- on_connection_info_received: Callable[
106
- [ErtServerConnectionInfo | Exception | None], None
107
- ],
108
- project: Path,
109
- ) -> None:
110
- super().__init__()
111
-
112
- self._shutdown = threading.Event()
113
-
114
- self._service_name = service_name
115
- self._exec_args = exec_args
116
- self._timeout = timeout
117
- self._propagate_connection_info_from_childproc = on_connection_info_received
118
- self._service_config_path = project / f"{self._service_name}_server.json"
119
-
120
- fd_read, fd_write = os.pipe()
121
- self._comm_pipe = os.fdopen(fd_read)
122
-
123
- env = os.environ.copy()
124
- env["ERT_COMM_FD"] = str(fd_write)
125
-
126
- SERVICE_CONF_PATHS.add(str(self._service_config_path))
127
-
128
- # The process is waited for in _do_shutdown()
129
- self._childproc = Popen(
130
- self._exec_args,
131
- pass_fds=(fd_write,),
132
- env=env,
133
- close_fds=True,
134
- )
135
- os.close(fd_write)
136
-
137
- def run(self) -> None:
138
- comm = self._read_connection_info_from_process(self._childproc)
139
-
140
- if comm is None:
141
- self._propagate_connection_info_from_childproc(TimeoutError())
142
- return # _read_conn_info() has already cleaned up in this case
143
-
144
- conn_info: ErtServerConnectionInfo | Exception | None = None
145
- try:
146
- conn_info = json.loads(comm)
147
- except json.JSONDecodeError:
148
- conn_info = ServerBootFail()
149
- except Exception as exc:
150
- conn_info = exc
151
-
152
- try:
153
- self._propagate_connection_info_from_childproc(conn_info)
154
-
155
- while True:
156
- if self._childproc.poll() is not None:
157
- break
158
- if self._shutdown.wait(1):
159
- self._do_shutdown()
160
- break
161
-
162
- except Exception as e:
163
- print(str(e))
164
- self.logger.exception(e)
165
-
166
- finally:
167
- self._ensure_connection_info_file_is_deleted()
168
-
169
- def shutdown(self) -> int:
170
- """Shutdown the server."""
171
- self._shutdown.set()
172
- self.join()
173
-
174
- return self._childproc.returncode
175
-
176
- def _read_connection_info_from_process(self, proc: Popen[bytes]) -> str | None:
177
- comm_buf = io.StringIO()
178
- first_iter = True
179
- while first_iter or proc.poll() is None:
180
- first_iter = False
181
- ready = select([self._comm_pipe], [], [], self._timeout)
182
-
183
- # Timeout reached, exit with a failure
184
- if ready == ([], [], []):
185
- self._do_shutdown()
186
- self._ensure_connection_info_file_is_deleted()
187
- return None
188
-
189
- x = self._comm_pipe.read(PIPE_BUF)
190
- if not x: # EOF
191
- break
192
- comm_buf.write(x)
193
- return comm_buf.getvalue()
194
-
195
- def _do_shutdown(self) -> None:
196
- if self._childproc is None:
197
- return
198
- try:
199
- self._childproc.terminate()
200
- self._childproc.wait(10) # Give it 10s to shut down cleanly..
201
- except TimeoutExpired:
202
- try:
203
- self._childproc.kill() # ... then kick it harder...
204
- self._childproc.wait(self._timeout) # ... and wait again
205
- except TimeoutExpired:
206
- self.logger.error(
207
- f"waiting for child-process exceeded timeout {self._timeout}s"
208
- )
209
-
210
- def _ensure_connection_info_file_is_deleted(self) -> None:
211
- """
212
- Ensure that the JSON connection information file is deleted
213
- """
214
- with contextlib.suppress(OSError):
215
- if self._service_config_path.exists():
216
- self._service_config_path.unlink()
217
-
218
- @property
219
- def logger(self) -> Logger:
220
- return getLogger(f"ert.shared.{self._service_name}")
221
-
222
-
223
- class ServerBootFail(RuntimeError):
224
- pass
225
-
226
-
227
- class BaseService:
228
- """
229
- BaseService provides a block-only-when-needed mechanism for starting and
230
- maintaining services as subprocesses.
231
-
232
- This is achieved by using a POSIX communication pipe, over which the service
233
- can communicate that it has started. The contents of the communication is
234
- also written to a file inside of the ERT storage directory.
235
-
236
- The service itself can implement the other side of the pipe as such::
237
-
238
- import os
239
-
240
- # ... perform initialisation ...
241
-
242
- # BaseService provides this environment variable with the pipe's FD
243
- comm_fd = os.environ["ERT_COMM_FD"]
244
-
245
- # Open the pipe with Python's IO classes for ease of use
246
- with os.fdopen(comm_fd, "wb") as comm:
247
- # Write JSON over the pipe, which will be interpreted by a subclass
248
- # of BaseService on ERT's side
249
- comm.write('{"some": "json"}')
250
-
251
- # The pipe is flushed and closed here. This tells BaseService that
252
- # initialisation is finished and it will try to read the JSON data.
253
- """
254
-
255
- _instance: BaseService | None = None
256
-
257
- def __init__(
258
- self,
259
- exec_args: Sequence[str] = (),
260
- timeout: int = 120,
261
- conn_info: ErtServerConnectionInfo | Exception | None = None,
262
- project: str | None = None,
263
- ) -> None:
264
- self._exec_args = exec_args
265
- self._timeout = timeout
266
-
267
- self._proc: _Proc | None = None
268
- self._conn_info: ErtServerConnectionInfo | Exception | None = conn_info
269
- self._conn_info_event = threading.Event()
270
- self._project = Path(project) if project is not None else Path.cwd()
271
-
272
- # Flag that we have connection information
273
- if self._conn_info:
274
- self._conn_info_event.set()
275
- else:
276
- self._proc = _Proc(
277
- self.service_name, exec_args, timeout, self.set_conn_info, self._project
278
- )
279
-
280
- @classmethod
281
- def start_server(cls, *args: Any, **kwargs: Any) -> _Context[Self]:
282
- if cls._instance is not None:
283
- raise RuntimeError("Server already running")
284
- cls._instance = obj = cls(*args, **kwargs)
285
- if obj._proc is not None:
286
- obj._proc.start()
287
- return _Context(obj)
288
-
289
- @classmethod
290
- def connect(
291
- cls,
292
- *,
293
- project: os.PathLike[str],
294
- timeout: int | None = None,
295
- ) -> Self:
296
- if cls._instance is not None:
297
- cls._instance.wait_until_ready()
298
- assert isinstance(cls._instance, cls)
299
- return cls._instance
300
-
301
- path = Path(project)
302
- name = f"{cls.service_name}_server.json"
303
- # Note: If the caller actually pass None, we override that here...
304
- if timeout is None:
305
- timeout = 240
306
- t = -1
307
- while t < timeout:
308
- if (path / name).exists():
309
- with (path / name).open() as f:
310
- return cls((), conn_info=json.load(f), project=str(path))
311
-
312
- sleep(1)
313
- t += 1
314
-
315
- raise TimeoutError("Server not started")
316
-
317
- def wait_until_ready(self, timeout: int | None = None) -> bool:
318
- if timeout is None:
319
- timeout = self._timeout
320
-
321
- if self._conn_info_event.wait(timeout):
322
- return not (
323
- self._conn_info is None or isinstance(self._conn_info, Exception)
324
- )
325
- if isinstance(self._conn_info, TimeoutError):
326
- self.logger.critical(f"startup exceeded defined timeout {timeout}s")
327
- return False # Timeout reached
328
-
329
- def wait(self) -> None:
330
- if self._proc is not None:
331
- self._proc.join()
332
-
333
- def set_conn_info(self, info: ErtServerConnectionInfo | Exception | None) -> None:
334
- if self._conn_info is not None:
335
- raise ValueError("Connection information already set")
336
- if info is None:
337
- raise ValueError
338
- self._conn_info = info
339
-
340
- if self._project is not None:
341
- if not Path(self._project).exists():
342
- raise RuntimeError(f"No storage exists at : {self._project}")
343
- path = f"{self._project}/{self.service_name}_server.json"
344
- else:
345
- path = f"{self.service_name}_server.json"
346
-
347
- if isinstance(info, Mapping):
348
- with NamedTemporaryFile(dir=f"{self._project}", delete=False) as f:
349
- f.write(json.dumps(info, indent=4).encode("utf-8"))
350
- f.flush()
351
- os.rename(f.name, path)
352
-
353
- self._conn_info_event.set()
354
-
355
- def fetch_conn_info(self) -> Mapping[str, Any]:
356
- is_ready = self.wait_until_ready(self._timeout)
357
- if isinstance(self._conn_info, Exception):
358
- raise self._conn_info
359
- if not is_ready:
360
- raise TimeoutError
361
- if self._conn_info is None:
362
- raise ValueError("conn_info is None")
363
- return self._conn_info
364
-
365
- def shutdown(self) -> int:
366
- """Shutdown the server."""
367
- if self._proc is None:
368
- return -1
369
- self.__class__._instance = None
370
- proc, self._proc = self._proc, None
371
- return proc.shutdown()
372
-
373
- @property
374
- def service_name(self) -> str:
375
- """
376
- Subclass should return the name of the service, eg 'storage' for ERT Storage.
377
- Used for identifying the server information JSON file.
378
- """
379
- raise NotImplementedError
380
-
381
- @property
382
- def logger(self) -> Logger:
383
- return getLogger(f"ert.shared.{self.service_name}")
384
-
385
- @property
386
- def _service_file(self) -> str:
387
- return f"{self.service_name}_server.json"