anemoi-utils 0.4.12__py3-none-any.whl → 0.4.14__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.

Potentially problematic release.


This version of anemoi-utils might be problematic. Click here for more details.

Files changed (37) hide show
  1. anemoi/utils/__init__.py +1 -0
  2. anemoi/utils/__main__.py +12 -2
  3. anemoi/utils/_version.py +9 -4
  4. anemoi/utils/caching.py +138 -13
  5. anemoi/utils/checkpoints.py +81 -13
  6. anemoi/utils/cli.py +83 -7
  7. anemoi/utils/commands/__init__.py +4 -0
  8. anemoi/utils/commands/config.py +19 -2
  9. anemoi/utils/commands/requests.py +18 -2
  10. anemoi/utils/compatibility.py +6 -5
  11. anemoi/utils/config.py +254 -23
  12. anemoi/utils/dates.py +204 -50
  13. anemoi/utils/devtools.py +68 -7
  14. anemoi/utils/grib.py +30 -9
  15. anemoi/utils/grids.py +85 -8
  16. anemoi/utils/hindcasts.py +25 -8
  17. anemoi/utils/humanize.py +357 -52
  18. anemoi/utils/logs.py +31 -3
  19. anemoi/utils/mars/__init__.py +46 -12
  20. anemoi/utils/mars/requests.py +15 -1
  21. anemoi/utils/provenance.py +189 -32
  22. anemoi/utils/registry.py +234 -44
  23. anemoi/utils/remote/__init__.py +386 -38
  24. anemoi/utils/remote/s3.py +252 -29
  25. anemoi/utils/remote/ssh.py +140 -8
  26. anemoi/utils/s3.py +77 -4
  27. anemoi/utils/sanitise.py +52 -7
  28. anemoi/utils/testing.py +182 -0
  29. anemoi/utils/text.py +218 -54
  30. anemoi/utils/timer.py +91 -15
  31. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/METADATA +8 -4
  32. anemoi_utils-0.4.14.dist-info/RECORD +38 -0
  33. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/WHEEL +1 -1
  34. anemoi_utils-0.4.12.dist-info/RECORD +0 -37
  35. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/entry_points.txt +0 -0
  36. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info/licenses}/LICENSE +0 -0
  37. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,9 @@ import logging
10
10
  import os
11
11
  import shutil
12
12
  from abc import abstractmethod
13
+ from typing import Any
14
+ from typing import Dict
15
+ from typing import Iterable
13
16
 
14
17
  import tqdm
15
18
 
@@ -18,13 +21,63 @@ from ..humanize import bytes_to_human
18
21
  LOGGER = logging.getLogger(__name__)
19
22
 
20
23
 
21
- def _ignore(number_of_files, total_size, total_transferred, transfering):
24
+ def robust(call: callable, *args, maximum_tries: int = 60, retry_after: int = 60, **kwargs) -> callable:
25
+ """Forwards the arguments to the multiurl robust function.
26
+ with default retry_after=60 and maximum_tries=60.
27
+ """
28
+ from multiurl import robust as robust_
29
+
30
+ return robust_(call, *args, retry_after=retry_after, maximum_tries=maximum_tries, **kwargs)
31
+
32
+
33
+ def _ignore(number_of_files: int, total_size: int, total_transferred: int, transfering: bool) -> None:
34
+ """A placeholder function for progress reporting.
35
+
36
+ Parameters
37
+ ----------
38
+ number_of_files : int
39
+ The number of files being transferred.
40
+ total_size : int
41
+ The total size of the files being transferred.
42
+ total_transferred : int
43
+ The total size of the files transferred so far.
44
+ transfering : bool
45
+ Whether the transfer is in progress.
46
+ """
22
47
  pass
23
48
 
24
49
 
25
50
  class Loader:
