completor 1.4.0__tar.gz → 1.5.0__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 (31) hide show
  1. {completor-1.4.0 → completor-1.5.0}/PKG-INFO +11 -3
  2. {completor-1.4.0 → completor-1.5.0}/README.md +8 -0
  3. {completor-1.4.0 → completor-1.5.0}/completor/constants.py +19 -0
  4. {completor-1.4.0 → completor-1.5.0}/completor/create_output.py +12 -3
  5. completor-1.5.0/completor/icv_file_handling.py +362 -0
  6. completor-1.5.0/completor/icv_functions.py +845 -0
  7. completor-1.5.0/completor/initialization.py +563 -0
  8. {completor-1.4.0 → completor-1.5.0}/completor/input_validation.py +63 -0
  9. {completor-1.4.0 → completor-1.5.0}/completor/logger.py +1 -1
  10. {completor-1.4.0 → completor-1.5.0}/completor/main.py +88 -19
  11. {completor-1.4.0 → completor-1.5.0}/completor/parse.py +5 -5
  12. {completor-1.4.0 → completor-1.5.0}/completor/read_casefile.py +429 -48
  13. {completor-1.4.0 → completor-1.5.0}/completor/utils.py +133 -2
  14. {completor-1.4.0 → completor-1.5.0}/pyproject.toml +9 -5
  15. {completor-1.4.0 → completor-1.5.0}/LICENSE +0 -0
  16. {completor-1.4.0 → completor-1.5.0}/completor/__init__.py +0 -0
  17. {completor-1.4.0 → completor-1.5.0}/completor/completion.py +0 -0
  18. {completor-1.4.0 → completor-1.5.0}/completor/config_jobs/run_completor +0 -0
  19. {completor-1.4.0 → completor-1.5.0}/completor/exceptions/__init__.py +0 -0
  20. {completor-1.4.0 → completor-1.5.0}/completor/exceptions/clean_exceptions.py +0 -0
  21. {completor-1.4.0 → completor-1.5.0}/completor/exceptions/exceptions.py +0 -0
  22. {completor-1.4.0 → completor-1.5.0}/completor/get_version.py +0 -0
  23. {completor-1.4.0 → completor-1.5.0}/completor/hook_implementations/__init__.py +0 -0
  24. {completor-1.4.0 → completor-1.5.0}/completor/hook_implementations/forward_model_steps.py +0 -0
  25. {completor-1.4.0 → completor-1.5.0}/completor/hook_implementations/run_completor.py +0 -0
  26. {completor-1.4.0 → completor-1.5.0}/completor/launch_args_parser.py +0 -0
  27. {completor-1.4.0 → completor-1.5.0}/completor/prepare_outputs.py +0 -0
  28. {completor-1.4.0 → completor-1.5.0}/completor/read_schedule.py +0 -0
  29. {completor-1.4.0 → completor-1.5.0}/completor/visualization.py +0 -0
  30. {completor-1.4.0 → completor-1.5.0}/completor/visualize_well.py +0 -0
  31. {completor-1.4.0 → completor-1.5.0}/completor/wells.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: completor
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Advanced multi-segmented well completion tool.
5
5
  Home-page: https://github.com/equinor/completor
6
6
  License: LGPL-3.0-only
@@ -16,14 +16,14 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Provides-Extra: ert
18
18
  Provides-Extra: test
19
- Requires-Dist: ert (>=14,<15) ; extra == "ert"
19
+ Requires-Dist: ert (>14) ; extra == "ert"
20
20
  Requires-Dist: matplotlib (>=3.9,<4.0)
21
21
  Requires-Dist: numpy (>=2.0,<3.0)
22
22
  Requires-Dist: pandas (>=2.2,<3.0)
23
23
  Requires-Dist: pytest (>=8.3,<9.0) ; extra == "test"
24
24
  Requires-Dist: pytest-env (>=1,<2) ; extra == "test"
25
25
  Requires-Dist: pytest-xdist (>=3.6,<4.0) ; extra == "test"
