ert 18.0.9__py3-none-any.whl → 19.0.0__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 (116) hide show
  1. _ert/forward_model_runner/client.py +6 -2
  2. ert/__main__.py +20 -6
  3. ert/cli/main.py +7 -3
  4. ert/config/__init__.py +3 -4
  5. ert/config/_create_observation_dataframes.py +85 -59
  6. ert/config/_get_num_cpu.py +1 -1
  7. ert/config/_observations.py +106 -31
  8. ert/config/distribution.py +1 -1
  9. ert/config/ensemble_config.py +3 -3
  10. ert/config/ert_config.py +50 -0
  11. ert/config/{ext_param_config.py → everest_control.py} +8 -12
  12. ert/config/everest_response.py +3 -5
  13. ert/config/field.py +76 -14
  14. ert/config/forward_model_step.py +12 -9
  15. ert/config/gen_data_config.py +3 -4
  16. ert/config/gen_kw_config.py +2 -12
  17. ert/config/parameter_config.py +1 -16
  18. ert/config/parsing/_option_dict.py +10 -2
  19. ert/config/parsing/config_keywords.py +1 -0
  20. ert/config/parsing/config_schema.py +8 -0
  21. ert/config/parsing/config_schema_deprecations.py +3 -3
  22. ert/config/parsing/config_schema_item.py +12 -3
  23. ert/config/parsing/context_values.py +3 -3
  24. ert/config/parsing/file_context_token.py +1 -1
  25. ert/config/parsing/observations_parser.py +12 -2
  26. ert/config/parsing/queue_system.py +9 -0
  27. ert/config/queue_config.py +0 -1
  28. ert/config/response_config.py +0 -1
  29. ert/config/rft_config.py +78 -33
  30. ert/config/summary_config.py +1 -2
  31. ert/config/surface_config.py +59 -16
  32. ert/dark_storage/common.py +1 -1
  33. ert/dark_storage/compute/misfits.py +4 -1
  34. ert/dark_storage/endpoints/compute/misfits.py +4 -2
  35. ert/dark_storage/endpoints/experiment_server.py +12 -9
  36. ert/dark_storage/endpoints/experiments.py +2 -2
  37. ert/dark_storage/endpoints/observations.py +14 -4
  38. ert/dark_storage/endpoints/parameters.py +2 -18
  39. ert/dark_storage/endpoints/responses.py +10 -5
  40. ert/dark_storage/json_schema/experiment.py +1 -1
  41. ert/data/_measured_data.py +6 -5
  42. ert/ensemble_evaluator/config.py +2 -1
  43. ert/field_utils/field_utils.py +1 -1
  44. ert/field_utils/roff_io.py +1 -1
  45. ert/gui/__init__.py +5 -2
  46. ert/gui/ertnotifier.py +1 -1
  47. ert/gui/ertwidgets/pathchooser.py +0 -3
  48. ert/gui/ertwidgets/suggestor/suggestor.py +63 -30
  49. ert/gui/main.py +27 -5
  50. ert/gui/main_window.py +0 -5
  51. ert/gui/simulation/experiment_panel.py +12 -3
  52. ert/gui/simulation/run_dialog.py +2 -16
  53. ert/gui/tools/manage_experiments/export_dialog.py +136 -0
  54. ert/gui/tools/manage_experiments/storage_info_widget.py +133 -28
  55. ert/gui/tools/plot/plot_api.py +24 -15
  56. ert/gui/tools/plot/plot_widget.py +19 -4
  57. ert/gui/tools/plot/plot_window.py +35 -18
  58. ert/gui/tools/plot/plottery/plots/__init__.py +2 -0
  59. ert/gui/tools/plot/plottery/plots/cesp.py +3 -1
  60. ert/gui/tools/plot/plottery/plots/distribution.py +6 -1
  61. ert/gui/tools/plot/plottery/plots/ensemble.py +3 -1
  62. ert/gui/tools/plot/plottery/plots/gaussian_kde.py +12 -2
  63. ert/gui/tools/plot/plottery/plots/histogram.py +3 -1
  64. ert/gui/tools/plot/plottery/plots/misfits.py +436 -0
  65. ert/gui/tools/plot/plottery/plots/observations.py +18 -4
  66. ert/gui/tools/plot/plottery/plots/statistics.py +3 -1
  67. ert/gui/tools/plot/plottery/plots/std_dev.py +3 -1
  68. ert/plugins/hook_implementations/workflows/csv_export.py +2 -3
  69. ert/plugins/plugin_manager.py +4 -0
  70. ert/resources/forward_models/run_reservoirsimulator.py +8 -3
  71. ert/run_models/_create_run_path.py +3 -3
  72. ert/run_models/everest_run_model.py +13 -11
  73. ert/run_models/initial_ensemble_run_model.py +2 -2
  74. ert/run_models/run_model.py +9 -0
  75. ert/services/_base_service.py +6 -5
  76. ert/services/ert_server.py +4 -4
  77. ert/shared/_doc_utils/__init__.py +4 -2
  78. ert/shared/net_utils.py +43 -18
  79. ert/shared/version.py +3 -3
  80. ert/storage/__init__.py +2 -0
  81. ert/storage/local_ensemble.py +25 -8
  82. ert/storage/local_experiment.py +2 -2
  83. ert/storage/local_storage.py +45 -25
  84. ert/storage/migration/to11.py +1 -1
  85. ert/storage/migration/to18.py +0 -1
  86. ert/storage/migration/to19.py +34 -0
  87. ert/storage/migration/to20.py +23 -0
  88. ert/storage/migration/to21.py +25 -0
  89. ert/storage/migration/to22.py +18 -0
  90. ert/storage/migration/to23.py +49 -0
  91. ert/workflow_runner.py +2 -1
  92. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/METADATA +1 -1
  93. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/RECORD +111 -109
  94. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/WHEEL +1 -1
  95. everest/bin/everlint_script.py +0 -2
  96. everest/bin/utils.py +2 -1
  97. everest/bin/visualization_script.py +4 -11
  98. everest/config/control_config.py +4 -4
  99. everest/config/control_variable_config.py +2 -2
  100. everest/config/everest_config.py +9 -0
  101. everest/config/utils.py +2 -2
  102. everest/config/validation_utils.py +7 -1
  103. everest/config_file_loader.py +0 -2
  104. everest/detached/client.py +3 -3
  105. everest/everest_storage.py +0 -2
  106. everest/gui/everest_client.py +2 -2
  107. everest/optimizer/everest2ropt.py +4 -4
  108. everest/optimizer/opt_model_transforms.py +2 -2
  109. ert/config/violations.py +0 -0
  110. ert/gui/tools/export/__init__.py +0 -3
  111. ert/gui/tools/export/export_panel.py +0 -83
  112. ert/gui/tools/export/export_tool.py +0 -69
  113. ert/gui/tools/export/exporter.py +0 -36
  114. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/entry_points.txt +0 -0
  115. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/licenses/COPYING +0 -0
  116. {ert-18.0.9.dist-info → ert-19.0.0.dist-info}/top_level.txt +0 -0