26
-
27
- def transfer_folder(self, *, source, target, overwrite=False, resume=False, verbosity=1, threads=1, progress=None):
51
+ def transfer_folder(
52
+ self,
53
+ *,
54
+ source: str,
55
+ target: str,
56
+ overwrite: bool = False,
57
+ resume: bool = False,
58
+ verbosity: int = 1,
59
+ threads: int = 1,
60
+ progress: callable = None,
61
+ ) -> None:
62
+ """Transfer a folder from the source to the target location.
63
+
64
+ Parameters
65
+ ----------
66
+ source : str
67
+ The source folder path.
68
+ target : str
69
+ The target folder path.
70
+ overwrite : bool, optional
71
+ Whether to overwrite the target if it exists, by default False.
72
+ resume : bool, optional
73
+ Whether to resume the transfer if possible, by default False.
74
+ verbosity : int, optional
75
+ The verbosity level, by default 1.
76
+ threads : int, optional
77
+ The number of threads to use, by default 1.
78
+ progress : callable, optional
79
+ A callable for progress reporting, by default None.
80
+ """
28
81
  assert verbosity == 1, verbosity
29
82
 
30
83
  if progress is None:
@@ -93,7 +146,48 @@ class Loader:
93
146
  executor.shutdown(wait=False, cancel_futures=True)
94
147
  raise
95
148
 
96
- def transfer_file(self, source, target, overwrite, resume, verbosity, threads=1, progress=None, config=None):
149
+ def transfer_file(
150
+ self,
151
+ source: str,
152
+ target: str,
153
+ overwrite: bool,
154
+ resume: bool,
155
+ verbosity: int,
156
+ threads: int = 1,
157
+ progress: callable = None,
158
+ config: dict = None,
159
+ ) -> int:
160
+ """Transfer a file from the source to the target location.
161
+
162
+ Parameters
163
+ ----------
164
+ source : str
165
+ The source file path.
166
+ target : str
167
+ The target file path.
168
+ overwrite : bool
169
+ Whether to overwrite the target if it exists.
170
+ resume : bool
171
+ Whether to resume the transfer if possible.
172
+ verbosity : int
173
+ The verbosity level.
174
+ threads : int, optional
175
+ The number of threads to use, by default 1.
176
+ progress : callable, optional
177
+ A callable for progress reporting, by default None.
178
+ config : dict, optional
179
+ Additional configuration options, by default None.
180
+
181
+ Returns
182
+ -------
183
+ int
184
+ The size of the transferred file.
185
+
186
+ Raises
187
+ ------
188
+ Exception
189
+ If an error occurs during the transfer.
190
+ """
97
191
  try:
98
192
  return self._transfer_file(source, target, overwrite, resume, verbosity, threads=threads, config=config)
99
193
  except Exception as e:
@@ -102,31 +196,119 @@ class Loader:
102
196
  raise
103
197
 
104
198
  @abstractmethod
105
- def list_source(self, source):
199
+ def list_source(self, source: str) -> Iterable:
200
+ """List the files in the source location.
201
+
202
+ Parameters
203
+ ----------
204
+ source : str
205
+ The source location.
206
+
207
+ Returns
208
+ -------
209
+ Iterable
210
+ An iterable of files in the source location.
211
+ """
106
212
  raise NotImplementedError
107
213
 
108
214
  @abstractmethod
109
- def source_path(self, local_path, source):
215
+ def source_path(self, local_path: str, source: str) -> str:
216
+ """Get the source path for a local file.
217
+
218
+ Parameters
219
+ ----------
220
+ local_path : str
221
+ The local file path.
222
+ source : str
223
+ The source location.
224
+
225
+ Returns
226
+ -------
227
+ str
228
+ The source path for the local file.
229
+ """
110
230
  raise NotImplementedError
111
231
 
112
232
  @abstractmethod
113
- def target_path(self, source_path, source, target):
233
+ def target_path(self, source_path: str, source: str, target: str) -> str:
234
+ """Get the target path for a source file.
235
+
236
+ Parameters
237
+ ----------
238
+ source_path : str
239
+ The source file path.
240
+ source : str
241
+ The source location.
242
+ target : str
243
+ The target location.
244
+
245
+ Returns
246
+ -------
247
+ str
248
+ The target path for the source file.
249
+ """
114
250
  raise NotImplementedError
