smashbox 1.0__py2.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.
- smashbox/.spyproject/config/backups/codestyle.ini.bak +8 -0
- smashbox/.spyproject/config/backups/encoding.ini.bak +6 -0
- smashbox/.spyproject/config/backups/vcs.ini.bak +7 -0
- smashbox/.spyproject/config/backups/workspace.ini.bak +12 -0
- smashbox/.spyproject/config/codestyle.ini +8 -0
- smashbox/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini +5 -0
- smashbox/.spyproject/config/defaults/defaults-encoding-0.2.0.ini +3 -0
- smashbox/.spyproject/config/defaults/defaults-vcs-0.2.0.ini +4 -0
- smashbox/.spyproject/config/defaults/defaults-workspace-0.2.0.ini +6 -0
- smashbox/.spyproject/config/encoding.ini +6 -0
- smashbox/.spyproject/config/vcs.ini +7 -0
- smashbox/.spyproject/config/workspace.ini +12 -0
- smashbox/__init__.py +8 -0
- smashbox/asset/flwdir/flowdir_fr_1000m.tif +0 -0
- smashbox/asset/outlets/.Rhistory +0 -0
- smashbox/asset/outlets/db_bnbv_fr.csv +142704 -0
- smashbox/asset/outlets/db_bnbv_light.csv +42084 -0
- smashbox/asset/outlets/db_sites.csv +8700 -0
- smashbox/asset/outlets/db_stations.csv +2916 -0
- smashbox/asset/outlets/db_stations_example.csv +19 -0
- smashbox/asset/outlets/edit_database.py +185 -0
- smashbox/asset/outlets/readme.txt +5 -0
- smashbox/asset/params/ci.tif +0 -0
- smashbox/asset/params/cp.tif +0 -0
- smashbox/asset/params/ct.tif +0 -0
- smashbox/asset/params/kexc.tif +0 -0
- smashbox/asset/params/kmlt.tif +0 -0
- smashbox/asset/params/llr.tif +0 -0
- smashbox/asset/setup/setup_rhax_gr4_dt3600.yaml +15 -0
- smashbox/asset/setup/setup_rhax_gr4_dt900.yaml +15 -0
- smashbox/asset/setup/setup_rhax_gr5_dt3600.yaml +15 -0
- smashbox/asset/setup/setup_rhax_gr5_dt900.yaml +15 -0
- smashbox/init/README.md +3 -0
- smashbox/init/__init__.py +3 -0
- smashbox/init/multimodel_statistics.py +405 -0
- smashbox/init/param.py +799 -0
- smashbox/init/smashbox.py +186 -0
- smashbox/model/__init__.py +1 -0
- smashbox/model/atmos_data_connector.py +518 -0
- smashbox/model/mesh.py +185 -0
- smashbox/model/model.py +829 -0
- smashbox/model/setup.py +109 -0
- smashbox/plot/__init__.py +1 -0
- smashbox/plot/myplot.py +1133 -0
- smashbox/plot/plot.py +1662 -0
- smashbox/read_inputdata/__init__.py +1 -0
- smashbox/read_inputdata/read_data.py +1229 -0
- smashbox/read_inputdata/smashmodel.py +395 -0
- smashbox/stats/__init__.py +1 -0
- smashbox/stats/mystats.py +1632 -0
- smashbox/stats/stats.py +2022 -0
- smashbox/test.py +532 -0
- smashbox/test_average_stats.py +122 -0
- smashbox/test_mesh.r +8 -0
- smashbox/test_mesh_from_graffas.py +69 -0
- smashbox/tools/__init__.py +1 -0
- smashbox/tools/geo_toolbox.py +1028 -0
- smashbox/tools/tools.py +461 -0
- smashbox/tutorial_R.r +182 -0
- smashbox/tutorial_R_graffas.r +88 -0
- smashbox/tutorial_R_graffas_local.r +33 -0
- smashbox/tutorial_python.py +102 -0
- smashbox/tutorial_readme.py +261 -0
- smashbox/tutorial_report.py +58 -0
- smashbox/tutorials/Python_tutorial.md +124 -0
- smashbox/tutorials/R_Graffas_tutorial.md +153 -0
- smashbox/tutorials/R_tutorial.md +121 -0
- smashbox/tutorials/__init__.py +6 -0
- smashbox/tutorials/generate_doc.md +7 -0
- smashbox-1.0.dist-info/METADATA +998 -0
- smashbox-1.0.dist-info/RECORD +73 -0
- smashbox-1.0.dist-info/WHEEL +5 -0
- smashbox-1.0.dist-info/licenses/LICENSE +100 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Created on Fri Sep 27 12:58:19 2024
|
|
5
|
+
|
|
6
|
+
@author: maxime
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import smash
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import datetime
|
|
15
|
+
import shutil
|
|
16
|
+
import glob
|
|
17
|
+
import random
|
|
18
|
+
from . import read_data
|
|
19
|
+
|
|
20
|
+
update_default_setup = {
|
|
21
|
+
"prcp_date_pattern": "",
|
|
22
|
+
"pet_date_pattern": "",
|
|
23
|
+
"snow_date_pattern": "",
|
|
24
|
+
"temp_date_pattern": "",
|
|
25
|
+
"prcp_directories": {},
|
|
26
|
+
"pet_directories": {},
|
|
27
|
+
"snow_directories": {},
|
|
28
|
+
"temp_directories": {},
|
|
29
|
+
"continuous_pet": {},
|
|
30
|
+
"timezone": "UTC",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def standardize_update_default_setup_options(model, update_default_setup):
|
|
35
|
+
|
|
36
|
+
for var in [
|
|
37
|
+
"prcp_date_pattern",
|
|
38
|
+
"snow_date_pattern",
|
|
39
|
+
"temp_date_pattern",
|
|
40
|
+
"pet_date_pattern",
|
|
41
|
+
]:
|
|
42
|
+
if update_default_setup[var] == "":
|
|
43
|
+
if model.setup.dt == 86_400:
|
|
44
|
+
update_default_setup[var] = "%Y%m%d"
|
|
45
|
+
else:
|
|
46
|
+
update_default_setup[var] = "%Y%m%d%H%M"
|
|
47
|
+
|
|
48
|
+
if model.setup.daily_interannual_pet:
|
|
49
|
+
update_default_setup["pet_date_pattern"] = "%Y%m%d"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def standardize_model_date_pattern(key: str, value: str) -> str:
|
|
53
|
+
if len(value) > 0:
|
|
54
|
+
if not isinstance(value, str):
|
|
55
|
+
raise TypeError(f"{key} model setup must be a str")
|
|
56
|
+
|
|
57
|
+
if not value.startswith("%"):
|
|
58
|
+
raise ValueError(f"{value} must start by character %")
|
|
59
|
+
|
|
60
|
+
sample = value.split("%")
|
|
61
|
+
sample.pop(0)
|
|
62
|
+
|
|
63
|
+
for s in sample:
|
|
64
|
+
if not isinstance(s, str) and len(s) > 1:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"%{s} in {value} must start by character % following by a unique letter: Ex: %Y%m%d%H%M"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def standardize_model_data_directories(key: str, value: dict) -> str:
|
|
73
|
+
|
|
74
|
+
if not isinstance(value, dict):
|
|
75
|
+
raise TypeError(f"{key} model setup must be a dict")
|
|
76
|
+
|
|
77
|
+
key_to_pop = []
|
|
78
|
+
for subkey, subvalue in value.items():
|
|
79
|
+
|
|
80
|
+
if isinstance(subkey, str):
|
|
81
|
+
try:
|
|
82
|
+
int(subkey)
|
|
83
|
+
except:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"{subkey}, must represent an integer as the dict key (order priority of the data)."
|
|
86
|
+
)
|
|
87
|
+
elif not isinstance(subkey, int):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"{subkey}, must be an integer as the dict key (order priority of the data)."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if not isinstance(subvalue, str):
|
|
93
|
+
raise ValueError(f"{subvalue}, must be a str, a pathlike.")
|
|
94
|
+
|
|
95
|
+
if len(subvalue) == 0:
|
|
96
|
+
key_to_pop.append(subkey)
|
|
97
|
+
|
|
98
|
+
if len(subvalue) > 0 and not os.path.exists(subvalue):
|
|
99
|
+
raise ValueError(f"'{subvalue}', is not an existing path.")
|
|
100
|
+
|
|
101
|
+
# remove empty keys
|
|
102
|
+
for subkey in key_to_pop:
|
|
103
|
+
value.pop(subkey)
|
|
104
|
+
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def standardize_model_continuous_pet(key: str, value: dict) -> str:
|
|
109
|
+
|
|
110
|
+
if not isinstance(value, dict):
|
|
111
|
+
raise TypeError(f"{key} model setup must be a dict")
|
|
112
|
+
|
|
113
|
+
true_count = 0
|
|
114
|
+
for subkey, subvalue in value.items():
|
|
115
|
+
|
|
116
|
+
if isinstance(subkey, str):
|
|
117
|
+
try:
|
|
118
|
+
int(subkey)
|
|
119
|
+
except:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"{subkey}, must represent an integer as the dict key (order priority of the data)."
|
|
122
|
+
)
|
|
123
|
+
elif not isinstance(subkey, int):
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"{subkey}, must be an integer as the dict key (order priority of the pet data)."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not isinstance(subvalue, bool):
|
|
129
|
+
raise ValueError(f"{subvalue}, must be a bool.")
|
|
130
|
+
|
|
131
|
+
if subvalue == True:
|
|
132
|
+
true_count = true_count + 1
|
|
133
|
+
|
|
134
|
+
if true_count > 1:
|
|
135
|
+
raise ValueError("Only one True data is allowed in continuous pet")
|
|
136
|
+
|
|
137
|
+
return value
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def update_model_setup(model, update_default_setup):
|
|
141
|
+
|
|
142
|
+
for key, value in update_default_setup.items():
|
|
143
|
+
setattr(model.setup, key, value)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Build a smash model from a setup and a mesh with external new options
|
|
147
|
+
# path_setup: path to the setup file
|
|
148
|
+
# path_mesh: path to the mesh file
|
|
149
|
+
# setup_options: dictionnary to pass setup options that will overwrite options in path_setup
|
|
150
|
+
# new options can be defined in path_setup:
|
|
151
|
+
# prcp_directories: dict
|
|
152
|
+
# pet_directories: dict
|
|
153
|
+
# snow_directories: dict
|
|
154
|
+
# temp_directories: dict
|
|
155
|
+
# prcp_date_pattern : str
|
|
156
|
+
# pet_date_pattern : str
|
|
157
|
+
# snow_date_pattern : str
|
|
158
|
+
# temp_date_pattern: str
|
|
159
|
+
# continuous_pet : dict of bool
|
|
160
|
+
# timezone: str
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def SmashModel(input_setup, input_mesh, setup_options={}):
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
Description
|
|
167
|
+
-----------
|
|
168
|
+
|
|
169
|
+
Build a model Smash like smash.Model() but extend capabilities of the atmos data reading
|
|
170
|
+
|
|
171
|
+
Paramerters
|
|
172
|
+
-----------
|
|
173
|
+
|
|
174
|
+
input_setup: str() | dict()
|
|
175
|
+
the path to the setup.yaml file or the setup dictionary
|
|
176
|
+
|
|
177
|
+
input_mesh: str() | dict()
|
|
178
|
+
the path to the mesh.hdf5 file or the mesh dictionary
|
|
179
|
+
|
|
180
|
+
setup_options: dict()
|
|
181
|
+
dictionnary of setup options that will erase setup.yaml
|
|
182
|
+
|
|
183
|
+
Return
|
|
184
|
+
------
|
|
185
|
+
|
|
186
|
+
smash.Model()
|
|
187
|
+
Un objet model smash.
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
if isinstance(input_setup, str):
|
|
192
|
+
setup = smash.io.read_setup(input_setup)
|
|
193
|
+
elif isinstance(input_setup, dict):
|
|
194
|
+
setup = input_setup.copy()
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError("Wrong setup data type: input_setup must be a path or a dict")
|
|
197
|
+
|
|
198
|
+
sending_options = setup_options.copy()
|
|
199
|
+
|
|
200
|
+
for key, value in setup_options.items():
|
|
201
|
+
setup.update({key: value})
|
|
202
|
+
|
|
203
|
+
# filter setup options not allowed in smash to prevent warnings
|
|
204
|
+
for key in update_default_setup:
|
|
205
|
+
if key in setup:
|
|
206
|
+
setup.pop(key)
|
|
207
|
+
|
|
208
|
+
if isinstance(input_mesh, str):
|
|
209
|
+
mesh = smash.io.read_mesh(input_mesh)
|
|
210
|
+
elif isinstance(input_mesh, dict):
|
|
211
|
+
mesh = input_mesh.copy()
|
|
212
|
+
else:
|
|
213
|
+
raise ValueError("Wrong mesh data type: setup must be a path or a dict")
|
|
214
|
+
|
|
215
|
+
# force to prevent reading data
|
|
216
|
+
if "read_prcp" in setup:
|
|
217
|
+
setup["read_prcp"] = False
|
|
218
|
+
if "read_pet" in setup:
|
|
219
|
+
setup["read_pet"] = False
|
|
220
|
+
if "read_temp" in setup:
|
|
221
|
+
setup["read_temp"] = False
|
|
222
|
+
if "read_snow" in setup:
|
|
223
|
+
setup["read_snow"] = False
|
|
224
|
+
if "compute_mean_atmos" in setup:
|
|
225
|
+
setup["compute_mean_atmos"] = False
|
|
226
|
+
|
|
227
|
+
print(
|
|
228
|
+
f"</> Building model from {setup['start_time']} to {setup['end_time']} with time-step {setup['dt']}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# build the model
|
|
232
|
+
model = smash.Model(setup, mesh)
|
|
233
|
+
|
|
234
|
+
# Updtade model.setup with new options
|
|
235
|
+
standardize_update_default_setup_options(model, update_default_setup)
|
|
236
|
+
update_model_setup(model, update_default_setup)
|
|
237
|
+
|
|
238
|
+
# read all options in setup, not only those for smash:
|
|
239
|
+
if isinstance(input_setup, str):
|
|
240
|
+
with open(input_setup, "r") as file:
|
|
241
|
+
setup_overwrite = yaml.safe_load(file)
|
|
242
|
+
elif isinstance(input_setup, dict):
|
|
243
|
+
setup_overwrite = input_setup.copy()
|
|
244
|
+
else:
|
|
245
|
+
raise ValueError("Wrong setup data type: input_setup must be a path or a dict")
|
|
246
|
+
|
|
247
|
+
# overwrite setup_overwrite with setup_options
|
|
248
|
+
for key, value in setup_options.items():
|
|
249
|
+
setup_overwrite.update({key: value})
|
|
250
|
+
|
|
251
|
+
# Overight options with the config file
|
|
252
|
+
for key, value in setup_overwrite.items():
|
|
253
|
+
if hasattr(model.setup, key):
|
|
254
|
+
setattr(model.setup, key, value)
|
|
255
|
+
|
|
256
|
+
# print(setup_overwrite)
|
|
257
|
+
# print(model.setup)
|
|
258
|
+
# standardize new user options
|
|
259
|
+
standardize_model_date_pattern("prcp_date_pattern", model.setup.prcp_date_pattern)
|
|
260
|
+
standardize_model_date_pattern("pet_date_pattern", model.setup.pet_date_pattern)
|
|
261
|
+
standardize_model_date_pattern("snow_date_pattern", model.setup.snow_date_pattern)
|
|
262
|
+
standardize_model_date_pattern("temp_date_pattern", model.setup.temp_date_pattern)
|
|
263
|
+
standardize_model_data_directories("prcp_directories", model.setup.prcp_directories)
|
|
264
|
+
standardize_model_data_directories("pet_directories", model.setup.pet_directories)
|
|
265
|
+
standardize_model_data_directories("temp_directories", model.setup.temp_directories)
|
|
266
|
+
standardize_model_data_directories("snow_directories", model.setup.snow_directories)
|
|
267
|
+
standardize_model_continuous_pet("continuous_pet", model.setup.continuous_pet)
|
|
268
|
+
|
|
269
|
+
# here we must manage etpc... => must be moved into read_data
|
|
270
|
+
# Get ready with Continuous ETP
|
|
271
|
+
path_to_smashbox = os.path.join(
|
|
272
|
+
os.path.expandvars("$HOME"), ".smashbox", "simlink_pet"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if not os.path.exists(path_to_smashbox):
|
|
276
|
+
os.makedirs(path_to_smashbox)
|
|
277
|
+
|
|
278
|
+
path_to_symlink_etp = None
|
|
279
|
+
|
|
280
|
+
if "continuous_pet" in setup_overwrite:
|
|
281
|
+
|
|
282
|
+
if "pet_directories" in setup_overwrite:
|
|
283
|
+
dir_etp = setup_overwrite["pet_directories"]
|
|
284
|
+
elif "pet_directory" in setup_overwrite:
|
|
285
|
+
dir_etp = {1: setup_overwrite["pet_directory"]}
|
|
286
|
+
else:
|
|
287
|
+
dir_etp = {}
|
|
288
|
+
|
|
289
|
+
continuous_pet = setup_overwrite["continuous_pet"]
|
|
290
|
+
list_dir = os.listdir(path_to_smashbox)
|
|
291
|
+
path_to_symlink_etp = os.path.join(
|
|
292
|
+
path_to_smashbox,
|
|
293
|
+
f"symlinked_etpc_{len(list_dir)+1}_{int(random.random()*1000000)}",
|
|
294
|
+
)
|
|
295
|
+
print("</> Path to symlinked pet is set to ", path_to_symlink_etp)
|
|
296
|
+
else:
|
|
297
|
+
continuous_pet = {}
|
|
298
|
+
|
|
299
|
+
for key, value in continuous_pet.items():
|
|
300
|
+
if value:
|
|
301
|
+
prepare_etp_c(
|
|
302
|
+
path_to_symlink_etp=path_to_symlink_etp,
|
|
303
|
+
start_time=model.setup.start_time,
|
|
304
|
+
end_time=model.setup.end_time,
|
|
305
|
+
pet_date_pattern=model.setup.pet_date_pattern,
|
|
306
|
+
fmt=".tif",
|
|
307
|
+
dir_etp=dir_etp[key],
|
|
308
|
+
)
|
|
309
|
+
dir_etp[key] = path_to_symlink_etp
|
|
310
|
+
sending_options.update({"pet_directories": dir_etp})
|
|
311
|
+
break # only one source of continuous PET
|
|
312
|
+
|
|
313
|
+
# print(model.setup)
|
|
314
|
+
# read atmos data with updated func
|
|
315
|
+
print("</> Reading atmospheric data ...")
|
|
316
|
+
read_data.read_atmos_data(model, setup_options=sending_options)
|
|
317
|
+
|
|
318
|
+
# clean path to symplink pet files
|
|
319
|
+
if path_to_symlink_etp is not None:
|
|
320
|
+
if os.path.exists(path_to_symlink_etp):
|
|
321
|
+
print("</> Removing path to symplink pet ", path_to_symlink_etp)
|
|
322
|
+
shutil.rmtree(path_to_symlink_etp)
|
|
323
|
+
|
|
324
|
+
return model
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def prepare_etp_c(
|
|
328
|
+
path_to_symlink_etp="symlinked_etp_c",
|
|
329
|
+
start_time=None,
|
|
330
|
+
end_time=None,
|
|
331
|
+
pet_date_pattern="%Y%m%d",
|
|
332
|
+
fmt=".tif",
|
|
333
|
+
dir_etp="",
|
|
334
|
+
):
|
|
335
|
+
|
|
336
|
+
if (start_time is None) or (end_time is None):
|
|
337
|
+
print("start_time or end_time are empty... ")
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
if dir_etp == "":
|
|
341
|
+
print("dir_etp empty...")
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
date_range = pd.date_range(
|
|
345
|
+
start=datetime.datetime.fromisoformat(start_time),
|
|
346
|
+
end=datetime.datetime.fromisoformat(end_time),
|
|
347
|
+
freq=datetime.timedelta(days=1),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
print("</> Prepare continuous PET ...")
|
|
351
|
+
|
|
352
|
+
if os.path.exists(path_to_symlink_etp):
|
|
353
|
+
shutil.rmtree(path_to_symlink_etp)
|
|
354
|
+
|
|
355
|
+
os.makedirs(path_to_symlink_etp)
|
|
356
|
+
|
|
357
|
+
warn_list = []
|
|
358
|
+
for date in date_range:
|
|
359
|
+
|
|
360
|
+
date_etp_c_to_read = (date - datetime.timedelta(days=1)).strftime(
|
|
361
|
+
pet_date_pattern
|
|
362
|
+
)
|
|
363
|
+
date_etp_c_j0 = (date).strftime("%m%d")
|
|
364
|
+
date_etp_c_j1 = (date + datetime.timedelta(days=1)).strftime("%m%d")
|
|
365
|
+
|
|
366
|
+
file_etp_c = glob.glob(f"{dir_etp}/**/*{date_etp_c_to_read}*.tif", recursive=True)
|
|
367
|
+
|
|
368
|
+
if len(file_etp_c) > 0:
|
|
369
|
+
file_etp_c = file_etp_c[0]
|
|
370
|
+
|
|
371
|
+
if os.path.exists(file_etp_c):
|
|
372
|
+
mylink = os.path.join(path_to_symlink_etp, f"ETPjc_{date_etp_c_j0}{fmt}")
|
|
373
|
+
|
|
374
|
+
if os.path.exists(mylink):
|
|
375
|
+
os.remove(mylink)
|
|
376
|
+
|
|
377
|
+
os.symlink(file_etp_c, mylink)
|
|
378
|
+
|
|
379
|
+
mylink = os.path.join(path_to_symlink_etp, f"ETPjc_{date_etp_c_j1}{fmt}")
|
|
380
|
+
|
|
381
|
+
if os.path.exists(mylink):
|
|
382
|
+
os.remove(mylink)
|
|
383
|
+
|
|
384
|
+
os.symlink(file_etp_c, mylink)
|
|
385
|
+
|
|
386
|
+
warn_list.append(
|
|
387
|
+
"Linked "
|
|
388
|
+
+ f"ETPjc_{date_etp_c_j0}{fmt} and ETPjc_{date_etp_c_j1}{fmt} "
|
|
389
|
+
+ f"to file {os.path.basename(file_etp_c)}"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if len(warn_list) > 1:
|
|
393
|
+
print(f"</> {warn_list[0]}, ..., {warn_list[-1]}")
|
|
394
|
+
elif len(warn_list) > 0:
|
|
395
|
+
print(f"</> {warn_list[0]}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|