@@ -141,13 +141,14 @@ class MeasuredData:
141
141
 
142
142
  # Pandas differentiates vs int and str keys.
143
143
  # Legacy-wise we use int keys for realizations
144
- pddf.rename(
145
- columns={str(k): int(k) for k in active_realizations},
146
- inplace=True,
144
+ pddf = (
145
+ pddf.rename(
146
+ columns={str(k): int(k) for k in active_realizations},
147
+ )
148
+ .set_index(["observation_key", "key_index"])
149
+ .transpose()
147
150
  )
148
151
 
149
- pddf = pddf.set_index(["observation_key", "key_index"]).transpose()
150
-
151
152
  return pddf
152
153
 
153
154
 
@@ -27,6 +27,7 @@ class EvaluatorServerConfig:
27
27
  use_token: bool = True,
28
28
  host: str | None = None,
29
29
  use_ipc_protocol: bool = True,
30
+ prioritize_private_ip_address: bool = False,
30
31
  ) -> None:
31
32
  self.host: str | None = host
32
33
  self.router_port: int | None = None
@@ -50,7 +51,7 @@ class EvaluatorServerConfig:
50
51
  if use_ipc_protocol:
51
52
  self.uri = f"ipc:///tmp/socket-{uuid.uuid4().hex[:8]}"
52
53
  elif self.host is None:
53
- self.host = get_ip_address()
54
+ self.host = get_ip_address(prioritize_private_ip_address)
54
55
 
55
56
  if use_token:
56
57
  self.server_public_key, self.server_secret_key = zmq.curve_keypair()
@@ -238,7 +238,7 @@ def read_field(
238
238
  ext = path.suffix
239
239
  raise ValueError(f'Could not read {field_path}. Unrecognized suffix "{ext}"')
240
240
 
241
- return np.ma.MaskedArray(data=values, fill_value=np.nan) # type: ignore
241
+ return np.ma.MaskedArray(data=values, fill_value=np.nan)
242
242
 
243
243
 
244
244
  def save_field(
@@ -23,7 +23,7 @@ def export_roff(
23
23
  binary: bool,
24
24
  ) -> None:
25
25
  dimensions = data.shape
26
- data = np.flip(data, -1).ravel() # type: ignore
26
+ data = np.flip(data, -1).ravel()
27
27
  data = data.astype(np.float32).filled(RMS_UNDEFINED_FLOAT) # type: ignore
28
28
  if not np.isfinite(data).all():
29
29
  raise ValueError(
ert/gui/__init__.py CHANGED
@@ -20,12 +20,15 @@ else:
20
20
 
21
21
  def is_high_contrast_mode() -> bool:
22
22
  app = cast(QWidget, QApplication.instance())
23
- return app.palette().color(QPalette.ColorRole.Window).lightness() > 245
23
+ return (
24
+ app is not None
25
+ and app.palette().color(QPalette.ColorRole.Window).lightness() > 245
26
+ )
24
27
 
25
28
 
26
29
  def is_dark_mode() -> bool:
27
30
  app = cast(QWidget, QApplication.instance())
28
- return app.palette().base().color().value() < 70
31
+ return app is not None and app.palette().base().color().value() < 70
29
32
 
30
33
 
31
34
  __version__ = ert.shared.__version__
ert/gui/ertnotifier.py CHANGED
@@ -74,7 +74,7 @@ class ErtNotifier(QObject):
74
74
  @Slot()
75
75
  def emitErtChange(self) -> None:
76
76
  if self._storage is not None:
77
- self._storage.refresh()
77
+ self._storage.reload()
78
78
 
79
79
  self.ertChanged.emit()
80
80
 
@@ -83,7 +83,6 @@ class PathChooser(QWidget):
83
83
  if self._model.pathMustExist():
84
84
  valid = False
85
85
  message = PathChooser.PATH_DOES_NOT_EXIST_MSG
86
- # todo: check if new (non-existing) file has directory or file format?
87
86
  elif path_exists:
88
87
  if self._model.pathMustBeExecutable() and is_file and not is_executable:
89
88
  valid = False
@@ -122,8 +121,6 @@ class PathChooser(QWidget):
122
121
 
123
122
  def selectPath(self) -> None:
124
123
  """Pops up the 'select a file/directory' dialog"""
125
- # todo: This probably needs some reworking to work properly with
126
- # different scenarios... (file + dir)
127
124
  self._editing = True
128
125
  current_directory = self.getPath()
129
126
 
@@ -19,9 +19,11 @@ from PyQt6.QtWidgets import (
19
19
  QVBoxLayout,
20
20
  QWidget,
21
21
  )
22
+ from typing_extensions import override
22
23
 
23
24
  from ert.gui import is_dark_mode
24
25
 
26
+ from .. import CopyButton
25
27
  from ._colors import BLUE_TEXT
26
28
  from ._suggestor_message import SuggestorMessage
27
29
 
@@ -105,6 +107,32 @@ QPushButton:hover {{
105
107
  """
106
108
 
107
109
 
110
+ class _CopyAllButton(CopyButton):
111
+ def __init__(
112
+ self,
113
+ errors: list[ErrorInfo],
114
+ warnings: list[WarningInfo],
115
+ deprecations: list[WarningInfo],
116
+ ) -> None:
117
+ super().__init__()
118
+ self.setText(" Copy all messages")
119
+ self.all_messages = "\n\n".join(
120
+ [
121
+ f"{info.message}" + (f"\n{info.location()}" if info.location() else "")
122
+ for info in (errors + warnings + deprecations)
123
+ ]
124
+ )
125
+ self.setStyleSheet(SECONDARY_BUTTON_STYLE)
126
+
127
+ @override
128
+ def copy(self) -> None:
129
+ logger.info(
130
+ "Copy all button in Suggestor used. "
131
+ f"Copied {len(self.all_messages)} characters"
132
+ )
133
+ self.copy_text(self.all_messages)
134
+
135
+
108
136
  class Suggestor(QWidget):
109
137
  def __init__(
110
138
  self,
@@ -145,7 +173,24 @@ class Suggestor(QWidget):
145
173
  data_layout.addWidget(self._help_panel(help_links))
146
174
  self.__layout.addWidget(data_widget)
147
175
 
148
- self.__layout.addWidget(self._action_buttons())
176
+ action_buttons = QWidget(parent=self)
177
+ action_buttons_layout = QHBoxLayout()
178
+ action_buttons_layout.setContentsMargins(0, 24, 0, 0)
179
+
180
+ if any([errors, warnings, deprecations]):
181
+ action_buttons_layout.addWidget(
182
+ _CopyAllButton(errors, warnings, deprecations)
183
+ )
184
+
185
+ action_buttons_layout.addStretch()
186
+
187
+ if continue_action:
188
+ action_buttons_layout.addWidget(self._continue_button())
189
+
190
+ action_buttons_layout.addWidget(self._close_button())
191
+
192
+ action_buttons.setLayout(action_buttons_layout)
193
+ self.__layout.addWidget(action_buttons)
149
194
 
150
195
  def _help_panel(self, help_links: dict[str, str]) -> QFrame:
151
196
  help_button_frame = QFrame(parent=self)
@@ -204,35 +249,23 @@ class Suggestor(QWidget):
204
249
  area_layout.addWidget(self._messages(errors, warnings, deprecations))
205
250
  return problem_area
206
251
 
207
- def _action_buttons(self) -> QWidget:
208
- def run_pressed() -> None:
209
- assert self._continue_action
210
- self._continue_action()
211
- self.close()
212
-
213
- buttons = QWidget(parent=self)
214
- buttons_layout = QHBoxLayout()
215
- buttons_layout.insertStretch(-1, -1)
216
- buttons_layout.setContentsMargins(0, 24, 0, 0)
217
-
218
- give_up = QPushButton("Close")
219
- give_up.setObjectName("close_button")
220
- give_up.setStyleSheet(BUTTON_STYLE)
221
- give_up.pressed.connect(self.close)
222
-
223
- if self._continue_action:
224
- run = QPushButton("Open ERT")
225
- run.setStyleSheet(BUTTON_STYLE)
226
- run.setObjectName("run_ert_button")
227
- run.pressed.connect(run_pressed)
228
- buttons_layout.addWidget(run)
229
-
230
- give_up.setStyleSheet(SECONDARY_BUTTON_STYLE)
231
-
232
- buttons_layout.addWidget(give_up)
233
- buttons.setLayout(buttons_layout)
234
-
235
- return buttons
252
+ def _continue_button(self) -> QWidget:
253
+ assert self._continue_action
254
+ continue_button = QPushButton("Open ERT")
255
+ continue_button.setStyleSheet(BUTTON_STYLE)
256
+ continue_button.setObjectName("run_ert_button")
257
+ continue_button.pressed.connect(self._continue_action)
258
+ continue_button.pressed.connect(self.close)
259
+ return continue_button
260
+
261
+ def _close_button(self) -> QPushButton:
262
+ close_button = QPushButton("Close")
263
+ close_button.setObjectName("close_button")
264
+ close_button.pressed.connect(self.close)
265
+ close_button.setStyleSheet(
266
+ SECONDARY_BUTTON_STYLE if self._continue_action else BUTTON_STYLE
267
+ )
268
+ return close_button
236
269
 
237
270
  def _messages(
238
271
  self,
ert/gui/main.py CHANGED
@@ -14,7 +14,7 @@ from signal import SIG_DFL, SIGINT, signal
14
14
  from opentelemetry.trace import Status, StatusCode
15
15
  from PyQt6.QtCore import QDir
16
16
  from PyQt6.QtGui import QIcon
17
- from PyQt6.QtWidgets import QApplication, QWidget
17
+ from PyQt6.QtWidgets import QApplication, QMessageBox, QWidget
18
18
 
19
19
  from ert.config import (
20
20
  ErrorInfo,
@@ -33,8 +33,8 @@ from ert.storage import (
33
33
  ErtStorageException,
34
34
  LocalStorage,
35
35
  local_storage_set_ert_config,
36
- open_storage,
37
36
  )
37
+ from ert.storage.local_storage import _LOCAL_STORAGE_VERSION, _storage_version
38
38
  from ert.trace import trace, tracer
39
39
 
40
40
  from .ertwidgets import Suggestor
@@ -164,9 +164,31 @@ def _start_initial_gui_window(
164
164
  if ert_config is not None:
165
165
  try:
166
166
  if LocalStorage.check_migration_needed(Path(ert_config.ens_path)):
167
- # Open in write mode to initialize the storage, so that
168
- # dark storage can be mounted onto it
169
- open_storage(ert_config.ens_path, mode="w").close()
167
+ storage_version = _storage_version(Path(ert_config.ens_path))
168
+ current_version = _LOCAL_STORAGE_VERSION
169
+
170
+ migrate_dialog = QMessageBox.warning(
171
+ None,
172
+ f"Migrate storage to version {current_version}?",
173
+ f"Ert storage is version {storage_version} and needs to be migrated"
174
+ f" to version {current_version} to be compatible with the current"
175
+ f" version of Ert\n\n"
176
+ f"After migration, this storage can only be opened with current or"
177
+ f" later versions of Ert\n\n"
178
+ "Do you wish to continue migrating storage?\n",
179
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
180
+ )
181
+
182
+ if migrate_dialog == QMessageBox.StandardButton.Yes:
183
+ logger.info(
184
+ f"Migrating from version {storage_version} to"
185
+ f" version {current_version}"
186
+ )
187
+ else:
188
+ logger.info("Storage migration cancelled by user")
189
+ os._exit(0)
190
+
191
+ LocalStorage.perform_migration(Path(ert_config.ens_path))
170
192
  storage_path = ert_config.ens_path
171
193
  except ErtStorageException as err:
172
194
  validation_messages.errors.append(
ert/gui/main_window.py CHANGED
@@ -31,7 +31,6 @@ from ert.gui.find_ert_info import find_ert_info
31
31
  from ert.gui.simulation import ExperimentPanel
32
32
  from ert.gui.simulation.run_dialog import RunDialog
33
33
  from ert.gui.tools.event_viewer import EventViewerTool, GUILogHandler
34
- from ert.gui.tools.export import ExportTool
35
34
  from ert.gui.tools.load_results import LoadResultsTool
36
35
  from ert.gui.tools.manage_experiments import ManageExperimentsPanel
37
36
  from ert.gui.tools.plot.plot_window import PlotWindow
@@ -373,10 +372,6 @@ class ErtMainWindow(QMainWindow):
373
372
  tools_menu.addAction(self._event_viewer_tool.getAction())
374
373
  self.close_signal.connect(self._event_viewer_tool.close_wnd)
375
374
 
376
- self.export_tool = ExportTool(self.ert_config, self.notifier)
377
- self.export_tool.setParent(self)
378
- tools_menu.addAction(self.export_tool.getAction())
379
-
380
375
  self.workflows_tool = WorkflowsTool(self.ert_config, self.notifier)
381
376
  self.workflows_tool.setParent(self)
382
377
  tools_menu.addAction(self.workflows_tool.getAction())
@@ -55,9 +55,15 @@ def create_md_table(kv: dict[str, str], output: str) -> str:
55
55
 
56
56
 
57
57
  def get_simulation_thread(
58
- model: Any, rerun_failed_realizations: bool = False, use_ipc_protocol: bool = False
58
+ model: Any,
59
+ rerun_failed_realizations: bool = False,
60
+ use_ipc_protocol: bool = False,
61
+ prioritize_private_ip_address: bool = False,
59
62
  ) -> ErtThread:
60
- evaluator_server_config = EvaluatorServerConfig(use_ipc_protocol=use_ipc_protocol)
63
+ evaluator_server_config = EvaluatorServerConfig(
64
+ use_ipc_protocol=use_ipc_protocol,
65
+ prioritize_private_ip_address=prioritize_private_ip_address,
66
+ )
61
67
 
62
68
  def run() -> None:
63
69
  model.api.start_simulations_thread(
@@ -368,7 +374,9 @@ class ExperimentPanel(QWidget):
368
374
  run_path=Path(self.config.runpath_config.runpath_format_string),
369
375
  storage_path=self._notifier.storage.path,
370
376
  )
371
- self._dialog.set_queue_system_name(model.queue_config.queue_system)
377
+ self._dialog.queue_system.setText(
378
+ f"Queue system:\n{model.queue_config.queue_system.formatted_name}"
379
+ )
372
380
  self.experiment_started.emit(self._dialog)
373
381
  self._simulation_done = False
374
382
  self.run_button.setEnabled(self._simulation_done)
@@ -379,6 +387,7 @@ class ExperimentPanel(QWidget):
379
387
  rerun_failed_realizations,
380
388
  use_ipc_protocol=self.config.queue_config.queue_system
381
389
  == QueueSystem.LOCAL,
390
+ prioritize_private_ip_address=self.config.prioritize_private_ip_address,
382
391
  )
383
392
  self._dialog.setup_event_monitoring(rerun_failed_realizations)
384
393
  simulation_thread.start()
@@ -4,7 +4,7 @@ import logging
4
4
  from datetime import datetime
5
5
  from pathlib import Path
6
6
  from queue import SimpleQueue
7
- from typing import assert_never, cast
7
+ from typing import cast
8
8
 
9
9
  import humanize
10
10
  from PyQt6.QtCore import QModelIndex, QSize, Qt, QThread, QTimer
@@ -32,7 +32,7 @@ from PyQt6.QtWidgets import (
32
32
  from typing_extensions import override
33
33
 
34
34
  from _ert.events import EnsembleEvaluationWarning
35
- from ert.config import ErrorInfo, QueueSystem, WarningInfo
35
+ from ert.config import ErrorInfo, WarningInfo
36
36
  from ert.ensemble_evaluator import (
37
37
  EndEvent,
38
38
  FullSnapshotEvent,
@@ -666,20 +666,6 @@ class RunDialog(QFrame):
666
666
  self.rerun_failed_realizations_experiment.emit()
667
667
  self.set_show_warning_button_to_initial_state()
668
668
 
669
- def set_queue_system_name(self, queue_system: QueueSystem) -> None:
670
- match queue_system:
671
- case QueueSystem.LSF:
672
- formatted_queue_system = "LSF"
673
- case QueueSystem.LOCAL:
674
- formatted_queue_system = "Local"
675
- case QueueSystem.TORQUE:
676
- formatted_queue_system = "Torque/OpenPBS"
677
- case QueueSystem.SLURM:
678
- formatted_queue_system = "Slurm"
679
- case default:
680
- assert_never(default)
681
- self.queue_system.setText(f"Queue system:\n{formatted_queue_system}")
682
-
683
669
  @override
684
670
  def hideEvent(self, event: QHideEvent | None) -> None:
685
671
  for file_dialog in self.findChildren(FileDialog):
@@ -0,0 +1,136 @@
1
+ import contextlib
2
+ from pathlib import Path
3
+ from typing import cast
4
+
5
+ import polars as pl
6
+ from PyQt6.QtCore import (
7
+ Qt,
8
+ )
9
+ from PyQt6.QtCore import (
10
+ pyqtSlot as Slot,
11
+ )
12
+ from PyQt6.QtGui import QColor, QPalette
13
+ from PyQt6.QtWidgets import (
14
+ QDialog,
15
+ QDialogButtonBox,
16
+ QFileDialog,
17
+ QHBoxLayout,
18
+ QLineEdit,
19
+ QPushButton,
20
+ QTextEdit,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ )
24
+
25
+
26
+ class ExportDialog(QDialog):
27
+ """Base dialog for exporting ensemble-related data to files."""
28
+
29
+ def __init__(
30
+ self,
31
+ export_data: pl.DataFrame,
32
+ window_title: str = "Export data",
33
+ parent: QWidget | None = None,
34
+ ) -> None:
35
+ QDialog.__init__(self, parent)
36
+ self._export_data = export_data
37
+ self.setWindowTitle(window_title)
38
+
39
+ self.setWindowFlags(Qt.WindowType.Dialog)
40
+ self.setModal(True)
41
+ self.setMinimumWidth(450)
42
+ self.setMinimumHeight(200)
43
+
44
+ main_layout = QVBoxLayout()
45
+
46
+ file_layout = QHBoxLayout()
47
+ self._file_path_edit = QLineEdit(self)
48
+ self._file_path_edit.setPlaceholderText("Select output file...")
49
+ self._file_path_edit.textChanged.connect(self.validate_file)
50
+ browse_button = QPushButton("Browse...", self)
51
+ browse_button.clicked.connect(self.browse_file)
52
+ file_layout.addWidget(self._file_path_edit)
53
+ file_layout.addWidget(browse_button)
54
+ main_layout.addLayout(file_layout)
55
+
56
+ self._export_text_area = QTextEdit(self)
57
+ self._export_text_area.setReadOnly(True)
58
+ self._export_text_area.setFixedHeight(100)
59
+ main_layout.addWidget(self._export_text_area)
60
+
61
+ button_box = QDialogButtonBox(self)
62
+ button_box.setStandardButtons(QDialogButtonBox.StandardButton.Cancel)
63
+ button_box.rejected.connect(self.cancel)
64
+
65
+ self._export_button = cast(
66
+ QPushButton,
67
+ button_box.addButton("Export", QDialogButtonBox.ButtonRole.AcceptRole),
68
+ )
69
+ self._export_button.clicked.connect(self.export)
70
+ self._export_button.setEnabled(False) # Initially disabled
71
+ main_layout.addWidget(button_box)
72
+
73
+ self.setLayout(main_layout)
74
+
75
+ @Slot()
76
+ def export(self) -> None:
77
+ self._export_text_area.insertPlainText("Exporting...\n")
78
+ try:
79
+ output_file: str = self._file_path_edit.text().strip()
80
+ self._export_button.setEnabled(False)
81
+ self._export_data.write_csv(output_file, float_precision=6)
82
+ self._export_text_area.insertPlainText(f"Data exported to: {output_file}\n")
83
+ except Exception as e:
84
+ self._export_text_area.insertHtml(
85
+ f"<span style='color: red;'>Could not export data: {e!s}</span><br>"
86
+ )
87
+ finally:
88
+ self._export_button.setEnabled(True)
89
+
90
+ @Slot()
91
+ def cancel(self) -> None:
92
+ self.reject()
93
+
94
+ @Slot()
95
+ def validate_file(self) -> None:
96
+ """Validation to check if the file path is not empty or invalid."""
97
+
98
+ def _set_invalid(tooltip_text: str = "Invalid file path") -> None:
99
+ palette = self._file_path_edit.palette()
100
+ palette.setColor(QPalette.ColorRole.Text, QColor("red"))
101
+ self._file_path_edit.setPalette(palette)
102
+ self._file_path_edit.setToolTip(tooltip_text)
103
+ self._export_button.setToolTip(tooltip_text)
104
+ self._export_button.setEnabled(False)
105
+
106
+ def _set_valid() -> None:
107
+ palette = self._file_path_edit.palette()
108
+ palette.setColor(QPalette.ColorRole.Text, QColor("black"))
109
+ self._file_path_edit.setPalette(palette)
110
+ self._file_path_edit.setToolTip("")
111
+ self._export_button.setToolTip("")
112
+ self._export_button.setEnabled(True)
113
+
114
+ path = Path(self._file_path_edit.text().strip())
115
+ if str(path) in {"", "."}:
116
+ _set_invalid(tooltip_text="No filename provided")
117
+ return
118
+
119
+ if path.is_dir():
120
+ _set_invalid(tooltip_text=f"'{path!s}' is an existing directory.")
121
+ return
122
+
123
+ with contextlib.suppress(Exception):
124
+ if path.parent.is_dir():
125
+ _set_valid()
126
+ return
127
+
128
+ _set_invalid()
129
+
130
+ @Slot()
131
+ def browse_file(self) -> None:
132
+ file_path, _ = QFileDialog.getSaveFileName(
133
+ self, "Select Output File", "", "All Files (*)"
134
+ )
135
+ if file_path:
136
+ self._file_path_edit.setText(file_path)