115
251
 
116
252
  @abstractmethod
117
- def source_size(self, local_path):
253
+ def source_size(self, local_path: str) -> int:
254
+ """Get the size of a local file.
255
+
256
+ Parameters
257
+ ----------
258
+ local_path : str
259
+ The local file path.
260
+
261
+ Returns
262
+ -------
263
+ int
264
+ The size of the local file.
265
+ """
118
266
  raise NotImplementedError
119
267
 
120
268
  @abstractmethod
121
- def copy(self, source, target, **kwargs):
269
+ def copy(self, source: str, target: str, **kwargs) -> None:
270
+ """Copy a file or folder from the source to the target location.
271
+
272
+ Parameters
273
+ ----------
274
+ source : str
275
+ The source location.
276
+ target : str
277
+ The target location.
278
+ kwargs : dict
279
+ Additional arguments for the transfer.
280
+ """
122
281
  raise NotImplementedError
123
282
 
124
283
  @abstractmethod
125
- def get_temporary_target(self, target, pattern):
284
+ def get_temporary_target(self, target: str, pattern: str) -> str:
285
+ """Get a temporary target path based on the given pattern.
286
+
287
+ Parameters
288
+ ----------
289
+ target : str
290
+ The original target path.
291
+ pattern : str
292
+ The pattern to format the temporary path.
293
+
294
+ Returns
295
+ -------
296
+ str
297
+ The temporary target path.
298
+ """
126
299
  raise NotImplementedError
127
300
 
128
301
  @abstractmethod
129
- def rename_target(self, target, temporary_target):
302
+ def rename_target(self, target: str, temporary_target: str) -> None:
303
+ """Rename the target to a new target path.
304
+
305
+ Parameters
306
+ ----------
307
+ target : str
308
+ The original target path.
309
+ temporary_target : str
310
+ The new target path.
311
+ """
130
312
  raise NotImplementedError
131
313
 
132
314
 
@@ -134,19 +316,60 @@ class BaseDownload(Loader):
134
316
  action = "Downloading"
135
317
 
136
318
  @abstractmethod
137
- def copy(self, source, target, **kwargs):
319
+ def copy(self, source: str, target: str, **kwargs) -> None:
320
+ """Copy a file or folder from the source to the target location.
321
+
322
+ Parameters
323
+ ----------
324
+ source : str
325
+ The source location.
326
+ target : str
327
+ The target location.
328
+ kwargs : dict
329
+ Additional arguments for the transfer.
330
+ """
138
331
  raise NotImplementedError
139
332
 
140
- def get_temporary_target(self, target, pattern):
333
+ def get_temporary_target(self, target: str, pattern: str) -> str:
334
+ """Get a temporary target path based on the given pattern.
335
+
336
+ Parameters
337
+ ----------
338
+ target : str
339
+ The original target path.
340
+ pattern : str
341
+ The pattern to format the temporary path.
342
+
343
+ Returns
344
+ -------
345
+ str
346
+ The temporary target path.
347
+ """
141
348
  if pattern is None:
142
349
  return target
143
350
  dirname, basename = os.path.split(target)
144
351
  return pattern.format(dirname=dirname, basename=basename)
145
352
 
146
- def rename_target(self, target, new_target):
353
+ def rename_target(self, target: str, new_target: str) -> None:
354
+ """Rename the target to a new target path.
355
+
356
+ Parameters
357
+ ----------
358
+ target : str
359
+ The original target path.
360
+ new_target : str
361
+ The new target path.
362
+ """
147
363
  os.rename(target, new_target)
148
364
 
149
- def delete_target(self, target):
365
+ def delete_target(self, target: str) -> None:
366
+ """Delete the target if it exists.
367
+
368
+ Parameters
369
+ ----------
370
+ target : str
371
+ The target path.
372
+ """
150
373
  if os.path.exists(target):
