anemoi-datasets 0.3.10__py3-none-any.whl → 0.4.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.
Files changed (61) hide show
  1. anemoi/datasets/_version.py +2 -2
  2. anemoi/datasets/commands/compare.py +59 -0
  3. anemoi/datasets/commands/create.py +84 -3
  4. anemoi/datasets/commands/inspect.py +9 -9
  5. anemoi/datasets/commands/scan.py +4 -4
  6. anemoi/datasets/compute/recentre.py +14 -9
  7. anemoi/datasets/create/__init__.py +44 -17
  8. anemoi/datasets/create/check.py +6 -5
  9. anemoi/datasets/create/chunks.py +1 -1
  10. anemoi/datasets/create/config.py +6 -27
  11. anemoi/datasets/create/functions/__init__.py +3 -3
  12. anemoi/datasets/create/functions/filters/empty.py +4 -4
  13. anemoi/datasets/create/functions/filters/rename.py +14 -6
  14. anemoi/datasets/create/functions/filters/rotate_winds.py +16 -60
  15. anemoi/datasets/create/functions/filters/unrotate_winds.py +14 -64
  16. anemoi/datasets/create/functions/sources/__init__.py +39 -0
  17. anemoi/datasets/create/functions/sources/accumulations.py +38 -56
  18. anemoi/datasets/create/functions/sources/constants.py +11 -4
  19. anemoi/datasets/create/functions/sources/empty.py +2 -2
  20. anemoi/datasets/create/functions/sources/forcings.py +3 -3
  21. anemoi/datasets/create/functions/sources/grib.py +8 -4
  22. anemoi/datasets/create/functions/sources/hindcasts.py +32 -364
  23. anemoi/datasets/create/functions/sources/mars.py +57 -26
  24. anemoi/datasets/create/functions/sources/netcdf.py +2 -60
  25. anemoi/datasets/create/functions/sources/opendap.py +3 -2
  26. anemoi/datasets/create/functions/sources/source.py +3 -3
  27. anemoi/datasets/create/functions/sources/tendencies.py +7 -7
  28. anemoi/datasets/create/functions/sources/xarray/__init__.py +73 -0
  29. anemoi/datasets/create/functions/sources/xarray/coordinates.py +234 -0
  30. anemoi/datasets/create/functions/sources/xarray/field.py +109 -0
  31. anemoi/datasets/create/functions/sources/xarray/fieldlist.py +171 -0
  32. anemoi/datasets/create/functions/sources/xarray/flavour.py +330 -0
  33. anemoi/datasets/create/functions/sources/xarray/grid.py +46 -0
  34. anemoi/datasets/create/functions/sources/xarray/metadata.py +161 -0
  35. anemoi/datasets/create/functions/sources/xarray/time.py +98 -0
  36. anemoi/datasets/create/functions/sources/xarray/variable.py +198 -0
  37. anemoi/datasets/create/functions/sources/xarray_kerchunk.py +42 -0
  38. anemoi/datasets/create/functions/sources/xarray_zarr.py +15 -0
  39. anemoi/datasets/create/functions/sources/zenodo.py +40 -0
  40. anemoi/datasets/create/input.py +309 -191
  41. anemoi/datasets/create/loaders.py +155 -77
  42. anemoi/datasets/create/patch.py +17 -14
  43. anemoi/datasets/create/persistent.py +1 -1
  44. anemoi/datasets/create/size.py +4 -5
  45. anemoi/datasets/create/statistics/__init__.py +51 -17
  46. anemoi/datasets/create/template.py +11 -61
  47. anemoi/datasets/create/trace.py +91 -0
  48. anemoi/datasets/create/utils.py +5 -52
  49. anemoi/datasets/create/zarr.py +24 -10
  50. anemoi/datasets/data/dataset.py +4 -4
  51. anemoi/datasets/data/misc.py +9 -37
  52. anemoi/datasets/data/stores.py +37 -14
  53. anemoi/datasets/dates/__init__.py +7 -1
  54. anemoi/datasets/dates/groups.py +3 -0
  55. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/METADATA +24 -8
  56. anemoi_datasets-0.4.2.dist-info/RECORD +86 -0
  57. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/WHEEL +1 -1
  58. anemoi_datasets-0.3.10.dist-info/RECORD +0 -73
  59. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/LICENSE +0 -0
  60. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/entry_points.txt +0 -0
  61. {anemoi_datasets-0.3.10.dist-info → anemoi_datasets-0.4.2.dist-info}/top_level.txt +0 -0
