disdrodb 0.1.2__py3-none-any.whl → 0.1.4__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.
- disdrodb/__init__.py +68 -34
- disdrodb/_config.py +5 -4
- disdrodb/_version.py +16 -3
- disdrodb/accessor/__init__.py +20 -0
- disdrodb/accessor/methods.py +125 -0
- disdrodb/api/checks.py +177 -24
- disdrodb/api/configs.py +3 -3
- disdrodb/api/info.py +13 -13
- disdrodb/api/io.py +281 -22
- disdrodb/api/path.py +184 -195
- disdrodb/api/search.py +18 -9
- disdrodb/cli/disdrodb_create_summary.py +103 -0
- disdrodb/cli/disdrodb_create_summary_station.py +91 -0
- disdrodb/cli/disdrodb_run_l0.py +1 -1
- disdrodb/cli/disdrodb_run_l0_station.py +1 -1
- disdrodb/cli/disdrodb_run_l0a_station.py +1 -1
- disdrodb/cli/disdrodb_run_l0b.py +1 -1
- disdrodb/cli/disdrodb_run_l0b_station.py +3 -3
- disdrodb/cli/disdrodb_run_l0c.py +1 -1
- disdrodb/cli/disdrodb_run_l0c_station.py +3 -3
- disdrodb/cli/disdrodb_run_l1_station.py +2 -2
- disdrodb/cli/disdrodb_run_l2e_station.py +2 -2
- disdrodb/cli/disdrodb_run_l2m_station.py +2 -2
- disdrodb/configs.py +149 -4
- disdrodb/constants.py +61 -0
- disdrodb/data_transfer/download_data.py +127 -11
- disdrodb/etc/configs/attributes.yaml +339 -0
- disdrodb/etc/configs/encodings.yaml +473 -0
- disdrodb/etc/products/L1/global.yaml +13 -0
- disdrodb/etc/products/L2E/10MIN.yaml +12 -0
- disdrodb/etc/products/L2E/1MIN.yaml +1 -0
- disdrodb/etc/products/L2E/global.yaml +22 -0
- disdrodb/etc/products/L2M/10MIN.yaml +12 -0
- disdrodb/etc/products/L2M/GAMMA_ML.yaml +8 -0
- disdrodb/etc/products/L2M/NGAMMA_GS_LOG_ND_MAE.yaml +6 -0
- disdrodb/etc/products/L2M/NGAMMA_GS_ND_MAE.yaml +6 -0
- disdrodb/etc/products/L2M/NGAMMA_GS_Z_MAE.yaml +6 -0
- disdrodb/etc/products/L2M/global.yaml +26 -0
- disdrodb/issue/writer.py +2 -0
- disdrodb/l0/__init__.py +13 -0
- disdrodb/l0/configs/LPM/l0b_cf_attrs.yml +4 -4
- disdrodb/l0/configs/PARSIVEL/l0b_cf_attrs.yml +1 -1
- disdrodb/l0/configs/PARSIVEL/l0b_encodings.yml +3 -3
- disdrodb/l0/configs/PARSIVEL/raw_data_format.yml +1 -1
- disdrodb/l0/configs/PARSIVEL2/l0b_cf_attrs.yml +5 -5
- disdrodb/l0/configs/PARSIVEL2/l0b_encodings.yml +3 -3
- disdrodb/l0/configs/PARSIVEL2/raw_data_format.yml +1 -1
- disdrodb/l0/configs/PWS100/l0b_cf_attrs.yml +4 -4
- disdrodb/l0/configs/PWS100/raw_data_format.yml +1 -1
- disdrodb/l0/l0a_processing.py +37 -32
- disdrodb/l0/l0b_nc_processing.py +118 -8
- disdrodb/l0/l0b_processing.py +30 -65
- disdrodb/l0/l0c_processing.py +369 -259
- disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +7 -0
- disdrodb/l0/readers/LPM/NETHERLANDS/DELFT_LPM_NC.py +66 -0
- disdrodb/l0/readers/LPM/SLOVENIA/{CRNI_VRH.py → UL.py} +3 -0
- disdrodb/l0/readers/LPM/SWITZERLAND/INNERERIZ_LPM.py +195 -0
- disdrodb/l0/readers/PARSIVEL/GPM/PIERS.py +0 -2
- disdrodb/l0/readers/PARSIVEL/JAPAN/JMA.py +4 -1
- disdrodb/l0/readers/PARSIVEL/NCAR/PECAN_MOBILE.py +1 -1
- disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2009.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +4 -0
- disdrodb/l0/readers/PARSIVEL2/BELGIUM/ILVO.py +168 -0
- disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +69 -0
- disdrodb/l0/readers/PARSIVEL2/DENMARK/DTU.py +165 -0
- disdrodb/l0/readers/PARSIVEL2/FINLAND/FMI_PARSIVEL2.py +69 -0
- disdrodb/l0/readers/PARSIVEL2/FRANCE/ENPC_PARSIVEL2.py +255 -134
- disdrodb/l0/readers/PARSIVEL2/FRANCE/OSUG.py +525 -0
- disdrodb/l0/readers/PARSIVEL2/FRANCE/SIRTA_PARSIVEL2.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/GPM/GCPEX.py +9 -7
- disdrodb/l0/readers/PARSIVEL2/KIT/BURKINA_FASO.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/KIT/TEAMX.py +123 -0
- disdrodb/l0/readers/PARSIVEL2/{NETHERLANDS/DELFT.py → MPI/BCO_PARSIVEL2.py} +41 -71
- disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +220 -0
- disdrodb/l0/readers/PARSIVEL2/NASA/APU.py +120 -0
- disdrodb/l0/readers/PARSIVEL2/NASA/LPVEX.py +109 -0
- disdrodb/l0/readers/PARSIVEL2/NCAR/FARM_PARSIVEL2.py +1 -0
- disdrodb/l0/readers/PARSIVEL2/NCAR/PECAN_FP3.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_MIPS.py +126 -0
- disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_PIPS.py +165 -0
- disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_P2.py +1 -1
- disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_PIPS.py +20 -12
- disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +5 -0
- disdrodb/l0/readers/PARSIVEL2/SPAIN/CENER.py +144 -0
- disdrodb/l0/readers/PARSIVEL2/SPAIN/CR1000DL.py +201 -0
- disdrodb/l0/readers/PARSIVEL2/SPAIN/LIAISE.py +137 -0
- disdrodb/l0/readers/PARSIVEL2/USA/C3WE.py +146 -0
- disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100.py +105 -99
- disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100_SIRTA.py +151 -0
- disdrodb/l1/__init__.py +5 -0
- disdrodb/l1/fall_velocity.py +46 -0
- disdrodb/l1/filters.py +34 -20
- disdrodb/l1/processing.py +46 -45
- disdrodb/l1/resampling.py +77 -66
- disdrodb/l1_env/routines.py +18 -3
- disdrodb/l2/__init__.py +7 -0
- disdrodb/l2/empirical_dsd.py +58 -10
- disdrodb/l2/processing.py +268 -117
- disdrodb/metadata/checks.py +132 -125
- disdrodb/metadata/standards.py +3 -1
- disdrodb/psd/fitting.py +631 -345
- disdrodb/psd/models.py +9 -6
- disdrodb/routines/__init__.py +54 -0
- disdrodb/{l0/routines.py → routines/l0.py} +316 -355
- disdrodb/{l1/routines.py → routines/l1.py} +76 -116
- disdrodb/routines/l2.py +1019 -0
- disdrodb/{routines.py → routines/wrappers.py} +98 -10
- disdrodb/scattering/__init__.py +16 -4
- disdrodb/scattering/axis_ratio.py +61 -37
- disdrodb/scattering/permittivity.py +504 -0
- disdrodb/scattering/routines.py +746 -184
- disdrodb/summary/__init__.py +17 -0
- disdrodb/summary/routines.py +4196 -0
- disdrodb/utils/archiving.py +434 -0
- disdrodb/utils/attrs.py +68 -125
- disdrodb/utils/cli.py +5 -5
- disdrodb/utils/compression.py +30 -1
- disdrodb/utils/dask.py +121 -9
- disdrodb/utils/dataframe.py +61 -7
- disdrodb/utils/decorators.py +31 -0
- disdrodb/utils/directories.py +35 -15
- disdrodb/utils/encoding.py +37 -19
- disdrodb/{l2 → utils}/event.py +15 -173
- disdrodb/utils/logger.py +14 -7
- disdrodb/utils/manipulations.py +81 -0
- disdrodb/utils/routines.py +166 -0
- disdrodb/utils/subsetting.py +214 -0
- disdrodb/utils/time.py +35 -177
- disdrodb/utils/writer.py +20 -7
- disdrodb/utils/xarray.py +5 -4
- disdrodb/viz/__init__.py +13 -0
- disdrodb/viz/plots.py +398 -0
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/METADATA +4 -3
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/RECORD +139 -98
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/entry_points.txt +2 -0
- disdrodb/l1/encoding_attrs.py +0 -642
- disdrodb/l2/processing_options.py +0 -213
- disdrodb/l2/routines.py +0 -868
- /disdrodb/l0/readers/PARSIVEL/SLOVENIA/{UL_FGG.py → UL.py} +0 -0
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/WHEEL +0 -0
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/top_level.txt +0 -0
disdrodb/api/checks.py
CHANGED
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
18
|
# -----------------------------------------------------------------------------.
|
|
19
19
|
"""DISDRODB Checks Functions."""
|
|
20
|
+
import datetime
|
|
21
|
+
import difflib
|
|
20
22
|
import logging
|
|
21
23
|
import os
|
|
22
24
|
import re
|
|
25
|
+
import sys
|
|
23
26
|
import warnings
|
|
24
27
|
|
|
25
28
|
import numpy as np
|
|
@@ -30,8 +33,10 @@ from disdrodb.api.path import (
|
|
|
30
33
|
define_issue_filepath,
|
|
31
34
|
define_metadata_filepath,
|
|
32
35
|
)
|
|
36
|
+
from disdrodb.constants import PRODUCTS, PRODUCTS_ARGUMENTS
|
|
33
37
|
from disdrodb.utils.directories import (
|
|
34
38
|
ensure_string_path,
|
|
39
|
+
list_directories,
|
|
35
40
|
list_files,
|
|
36
41
|
)
|
|
37
42
|
|
|
@@ -83,7 +88,7 @@ def check_path_is_a_directory(dir_path, path_name=""):
|
|
|
83
88
|
|
|
84
89
|
def check_directories_inside(dir_path):
|
|
85
90
|
"""Check there are directories inside the specified ``dir_path``."""
|
|
86
|
-
dir_paths =
|
|
91
|
+
dir_paths = list_directories(dir_path, recursive=False)
|
|
87
92
|
if len(dir_paths) == 0:
|
|
88
93
|
raise ValueError(f"There are not directories within {dir_path}")
|
|
89
94
|
|
|
@@ -106,6 +111,15 @@ def check_metadata_archive_dir(metadata_archive_dir: str):
|
|
|
106
111
|
return metadata_archive_dir
|
|
107
112
|
|
|
108
113
|
|
|
114
|
+
def check_scattering_table_dir(scattering_table_dir: str):
|
|
115
|
+
"""Raise an error if the directory does not exist."""
|
|
116
|
+
scattering_table_dir = str(scattering_table_dir) # convert Pathlib to string
|
|
117
|
+
scattering_table_dir = os.path.normpath(scattering_table_dir)
|
|
118
|
+
if not os.path.exists(scattering_table_dir):
|
|
119
|
+
raise ValueError(f"The DISDRODB T-Matrix scattering tables directory {scattering_table_dir} does not exists.")
|
|
120
|
+
return scattering_table_dir
|
|
121
|
+
|
|
122
|
+
|
|
109
123
|
def check_measurement_interval(measurement_interval):
|
|
110
124
|
"""Check measurement interval validity."""
|
|
111
125
|
if isinstance(measurement_interval, str) and measurement_interval == "":
|
|
@@ -130,14 +144,14 @@ def check_measurement_intervals(measurement_intervals):
|
|
|
130
144
|
|
|
131
145
|
def check_sample_interval(sample_interval):
|
|
132
146
|
"""Check sample_interval argument validity."""
|
|
133
|
-
if not isinstance(sample_interval, int):
|
|
134
|
-
raise
|
|
147
|
+
if not isinstance(sample_interval, int) or isinstance(sample_interval, bool):
|
|
148
|
+
raise TypeError("'sample_interval' must be an integer.")
|
|
135
149
|
|
|
136
150
|
|
|
137
151
|
def check_rolling(rolling):
|
|
138
152
|
"""Check rolling argument validity."""
|
|
139
153
|
if not isinstance(rolling, bool):
|
|
140
|
-
raise
|
|
154
|
+
raise TypeError("'rolling' must be a boolean.")
|
|
141
155
|
|
|
142
156
|
|
|
143
157
|
def check_folder_partitioning(folder_partitioning):
|
|
@@ -149,12 +163,12 @@ def check_folder_partitioning(folder_partitioning):
|
|
|
149
163
|
folder_partitioning : str or None
|
|
150
164
|
Defines the subdirectory structure based on the dataset's start time.
|
|
151
165
|
Allowed values are:
|
|
152
|
-
- "": No additional subdirectories, files are saved directly in
|
|
153
|
-
- "year": Files are stored under a subdirectory for the year (<
|
|
154
|
-
- "year/month": Files are stored under subdirectories by year and month (<
|
|
155
|
-
- "year/month/day": Files are stored under subdirectories by year, month and day (<
|
|
156
|
-
- "year/month_name": Files are stored under subdirectories by year and month name (<
|
|
157
|
-
- "year/quarter": Files are stored under subdirectories by year and quarter (<
|
|
166
|
+
- "" or None: No additional subdirectories, files are saved directly in dir.
|
|
167
|
+
- "year": Files are stored under a subdirectory for the year (<dir>/2025).
|
|
168
|
+
- "year/month": Files are stored under subdirectories by year and month (<dir>/2025/04).
|
|
169
|
+
- "year/month/day": Files are stored under subdirectories by year, month and day (<dir>/2025/04/01).
|
|
170
|
+
- "year/month_name": Files are stored under subdirectories by year and month name (<dir>/2025/April).
|
|
171
|
+
- "year/quarter": Files are stored under subdirectories by year and quarter (<dir>/2025/Q2).
|
|
158
172
|
|
|
159
173
|
Returns
|
|
160
174
|
-------
|
|
@@ -162,6 +176,8 @@ def check_folder_partitioning(folder_partitioning):
|
|
|
162
176
|
The verified folder partitioning scheme.
|
|
163
177
|
"""
|
|
164
178
|
valid_options = ["", "year", "year/month", "year/month/day", "year/month_name", "year/quarter"]
|
|
179
|
+
if folder_partitioning is None:
|
|
180
|
+
folder_partitioning = ""
|
|
165
181
|
if folder_partitioning not in valid_options:
|
|
166
182
|
raise ValueError(
|
|
167
183
|
f"Invalid folder_partitioning scheme '{folder_partitioning}'. Valid options are: {valid_options}.",
|
|
@@ -216,8 +232,6 @@ def check_data_source(data_source):
|
|
|
216
232
|
|
|
217
233
|
def check_product(product):
|
|
218
234
|
"""Check DISDRODB product."""
|
|
219
|
-
from disdrodb import PRODUCTS
|
|
220
|
-
|
|
221
235
|
if not isinstance(product, str):
|
|
222
236
|
raise TypeError("`product` must be a string.")
|
|
223
237
|
valid_products = PRODUCTS
|
|
@@ -248,8 +262,6 @@ def check_product_kwargs(product, product_kwargs):
|
|
|
248
262
|
ValueError
|
|
249
263
|
If required arguments are missing or if there are unexpected extra arguments.
|
|
250
264
|
"""
|
|
251
|
-
from disdrodb import PRODUCTS_ARGUMENTS
|
|
252
|
-
|
|
253
265
|
required = set(PRODUCTS_ARGUMENTS.get(product, []))
|
|
254
266
|
provided = set(product_kwargs.keys())
|
|
255
267
|
missing = required - provided
|
|
@@ -267,8 +279,6 @@ def check_product_kwargs(product, product_kwargs):
|
|
|
267
279
|
|
|
268
280
|
def select_required_product_kwargs(product, product_kwargs):
|
|
269
281
|
"""Select the required product arguments."""
|
|
270
|
-
from disdrodb import PRODUCTS_ARGUMENTS
|
|
271
|
-
|
|
272
282
|
required = set(PRODUCTS_ARGUMENTS.get(product, []))
|
|
273
283
|
provided = set(product_kwargs.keys())
|
|
274
284
|
missing = required - provided
|
|
@@ -323,16 +333,37 @@ def check_valid_fields(fields, available_fields, field_name, invalid_fields_poli
|
|
|
323
333
|
fields = [fields]
|
|
324
334
|
fields = np.unique(np.array(fields))
|
|
325
335
|
invalid_fields_policy = check_invalid_fields_policy(invalid_fields_policy)
|
|
336
|
+
|
|
326
337
|
# Check for invalid fields
|
|
327
338
|
fields = np.array(fields)
|
|
328
339
|
is_valid = np.isin(fields, available_fields)
|
|
329
340
|
invalid_fields_values = fields[~is_valid].tolist()
|
|
330
341
|
fields = fields[is_valid].tolist()
|
|
342
|
+
|
|
343
|
+
# If invalid fields, suggest corrections using difflib
|
|
344
|
+
if invalid_fields_values:
|
|
345
|
+
|
|
346
|
+
# Format invalid fields nicely (avoid single-element lists)
|
|
347
|
+
if len(invalid_fields_values) == 1:
|
|
348
|
+
invalid_fields_str = f"'{invalid_fields_values[0]}'"
|
|
349
|
+
else:
|
|
350
|
+
invalid_fields_str = f"{invalid_fields_values}"
|
|
351
|
+
|
|
352
|
+
# Prepare suggestion string
|
|
353
|
+
suggestions = []
|
|
354
|
+
for invalid in invalid_fields_values:
|
|
355
|
+
matches = difflib.get_close_matches(invalid, available_fields, n=1, cutoff=0.4)
|
|
356
|
+
if matches:
|
|
357
|
+
suggestions.append(f"Did you mean '{matches[0]}' instead of '{invalid}'?")
|
|
358
|
+
suggestion_msg = " " + " ".join(suggestions) if suggestions else ""
|
|
359
|
+
|
|
331
360
|
# Error handling for invalid fields were found
|
|
332
361
|
if invalid_fields_policy == "warn" and invalid_fields_values:
|
|
333
|
-
|
|
362
|
+
msg = f"Ignoring invalid {field_name}: {invalid_fields_str}.{suggestion_msg}"
|
|
363
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
334
364
|
elif invalid_fields_policy == "raise" and invalid_fields_values:
|
|
335
|
-
|
|
365
|
+
msg = f"These {field_name} do not exist: {invalid_fields_str}.{suggestion_msg}"
|
|
366
|
+
raise ValueError(msg)
|
|
336
367
|
else: # "ignore" silently drop invalid entries
|
|
337
368
|
pass
|
|
338
369
|
# If no valid fields left, raise error
|
|
@@ -341,6 +372,46 @@ def check_valid_fields(fields, available_fields, field_name, invalid_fields_poli
|
|
|
341
372
|
return fields
|
|
342
373
|
|
|
343
374
|
|
|
375
|
+
def check_station_inputs(
|
|
376
|
+
data_source,
|
|
377
|
+
campaign_name,
|
|
378
|
+
station_name,
|
|
379
|
+
metadata_archive_dir=None,
|
|
380
|
+
):
|
|
381
|
+
"""Check validity of stations inputs."""
|
|
382
|
+
import disdrodb
|
|
383
|
+
|
|
384
|
+
# Check data source
|
|
385
|
+
valid_data_sources = disdrodb.available_data_sources(metadata_archive_dir=metadata_archive_dir)
|
|
386
|
+
if data_source not in valid_data_sources:
|
|
387
|
+
matches = difflib.get_close_matches(data_source, valid_data_sources, n=1, cutoff=0.4)
|
|
388
|
+
suggestion = f"Did you mean '{matches[0]}'?" if matches else ""
|
|
389
|
+
raise ValueError(f"DISDRODB does not include a data source named {data_source}. {suggestion}")
|
|
390
|
+
|
|
391
|
+
# Check campaign name
|
|
392
|
+
valid_campaigns = disdrodb.available_campaigns(data_sources=data_source, metadata_archive_dir=metadata_archive_dir)
|
|
393
|
+
if campaign_name not in valid_campaigns:
|
|
394
|
+
matches = difflib.get_close_matches(campaign_name, valid_campaigns, n=1, cutoff=0.4)
|
|
395
|
+
suggestion = f"Did you mean campaign '{matches[0]}' ?" if matches else ""
|
|
396
|
+
raise ValueError(
|
|
397
|
+
f"The {data_source} data source does not include a campaign named {campaign_name}. {suggestion}",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Check station name
|
|
401
|
+
valid_stations = disdrodb.available_stations(
|
|
402
|
+
data_sources=data_source,
|
|
403
|
+
campaign_names=campaign_name,
|
|
404
|
+
metadata_archive_dir=metadata_archive_dir,
|
|
405
|
+
return_tuple=False,
|
|
406
|
+
)
|
|
407
|
+
if station_name not in valid_stations:
|
|
408
|
+
matches = difflib.get_close_matches(station_name, valid_stations, n=1, cutoff=0.4)
|
|
409
|
+
suggestion = f"Did you mean station '{matches[0]}'?" if matches else ""
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"The {data_source} {campaign_name} campaign does not have a station named {station_name}. {suggestion}",
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
344
415
|
def has_available_data(
|
|
345
416
|
data_source,
|
|
346
417
|
campaign_name,
|
|
@@ -368,7 +439,7 @@ def has_available_data(
|
|
|
368
439
|
return False
|
|
369
440
|
|
|
370
441
|
# If no files, return False
|
|
371
|
-
filepaths = list_files(data_dir,
|
|
442
|
+
filepaths = list_files(data_dir, recursive=True)
|
|
372
443
|
nfiles = len(filepaths)
|
|
373
444
|
return nfiles >= 1
|
|
374
445
|
|
|
@@ -412,7 +483,6 @@ def check_metadata_file(metadata_archive_dir, data_source, campaign_name, statio
|
|
|
412
483
|
f"The metadata YAML file of {data_source} {campaign_name} {station_name} does not exist at"
|
|
413
484
|
f" {metadata_filepath}."
|
|
414
485
|
)
|
|
415
|
-
logger.error(msg)
|
|
416
486
|
raise ValueError(msg)
|
|
417
487
|
|
|
418
488
|
# Check validity
|
|
@@ -434,10 +504,9 @@ def check_issue_dir(data_source, campaign_name, metadata_archive_dir=None):
|
|
|
434
504
|
campaign_name=campaign_name,
|
|
435
505
|
check_exists=False,
|
|
436
506
|
)
|
|
437
|
-
if not os.path.exists(issue_dir)
|
|
438
|
-
msg = "The issue directory does not exist at {issue_dir}."
|
|
507
|
+
if not os.path.exists(issue_dir) or not os.path.isdir(issue_dir):
|
|
508
|
+
msg = f"The issue directory does not exist at {issue_dir}."
|
|
439
509
|
logger.error(msg)
|
|
440
|
-
raise ValueError(msg)
|
|
441
510
|
return issue_dir
|
|
442
511
|
|
|
443
512
|
|
|
@@ -458,7 +527,7 @@ def check_issue_file(data_source, campaign_name, station_name, metadata_archive_
|
|
|
458
527
|
station_name=station_name,
|
|
459
528
|
check_exists=False,
|
|
460
529
|
)
|
|
461
|
-
# Check existence
|
|
530
|
+
# Check existence. If not, create one !
|
|
462
531
|
if not os.path.exists(issue_filepath):
|
|
463
532
|
create_station_issue(
|
|
464
533
|
metadata_archive_dir=metadata_archive_dir,
|
|
@@ -475,3 +544,87 @@ def check_issue_file(data_source, campaign_name, station_name, metadata_archive_
|
|
|
475
544
|
station_name=station_name,
|
|
476
545
|
)
|
|
477
546
|
return issue_filepath
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def check_filepaths(filepaths):
|
|
550
|
+
"""Ensure filepaths is a list of string."""
|
|
551
|
+
if isinstance(filepaths, str):
|
|
552
|
+
filepaths = [filepaths]
|
|
553
|
+
if not isinstance(filepaths, list):
|
|
554
|
+
raise TypeError("Expecting a list of filepaths.")
|
|
555
|
+
return filepaths
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def get_current_utc_time():
|
|
559
|
+
"""Get current UTC time."""
|
|
560
|
+
if sys.version_info >= (3, 11):
|
|
561
|
+
return datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
|
|
562
|
+
return datetime.datetime.utcnow()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def check_start_end_time(start_time, end_time):
|
|
566
|
+
"""Check start_time and end_time value validity."""
|
|
567
|
+
start_time = check_time(start_time)
|
|
568
|
+
end_time = check_time(end_time)
|
|
569
|
+
|
|
570
|
+
# Check start_time and end_time are chronological
|
|
571
|
+
if start_time > end_time:
|
|
572
|
+
raise ValueError("Provide 'start_time' occurring before of 'end_time'.")
|
|
573
|
+
# Check start_time and end_time are in the past
|
|
574
|
+
if start_time > get_current_utc_time():
|
|
575
|
+
raise ValueError("Provide 'start_time' occurring in the past.")
|
|
576
|
+
if end_time > get_current_utc_time():
|
|
577
|
+
raise ValueError("Provide 'end_time' occurring in the past.")
|
|
578
|
+
return (start_time, end_time)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def check_time(time):
|
|
582
|
+
"""Check time validity.
|
|
583
|
+
|
|
584
|
+
It returns a :py:class:`datetime.datetime` object to seconds precision.
|
|
585
|
+
|
|
586
|
+
Parameters
|
|
587
|
+
----------
|
|
588
|
+
time : datetime.datetime, datetime.date, numpy.datetime64 or str
|
|
589
|
+
Time object.
|
|
590
|
+
Accepted types: ``datetime.datetime``, ``datetime.date``, ``numpy.datetime64`` or ``str``.
|
|
591
|
+
If string type, it expects the isoformat ``YYYY-MM-DD hh:mm:ss``.
|
|
592
|
+
|
|
593
|
+
Returns
|
|
594
|
+
-------
|
|
595
|
+
time: datetime.datetime
|
|
596
|
+
|
|
597
|
+
"""
|
|
598
|
+
if not isinstance(time, (datetime.datetime, datetime.date, np.datetime64, np.ndarray, str)):
|
|
599
|
+
raise TypeError(
|
|
600
|
+
"Specify time with datetime.datetime objects or a " "string of format 'YYYY-MM-DD hh:mm:ss'.",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# If numpy array with datetime64 (and size=1)
|
|
604
|
+
if isinstance(time, np.ndarray):
|
|
605
|
+
if np.issubdtype(time.dtype, np.datetime64):
|
|
606
|
+
if time.size == 1:
|
|
607
|
+
time = time[0].astype("datetime64[s]").tolist()
|
|
608
|
+
else:
|
|
609
|
+
raise ValueError("Expecting a single timestep!")
|
|
610
|
+
else:
|
|
611
|
+
raise ValueError("The numpy array does not have a numpy.datetime64 dtype!")
|
|
612
|
+
|
|
613
|
+
# If np.datetime64, convert to datetime.datetime
|
|
614
|
+
if isinstance(time, np.datetime64):
|
|
615
|
+
time = time.astype("datetime64[s]").tolist()
|
|
616
|
+
# If datetime.date, convert to datetime.datetime
|
|
617
|
+
if not isinstance(time, (datetime.datetime, str)):
|
|
618
|
+
time = datetime.datetime(time.year, time.month, time.day, 0, 0, 0)
|
|
619
|
+
if isinstance(time, str):
|
|
620
|
+
try:
|
|
621
|
+
time = datetime.datetime.fromisoformat(time)
|
|
622
|
+
except ValueError:
|
|
623
|
+
raise ValueError("The time string must have format 'YYYY-MM-DD hh:mm:ss'")
|
|
624
|
+
# If datetime object carries timezone that is not UTC, raise error
|
|
625
|
+
if time.tzinfo is not None:
|
|
626
|
+
if str(time.tzinfo) != "UTC":
|
|
627
|
+
raise ValueError("The datetime object must be in UTC timezone if timezone is given.")
|
|
628
|
+
# If UTC, strip timezone information
|
|
629
|
+
time = time.replace(tzinfo=None)
|
|
630
|
+
return time
|
disdrodb/api/configs.py
CHANGED
|
@@ -23,6 +23,7 @@ import os
|
|
|
23
23
|
|
|
24
24
|
from disdrodb.api.checks import check_product, check_sensor_name
|
|
25
25
|
from disdrodb.api.path import define_config_dir
|
|
26
|
+
from disdrodb.utils.directories import list_directories
|
|
26
27
|
from disdrodb.utils.yaml import read_yaml
|
|
27
28
|
|
|
28
29
|
logger = logging.getLogger(__name__)
|
|
@@ -53,8 +54,6 @@ def get_sensor_configs_dir(sensor_name: str, product: str) -> str:
|
|
|
53
54
|
config_dir = define_config_dir(product=product)
|
|
54
55
|
config_sensor_dir = os.path.join(config_dir, sensor_name)
|
|
55
56
|
if not os.path.exists(config_sensor_dir):
|
|
56
|
-
list_sensors = sorted(os.listdir(config_dir))
|
|
57
|
-
print(f"Available sensor_name are {list_sensors}")
|
|
58
57
|
raise ValueError(f"The config directory {config_sensor_dir} does not exist.")
|
|
59
58
|
return config_sensor_dir
|
|
60
59
|
|
|
@@ -102,4 +101,5 @@ def available_sensor_names() -> list:
|
|
|
102
101
|
Sorted list of the available sensors
|
|
103
102
|
"""
|
|
104
103
|
config_dir = define_config_dir(product="L0A")
|
|
105
|
-
|
|
104
|
+
list_sensors = sorted(list_directories(config_dir, recursive=False, return_paths=False))
|
|
105
|
+
return list_sensors
|
disdrodb/api/info.py
CHANGED
|
@@ -25,7 +25,7 @@ from pathlib import Path
|
|
|
25
25
|
import numpy as np
|
|
26
26
|
from trollsift import Parser
|
|
27
27
|
|
|
28
|
-
from disdrodb.utils.time import
|
|
28
|
+
from disdrodb.utils.time import temporal_resolution_to_seconds
|
|
29
29
|
|
|
30
30
|
####---------------------------------------------------------------------------
|
|
31
31
|
########################
|
|
@@ -35,13 +35,13 @@ DISDRODB_FNAME_L0_PATTERN = (
|
|
|
35
35
|
"{product:s}.{campaign_name:s}.{station_name:s}.s{start_time:%Y%m%d%H%M%S}.e{end_time:%Y%m%d%H%M%S}"
|
|
36
36
|
".{version:s}.{data_format:s}"
|
|
37
37
|
)
|
|
38
|
-
DISDRODB_FNAME_L2E_PATTERN = ( # also L0C and L1
|
|
39
|
-
"{product:s}.{
|
|
38
|
+
DISDRODB_FNAME_L2E_PATTERN = ( # also L0C and L1
|
|
39
|
+
"{product:s}.{temporal_resolution}.{campaign_name:s}.{station_name:s}.s{start_time:%Y%m%d%H%M%S}.e{end_time:%Y%m%d%H%M%S}"
|
|
40
40
|
".{version:s}.{data_format:s}"
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
DISDRODB_FNAME_L2M_PATTERN = (
|
|
44
|
-
"{product:s}_{subproduct:s}.{
|
|
44
|
+
"{product:s}_{subproduct:s}.{temporal_resolution}.{campaign_name:s}.{station_name:s}.s{start_time:%Y%m%d%H%M%S}.e{end_time:%Y%m%d%H%M%S}"
|
|
45
45
|
".{version:s}.{data_format:s}"
|
|
46
46
|
)
|
|
47
47
|
|
|
@@ -76,8 +76,8 @@ def _get_info_from_filename(filename):
|
|
|
76
76
|
raise ValueError(f"{filename} can not be parsed. Report the issue.")
|
|
77
77
|
|
|
78
78
|
# Add additional information to info dictionary
|
|
79
|
-
if "
|
|
80
|
-
info_dict["sample_interval"] =
|
|
79
|
+
if "temporal_resolution" in info_dict:
|
|
80
|
+
info_dict["sample_interval"] = temporal_resolution_to_seconds(info_dict["temporal_resolution"])
|
|
81
81
|
|
|
82
82
|
# Return info dictionary
|
|
83
83
|
return info_dict
|
|
@@ -162,8 +162,8 @@ def get_start_end_time_from_filepaths(filepaths):
|
|
|
162
162
|
|
|
163
163
|
def get_sample_interval_from_filepaths(filepaths):
|
|
164
164
|
"""Return the sample interval of the specified files."""
|
|
165
|
-
|
|
166
|
-
list_sample_interval = [
|
|
165
|
+
list_temporal_resolution = get_key_from_filepaths(filepaths, key="temporal_resolution")
|
|
166
|
+
list_sample_interval = [temporal_resolution_to_seconds(s) for s in list_temporal_resolution]
|
|
167
167
|
return list_sample_interval
|
|
168
168
|
|
|
169
169
|
|
|
@@ -345,7 +345,7 @@ FILE_KEYS = [
|
|
|
345
345
|
"start_time",
|
|
346
346
|
"end_time",
|
|
347
347
|
"data_format",
|
|
348
|
-
"
|
|
348
|
+
"temporal_resolution",
|
|
349
349
|
"sample_interval",
|
|
350
350
|
]
|
|
351
351
|
|
|
@@ -410,8 +410,8 @@ def get_time_component(time, component):
|
|
|
410
410
|
return str(func_dict[component](time))
|
|
411
411
|
|
|
412
412
|
|
|
413
|
-
def
|
|
414
|
-
"""Return
|
|
413
|
+
def get_groups_value(groups, filepath):
|
|
414
|
+
"""Return a string associated to the groups keys.
|
|
415
415
|
|
|
416
416
|
If multiple keys are specified, the value returned is a string of format: ``<group_value_1>/<group_value_2>/...``
|
|
417
417
|
|
|
@@ -444,7 +444,7 @@ def group_filepaths(filepaths, groups=None):
|
|
|
444
444
|
groups: list or str
|
|
445
445
|
The group keys by which to group the filepaths.
|
|
446
446
|
Valid group keys are ``product``, ``subproduct``, ``campaign_name``, ``station_name``,
|
|
447
|
-
``start_time``, ``end_time``,``
|
|
447
|
+
``start_time``, ``end_time``,``temporal_resolution``,``sample_interval``,
|
|
448
448
|
``data_format``,
|
|
449
449
|
``year``, ``month``, ``day``, ``doy``, ``dow``, ``hour``, ``minute``, ``second``,
|
|
450
450
|
``month_name``, ``quarter``, ``season``.
|
|
@@ -463,5 +463,5 @@ def group_filepaths(filepaths, groups=None):
|
|
|
463
463
|
return filepaths
|
|
464
464
|
groups = check_groups(groups)
|
|
465
465
|
filepaths_dict = defaultdict(list)
|
|
466
|
-
_ = [filepaths_dict[
|
|
466
|
+
_ = [filepaths_dict[get_groups_value(groups, filepath)].append(filepath) for filepath in filepaths]
|
|
467
467
|
return dict(filepaths_dict)
|