151
374
  shutil.rmtree(target)
152
375
 
@@ -154,26 +377,91 @@ class BaseDownload(Loader):
154
377
  class BaseUpload(Loader):
155
378
  action = "Uploading"
156
379
 
157
- def copy(self, source, target, **kwargs):
380
+ def copy(self, source: str, target: str, **kwargs) -> None:
381
+ """Copy a file or folder from the source to the target location.
382
+
383
+ Parameters
384
+ ----------
385
+ source : str
386
+ The source location.
387
+ target : str
388
+ The target location.
389
+ kwargs : dict
390
+ Additional arguments for the transfer.
391
+ """
158
392
  if os.path.isdir(source):
159
393
  self.transfer_folder(source=source, target=target, **kwargs)
160
394
  else:
161
395
  self.transfer_file(source=source, target=target, **kwargs)
162
396
 
163
- def list_source(self, source):
397
+ def list_source(self, source: str) -> Iterable:
398
+ """List the files in the source location.
399
+
400
+ Parameters
401
+ ----------
402
+ source : str
403
+ The source location.
404
+
405
+ Returns
406
+ -------
407
+ Iterable
408
+ An iterable of files in the source location.
409
+ """
164
410
  for root, _, files in os.walk(source):
165
411
  for file in files:
166
412
  yield os.path.join(root, file)
167
413
 
168
- def source_path(self, local_path, source):
414
+ def source_path(self, local_path: str, source: str) -> str:
415
+ """Get the source path for a local file.
416
+
417
+ Parameters
418
+ ----------
419
+ local_path : str
420
+ The local file path.
421
+ source : str
422
+ The source location.
423
+
424
+ Returns
425
+ -------
426
+ str
427
+ The source path for the local file.
428
+ """
169
429
  return local_path
170
430
 
171
- def target_path(self, source_path, source, target):
431
+ def target_path(self, source_path: str, source: str, target: str) -> str:
432
+ """Get the target path for a source file.
433
+
434
+ Parameters
435
+ ----------
436
+ source_path : str
437
+ The source file path.
438
+ source : str
439
+ The source location.
440
+ target : str
441
+ The target location.
442
+
443
+ Returns
444
+ -------
445
+ str
446
+ The target path for the source file.
447
+ """
172
448
  relative_path = os.path.relpath(source_path, source)
173
449
  path = os.path.join(target, relative_path)
174
450
  return path
175
451
 
176
- def source_size(self, local_path):
452
+ def source_size(self, local_path: str) -> int:
453
+ """Get the size of a local file.
454
+
455
+ Parameters
456
+ ----------
457
+ local_path : str
458
+ The local file path.
459
+
460
+ Returns
461
+ -------
462
+ int
463
+ The size of the local file.
464
+ """
177
465
  return os.path.getsize(local_path)
178
466
 
179
467
 
@@ -188,21 +476,22 @@ class Transfer:
188
476
 
189
477
  def __init__(
190
478
  self,
191
- source,
192
- target,
193
- overwrite=False,
194
- resume=False,
195
- verbosity=1,
196
- threads=1,
197
- progress=None,
198
- temporary_target=False,
479
+ *,
480
+ source: str,
481
+ target: str,
482
+ overwrite: bool = False,
483
+ resume: bool = False,
484
+ verbosity: int = 1,
485
+ threads: int = 1,
486
+ progress: callable = None,
487
+ temporary_target: bool = False,
199
488
  ):
200
489
  if target == ".":
201
490
  target = os.path.basename(source)
202
491
  if not target:
203
492
  target = os.path.basename(os.path.dirname(source))
204
493
 