@@ -9,61 +9,10 @@
9
9
 
10
10
  from collections import defaultdict
11
11
 
12
- from climetlab.indexing.fieldset import FieldArray
13
-
14
-
15
- def rotate_winds(lats, lons, x_wind, y_wind, source_projection, target_projection):
16
- """Code provided by MetNO"""
17
- import numpy as np
18
- import pyproj
19
-
20
- if source_projection == target_projection:
21
- return x_wind, x_wind
22
-
23
- source_projection = pyproj.Proj(source_projection)
24
- target_projection = pyproj.Proj(target_projection)
25
-
26
- transformer = pyproj.transformer.Transformer.from_proj(source_projection, target_projection)
27
-
28
- # To compute the new vector components:
29
- # 1) perturb each position in the direction of the winds
30
- # 2) convert the perturbed positions into the new coordinate system
31
- # 3) measure the new x/y components.
32
- #
33
- # A complication occurs when using the longlat "projections", since this is not a cartesian grid
34
- # (i.e. distances in each direction is not consistent), we need to deal with the fact that the
35
- # width of a longitude varies with latitude
36
- orig_speed = np.sqrt(x_wind**2 + y_wind**2)
37
-
38
- x0, y0 = source_projection(lons, lats)
39
-
40
- if source_projection.name != "longlat":
41
- x1 = x0 + x_wind
42
- y1 = y0 + y_wind
43
- else:
44
- # Reduce the perturbation, since x_wind and y_wind are in meters, which would create
45
- # large perturbations in lat, lon. Also, deal with the fact that the width of longitude
46
- # varies with latitude.
47
- factor = 3600000.0
48
- x1 = x0 + x_wind / factor / np.cos(np.deg2rad(lats))
49
- y1 = y0 + y_wind / factor
50
-
51
- X0, Y0 = transformer.transform(x0, y0)
52
- X1, Y1 = transformer.transform(x1, y1)
53
-
54
- new_x_wind = X1 - X0
55
- new_y_wind = Y1 - Y0
56
- if target_projection.name == "longlat":
57
- new_x_wind *= np.cos(np.deg2rad(lats))
58
-
59
- if target_projection.name == "longlat" or source_projection.name == "longlat":
60
- # Ensure the wind speed is not changed (which might not the case since the units in longlat
61
- # is degrees, not meters)
62
- curr_speed = np.sqrt(new_x_wind**2 + new_y_wind**2)
63
- new_x_wind *= orig_speed / curr_speed
64
- new_y_wind *= orig_speed / curr_speed
65
-
66
- return new_x_wind, new_y_wind
12
+ import tqdm
13
+ from anemoi.utils.humanize import plural
14
+ from earthkit.data.indexing.fieldlist import FieldArray
15
+ from earthkit.geo.rotate import rotate_vector
67
16
 
68
17
 
69
18
  class NewDataField:
@@ -77,6 +26,9 @@ class NewDataField:
77
26
  def __getattr__(self, name):
78
27
  return getattr(self.field, name)
79
28
 
29
+ def __repr__(self) -> str:
30
+ return repr(self.field)
31
+
80
32
 
