ert 18.0.9__py3-none-any.whl → 19.0.0rc0__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 (117) hide show
  1. _ert/forward_model_runner/client.py +6 -2
  2. ert/__main__.py +20 -6
  3. ert/analysis/_es_update.py +6 -19
  4. ert/cli/main.py +7 -3
  5. ert/config/__init__.py +3 -4
  6. ert/config/_create_observation_dataframes.py +57 -8
  7. ert/config/_get_num_cpu.py +1 -1
  8. ert/config/_observations.py +77 -1
  9. ert/config/distribution.py +1 -1
  10. ert/config/ensemble_config.py +3 -3
  11. ert/config/ert_config.py +50 -8
  12. ert/config/{ext_param_config.py → everest_control.py} +8 -12
  13. ert/config/everest_response.py +3 -5
  14. ert/config/field.py +76 -14
  15. ert/config/forward_model_step.py +12 -9
  16. ert/config/gen_data_config.py +3 -4
  17. ert/config/gen_kw_config.py +2 -12
  18. ert/config/parameter_config.py +1 -16
  19. ert/config/parsing/_option_dict.py +10 -2
  20. ert/config/parsing/config_keywords.py +1 -0
  21. ert/config/parsing/config_schema.py +8 -0
  22. ert/config/parsing/config_schema_deprecations.py +14 -3
  23. ert/config/parsing/config_schema_item.py +12 -3
  24. ert/config/parsing/context_values.py +3 -3
  25. ert/config/parsing/file_context_token.py +1 -1
  26. ert/config/parsing/observations_parser.py +6 -2
  27. ert/config/parsing/queue_system.py +9 -0
  28. ert/config/queue_config.py +0 -1
  29. ert/config/response_config.py +0 -1
  30. ert/config/rft_config.py +78 -33
  31. ert/config/summary_config.py +1 -2
  32. ert/config/surface_config.py +59 -16
  33. ert/dark_storage/common.py +1 -1
  34. ert/dark_storage/compute/misfits.py +4 -1
  35. ert/dark_storage/endpoints/compute/misfits.py +4 -2
  36. ert/dark_storage/endpoints/experiment_server.py +12 -9
  37. ert/dark_storage/endpoints/experiments.py +2 -2
  38. ert/dark_storage/endpoints/observations.py +4 -2
  39. ert/dark_storage/endpoints/parameters.py +2 -18
  40. ert/dark_storage/endpoints/responses.py +10 -5
  41. ert/dark_storage/json_schema/experiment.py +1 -1
  42. ert/data/_measured_data.py +6 -5
  43. ert/ensemble_evaluator/config.py +2 -1
  44. ert/field_utils/field_utils.py +1 -1
  45. ert/field_utils/grdecl_io.py +9 -26
  46. ert/field_utils/roff_io.py +1 -1
  47. ert/gui/__init__.py +5 -2
  48. ert/gui/ertnotifier.py +1 -1
  49. ert/gui/ertwidgets/pathchooser.py +0 -3
  50. ert/gui/ertwidgets/suggestor/suggestor.py +63 -30
  51. ert/gui/main.py +27 -5
  52. ert/gui/main_window.py +0 -5
  53. ert/gui/simulation/experiment_panel.py +12 -7
  54. ert/gui/simulation/run_dialog.py +2 -16
  55. ert/gui/summarypanel.py +0 -19
  56. ert/gui/tools/manage_experiments/export_dialog.py +136 -0
  57. ert/gui/tools/manage_experiments/storage_info_widget.py +110 -9
  58. ert/gui/tools/plot/plot_api.py +24 -15
  59. ert/gui/tools/plot/plot_widget.py +10 -2
  60. ert/gui/tools/plot/plot_window.py +26 -18
  61. ert/gui/tools/plot/plottery/plots/__init__.py +2 -0
  62. ert/gui/tools/plot/plottery/plots/cesp.py +3 -1
  63. ert/gui/tools/plot/plottery/plots/distribution.py +6 -1
  64. ert/gui/tools/plot/plottery/plots/ensemble.py +3 -1
  65. ert/gui/tools/plot/plottery/plots/gaussian_kde.py +12 -2
  66. ert/gui/tools/plot/plottery/plots/histogram.py +3 -1
  67. ert/gui/tools/plot/plottery/plots/misfits.py +436 -0
  68. ert/gui/tools/plot/plottery/plots/observations.py +18 -4
  69. ert/gui/tools/plot/plottery/plots/statistics.py +3 -1
  70. ert/gui/tools/plot/plottery/plots/std_dev.py +3 -1
  71. ert/plugins/hook_implementations/workflows/csv_export.py +2 -3
  72. ert/plugins/plugin_manager.py +4 -0
  73. ert/resources/forward_models/run_reservoirsimulator.py +8 -3
  74. ert/run_models/_create_run_path.py +3 -3
  75. ert/run_models/everest_run_model.py +13 -11
  76. ert/run_models/initial_ensemble_run_model.py +2 -2
  77. ert/run_models/run_model.py +30 -1
  78. ert/services/_base_service.py +6 -5
  79. ert/services/ert_server.py +4 -4
  80. ert/shared/_doc_utils/__init__.py +4 -2
  81. ert/shared/net_utils.py +43 -18
  82. ert/shared/version.py +3 -3
  83. ert/storage/__init__.py +2 -0
  84. ert/storage/local_ensemble.py +13 -7
  85. ert/storage/local_experiment.py +2 -2
  86. ert/storage/local_storage.py +41 -25
  87. ert/storage/migration/to11.py +1 -1
  88. ert/storage/migration/to18.py +0 -1
  89. ert/storage/migration/to19.py +34 -0
  90. ert/storage/migration/to20.py +23 -0
  91. ert/storage/migration/to21.py +25 -0
  92. ert/workflow_runner.py +2 -1
  93. {ert-18.0.9.dist-info → ert-19.0.0rc0.dist-info}/METADATA +1 -1
  94. {ert-18.0.9.dist-info → ert-19.0.0rc0.dist-info}/RECORD +112 -112
  95. {ert-18.0.9.dist-info → ert-19.0.0rc0.dist-info}/WHEEL +1 -1
  96. everest/bin/everlint_script.py +0 -2
  97. everest/bin/utils.py +2 -1
  98. everest/bin/visualization_script.py +4 -11
  99. everest/config/control_config.py +4 -4
  100. everest/config/control_variable_config.py +2 -2
  101. everest/config/everest_config.py +9 -0
  102. everest/config/utils.py +2 -2
  103. everest/config/validation_utils.py +7 -1
  104. everest/config_file_loader.py +0 -2
  105. everest/detached/client.py +3 -3
  106. everest/everest_storage.py +0 -2
  107. everest/gui/everest_client.py +2 -2
  108. everest/optimizer/everest2ropt.py +4 -4
  109. everest/optimizer/opt_model_transforms.py +2 -2
  110. ert/config/violations.py +0 -0
  111. ert/gui/tools/export/__init__.py +0 -3
  112. ert/gui/tools/export/export_panel.py +0 -83
  113. ert/gui/tools/export/export_tool.py +0 -69
  114. ert/gui/tools/export/exporter.py +0 -36
  115. {ert-18.0.9.dist-info → ert-19.0.0rc0.dist-info}/entry_points.txt +0 -0
  116. {ert-18.0.9.dist-info → ert-19.0.0rc0.dist-info}/licenses/COPYING +0 -0
  117. {ert-18.0.9.dist-info → ert-19.0.0rc0.dist-info}/top_level.txt +0 -0
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,
@@ -29,12 +29,13 @@ from ert.gui.tools.event_viewer import (
29
29
  from ert.namespace import Namespace
30
30
  from ert.plugins import ErtRuntimePlugins, get_site_plugins
31
31
  from ert.services import ErtServer
32
+ from ert.shared import __version__
32
33
  from ert.storage import (
33
34
  ErtStorageException,
34
35
  LocalStorage,
35
36
  local_storage_set_ert_config,
36
- open_storage,
37
37
  )
38
+ from ert.storage.local_storage import _LOCAL_STORAGE_VERSION, _storage_version
38
39
  from ert.trace import trace, tracer
39
40
 
40
41
  from .ertwidgets import Suggestor
@@ -164,9 +165,30 @@ def _start_initial_gui_window(
164
165
  if ert_config is not None:
165
166
  try:
166
167
  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()
168
+ storage_version = _storage_version(Path(ert_config.ens_path))
169
+ current_version = _LOCAL_STORAGE_VERSION
170
+
171
+ migrate_dialog = QMessageBox.warning(
172
+ None,
173
+ f"Migrate storage to version {current_version}?",
174
+ f"Ert storage is version {storage_version} and needs to be migrated"
175
+ f" to version {current_version} to be able to continue\n\n"
176
+ f"After migration, this storage can only be opened with Ert"
177
+ f" version {__version__} or newer\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(
@@ -354,10 +360,6 @@ class ExperimentPanel(QWidget):
354
360
  return
355
361
  QApplication.restoreOverrideCursor()
356
362
 
357
- self.configuration_summary.log_summary(
358
- args.mode, model.get_number_of_active_realizations()
359
- )
360
-
361
363
  self._dialog = RunDialog(
362
364
  f"Experiment - {self._config_file} {find_ert_info()}",
363
365
  model.api,
@@ -368,7 +370,9 @@ class ExperimentPanel(QWidget):
368
370
  run_path=Path(self.config.runpath_config.runpath_format_string),
369
371
  storage_path=self._notifier.storage.path,
370
372
  )
371
- self._dialog.set_queue_system_name(model.queue_config.queue_system)
373
+ self._dialog.queue_system.setText(
374
+ f"Queue system:\n{model.queue_config.queue_system.formatted_name}"
375
+ )
372
376
  self.experiment_started.emit(self._dialog)
373
377
  self._simulation_done = False
374
378
  self.run_button.setEnabled(self._simulation_done)
@@ -379,6 +383,7 @@ class ExperimentPanel(QWidget):
379
383
  rerun_failed_realizations,
380
384
  use_ipc_protocol=self.config.queue_config.queue_system
381
385
  == QueueSystem.LOCAL,
386
+ prioritize_private_ip_address=self.config.prioritize_private_ip_address,
382
387
  )
383
388
  self._dialog.setup_event_monitoring(rerun_failed_realizations)
384
389
  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):
ert/gui/summarypanel.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
3
  from typing import TYPE_CHECKING, Any
5
4
 
6
5
  from PyQt6.QtCore import Qt
@@ -19,8 +18,6 @@ from ert.gui.ertwidgets import ErtSummary
19
18
  if TYPE_CHECKING:
20
19
  from ert.config import ErtConfig
21
20
 
22
- logger = logging.getLogger(__name__)
23
-
24
21
 
25
22
  class SummaryTemplate:
26
23
  def __init__(self, title: str) -> None:
@@ -126,22 +123,6 @@ class SummaryPanel(QFrame):
126
123
 
127
124
  self._layout.addLayout(layout)
128
125
 
129
- def log_summary(self, run_model: str, num_realizations: int) -> None:
130
- summary = ErtSummary(self.config)
131
-
132
- observations = summary.getObservations()
133
- observations_count = sum(e["count"] for e in observations)
134
-
135
- _, parameter_count = summary.get_parameters()
136
-
137
- logger.info(
138
- f"Experiment summary:\n"
139
- f"Runmodel: {run_model}\n"
140
- f"Realizations: {num_realizations}\n"
141
- f"Parameters: {parameter_count}\n"
142
- f"Observations: {observations_count}"
143
- )
144
-
145
126
  @staticmethod
146
127
  def _runlength_encode_list(strings: list[str]) -> list[tuple[str, int]]:
147
128
  """Runlength encode a list of strings.
@@ -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)