205
- temporary_target = {
494
+ temporary_target: Dict[Any, Any] = {
206
495
  False: None,
207
496
  True: "{dirname}-downloading/{basename}",
208
497
  "-tmp/*": "{dirname}-tmp/{basename}",
@@ -223,8 +512,14 @@ class Transfer:
223
512
  cls = _find_transfer_class(self.source, self.target)
224
513
  self.loader = cls()
225
514
 
226
- def run(self):
515
+ def run(self) -> "Transfer":
516
+ """Execute the transfer process.
227
517
 
518
+ Returns
519
+ -------
520
+ Transfer
521
+ The Transfer instance.
522
+ """
228
523
  target = self.loader.get_temporary_target(self.target, self.temporary_target)
229
524
  if target != self.target:
230
525
  LOGGER.info(f"Using temporary target {target} to copy to {self.target}")
@@ -256,16 +551,51 @@ class Transfer:
256
551
 
257
552
  return self
258
553
 
259
- def rename_target(self, target, new_target):
554
+ def rename_target(self, target: str, new_target: str) -> None:
555
+ """Rename the target to a new target path.
556
+
557
+ Parameters
558
+ ----------
559
+ target : str
560
+ The original target path.
561
+ new_target : str
562
+ The new target path.
563
+ """
260
564
  if target != new_target:
261
565
  LOGGER.info(f"Renaming temporary target {target} into {self.target}")
262
566
  return self.loader.rename_target(target, new_target)
263
567
 
264
- def delete_target(self, target):
568
+ def delete_target(self, target: str) -> None:
569
+ """Delete the target if it exists.
570
+
571
+ Parameters
572
+ ----------
573
+ target : str
574
+ The target path.
575
+ """
265
576
  return self.loader.delete_target(target)
266
577
 
267
578
 
268
- def _find_transfer_class(source, target):
579
+ def _find_transfer_class(source: str, target: str) -> type:
580
+ """Find the appropriate transfer class based on the source and target locations.
581
+
582
+ Parameters
583
+ ----------
584
+ source : str
585
+ The source location.
586
+ target : str
587
+ The target location.
588
+
589
+ Returns
590
+ -------
591
+ type
592
+ The transfer class.
593
+
594
+ Raises
595
+ ------
596
+ TransferMethodNotImplementedError
597
+ If the transfer method is not implemented.
598
+ """
269
599
  from_ssh = source.startswith("ssh://")
270
600
  into_ssh = target.startswith("ssh://")
271
601
 
@@ -298,8 +628,12 @@ def _find_transfer_class(source, target):
298
628
 
299
629
 
300
630
  # this is the public API
301
- def transfer(*args, **kwargs) -> Loader:
302
- """Parameters
631
+ def transfer(
632
+ source, target, *, overwrite=False, resume=False, verbosity=1, progress=None, threads=1, temporary_target=False
633
+ ) -> Loader:
634
+ """Transfer files or folders from the source to the target location.
635
+
636
+ Parameters
303
637
  ----------
304
638
  source : str
305
639
  A path to a local file or folder or a URL to a file or a folder on S3.
@@ -316,7 +650,7 @@ def transfer(*args, **kwargs) -> Loader:
316
650
  By default False
317
651
  verbosity : int, optional
318
652
  The level of verbosity, by default 1
319
- progress: callable, optional
653
+ progress : callable, optional
320
654
  A callable that will be called with the number of files, the total size of the files, the total size
321
655
  transferred and a boolean indicating if the transfer has started. By default None
322
656
  threads : int, optional
@@ -325,8 +659,22 @@ def transfer(*args, **kwargs) -> Loader:
325
659
  Experimental feature
326
660
  If True and if the target location supports it, the data will be uploaded to a temporary location
327
661
  then renamed to the final location. Supported by SSH and local targets, not supported by S3.
328
- By default False
662
+ By default False.
663
+
664
+ Returns
665
+ -------
666
+ Loader
667
+ The Loader instance.
329
668
  """
330
- copier = Transfer(*args, **kwargs)
669
+ copier = Transfer(
670
+ source=source,
671
+ target=target,
672
+ overwrite=overwrite,
673
+ resume=resume,
674
+ verbosity=verbosity,
675
+ progress=progress,
676
+ threads=threads,
677
+ temporary_target=temporary_target,
678
+ )
331
679
  copier.run()
332
680
  return copier