81
33
  def execute(
82
34
  context,
@@ -88,13 +40,15 @@ def execute(
88
40
  ):
89
41
  from pyproj import CRS
90
42
 
43
+ context.trace("🔄", "Rotating winds (extracting winds from ", plural(len(input), "field"))
44
+
91
45
  result = FieldArray()
92
46
 
93
47
  wind_params = (x_wind, y_wind)
94
48
  wind_pairs = defaultdict(dict)
95
49
 
96
50
  for f in input:
97
- key = f.as_mars()
51
+ key = f.metadata(namespace="mars")
98
52
  param = key.pop("param")
99
53
 
100
54
  if param not in wind_params:
@@ -108,7 +62,9 @@ def execute(
108
62
 
109
63
  wind_pairs[key][param] = f
110
64
 
111
- for _, pairs in wind_pairs.items():
65
+ context.trace("🔄", "Rotating", plural(len(wind_pairs), "wind"), "(speed will likely include data download)")
66
+
67
+ for _, pairs in tqdm.tqdm(list(wind_pairs.items())):
112
68
  if len(pairs) != 2:
113
69
  raise ValueError("Missing wind component")
114
70
 
@@ -118,11 +74,11 @@ def execute(
118
74
  assert x.grid_mapping == y.grid_mapping
119
75
 
120
76
  lats, lons = x.grid_points()
121
- x_new, y_new = rotate_winds(
77
+ x_new, y_new = rotate_vector(
122
78
  lats,
123
79
  lons,
124
- x.to_numpy(reshape=False),
125
- y.to_numpy(reshape=False),
80
+ x.to_numpy(flatten=True),
81
+ y.to_numpy(flatten=True),
126
82
  (source_projection if source_projection is not None else CRS.from_cf(x.grid_mapping)),
127
83
  target_projection,
128
84
  )
@@ -9,60 +9,9 @@
9
9
 
10
10
  from collections import defaultdict
11
11
 
12
- import numpy as np
13
- from climetlab.indexing.fieldset import FieldArray
14
-
15
-
16
- def normalise(x):
17
- return max(min(x, 1.0), -1.0)
18
-
19
-
20
- def normalise_longitude(lon, minimum):
21
- while lon < minimum:
22
- lon += 360
23
-
24
- while lon >= minimum + 360:
25
- lon -= 360
26
-
27
- return lon
28
-
29
-
30
- def rotate_winds(
31
- lats,
32
- lons,
33
- raw_lats,
34
- raw_lons,
35
- x_wind,
36
- y_wind,
37
- south_pole_latitude,
38
- south_pole_longitude,
39
- south_pole_rotation_angle=0,
40
- ):
41
- # Code from MIR
42
- assert south_pole_rotation_angle == 0
43
- C = np.deg2rad(90 - south_pole_latitude)
44
- cos_C = np.cos(C)
45
- sin_C = np.sin(C)
46
-
47
- new_x = np.zeros_like(x_wind)
48
- new_y = np.zeros_like(y_wind)
49
-
50
- for i, (vx, vy, lat, lon, raw_lat, raw_lon) in enumerate(zip(x_wind, y_wind, lats, lons, raw_lats, raw_lons)):
51
- lonRotated = south_pole_longitude - lon
52
- lon_rotated = normalise_longitude(lonRotated, -180)
53
- lon_unrotated = raw_lon
54
-
55
- a = np.deg2rad(lon_rotated)
56
- b = np.deg2rad(lon_unrotated)
57
- q = 1 if (sin_C * lon_rotated < 0.0) else -1.0 # correct quadrant
58
-
59
- cos_c = normalise(np.cos(a) * np.cos(b) + np.sin(a) * np.sin(b) * cos_C)
60
- sin_c = q * np.sqrt(1.0 - cos_c * cos_c)
61
-
62
- new_x[i] = cos_c * vx + sin_c * vy
63
- new_y[i] = -sin_c * vx + cos_c * vy
64
-
65
- return new_x, new_y
12
+ # import numpy as np
13
+ from earthkit.data.indexing.fieldlist import FieldArray
14
+ from earthkit.geo.rotate import unrotate_vector
66
15
 
67
16
 
68
17
  class NewDataField:
@@ -85,7 +34,7 @@ def execute(context, input, u, v):
85
34
  wind_pairs = defaultdict(dict)
86
35
 
87
36
  for f in input:
88
- key = f.as_mars()
37
+ key = f.metadata(namespace="mars")
89
38
  param = key.pop("param")
90
39
 
91
40
  if param not in wind_params:
@@ -107,18 +56,19 @@ def execute(context, input, u, v):
107
56
  y = pairs[v]
108
57
 
109
58
  lats, lons = x.grid_points()
110
- raw_lats, raw_longs = x.grid_points_raw()
59
+ raw_lats, raw_longs = x.grid_points_unrotated()
111
60
 
112
61
  assert x.rotation == y.rotation
113
62
 
114
- u_new, v_new = rotate_winds(
63
+ u_new, v_new = unrotate_vector(
115
64
  lats,
116
65
  lons,
117
- raw_lats,
118
- raw_longs,
119
- x.to_numpy(reshape=False),
120
- y.to_numpy(reshape=False),
121
- *x.rotation,
66
+ x.to_numpy(flatten=True),
67
+ y.to_numpy(flatten=True),
68
+ *x.rotation[:2],
69
+ south_pole_rotation_angle=x.rotation[2],
70
+ lat_unrotated=raw_lats,
71
+ lon_unrotated=raw_longs,
122
72
  )
123
73
 
124
74
  result.append(NewDataField(x, u_new))
@@ -128,9 +78,9 @@ def execute(context, input, u, v):
128
78
 
129
79
 
130
80
  if __name__ == "__main__":
131
- from climetlab import load_source
81
+ from earthkit.data import from_source
132
82
 
133
- source = load_source(
83
+ source = from_source(
134
84
  "mars",
135
85
  date=-1,
136
86
  param="10u/10v",
@@ -6,3 +6,42 @@
6
6
  # granted to it by virtue of its status as an intergovernmental organisation
7
7
  # nor does it submit to any jurisdiction.
8
8
  #
9
+
10
+ import glob
11
+ import logging
12
+
13
+ from earthkit.data.utils.patterns import Pattern
14
+
15
+ LOG = logging.getLogger(__name__)
16
+
17
+
18
+ def _expand(paths):
19
+ for path in paths:
20
+ if path.startswith("file://"):
21
+ path = path[7:]
22
+
23
+ if path.startswith("http://"):
24
+ yield path
25
+ continue
26
+
27
+ if path.startswith("https://"):
28
+ yield path
29
+ continue
30
+
31
+ cnt = 0
32
+ for p in glob.glob(path):
33
+ yield p
34
+ cnt += 1
35
+ if cnt == 0:
36
+ yield path
37
+
38
+
39
+ def iterate_patterns(path, dates, **kwargs):
40
+ given_paths = path if isinstance(path, list) else [path]
41
+
42
+ dates = [d.isoformat() for d in dates]
43
+
44
+ for path in given_paths:
45
+ paths = Pattern(path, ignore_missing_keys=True).substitute(date=dates, **kwargs)
46
+ for path in _expand(paths):
47
+ yield path, dates
@@ -11,11 +11,11 @@ import logging
11
11
  import warnings
12
12
  from copy import deepcopy
13
13
 
14
- import climetlab as cml
14
+ import earthkit.data as ekd
15
15
  import numpy as np
16
- from climetlab.core.temporary import temp_file
17
- from climetlab.readers.grib.output import new_grib_output
18
- from climetlab.utils.availability import Availability
16
+ from earthkit.data.core.temporary import temp_file
17
+ from earthkit.data.readers.grib.output import new_grib_output
18
+ from earthkit.data.utils.availability import Availability
19
19
 
20
20
  from anemoi.datasets.create.utils import to_datetime_list
21
21
 
@@ -24,9 +24,9 @@ from .mars import use_grib_paramid
24
24
  LOG = logging.getLogger(__name__)
25
25
 
26
26
 
27
- def member(field):
27
+ def _member(field):
28
28
  # Bug in eccodes has number=0 randomly
29
- number = field.metadata("number")
29
+ number = field.metadata("number", default=0)
30
30
  if number is None:
31
31
  number = 0
32
32
  return number
@@ -54,16 +54,25 @@ class Accumulation:
54
54
 
55
55
  def check(self, field):
56
56
  if self._check is None:
57
- self._check = field.as_mars()
57
+ self._check = field.metadata(namespace="mars")
58
58
 
59
- assert self.param == field.metadata("param"), (self.param, field.metadata("param"))
60
- assert self.date == field.metadata("date"), (self.date, field.metadata("date"))
61
- assert self.time == field.metadata("time"), (self.time, field.metadata("time"))
62
- assert self.number == member(field), (self.number, member(field))
59
+ assert self.param == field.metadata("param"), (
60
+ self.param,
61
+ field.metadata("param"),
62
+ )
63
+ assert self.date == field.metadata("date"), (
64
+ self.date,
65
+ field.metadata("date"),
66
+ )
67
+ assert self.time == field.metadata("time"), (
68
+ self.time,
69
+ field.metadata("time"),
70
+ )
71
+ assert self.number == _member(field), (self.number, _member(field))
63
72
 
64
73
  return
65
74
 
66
- mars = field.as_mars()
75
+ mars = field.metadata(namespace="mars")
67
76
  keys1 = sorted(self._check.keys())
68
77
  keys2 = sorted(mars.keys())
69
78
 
@@ -196,7 +205,11 @@ class AccumulationFromLastStep(Accumulation):
196
205
 
197
206
  def compute(self, values, startStep, endStep):
198
207
 
199
- assert endStep - startStep == self.frequency, (startStep, endStep, self.frequency)
208
+ assert endStep - startStep == self.frequency, (
209
+ startStep,
210
+ endStep,
211
+ self.frequency,
212
+ )
200
213
 
201
214
  if self.startStep is None:
202
215
  self.startStep = startStep
@@ -228,17 +241,17 @@ class AccumulationFromLastStep(Accumulation):
228
241
  )
229
242
 
230
243
 
231
- def identity(x):
244
+ def _identity(x):
232
245
  return x
233
246
 
234
247
 
235
- def compute_accumulations(
248
+ def _compute_accumulations(
236
249
  context,
237
250
  dates,
238
251
  request,
239
252
  user_accumulation_period=6,
240
253
  data_accumulation_period=None,
241
- patch=identity,
254
+ patch=_identity,
242
255
  base_times=None,
243
256
  ):
244
257
  adjust_step = isinstance(user_accumulation_period, int)
@@ -307,14 +320,13 @@ def compute_accumulations(
307
320
  )
308
321
 
309
322
  compressed = Availability(requests)
310
- ds = cml.load_source("empty")
323
+ ds = ekd.from_source("empty")
311
324
  for r in compressed.iterate():
312
325
  request.update(r)
313
326
  if context.use_grib_paramid and "param" in request:
314
327
  request = use_grib_paramid(request)
315
328
  print("🌧️", request)
316
-
317
- ds = ds + cml.load_source("mars", **request)
329
+ ds = ds + ekd.from_source("mars", **request)
318
330
 
319
331
  accumulations = {}
320
332
  for a in [AccumulationClass(out, frequency=frequency, **r) for r in requests]:
@@ -328,7 +340,7 @@ def compute_accumulations(
328
340
  field.metadata("date"),
329
341
  field.metadata("time"),
330
342
  field.metadata("step"),
331
- member(field),
343
+ _member(field),
332
344
  )
333
345
  values = field.values # optimisation
334
346
  assert accumulations[key], key
@@ -341,7 +353,7 @@ def compute_accumulations(
341
353
 
342
354
  out.close()
343
355
 
344
- ds = cml.load_source("file", path)
356
+ ds = ekd.from_source("file", path)
345
357
 
346
358
  assert len(ds) / len(param) / len(number) == len(dates), (
347
359
  len(ds),
@@ -353,43 +365,13 @@ def compute_accumulations(
353
365
  return ds
354
366
 
355
367
 
356
- def to_list(x):
368
+ def _to_list(x):
357
369
  if isinstance(x, (list, tuple)):
358
370
  return x
359
371
  return [x]
360
372
 
361
373
 
362
- def normalise_time_to_hours(r):
363
- r = deepcopy(r)
364
- if "time" not in r:
365
- return r
366
-
367
- times = []
368
- for t in to_list(r["time"]):
369
- assert len(t) == 4, r
370
- assert t.endswith("00"), r
371
- times.append(int(t) // 100)
372
- r["time"] = tuple(times)
373
- return r
374
-
375
-
376
- def normalise_number(r):
377
- if "number" not in r:
378
- return r
379
- number = r["number"]
380
- number = to_list(number)
381
-
382
- if len(number) > 4 and (number[1] == "to" and number[3] == "by"):
383
- return list(range(int(number[0]), int(number[2]) + 1, int(number[4])))
384
-
385
- if len(number) > 2 and number[1] == "to":
386
- return list(range(int(number[0]), int(number[2]) + 1))
387
-
388
- r["number"] = number
389
- return r
390
-
391
-
392
- def scda(request):
374
+ def _scda(request):
393
375
  if request["time"] in (6, 18, 600, 1800):
394
376
  request["stream"] = "scda"
395
377
  else:
@@ -398,14 +380,14 @@ def scda(request):
398
380
 
399
381
 
400
382
  def accumulations(context, dates, **request):
401
- to_list(request["param"])
383
+ _to_list(request["param"])
402
384
  class_ = request.get("class", "od")
403
385
  stream = request.get("stream", "oper")
404
386
 
405
387
  user_accumulation_period = request.pop("accumulation_period", 6)
406
388
 
407
389
  KWARGS = {
408
- ("od", "oper"): dict(patch=scda),
390
+ ("od", "oper"): dict(patch=_scda),
409
391
  ("od", "elda"): dict(base_times=(6, 18)),
410
392
  ("ea", "oper"): dict(data_accumulation_period=1, base_times=(6, 18)),
411
393
  ("ea", "enda"): dict(data_accumulation_period=3, base_times=(6, 18)),
@@ -415,7 +397,7 @@ def accumulations(context, dates, **request):
415
397
 
416
398
  context.trace("🌧️", f"accumulations {request} {user_accumulation_period} {kwargs}")
417
399
 
418
- return compute_accumulations(
400
+ return _compute_accumulations(
419
401
  context,
420
402
  dates,
421
403
  request,
@@ -6,15 +6,22 @@
6
6
  # granted to it by virtue of its status as an intergovernmental organisation
7
7
  # nor does it submit to any jurisdiction.
8
8
  #
9
- from climetlab import load_source
9
+ from earthkit.data import from_source
10
10
 
11
11
 
12
12
  def constants(context, dates, template, param):
13
13
  from warnings import warn
14
14
 
15
- warn("The source `constants` is deprecated, use `forcings` instead.", DeprecationWarning, stacklevel=2)
16
- context.trace("✅", f"load_source(constants, {template}, {param}")
17
- return load_source("constants", source_or_dataset=template, date=dates, param=param)
15
+ warn(
16
+ "The source `constants` is deprecated, use `forcings` instead.",
17
+ DeprecationWarning,
18
+ stacklevel=2,
19
+ )
20
+ context.trace("✅", f"from_source(constants, {template}, {param}")
21
+ if len(template) == 0:
22
+ raise ValueError("Forcings template is empty.")
23
+
24
+ return from_source("forcings", source_or_dataset=template, date=dates, param=param)
18
25
 
19
26
 
20
27
  execute = constants
@@ -7,8 +7,8 @@
7
7
  # nor does it submit to any jurisdiction.
8
8
  #
9
9
 
10
- import climetlab as cml
10
+ import earthkit.data as ekd
11
11
 
12
12
 
13
13
  def execute(context, dates, **kwargs):
14
- return cml.load_source("empty")
14
+ return ekd.from_source("empty")
@@ -6,12 +6,12 @@
6
6
  # granted to it by virtue of its status as an intergovernmental organisation
7
7
  # nor does it submit to any jurisdiction.
8
8
  #
9
- from climetlab import load_source
9
+ from earthkit.data import from_source
10
10
 
11
11
 
12
12
  def forcings(context, dates, template, param):
13
- context.trace("✅", f"load_source(forcings, {template}, {param}")
14
- return load_source("constants", source_or_dataset=template, date=dates, param=param)
13
+ context.trace("✅", f"from_source(forcings, {template}, {param}")
14
+ return from_source("forcings", source_or_dataset=template, date=dates, param=param)
15
15
 
16
16
 
17
17
  execute = forcings
@@ -10,8 +10,8 @@
10
10
 
11
11
  import glob
12
12
 
13
- from climetlab import load_source
14
- from climetlab.utils.patterns import Pattern
13
+ from earthkit.data import from_source
14
+ from earthkit.data.utils.patterns import Pattern
15
15
 
16
16
 
17
17
  def check(ds, paths, **kwargs):
@@ -26,14 +26,18 @@ def check(ds, paths, **kwargs):
26
26
 
27
27
  def _expand(paths):
28
28
  for path in paths:
29
+ cnt = 0
29
30
  for p in glob.glob(path):
30
31
  yield p
32
+ cnt += 1
33
+ if cnt == 0:
34
+ yield path
31
35
 
32
36
 
33
37
  def execute(context, dates, path, *args, **kwargs):
34
38
  given_paths = path if isinstance(path, list) else [path]
35
39
 
36
- ds = load_source("empty")
40
+ ds = from_source("empty")
37
41
  dates = [d.isoformat() for d in dates]
38
42
 
39
43
  for path in given_paths:
@@ -45,7 +49,7 @@ def execute(context, dates, path, *args, **kwargs):
45
49
 
46
50
  for path in _expand(paths):
47
51
  context.trace("📁", "PATH", path)
48
- s = load_source("file", path)
52
+ s = from_source("file", path)
49
53
  s = s.sel(valid_datetime=dates, **kwargs)
50
54
  ds = ds + s
51
55