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.
Files changed (142) hide show
  1. disdrodb/__init__.py +68 -34
  2. disdrodb/_config.py +5 -4
  3. disdrodb/_version.py +16 -3
  4. disdrodb/accessor/__init__.py +20 -0
  5. disdrodb/accessor/methods.py +125 -0
  6. disdrodb/api/checks.py +177 -24
  7. disdrodb/api/configs.py +3 -3
  8. disdrodb/api/info.py +13 -13
  9. disdrodb/api/io.py +281 -22
  10. disdrodb/api/path.py +184 -195
  11. disdrodb/api/search.py +18 -9
  12. disdrodb/cli/disdrodb_create_summary.py +103 -0
  13. disdrodb/cli/disdrodb_create_summary_station.py +91 -0
  14. disdrodb/cli/disdrodb_run_l0.py +1 -1
  15. disdrodb/cli/disdrodb_run_l0_station.py +1 -1
  16. disdrodb/cli/disdrodb_run_l0a_station.py +1 -1
  17. disdrodb/cli/disdrodb_run_l0b.py +1 -1
  18. disdrodb/cli/disdrodb_run_l0b_station.py +3 -3
  19. disdrodb/cli/disdrodb_run_l0c.py +1 -1
  20. disdrodb/cli/disdrodb_run_l0c_station.py +3 -3
  21. disdrodb/cli/disdrodb_run_l1_station.py +2 -2
  22. disdrodb/cli/disdrodb_run_l2e_station.py +2 -2
  23. disdrodb/cli/disdrodb_run_l2m_station.py +2 -2
  24. disdrodb/configs.py +149 -4
  25. disdrodb/constants.py +61 -0
  26. disdrodb/data_transfer/download_data.py +127 -11
  27. disdrodb/etc/configs/attributes.yaml +339 -0
  28. disdrodb/etc/configs/encodings.yaml +473 -0
  29. disdrodb/etc/products/L1/global.yaml +13 -0
  30. disdrodb/etc/products/L2E/10MIN.yaml +12 -0
  31. disdrodb/etc/products/L2E/1MIN.yaml +1 -0
  32. disdrodb/etc/products/L2E/global.yaml +22 -0
  33. disdrodb/etc/products/L2M/10MIN.yaml +12 -0
  34. disdrodb/etc/products/L2M/GAMMA_ML.yaml +8 -0
  35. disdrodb/etc/products/L2M/NGAMMA_GS_LOG_ND_MAE.yaml +6 -0
  36. disdrodb/etc/products/L2M/NGAMMA_GS_ND_MAE.yaml +6 -0
  37. disdrodb/etc/products/L2M/NGAMMA_GS_Z_MAE.yaml +6 -0
  38. disdrodb/etc/products/L2M/global.yaml +26 -0
  39. disdrodb/issue/writer.py +2 -0
  40. disdrodb/l0/__init__.py +13 -0
  41. disdrodb/l0/configs/LPM/l0b_cf_attrs.yml +4 -4
  42. disdrodb/l0/configs/PARSIVEL/l0b_cf_attrs.yml +1 -1
  43. disdrodb/l0/configs/PARSIVEL/l0b_encodings.yml +3 -3
  44. disdrodb/l0/configs/PARSIVEL/raw_data_format.yml +1 -1
  45. disdrodb/l0/configs/PARSIVEL2/l0b_cf_attrs.yml +5 -5
  46. disdrodb/l0/configs/PARSIVEL2/l0b_encodings.yml +3 -3
  47. disdrodb/l0/configs/PARSIVEL2/raw_data_format.yml +1 -1
  48. disdrodb/l0/configs/PWS100/l0b_cf_attrs.yml +4 -4
  49. disdrodb/l0/configs/PWS100/raw_data_format.yml +1 -1
  50. disdrodb/l0/l0a_processing.py +37 -32
  51. disdrodb/l0/l0b_nc_processing.py +118 -8
  52. disdrodb/l0/l0b_processing.py +30 -65
  53. disdrodb/l0/l0c_processing.py +369 -259
  54. disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +7 -0
  55. disdrodb/l0/readers/LPM/NETHERLANDS/DELFT_LPM_NC.py +66 -0
  56. disdrodb/l0/readers/LPM/SLOVENIA/{CRNI_VRH.py → UL.py} +3 -0
  57. disdrodb/l0/readers/LPM/SWITZERLAND/INNERERIZ_LPM.py +195 -0
  58. disdrodb/l0/readers/PARSIVEL/GPM/PIERS.py +0 -2
  59. disdrodb/l0/readers/PARSIVEL/JAPAN/JMA.py +4 -1
  60. disdrodb/l0/readers/PARSIVEL/NCAR/PECAN_MOBILE.py +1 -1
  61. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2009.py +1 -1
  62. disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +4 -0
  63. disdrodb/l0/readers/PARSIVEL2/BELGIUM/ILVO.py +168 -0
  64. disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +69 -0
  65. disdrodb/l0/readers/PARSIVEL2/DENMARK/DTU.py +165 -0
  66. disdrodb/l0/readers/PARSIVEL2/FINLAND/FMI_PARSIVEL2.py +69 -0
  67. disdrodb/l0/readers/PARSIVEL2/FRANCE/ENPC_PARSIVEL2.py +255 -134
  68. disdrodb/l0/readers/PARSIVEL2/FRANCE/OSUG.py +525 -0
  69. disdrodb/l0/readers/PARSIVEL2/FRANCE/SIRTA_PARSIVEL2.py +1 -1
  70. disdrodb/l0/readers/PARSIVEL2/GPM/GCPEX.py +9 -7
  71. disdrodb/l0/readers/PARSIVEL2/KIT/BURKINA_FASO.py +1 -1
  72. disdrodb/l0/readers/PARSIVEL2/KIT/TEAMX.py +123 -0
  73. disdrodb/l0/readers/PARSIVEL2/{NETHERLANDS/DELFT.py → MPI/BCO_PARSIVEL2.py} +41 -71
  74. disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +220 -0
  75. disdrodb/l0/readers/PARSIVEL2/NASA/APU.py +120 -0
  76. disdrodb/l0/readers/PARSIVEL2/NASA/LPVEX.py +109 -0
  77. disdrodb/l0/readers/PARSIVEL2/NCAR/FARM_PARSIVEL2.py +1 -0
  78. disdrodb/l0/readers/PARSIVEL2/NCAR/PECAN_FP3.py +1 -1
  79. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_MIPS.py +126 -0
  80. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_PIPS.py +165 -0
  81. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_P2.py +1 -1
  82. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_PIPS.py +20 -12
  83. disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +5 -0
  84. disdrodb/l0/readers/PARSIVEL2/SPAIN/CENER.py +144 -0
  85. disdrodb/l0/readers/PARSIVEL2/SPAIN/CR1000DL.py +201 -0
  86. disdrodb/l0/readers/PARSIVEL2/SPAIN/LIAISE.py +137 -0
  87. disdrodb/l0/readers/PARSIVEL2/USA/C3WE.py +146 -0
  88. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100.py +105 -99
  89. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100_SIRTA.py +151 -0
  90. disdrodb/l1/__init__.py +5 -0
  91. disdrodb/l1/fall_velocity.py +46 -0
  92. disdrodb/l1/filters.py +34 -20
  93. disdrodb/l1/processing.py +46 -45
  94. disdrodb/l1/resampling.py +77 -66
  95. disdrodb/l1_env/routines.py +18 -3
  96. disdrodb/l2/__init__.py +7 -0
  97. disdrodb/l2/empirical_dsd.py +58 -10
  98. disdrodb/l2/processing.py +268 -117
  99. disdrodb/metadata/checks.py +132 -125
  100. disdrodb/metadata/standards.py +3 -1
  101. disdrodb/psd/fitting.py +631 -345
  102. disdrodb/psd/models.py +9 -6
  103. disdrodb/routines/__init__.py +54 -0
  104. disdrodb/{l0/routines.py → routines/l0.py} +316 -355
  105. disdrodb/{l1/routines.py → routines/l1.py} +76 -116
  106. disdrodb/routines/l2.py +1019 -0
  107. disdrodb/{routines.py → routines/wrappers.py} +98 -10
  108. disdrodb/scattering/__init__.py +16 -4
  109. disdrodb/scattering/axis_ratio.py +61 -37
  110. disdrodb/scattering/permittivity.py +504 -0
  111. disdrodb/scattering/routines.py +746 -184
  112. disdrodb/summary/__init__.py +17 -0
  113. disdrodb/summary/routines.py +4196 -0
  114. disdrodb/utils/archiving.py +434 -0
  115. disdrodb/utils/attrs.py +68 -125
  116. disdrodb/utils/cli.py +5 -5
  117. disdrodb/utils/compression.py +30 -1
  118. disdrodb/utils/dask.py +121 -9
  119. disdrodb/utils/dataframe.py +61 -7
  120. disdrodb/utils/decorators.py +31 -0
  121. disdrodb/utils/directories.py +35 -15
  122. disdrodb/utils/encoding.py +37 -19
  123. disdrodb/{l2 → utils}/event.py +15 -173
  124. disdrodb/utils/logger.py +14 -7
  125. disdrodb/utils/manipulations.py +81 -0
  126. disdrodb/utils/routines.py +166 -0
  127. disdrodb/utils/subsetting.py +214 -0
  128. disdrodb/utils/time.py +35 -177
  129. disdrodb/utils/writer.py +20 -7
  130. disdrodb/utils/xarray.py +5 -4
  131. disdrodb/viz/__init__.py +13 -0
  132. disdrodb/viz/plots.py +398 -0
  133. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/METADATA +4 -3
  134. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/RECORD +139 -98
  135. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/entry_points.txt +2 -0
  136. disdrodb/l1/encoding_attrs.py +0 -642
  137. disdrodb/l2/processing_options.py +0 -213
  138. disdrodb/l2/routines.py +0 -868
  139. /disdrodb/l0/readers/PARSIVEL/SLOVENIA/{UL_FGG.py → UL.py} +0 -0
  140. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/WHEEL +0 -0
  141. {disdrodb-0.1.2.dist-info → disdrodb-0.1.4.dist-info}/licenses/LICENSE +0 -0
  142. {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 = os.listdir(dir_path)
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 ValueError("'sample_interval' must be an integer.")
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 ValueError("'rolling' must be a boolean.")
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 data_dir.
153
- - "year": Files are stored under a subdirectory for the year (<data_dir>/2025).
154
- - "year/month": Files are stored under subdirectories by year and month (<data_dir>/2025/04).
155
- - "year/month/day": Files are stored under subdirectories by year, month and day (<data_dir>/2025/04/01).
156
- - "year/month_name": Files are stored under subdirectories by year and month name (<data_dir>/2025/April).
157
- - "year/quarter": Files are stored under subdirectories by year and quarter (<data_dir>/2025/Q2).
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
- warnings.warn(f"Ignoring invalid {field_name}: {invalid_fields_values}", UserWarning, stacklevel=2)
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
- raise ValueError(f"These {field_name} does not exist: {invalid_fields_values}.")
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, glob_pattern="*", recursive=True)
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) and os.path.isdir(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
- return sorted(os.listdir(config_dir))
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 acronym_to_seconds
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 --> accumulation_acronym = sample_interval
39
- "{product:s}.{accumulation_acronym}.{campaign_name:s}.{station_name:s}.s{start_time:%Y%m%d%H%M%S}.e{end_time:%Y%m%d%H%M%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}.{accumulation_acronym}.{campaign_name:s}.{station_name:s}.s{start_time:%Y%m%d%H%M%S}.e{end_time:%Y%m%d%H%M%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 "accumulation_acronym" in info_dict:
80
- info_dict["sample_interval"] = acronym_to_seconds(info_dict["accumulation_acronym"])
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
- list_accumulation_acronym = get_key_from_filepaths(filepaths, key="accumulation_acronym")
166
- list_sample_interval = [acronym_to_seconds(s) for s in list_accumulation_acronym]
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
- "accumulation_acronym",
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 _get_groups_value(groups, filepath):
414
- """Return the value associated to the groups keys.
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``,``accumulation_acronym``,``sample_interval``,
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[_get_groups_value(groups, filepath)].append(filepath) for filepath in filepaths]
466
+ _ = [filepaths_dict[get_groups_value(groups, filepath)].append(filepath) for filepath in filepaths]
467
467
  return dict(filepaths_dict)