completor 0.1.2__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.
- completor/__init__.py +3 -0
- completor/completion.py +1042 -0
- completor/config_jobs/run_completor +18 -0
- completor/constants.py +256 -0
- completor/create_output.py +462 -0
- completor/create_wells.py +314 -0
- completor/exceptions/__init__.py +4 -0
- completor/exceptions/clean_exceptions.py +10 -0
- completor/exceptions/exceptions.py +134 -0
- completor/hook_implementations/jobs.py +63 -0
- completor/input_validation.py +340 -0
- completor/launch_args_parser.py +41 -0
- completor/logger.py +138 -0
- completor/main.py +486 -0
- completor/parse.py +581 -0
- completor/prepare_outputs.py +1473 -0
- completor/pvt_model.py +14 -0
- completor/read_casefile.py +677 -0
- completor/read_schedule.py +160 -0
- completor/utils.py +185 -0
- completor/visualization.py +164 -0
- completor/visualize_well.py +217 -0
- completor-0.1.2.dist-info/LICENSE +165 -0
- completor-0.1.2.dist-info/METADATA +205 -0
- completor-0.1.2.dist-info/RECORD +27 -0
- completor-0.1.2.dist-info/WHEEL +4 -0
- completor-0.1.2.dist-info/entry_points.txt +6 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Functions to validate user input for Completor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from completor.constants import Headers
|
|
9
|
+
from completor.exceptions import CompletorError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def set_default_packer_section(df_comp: pd.DataFrame) -> pd.DataFrame:
|
|
13
|
+
"""Set the default value for the packer section.
|
|
14
|
+
|
|
15
|
+
This procedure sets the default values of the completion_table in read_casefile class if the annulus is packer (PA).
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
df_comp: Completion data.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Updated completion data for packers.
|
|
22
|
+
"""
|
|
23
|
+
# Set default values for packer sections
|
|
24
|
+
df_comp[Headers.INNER_DIAMETER] = np.where(df_comp[Headers.ANNULUS] == "PA", 0.0, df_comp[Headers.INNER_DIAMETER])
|
|
25
|
+
df_comp[Headers.OUTER_DIAMETER] = np.where(df_comp[Headers.ANNULUS] == "PA", 0.0, df_comp[Headers.OUTER_DIAMETER])
|
|
26
|
+
df_comp[Headers.ROUGHNESS] = np.where(df_comp[Headers.ANNULUS] == "PA", 0.0, df_comp[Headers.ROUGHNESS])
|
|
27
|
+
df_comp[Headers.VALVES_PER_JOINT] = np.where(
|
|
28
|
+
df_comp[Headers.ANNULUS] == "PA", 0.0, df_comp[Headers.VALVES_PER_JOINT]
|
|
29
|
+
)
|
|
30
|
+
df_comp[Headers.DEVICE_TYPE] = np.where(df_comp[Headers.ANNULUS] == "PA", "PERF", df_comp[Headers.DEVICE_TYPE])
|
|
31
|
+
df_comp[Headers.DEVICE_NUMBER] = np.where(df_comp[Headers.ANNULUS] == "PA", 0, df_comp[Headers.DEVICE_NUMBER])
|
|
32
|
+
return df_comp
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_default_perf_section(df_comp: pd.DataFrame) -> pd.DataFrame:
|
|
36
|
+
"""Set the default value for the perforated (PERF) section.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
df_comp: Completion data.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Updated completion data for perforated sections.
|
|
43
|
+
"""
|
|
44
|
+
df_comp[Headers.VALVES_PER_JOINT] = np.where(
|
|
45
|
+
df_comp[Headers.DEVICE_TYPE] == "PERF", 0.0, df_comp[Headers.VALVES_PER_JOINT]
|
|
46
|
+
)
|
|
47
|
+
df_comp[Headers.DEVICE_NUMBER] = np.where(df_comp[Headers.DEVICE_TYPE] == "PERF", 0, df_comp[Headers.DEVICE_NUMBER])
|
|
48
|
+
return df_comp
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check_default_non_packer(df_comp: pd.DataFrame) -> pd.DataFrame:
|
|
52
|
+
"""Check default values for non-packers.
|
|
53
|
+
|
|
54
|
+
This procedure checks if the user enters default values 1* for non-packer annulus content,
|
|
55
|
+
e.g. Open annulus (OA) and gravel packed (GP).
|
|
56
|
+
If this is the case, the program will report errors.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
df_comp: Completion data.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Updated completion with replaced roughness.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
CompletorError: If default value '1*' in non-packer columns
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
df_comp = df_comp.copy(True)
|
|
69
|
+
# set default value of roughness
|
|
70
|
+
df_comp[Headers.ROUGHNESS] = (
|
|
71
|
+
df_comp[Headers.ROUGHNESS].replace("1*", "1e-5").astype(np.float64)
|
|
72
|
+
) # Ensures float after replacing!
|
|
73
|
+
df_nonpa = df_comp[df_comp[Headers.ANNULUS] != "PA"]
|
|
74
|
+
df_columns = df_nonpa.columns.to_numpy()
|
|
75
|
+
for column in df_columns:
|
|
76
|
+
if "1*" in df_nonpa[column]:
|
|
77
|
+
raise CompletorError(f"No default value 1* is allowed in {column} entry.")
|
|
78
|
+
return df_comp
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def set_format_completion(df_comp: pd.DataFrame) -> pd.DataFrame:
|
|
82
|
+
"""Set the column data format.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
df_comp: Completion data.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Updated completion data with enforced data types.
|
|
89
|
+
"""
|
|
90
|
+
return df_comp.astype(
|
|
91
|
+
{
|
|
92
|
+
Headers.WELL: str,
|
|
93
|
+
Headers.BRANCH: np.int64,
|
|
94
|
+
Headers.START_MEASURED_DEPTH: np.float64,
|
|
95
|
+
Headers.END_MEASURED_DEPTH: np.float64,
|
|
96
|
+
Headers.INNER_DIAMETER: np.float64,
|
|
97
|
+
Headers.OUTER_DIAMETER: np.float64,
|
|
98
|
+
Headers.ROUGHNESS: np.float64,
|
|
99
|
+
Headers.ANNULUS: str,
|
|
100
|
+
Headers.VALVES_PER_JOINT: np.float64,
|
|
101
|
+
Headers.DEVICE_TYPE: str,
|
|
102
|
+
Headers.DEVICE_NUMBER: np.int64,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def assess_completion(df_comp: pd.DataFrame) -> None:
|
|
108
|
+
"""Assess the user completion inputs.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
df_comp: Completion data.
|
|
112
|
+
"""
|
|
113
|
+
list_wells = df_comp[Headers.WELL].unique()
|
|
114
|
+
for well_name in list_wells:
|
|
115
|
+
df_well = df_comp[df_comp[Headers.WELL] == well_name]
|
|
116
|
+
list_branches = df_well[Headers.BRANCH].unique()
|
|
117
|
+
for branch in list_branches:
|
|
118
|
+
df_comp = df_well[df_well[Headers.BRANCH] == branch]
|
|
119
|
+
nrow = df_comp.shape[0]
|
|
120
|
+
for idx in range(0, nrow):
|
|
121
|
+
_check_for_errors(df_comp, well_name, idx)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _check_for_errors(df_comp: pd.DataFrame, well_name: str, idx: int) -> None:
|
|
125
|
+
"""Check for errors in completion.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
df_comp: Completion data frame.
|
|
129
|
+
well_name: Well name.
|
|
130
|
+
idx: Index.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
CompletorError:
|
|
134
|
+
If packer segments are missing length.
|
|
135
|
+
If non-packer segments are missing length.
|
|
136
|
+
If the completion description is incomplete for some range of depth.
|
|
137
|
+
If the completion description is overlapping for some range of depth.
|
|
138
|
+
"""
|
|
139
|
+
if df_comp[Headers.ANNULUS].iloc[idx] == "PA" and (
|
|
140
|
+
df_comp[Headers.START_MEASURED_DEPTH].iloc[idx] != df_comp[Headers.END_MEASURED_DEPTH].iloc[idx]
|
|
141
|
+
):
|
|
142
|
+
raise CompletorError("Packer segments must not have length")
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
df_comp[Headers.ANNULUS].iloc[idx] != "PA"
|
|
146
|
+
and df_comp[Headers.DEVICE_TYPE].iloc[idx] != "ICV"
|
|
147
|
+
and df_comp[Headers.START_MEASURED_DEPTH].iloc[idx] == df_comp[Headers.END_MEASURED_DEPTH].iloc[idx]
|
|
148
|
+
):
|
|
149
|
+
raise CompletorError("Non packer segments must have length")
|
|
150
|
+
|
|
151
|
+
if idx > 0:
|
|
152
|
+
if df_comp[Headers.START_MEASURED_DEPTH].iloc[idx] > df_comp[Headers.END_MEASURED_DEPTH].iloc[idx - 1]:
|
|
153
|
+
raise CompletorError(
|
|
154
|
+
f"Incomplete completion description in well {well_name} from depth "
|
|
155
|
+
f"{df_comp[Headers.END_MEASURED_DEPTH].iloc[idx - 1]} "
|
|
156
|
+
f"to depth {df_comp[Headers.START_MEASURED_DEPTH].iloc[idx]}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if df_comp[Headers.START_MEASURED_DEPTH].iloc[idx] < df_comp[Headers.END_MEASURED_DEPTH].iloc[idx - 1]:
|
|
160
|
+
raise CompletorError(
|
|
161
|
+
f"Overlapping completion description in well '{well_name}' from depth "
|
|
162
|
+
f"t{df_comp[Headers.END_MEASURED_DEPTH].iloc[idx - 1]} "
|
|
163
|
+
f"to depth {(df_comp[Headers.START_MEASURED_DEPTH].iloc[idx])}"
|
|
164
|
+
)
|
|
165
|
+
if df_comp[Headers.DEVICE_TYPE].iloc[idx] not in ["PERF", "AICD", "ICD", "VALVE", "DAR", "AICV", "ICV"]:
|
|
166
|
+
raise CompletorError(
|
|
167
|
+
f"{df_comp[Headers.DEVICE_TYPE].iloc[idx]} not a valid device type. "
|
|
168
|
+
"Valid types are PERF, AICD, ICD, VALVE, DAR, AICV, and ICV."
|
|
169
|
+
)
|
|
170
|
+
if df_comp[Headers.ANNULUS].iloc[idx] not in ["GP", "OA", "PA"]:
|
|
171
|
+
raise CompletorError(
|
|
172
|
+
f"{df_comp[Headers.ANNULUS].iloc[idx]} not a valid annulus type. Valid types are GP, OA, and PA"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def set_format_wsegvalv(df_temp: pd.DataFrame) -> pd.DataFrame:
|
|
177
|
+
"""Format the well segments valve (WSEGVALV) table.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
df_temp: Well segments valve data.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Updated data with enforced data types and device type filled with default values.
|
|
184
|
+
"""
|
|
185
|
+
df_temp[Headers.DEVICE_NUMBER] = df_temp[Headers.DEVICE_NUMBER].astype(np.int64)
|
|
186
|
+
df_temp[[Headers.CV, Headers.AC, Headers.AC_MAX]] = df_temp[[Headers.CV, Headers.AC, Headers.AC_MAX]].astype(
|
|
187
|
+
np.float64
|
|
188
|
+
)
|
|
189
|
+
# allows column L to have default value 1* thus it is not set to float
|
|
190
|
+
# Create ID device column
|
|
191
|
+
df_temp.insert(0, Headers.DEVICE_TYPE, np.full(df_temp.shape[0], fill_value="VALVE"))
|
|
192
|
+
return df_temp
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def set_format_wsegsicd(df_temp: pd.DataFrame) -> pd.DataFrame:
|
|
196
|
+
"""Format the well segments inflow control device (WSEGSICD) table.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
df_temp: Well segments inflow control device data.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Updated data.
|
|
203
|
+
"""
|
|
204
|
+
# if WCUT is defaulted then set to 0.5, the same default value as in simulator
|
|
205
|
+
df_temp[Headers.WATER_CUT] = df_temp[Headers.WATER_CUT].replace("1*", 0.5).astype(np.float64)
|
|
206
|
+
# set data type
|
|
207
|
+
df_temp[Headers.DEVICE_NUMBER] = df_temp[Headers.DEVICE_NUMBER].astype(np.int64)
|
|
208
|
+
# left out device number because it has been formatted as integer
|
|
209
|
+
columns = df_temp.columns.to_numpy()[1:]
|
|
210
|
+
df_temp[columns] = df_temp[columns].astype(np.float64)
|
|
211
|
+
# Create ID device column
|
|
212
|
+
df_temp.insert(0, Headers.DEVICE_TYPE, np.full(df_temp.shape[0], "ICD"))
|
|
213
|
+
return df_temp
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def set_format_wsegaicd(df_temp: pd.DataFrame) -> pd.DataFrame:
|
|
217
|
+
"""Format the well segments automatic inflow control device (WSEGAICD) table.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
df_temp: Well segments automatic inflow control device data.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Updated data.
|
|
224
|
+
"""
|
|
225
|
+
# Fix table format
|
|
226
|
+
df_temp[Headers.DEVICE_NUMBER] = df_temp[Headers.DEVICE_NUMBER].astype(np.int64)
|
|
227
|
+
# left out device number because it has been formatted as integer
|
|
228
|
+
columns = df_temp.columns.to_numpy()[1:]
|
|
229
|
+
df_temp[columns] = df_temp[columns].astype(np.float64)
|
|
230
|
+
# Create ID device column
|
|
231
|
+
df_temp.insert(0, Headers.DEVICE_TYPE, np.full(df_temp.shape[0], "AICD"))
|
|
232
|
+
return df_temp
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def set_format_wsegdar(df_temp: pd.DataFrame) -> pd.DataFrame:
|
|
236
|
+
"""Format the well segments DAR (WSEGDAR) data.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
df_temp: Well segments DAR device data.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Updated data.
|
|
243
|
+
"""
|
|
244
|
+
df_temp[Headers.DEVICE_NUMBER] = df_temp[Headers.DEVICE_NUMBER].astype(np.int64)
|
|
245
|
+
# left out devicenumber because it has been formatted as integer
|
|
246
|
+
columns = df_temp.columns.to_numpy()[1:]
|
|
247
|
+
df_temp[columns] = df_temp[columns].astype(np.float64)
|
|
248
|
+
# Create ID device column
|
|
249
|
+
df_temp.insert(0, Headers.DEVICE_TYPE, np.full(df_temp.shape[0], "DAR"))
|
|
250
|
+
return df_temp
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def set_format_wsegaicv(df_temp: pd.DataFrame) -> pd.DataFrame:
|
|
254
|
+
"""Format the well segments automatic inflow control valve (WSEGAICV) table.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
df_temp: Well segments automatic inflow control valve table.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Updated data.
|
|
261
|
+
"""
|
|
262
|
+
df_temp[Headers.DEVICE_NUMBER] = df_temp[Headers.DEVICE_NUMBER].astype(np.int64)
|
|
263
|
+
# left out devicenumber because it has been formatted as integer
|
|
264
|
+
columns = df_temp.columns.to_numpy()[1:]
|
|
265
|
+
df_temp[columns] = df_temp[columns].astype(np.float64)
|
|
266
|
+
# Create ID device column
|
|
267
|
+
df_temp.insert(0, Headers.DEVICE_TYPE, np.full(df_temp.shape[0], "AICV"))
|
|
268
|
+
return df_temp
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def set_format_wsegicv(df_temp: pd.DataFrame) -> pd.DataFrame:
|
|
272
|
+
"""Format the well segments inflow control valve (WSEGICV) table.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
df_temp: Well segments inflow control valve table.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Updated data.
|
|
279
|
+
"""
|
|
280
|
+
df_temp[Headers.DEVICE_NUMBER] = df_temp[Headers.DEVICE_NUMBER].astype(np.int64)
|
|
281
|
+
df_temp[[Headers.CV, Headers.AC, Headers.AC_MAX]] = df_temp[[Headers.CV, Headers.AC, Headers.AC_MAX]].astype(
|
|
282
|
+
np.float64
|
|
283
|
+
)
|
|
284
|
+
# allows column DEFAULTS to have default value 5*, thus it is not set to float
|
|
285
|
+
# Create ID device column
|
|
286
|
+
df_temp.insert(0, Headers.DEVICE_TYPE, np.full(df_temp.shape[0], fill_value="ICV"))
|
|
287
|
+
return df_temp
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def validate_lateral_to_device(df_lat2dev: pd.DataFrame, df_comp: pd.DataFrame) -> None:
|
|
291
|
+
"""Assess the lateral-to-device inputs.
|
|
292
|
+
|
|
293
|
+
Abort if a lateral is connected to a device layer in a well with open annuli.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
df_lat2dev: Lateral to device contents.
|
|
297
|
+
df_comp: Completion data.
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
Completor: If the LATERAL_TO_DEVICE keyword is set for a multisegmented well with open annulus.
|
|
301
|
+
"""
|
|
302
|
+
try:
|
|
303
|
+
df_lat2dev[Headers.BRANCH].astype(np.int64)
|
|
304
|
+
except ValueError:
|
|
305
|
+
raise CompletorError(
|
|
306
|
+
f"Could not convert BRANCH {df_lat2dev[Headers.BRANCH].values} "
|
|
307
|
+
"to integer. Make sure that BRANCH is an integer."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
nrow = df_lat2dev.shape[0]
|
|
311
|
+
for idx in range(0, nrow):
|
|
312
|
+
l2d_well = df_lat2dev[Headers.WELL].iloc[idx]
|
|
313
|
+
if (df_comp[df_comp[Headers.WELL] == l2d_well][Headers.ANNULUS] == "OA").any():
|
|
314
|
+
raise CompletorError(
|
|
315
|
+
f"Please do not connect a lateral to the mother bore in well {l2d_well} that has open annuli. "
|
|
316
|
+
"This may trigger an error in reservoir simulator."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def validate_minimum_segment_length(minimum_segment_length: str | float) -> float:
|
|
321
|
+
"""Assess the minimum segment length.
|
|
322
|
+
|
|
323
|
+
Abort if the minimum segment length is not a number >= 0.0.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
minimum_segment_length: Possible user input.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Minimum segment length if no errors occurred.
|
|
330
|
+
|
|
331
|
+
Raises:
|
|
332
|
+
CompletorError: If the minimum_segment_length is not greater or equals to 0.0.
|
|
333
|
+
"""
|
|
334
|
+
try:
|
|
335
|
+
minimum_segment_length = float(minimum_segment_length)
|
|
336
|
+
except ValueError:
|
|
337
|
+
raise CompletorError(f"The MINIMUM_SEGMENT_LENGTH {minimum_segment_length} has to be a float.")
|
|
338
|
+
if minimum_segment_length < 0.0:
|
|
339
|
+
raise CompletorError(f"The MINIMUM_SEGMENT_LENGTH {minimum_segment_length} cannot be less than 0.0.")
|
|
340
|
+
return minimum_segment_length
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Parser for launch arguments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
import completor
|
|
8
|
+
|
|
9
|
+
COMPLETOR_DESCRIPTION = """Completor models advanced well completions for reservoir simulators.
|
|
10
|
+
It generates all necessary keywords for reservoir simulation
|
|
11
|
+
according to a completion description. See the Completor Documentations
|
|
12
|
+
for modeling details.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_parser() -> argparse.ArgumentParser:
|
|
17
|
+
"""Parse user input from the command line.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
argparse.ArgumentParser.
|
|
21
|
+
"""
|
|
22
|
+
parser = argparse.ArgumentParser(description=COMPLETOR_DESCRIPTION)
|
|
23
|
+
parser.add_argument("-i", "--inputfile", required=True, type=str, help="(Compulsory) Completor case file")
|
|
24
|
+
parser.add_argument("-s", "--schedulefile", type=str, help="(Optional) if it is specified in the case file")
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-o", "--outputfile", type=str, help="(Optional) name of output file. Defaults to <schedule>_advanced.wells"
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-f", "--figure", action="store_true", help="(Optional) to generate well completion diagrams in pdf format"
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"-l", "--loglevel", action="store", type=int, help="(Optional) log-level. Lower values gives more info (0-50)"
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-v",
|
|
36
|
+
"--version",
|
|
37
|
+
action="version",
|
|
38
|
+
version="%(prog)s (completor version " + completor.__version__ + ")",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return parser
|
completor/logger.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import completor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_logger(module_name="completor"):
|
|
14
|
+
"""Configure logger.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
module_name: The name for logger.
|
|
18
|
+
"""
|
|
19
|
+
logger_ = logging.getLogger(module_name)
|
|
20
|
+
|
|
21
|
+
formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s")
|
|
22
|
+
|
|
23
|
+
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
24
|
+
stdout_handler.addFilter(lambda record: record.levelno < logging.ERROR)
|
|
25
|
+
stdout_handler.setFormatter(formatter)
|
|
26
|
+
|
|
27
|
+
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
28
|
+
stderr_handler.addFilter(lambda record: record.levelno >= logging.ERROR)
|
|
29
|
+
stderr_handler.setFormatter(formatter)
|
|
30
|
+
|
|
31
|
+
logger_.addHandler(stdout_handler)
|
|
32
|
+
logger_.addHandler(stderr_handler)
|
|
33
|
+
|
|
34
|
+
return logger_
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def handle_error_messages(func):
|
|
41
|
+
"""Decorator to catch any exceptions it might throw (with some exceptions, such as KeyboardInterrupt).
|
|
42
|
+
|
|
43
|
+
If there are any error messages from the exception, they are logged.
|
|
44
|
+
If completor fails, the decorator will write a zip file to disk;
|
|
45
|
+
Completor-<year><month><day>-<hour><minute><second>-<letter><5 numbers>.zip
|
|
46
|
+
The last letter and numbers are chosen at random.
|
|
47
|
+
|
|
48
|
+
The zip file contains:
|
|
49
|
+
* traceback.txt - a trace back.
|
|
50
|
+
* machine.txt - which machine it happened on.
|
|
51
|
+
* arguments.json - all input arguments.
|
|
52
|
+
* The content of any files passed.
|
|
53
|
+
For the main method of Completor, these are (if provided).
|
|
54
|
+
* input_file.txt - The case file.
|
|
55
|
+
* schedule_file.txt - The schedule file.
|
|
56
|
+
* new_file.txt - The output file.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
@wraps(func)
|
|
60
|
+
def wrapper(*args, **kwargs):
|
|
61
|
+
try:
|
|
62
|
+
func(*args, **kwargs)
|
|
63
|
+
except (Exception, SystemExit) as ex:
|
|
64
|
+
# SystemExit does not inherit from Exception
|
|
65
|
+
if isinstance(ex, SystemExit):
|
|
66
|
+
exit_code = ex.code
|
|
67
|
+
else:
|
|
68
|
+
exit_code = 1
|
|
69
|
+
logger.error(ex)
|
|
70
|
+
if len(args) > 0:
|
|
71
|
+
_kwargs = {}
|
|
72
|
+
_kwargs["input_file"] = kwargs["paths"][0]
|
|
73
|
+
_kwargs["schedule_file"] = kwargs["paths"][1]
|
|
74
|
+
_kwargs["new_file"] = args[2]
|
|
75
|
+
_kwargs["show_fig"] = args[3]
|
|
76
|
+
kwargs = _kwargs
|
|
77
|
+
|
|
78
|
+
dump_debug_information(**kwargs)
|
|
79
|
+
exit(exit_code)
|
|
80
|
+
|
|
81
|
+
return wrapper
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _convert_paths_to_strings(dict_) -> dict:
|
|
85
|
+
kwargs = {}
|
|
86
|
+
for key, value in dict_.items():
|
|
87
|
+
if len(str(value).splitlines()) < 2:
|
|
88
|
+
if isinstance(value, Path):
|
|
89
|
+
value = str(value)
|
|
90
|
+
kwargs[key] = value
|
|
91
|
+
return kwargs
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def dump_debug_information(**kwargs) -> None:
|
|
95
|
+
"""Helper method to create, and write all the files to a zip archive."""
|
|
96
|
+
import random
|
|
97
|
+
import socket
|
|
98
|
+
import string
|
|
99
|
+
import traceback
|
|
100
|
+
from zipfile import ZIP_DEFLATED, ZipFile
|
|
101
|
+
|
|
102
|
+
when = time.localtime()
|
|
103
|
+
random_suffix = "".join(random.choices(string.ascii_letters) + random.choices(string.digits, k=5))
|
|
104
|
+
name = (
|
|
105
|
+
f"Completor-{when.tm_year}{when.tm_mon:02}{when.tm_mday:02}-"
|
|
106
|
+
f"{when.tm_hour:02}{when.tm_min:02}{when.tm_sec:02}-{random_suffix}"
|
|
107
|
+
)
|
|
108
|
+
logger.error(
|
|
109
|
+
"Completor failed. Writing debugging information to %s.zip. "
|
|
110
|
+
"Please submit issue for questions and include said file.\n"
|
|
111
|
+
"Do not submit internal or restricted files to the issue,"
|
|
112
|
+
"please contact Equinor internal support to handle internal files.\n"
|
|
113
|
+
"NOTE: the file includes all input you gave to Completor including the content of the input files",
|
|
114
|
+
name,
|
|
115
|
+
)
|
|
116
|
+
logger.debug(traceback.format_exc())
|
|
117
|
+
|
|
118
|
+
with ZipFile(name + ".zip", mode="x", compression=ZIP_DEFLATED) as zipfile:
|
|
119
|
+
|
|
120
|
+
def dump(file_name: str, data: str | bytes, encoding: str = "UTF-8") -> None:
|
|
121
|
+
path = Path(name) / file_name
|
|
122
|
+
with zipfile.open(str(path), "w") as f:
|
|
123
|
+
if isinstance(data, str):
|
|
124
|
+
data = data.encode(encoding)
|
|
125
|
+
f.write(data)
|
|
126
|
+
|
|
127
|
+
dump("traceback.txt", traceback.format_exc())
|
|
128
|
+
dump("machine.txt", socket.getfqdn())
|
|
129
|
+
dump("version.txt", completor.__version__)
|
|
130
|
+
dump("arguments.json", json.dumps(_convert_paths_to_strings(kwargs), indent=4))
|
|
131
|
+
for key, value in kwargs.items():
|
|
132
|
+
if isinstance(value, (Path, str)):
|
|
133
|
+
try:
|
|
134
|
+
with open(value, encoding="utf-8") as f:
|
|
135
|
+
dump(f"{key}.txt", f.read())
|
|
136
|
+
except Exception as ex:
|
|
137
|
+
dump(f"{key}.traceback.txt", traceback.format_exc())
|
|
138
|
+
dump(f"{key}.txt", ex.__repr__())
|