26
- Requires-Dist: rstcheck-core (>=1.2,<2.0) ; extra == "test"
26
+ Requires-Dist: scipy (>=1.14,<2.0)
27
27
  Requires-Dist: tqdm (>=4.66,<5.0)
28
28
  Project-URL: Bug Tracker, https://github.com/equinor/completor/issues
29
29
  Project-URL: Documentation, https://equinor.github.io/completor
@@ -76,6 +76,14 @@ This should run for some time and display a help message.
76
76
  ### Create and run your first model
77
77
  Some examples of Completor case file are available in [Examples](documentation/docs/about/examples.mdx) and detailed explanation is available in [Configuration](documentation/docs/about/configuration.mdx).
78
78
 
79
+ ### References
80
+ Some technical paper related to Completor and ICV Control applications are available to read and cite:
81
+ - Handita Sutoyo, Filippo Panini, Cuthbert Shang Wui Ng, Martin Halvorsen, Geir Elseth, Ingvild Berg Martiniussen, Corentin Cochard, Erik Johan Helland, Sean Robert Smith, and Lene Amundsen.
82
+ "Maximize Value from Inflow Control Technology Implementations Through Novel Standardized Workflow and Open-Source Modelling Tool."
83
+ Paper presented at the OTC Brasil, Rio de Janeiro, Brazil, October 2025. doi: [10.4043/36167-MS](https://doi.org/10.4043/36167-MS)
84
+ - Handita Sutoyo, Mathias Bellout, Margrete Hånes, and Rodolfo Oliveira. "Ensemble-Based Proactive Optimization using a Reactive Strategy
85
+ for ICV Control." Paper presented at the SPE Norway Subsurface Conference, Bergen, Norway, April 2024. doi: [10.2118/218472-MS](https://doi.org/10.2118/218472-MS)
86
+
79
87
  ## Getting started as a contributor
80
88
  ### Contribution guide
81
89
  We welcome all kinds of contributions, including code, bug reports, issues, feature requests, and documentation.
@@ -44,6 +44,14 @@ This should run for some time and display a help message.
44
44
  ### Create and run your first model
45
45
  Some examples of Completor case file are available in [Examples](documentation/docs/about/examples.mdx) and detailed explanation is available in [Configuration](documentation/docs/about/configuration.mdx).
46
46
 
47
+ ### References
48
+ Some technical paper related to Completor and ICV Control applications are available to read and cite:
49
+ - Handita Sutoyo, Filippo Panini, Cuthbert Shang Wui Ng, Martin Halvorsen, Geir Elseth, Ingvild Berg Martiniussen, Corentin Cochard, Erik Johan Helland, Sean Robert Smith, and Lene Amundsen.
50
+ "Maximize Value from Inflow Control Technology Implementations Through Novel Standardized Workflow and Open-Source Modelling Tool."
51
+ Paper presented at the OTC Brasil, Rio de Janeiro, Brazil, October 2025. doi: [10.4043/36167-MS](https://doi.org/10.4043/36167-MS)
52
+ - Handita Sutoyo, Mathias Bellout, Margrete Hånes, and Rodolfo Oliveira. "Ensemble-Based Proactive Optimization using a Reactive Strategy
53
+ for ICV Control." Paper presented at the SPE Norway Subsurface Conference, Bergen, Norway, April 2024. doi: [10.2118/218472-MS](https://doi.org/10.2118/218472-MS)
54
+
47
55
  ## Getting started as a contributor
48
56
  ### Contribution guide
49
57
  We welcome all kinds of contributions, including code, bug reports, issues, feature requests, and documentation.
@@ -266,6 +266,7 @@ class _Keywords:
266
266
  MAP_FILE = "MAPFILE"
267
267
  SCHEDULE_FILE = "SCHFILE"
268
268
  OUT_FILE = "OUTFILE"
269
+ ICVC_KEYWORD = "ICVCONTROL"
269
270
 
270
271
  # Note: Alphabetically sorted, which matters for check vs. missing keys in input data.
271
272
  main_keywords = [COMPLETION_DATA, COMPLETION_SEGMENTS, WELL_SEGMENTS, WELL_SPECIFICATION]
@@ -347,3 +348,21 @@ class Method(Enum):
347
348
  elif isinstance(other, str):
348
349
  return self.name == other
349
350
  return False
351
+
352
+
353
+ class ICVMethod(Enum):
354
+ """Enum class representing the possible states for an ICV valve."""
355
+
356
+ OPEN = "OPEN"
357
+ OPEN_WAIT = "OPEN_WAIT"
358
+ OPEN_READY = "OPEN_READY"
359
+ OPEN_STOP = "OPEN_STOP"
360
+ OPEN_WAIT_STOP = "OPEN_WAIT_STOP"
361
+
362
+ CHOKE = "CHOKE"
363
+ CHOKE_READY = "CHOKE_READY"
364
+ CHOKE_WAIT = "CHOKE_WAIT"
365
+ CHOKE_STOP = "CHOKE_STOP"
366
+ CHOKE_WAIT_STOP = "CHOKE_WAIT_STOP"
367
+
368
+ UDQ = "UDQ"
@@ -21,7 +21,9 @@ from completor.visualize_well import visualize_well
21
21
  from completor.wells import Lateral, Well
22
22
 
23
23
 
24
- def format_output(well: Well, case: ReadCasefile, pdf: PdfPages | None = None) -> tuple[str, str, str, str]:
24
+ def format_output(
25
+ well: Well, case: ReadCasefile, pdf: PdfPages | None = None
26
+ ) -> tuple[str, str, str, str, pd.DataFrame]:
25
27
  """Formats the finished output string to be written to a file.
26
28
 
27
29
  Args:
@@ -49,7 +51,7 @@ def format_output(well: Well, case: ReadCasefile, pdf: PdfPages | None = None) -
49
51
 
50
52
  start_segment = 2
51
53
  start_branch = 1
52
-
54
+ df_inflow_control_output = pd.DataFrame()
53
55
  header_written = False
54
56
  first = True
55
57
  for lateral in well.active_laterals:
@@ -128,6 +130,7 @@ def format_output(well: Well, case: ReadCasefile, pdf: PdfPages | None = None) -
128
130
  case.completion_icv_tubing,
129
131
  case.wsegicv_table,
130
132
  )
133
+ df_inflow_control_output = pd.concat([df_inflow_control_output, df_inflow_control_valve], ignore_index=True)
131
134
  completion_data_list.append(
132
135
  _format_completion_data(well.well_name, lateral.lateral_number, df_completion_data, first)
133
136
  )
@@ -241,7 +244,13 @@ def format_output(well: Well, case: ReadCasefile, pdf: PdfPages | None = None) -
241
244
  )
242
245
  bonus.append(metadata + print_density_driven_include + "\n\n\n\n")
243
246
 
244
- return print_completion_data, print_well_segments, print_completion_segments, "".join(bonus)
247
+ return (
248
+ print_completion_data,
249
+ print_well_segments,
250
+ print_completion_segments,
251
+ "".join(bonus),
252
+ df_inflow_control_output,
253
+ )
245
254
 
246
255
 
247
256
  def _check_well_segments_header(welsegs_header: pd.DataFrame, start_measured_depths: pd.Series) -> pd.DataFrame:
@@ -0,0 +1,362 @@
1
+ """A file handling class for the icv-control algorithm"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from completor import icv_functions
10
+ from completor.constants import ICVMethod
11
+ from completor.get_version import get_version
12
+ from completor.initialization import Initialization
13
+ from completor.logger import logger
14
+
15
+
16
+ class IcvFileHandling:
17
+ """Create paths, directories, and output files."""
18
+
19
+ def __init__(self, file_data: dict, initials: Initialization):
20
+ self.initials = initials
21
+ self.icv_functions = icv_functions.IcvFunctions(initials)
22
+ self.current_working_directory = Path.cwd()
23
+ self.output_file_name = Path(file_data["output_file_name"])
24
+ self.output_directory = Path(file_data["output_directory"])
25
+ self.input_case_file = Path(file_data["input_case_file"])
26
+ self.create_ordered_filenames()
27
+ self.create_include_files()
28
+ self.create_main_schedule_file(Path(file_data["schedule_file_path"]))
29
+ self.include_file_path = None
30
+ self.schedule_include_file_path = None
31
+
32
+ def create_ordered_filenames(self):
33
+ """Create a dict with datetime keys and include file name values."""
34
+
35
+ self.ordered_filenames = {}
36
+ for icv_name in self.initials.icv_names:
37
+ icv_date = self.initials.icv_dates[icv_name]
38
+ if icv_date not in self.ordered_filenames:
39
+ self.ordered_filenames[icv_date] = {}
40
+ if icv_name not in self.ordered_filenames[icv_date]:
41
+ self.ordered_filenames[icv_date][icv_name] = []
42
+
43
+ def create_include_file_content(self, icv_name: str, file_type: ICVMethod, criteria: int | None = None) -> str:
44
+ """Create include file content for icv functions.
45
+
46
+ Args:
47
+ icv_name: Icv name. One or two symbols, I.E: A or Z9.
48
+ file_type: File type CHOKE_WAIT, OPEN_WAIT, CHOKE, OPEN.
49
+ criteria: Integer value denoting the criteria.
50
+
51
+ Returns:
52
+ Content of the file_type include file.
53
+
54
+ Raises:
55
+ ValueError: If criteria is None for CHOKE or OPEN file type.
56
+
57
+ """
58
+ trigger_number_times = 10000
59
+ file_content = ""
60
+ actionx_repeater = self.initials.case.step_table[icv_name][0]
61
+ if actionx_repeater > 9999 or len(str(actionx_repeater)) > 4:
62
+ logger.warning(f"MAX 9999 steps:'{actionx_repeater}'steps on ICV '{icv_name}'")
63
+ if file_type == ICVMethod.CHOKE_WAIT:
64
+ trigger_minimum_interval = 10
65
+ file_content += self.icv_functions.create_choke_wait(
66
+ icv_name, trigger_number_times, trigger_minimum_interval, actionx_repeater, criteria
67
+ )
68
+ return file_content
69
+
70
+ if file_type == ICVMethod.OPEN_WAIT:
71
+ file_content += self.icv_functions.create_open_wait(icv_name, actionx_repeater, criteria)
72
+ return file_content
73
+
74
+ if file_type == ICVMethod.CHOKE:
75
+ trigger_minimum_interval_str = ""
76
+ file_content = self.icv_functions.create_choke_ready(
77
+ icv_name, criteria, trigger_number_times, trigger_minimum_interval_str
78
+ )
79
+ file_content += self.icv_functions.create_choke_wait_stop(icv_name, criteria)
80
+ file_content += self.icv_functions.create_choke_stop(
81
+ icv_name, criteria, trigger_number_times, trigger_minimum_interval_str
82
+ )
83
+ trigger_number_times = 1
84
+ file_content += self.icv_functions.create_choke(
85
+ icv_name, criteria, trigger_number_times, trigger_minimum_interval_str, actionx_repeater
86
+ )
87
+ return file_content
88
+
89
+ if file_type == ICVMethod.OPEN:
90
+ trigger_minimum_interval_str = ""
91
+ file_content += self.icv_functions.create_open_ready(
92
+ icv_name, criteria, trigger_number_times, trigger_minimum_interval_str
93
+ )
94
+ file_content += self.icv_functions.create_open_wait_stop(icv_name, criteria)
95
+ file_content += self.icv_functions.create_open_stop(
96
+ icv_name, criteria, trigger_number_times, trigger_minimum_interval_str
97
+ )
98
+ trigger_number_times = 1
99
+ file_content += self.icv_functions.create_open(
100
+ icv_name, criteria, trigger_number_times, trigger_minimum_interval_str, actionx_repeater
101
+ )
102
+ return file_content
103
+
104
+ raise ValueError(f"The file type '{file_type}' is not recognized.")
105
+
106
+ def append_control_criteria_to_file(
107
+ self, file_path: Path, well_name: str, icv_name: str, file_type: ICVMethod, icv_date: str, criteria: int | None
108
+ ):
109
+ """Append to the file.
110
+
111
+ Args:
112
+ file_path: Path to the file
113
+ well_name: Well name.
114
+ icv_name: Icv name, one or two symbols, I.E: A or Z9.
115
+ file_type: Type of icv function file.
116
+ icv_date: Date of Icv change.
117
+ criteria: Integer value denoting the criteria.
118
+
119
+ """
120
+ self.ordered_filenames[icv_date][icv_name].append(file_path)
121
+ content = self.create_include_file_content(icv_name, file_type, criteria)
122
+ content = self.add_section_header(content, f"{well_name} {icv_name} {file_type}")
123
+ self.append_content_to_file(file_path, content)
124
+
125
+ def append_content_to_file(self, file_path: Path, content: str):
126
+ """Append content to the file. Create the file if it does not exist.
127
+
128
+ Args:
129
+ file_path: Path to the file.
130
+ content: Content to append to the file.
131
+
132
+ """
133
+ file_path.touch(exist_ok=True)
134
+
135
+ if not content.endswith("\n\n"):
136
+ content += "\n\n"
137
+
138
+ with open(file_path, "a", encoding="utf-8") as file:
139
+ file.write(content)
140
+
141
+ def add_section_header(self, content: str, header: str) -> str:
142
+ """Add a section header to the content.
143
+
144
+ Args:
145
+ content: Content to add the header to.
146
+ header: Header to add to the content.
147
+
148
+ Returns:
149
+ Content with the header added.
150
+
151
+ """
152
+ return f"--- {header} ---\n{content}"
153
+
154
+ def create_include_files(self):
155
+ """Create the include files for icv-control.
156
+
157
+ The include files are as follows:
158
+ - include.sch
159
+ - icv-control tables
160
+ - init
161
+ - input
162
+ - control critieria for each well and icv.
163
+ - summary.sch
164
+ """
165
+ base_folder = Path(self.output_directory)
166
+ fmu_path = Path("eclipse/include/")
167
+ if str(fmu_path) in str(base_folder):
168
+ base_include_path = Path("../include/schedule")
169
+ else:
170
+ base_include_path = Path("")
171
+
172
+ base_folder.mkdir(parents=True, exist_ok=True)
173
+
174
+ summary_file_path = Path(base_folder / "summary_icvc.sch")
175
+ self.include_file_path = Path(base_folder / "include_icvc.sch")
176
+ self.schedule_include_file_path = Path(base_include_path / "include_icvc.sch")
177
+ # case_file_path = Path(base_folder / "case_icvc.case")
178
+
179
+ summary_file_path.unlink(missing_ok=True)
180
+ self.include_file_path.unlink(missing_ok=True)
181
+ # case_file_path.unlink(missing_ok=True)
182
+
183
+ content = self.add_section_header(self.initials.init_icvcontrol, "INIT")
184
+ self.append_content_to_file(self.include_file_path, content)
185
+
186
+ content = self.add_section_header(self.initials.input_icvcontrol, "INPUT")
187
+ self.append_content_to_file(self.include_file_path, content)
188
+
189
+ content = self.add_section_header(self.initials.summary, "SUMMARY")
190
+ self.append_content_to_file(summary_file_path, content)
191
+ logger.info(f"Created summary file: '{summary_file_path}'.")
192
+
193
+ content = self.add_section_header(content, f"Completor version: {get_version()}")
194
+ # self.append_content_to_file(case_file_path, content)
195
+
196
+ for icv_name in self.initials.icv_names:
197
+ well_name = self.initials.well_names[icv_name]
198
+ icv_date = self.initials.icv_dates[icv_name]
199
+
200
+ for file_type in [ICVMethod.CHOKE_WAIT, ICVMethod.CHOKE, ICVMethod.OPEN_WAIT, ICVMethod.OPEN]:
201
+ if (
202
+ self.icv_functions.custom_conditions is not None
203
+ and file_type in self.icv_functions.custom_conditions
204
+ and icv_name in self.icv_functions.custom_conditions[file_type]
205
+ ):
206
+ custom_criteria = self.icv_functions.custom_conditions[file_type][icv_name].keys()
207
+ for criteria in custom_criteria:
208
+ if criteria != "map":
209
+ self.append_control_criteria_to_file(
210
+ self.include_file_path, well_name, icv_name, file_type, icv_date, criteria
211
+ )
212
+ else:
213
+ logger.warning(
214
+ f"No criteria found for well '{well_name}' icv '{icv_name}' " f"function '{file_type}'."
215
+ )
216
+ self.append_control_criteria_to_file(
217
+ self.include_file_path, well_name, icv_name, file_type, icv_date, None
218
+ )
219
+ logger.info(f"Created include file: '{self.include_file_path}'.")
220
+
221
+ def create_main_schedule_file(self, input_schedule: Path):
222
+ """Creates the main output schedule file from the input schedule file.
223
+
224
+ Args:
225
+ input_schedule: Input schedule file name.
226
+
227
+ """
228
+ main_schedule_file = Path(self.output_directory / self.output_file_name)
229
+
230
+ dates = [datetime.datetime.strptime(d, "%d %b %Y") for d in set(self.initials.icv_dates.values())]
231
+ with open(input_schedule, encoding="utf-8") as fh:
232
+ sch_files = fh.readlines()
233
+ for index, line in enumerate(sch_files):
234
+ line = " ".join(line.split())
235
+ if "DATES" in line:
236
+ sch_files[index] = line.replace("DATES\n", "DATES \n")
237
+
238
+ sch_data = "".join(sch_files).split("DATES")
239
+ sch_data_before_date = sch_data[0]
240
+ sch_data_after_first_date, date_comment = format_date(sch_data[1:])
241
+ for date in sch_data_after_first_date:
242
+ try:
243
+ dates.append(datetime.datetime.strptime(date, "%d %b %Y %H %M %S"))
244
+ except ValueError:
245
+ dates.append(datetime.datetime.strptime(date, "%d %b %Y"))
246
+ sorted_dates = []
247
+ # Sort dates chronologically and remove duplicate date entries.
248
+ for date in sorted(set(dates)):
249
+ # When the timestamp is zero, the timestamp is not written.
250
+ if date.hour == 0 and date.minute == 0 and date.second == 0:
251
+ sorted_dates.append(date.strftime("%d %b %Y").upper())
252
+ else:
253
+ sorted_dates.append(date.strftime("%d %b %Y %H:%M:%S").upper())
254
+
255
+ lines_path_to_include = list(sch_data_before_date)
256
+ lines_path = self.format_sch_file(sorted_dates, sch_data_after_first_date, date_comment)
257
+ lines_path_to_include += lines_path
258
+ main_schedule_lines = [line.replace("//", "/") for line in lines_path_to_include]
259
+
260
+ with open(main_schedule_file, "w", encoding="utf-8") as file:
261
+ file.write("".join(main_schedule_lines))
262
+ logger.info(f"Created main schedule file: '{main_schedule_file}'.")
263
+
264
+ def format_sch_file(
265
+ self, sorted_dates: list[str], sch_data_after_first_date: dict[str, str], date_comment: dict[str, str]
266
+ ) -> list[str]:
267
+ """
268
+ Formats the SCH file by including relevant paths and
269
+ content based on sorted dates and SCH data.
270
+
271
+ Args:
272
+ sorted_dates: A list of sorted dates.
273
+ sch_data_after_first_date: Containing SCH data after the first date.
274
+
275
+ Returns:
276
+ lines_path_to_include: Formatted paths to include in the SCH file.
277
+
278
+ """
279
+ lines_path_to_include = []
280
+ base_include = "INCLUDE\n"
281
+
282
+ for icvdate in sorted_dates:
283
+ try:
284
+ lines_path_to_include.append(f"DATES\n {icvdate} /{date_comment[icvdate]}\n/\n")
285
+ except KeyError:
286
+ lines_path_to_include.append(f"DATES\n {icvdate} /\n/\n")
287
+ try:
288
+ # Append content from SCH file after the first date.
289
+ lines_path_to_include.append(sch_data_after_first_date[icvdate].strip() + "\n\n")
290
+ except KeyError:
291
+ # Skip if SCH data for the icvdate is not found.
292
+ pass
293
+ try:
294
+ self.ordered_filenames[icvdate].keys()
295
+ except KeyError:
296
+ continue
297
+ # The includes of 'init.udq' and 'input.udq' should be placed close to the
298
+ # include 'well' statement.
299
+ lines_path_to_include.append(f"{base_include} '{self.schedule_include_file_path}' /\n\n".replace("//", "/"))
300
+ return lines_path_to_include
301
+
302
+
303
+ def format_date(date_contents: list) -> tuple[dict, dict]:
304
+ """
305
+ This function takes a list of dates in the format "day/month/year time" or
306
+ "day/month/year" and returns a dictionary of the formatted dates as keys and
307
+ the corresponding schedule data as values.
308
+
309
+ Args:
310
+ sch_dates: List of strings representing dates and schedule data.
311
+
312
+ Returns:
313
+ sch_data: Dictionary with keys as formatted dates and values as schedule data.
314
+
315
+ Raises:
316
+ ValueError: if the input date string is not in the format of
317
+ "day/month/year time" or "day/month/year".
318
+
319
+ """
320
+ formatted_data = {}
321
+ date_comment = {}
322
+ # iterate over the list of schedule dates
323
+ for date_content in date_contents:
324
+ if date_content:
325
+ date_line, content = date_content.split("\n", 1)
326
+ date, comment = date_line.split("/", 1)
327
+ comment = comment.lstrip()
328
+ date = date.strip()
329
+ if comment:
330
+ comment = " " + comment
331
+ if content.lstrip().startswith("/"):
332
+ content = content.replace("/", "", 1)
333
+ # Remove special characters from the date string, replacing "JLY" with "JUL"
334
+ date_formated = re.sub(r"[^a-zA-Z0-9]", " ", date).strip()
335
+ date_formated = date_formated.replace("JLY", "JUL")
336
+ try:
337
+ # Try to parse the date using the expected format
338
+ date = datetime.datetime.strptime(date_formated, "%d %b %Y %H %M %S")
339
+ except ValueError:
340
+ try:
341
+ date = datetime.datetime.strptime(date_formated, "%d %b %Y")
342
+ except ValueError:
343
+ try:
344
+ date = datetime.datetime.strptime(date_formated, "%d%b%Y")
345
+ except ValueError:
346
+ logger.warning(
347
+ "Date format seems to be wrong in the schedule file.\n"
348
+ "Format: 1 JAN 2030. The day is an integer between 1-31.\n"
349
+ "The months are JAN, FEB, MAR, APR, MAY, JUN, JLY/JUL, "
350
+ "AUG, SEP, OCT, NOV or DEC.\nThe year is a 4 digit integer."
351
+ f" See line that states:'{date}'."
352
+ )
353
+ try:
354
+ if date.hour == 0 and date.minute == 0 and date.second == 0:
355
+ date = date.strftime("%d %b %Y").upper()
356
+ else:
357
+ date = date.strftime("%d %b %Y %H %M %S").upper()
358
+ except AttributeError:
359
+ logger.warning(f"Something wrong in the date format in line: {date}")
360
+ formatted_data[date] = content
361
+ date_comment[date] = comment
362
+ return formatted_data, date_comment