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
anemoi/utils/__init__.py CHANGED
@@ -7,6 +7,7 @@
7
7
  # granted to it by virtue of its status as an intergovernmental organisation
8
8
  # nor does it submit to any jurisdiction.
9
9
 
10
+ """Anemoi Utils package."""
10
11
 
11
12
  try:
12
13
  # NOTE: the `_version.py` file must not be present in the git repository
anemoi/utils/__main__.py CHANGED
@@ -7,6 +7,8 @@
7
7
  # granted to it by virtue of its status as an intergovernmental organisation
8
8
  # nor does it submit to any jurisdiction.
9
9
 
10
+ from typing import Any
11
+
10
12
  from anemoi.utils.cli import cli_main
11
13
  from anemoi.utils.cli import make_parser
12
14
 
@@ -15,11 +17,19 @@ from .commands import COMMANDS
15
17
 
16
18
 
17
19
  # For read-the-docs
18
- def create_parser():
20
+ def create_parser() -> Any:
21
+ """Create the argument parser for the CLI.
22
+
23
+ Returns
24
+ -------
25
+ Any
26
+ The argument parser
27
+ """
19
28
  return make_parser(__doc__, COMMANDS)
20
29
 
21
30
 
22
- def main():
31
+ def main() -> None:
32
+ """Main entry point for the CLI."""
23
33
  cli_main(__version__, __doc__, COMMANDS)
24
34
 
25
35
 
anemoi/utils/_version.py CHANGED
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.4.12'
16
- __version_tuple__ = version_tuple = (0, 4, 12)
20
+ __version__ = version = '0.4.14'
21
+ __version_tuple__ = version_tuple = (0, 4, 14)
anemoi/utils/caching.py CHANGED
@@ -13,6 +13,9 @@ import json
13
13
  import os
14
14
  import time
15
15
  from threading import Lock
16
+ from typing import Any
17
+ from typing import Callable
18
+ from typing import Optional
16
19
 
17
20
  import numpy as np
18
21
 
@@ -20,11 +23,30 @@ LOCK = Lock()
20
23
  CACHE = {}
21
24
 
22
25
 
23
- def _get_cache_path(collection):
26
+ def _get_cache_path(collection: str) -> str:
27
+ """Get the cache path for a collection.
28
+
29
+ Parameters
30
+ ----------
31
+ collection : str
32
+ The name of the collection
33
+
34
+ Returns
35
+ -------
36
+ str
37
+ The cache path
38
+ """
24
39
  return os.path.join(os.path.expanduser("~"), ".cache", "anemoi", collection)
25
40
 
26
41
 
27
- def clean_cache(collection="default"):
42
+ def clean_cache(collection: str = "default") -> None:
43
+ """Clean the cache for a collection.
44
+
45
+ Parameters
46
+ ----------
47
+ collection : str, optional
48
+ The name of the collection, by default "default"
49
+ """
28
50
  global CACHE
29
51
  CACHE = {}
30
52
  path = _get_cache_path(collection)
@@ -36,14 +58,35 @@ def clean_cache(collection="default"):
36
58
 
37
59
  class Cacher:
38
60
  """This class implements a simple caching mechanism.
39
- Private class, do not use directly"""
61
+ Private class, do not use directly.
62
+ """
40
63
 
41
- def __init__(self, collection, expires):
64
+ def __init__(self, collection: str, expires: Optional[int]):
65
+ """Initialize the Cacher.
66
+
67
+ Parameters
68
+ ----------
69
+ collection : str
70
+ The name of the collection
71
+ expires : int, optional
72
+ The expiration time in seconds, or None for no expiration
73
+ """
42
74
  self.collection = collection
43
75
  self.expires = expires
44
76
 
45
- def __call__(self, func):
77
+ def __call__(self, func: Callable) -> Callable:
78
+ """Wrap a function with caching.
79
+
80
+ Parameters
81
+ ----------
82
+ func : Callable
83
+ The function to wrap
46
84
 
85
+ Returns
86
+ -------
87
+ Callable
88
+ The wrapped function
89
+ """
47
90
  full = f"{func.__module__}.{func.__name__}"
48
91
 
49
92
  def wrapped(*args, **kwargs):
@@ -55,8 +98,21 @@ class Cacher:
55
98
 
56
99
  return wrapped
57
100
 
58
- def cache(self, key, proc):
59
-
101
+ def cache(self, key: tuple, proc: Callable) -> Any:
102
+ """Cache the result of a function.
103
+
104
+ Parameters
105
+ ----------
106
+ key : tuple
107
+ The cache key
108
+ proc : Callable
109
+ The function to call if the result is not cached
110
+
111
+ Returns
112
+ -------
113
+ Any
114
+ The cached result
115
+ """
60
116
  key = json.dumps(key, sort_keys=True)
61
117
  m = hashlib.md5()
62
118
  m.update(key.encode("utf-8"))
@@ -88,37 +144,106 @@ class Cacher:
88
144
 
89
145
 
90
146
  class JsonCacher(Cacher):
147
+ """Cacher that uses JSON files."""
148
+
91
149
  ext = ""
92
150
 
93
- def save(self, path, data):
151
+ def save(self, path: str, data: dict) -> str:
152
+ """Save data to a JSON file.
153
+
154
+ Parameters
155
+ ----------
156
+ path : str
157
+ The path to the JSON file
158
+ data : dict
159
+ The data to save
160
+
161
+ Returns
162
+ -------
163
+ str
164
+ The temporary file path
165
+ """
94
166
  temp_path = path + ".tmp"
95
167
  with open(temp_path, "w") as f:
96
168
  json.dump(data, f)
97
169
  return temp_path
98
170
 
99
- def load(self, path):
171
+ def load(self, path: str) -> dict:
172
+ """Load data from a JSON file.
173
+
174
+ Parameters
175
+ ----------
176
+ path : str
177
+ The path to the JSON file
178
+
179
+ Returns
180
+ -------
181
+ dict
182
+ The loaded data
183
+ """
100
184
  with open(path, "r") as f:
101
185
  return json.load(f)
102
186
 
103
187
 
104
188
  class NpzCacher(Cacher):
189
+ """Cacher that uses NPZ files."""
190
+
105
191
  ext = ".npz"
106
192
 
107
- def save(self, path, data):
193
+ def save(self, path: str, data: dict) -> str:
194
+ """Save data to an NPZ file.
195
+
196
+ Parameters
197
+ ----------
198
+ path : str
199
+ The path to the NPZ file
200
+ data : dict
201
+ The data to save
202
+
203
+ Returns
204
+ -------
205
+ str
206
+ The temporary file path
207
+ """
108
208
  temp_path = path + ".tmp.npz"
109
209
  np.savez(temp_path, **data)
110
210
  return temp_path
111
211
 
112
- def load(self, path):
212
+ def load(self, path: str) -> dict:
213
+ """Load data from an NPZ file.
214
+
215
+ Parameters
216
+ ----------
217
+ path : str
218
+ The path to the NPZ file
219
+
220
+ Returns
221
+ -------
222
+ dict
223
+ The loaded data
224
+ """
113
225
  return np.load(path, allow_pickle=True)
114
226
 
115
227
 
116
- # PUBLIC API
117
- def cached(collection="default", expires=None, encoding="json"):
228
+ # This function is the main entry point for the caching mechanism for the other anemoi packages
229
+ def cached(collection: str = "default", expires: Optional[int] = None, encoding: str = "json") -> Callable:
118
230
  """Decorator to cache the result of a function.
119
231
 
120
232
  Default is to use a json file to store the cache, but you can also use npz files
121
233
  to cache dict of numpy arrays.
122
234
 
235
+ Parameters
236
+ ----------
237
+ collection : str, optional
238
+ The name of the collection, by default "default"
239
+ expires : int, optional
240
+ The expiration time in seconds, or None for no expiration, by default None
241
+ encoding : str, optional
242
+ The encoding type, either "json" or "npz", by default "json"
243
+
244
+ Returns
245
+ -------
246
+ Callable
247
+ The decorated function
123
248
  """
124
249
  return dict(json=JsonCacher, npz=NpzCacher)[encoding](collection, expires)
@@ -18,6 +18,7 @@ import os
18
18
  import time
19
19
  import zipfile
20
20
  from tempfile import TemporaryDirectory
21
+ from typing import Callable
21
22
 
22
23
  import tqdm
23
24
 
@@ -28,7 +29,7 @@ DEFAULT_FOLDER = "anemoi-metadata"
28
29
 
29
30
 
30
31
  def has_metadata(path: str, *, name: str = DEFAULT_NAME) -> bool:
31
- """Check if a checkpoint file has a metadata file
32
+ """Check if a checkpoint file has a metadata file.
32
33
 
33
34
  Parameters
34
35
  ----------
@@ -49,8 +50,26 @@ def has_metadata(path: str, *, name: str = DEFAULT_NAME) -> bool:
49
50
  return False
50
51
 
51
52
 
52
- def metadata_root(path: str, *, name: str = DEFAULT_NAME) -> bool:
53
+ def metadata_root(path: str, *, name: str = DEFAULT_NAME) -> str:
54
+ """Get the root directory of the metadata file.
53
55
 
56
+ Parameters
57
+ ----------
58
+ path : str
59
+ The path to the checkpoint file
60
+ name : str, optional
61
+ The name of the metadata file in the zip archive
62
+
63
+ Returns
64
+ -------
65
+ str
66
+ The root directory of the metadata file
67
+
68
+ Raises
69
+ ------
70
+ ValueError
71
+ If the metadata file is not found
72
+ """
54
73
  with zipfile.ZipFile(path, "r") as f:
55
74
  for b in f.namelist():
56
75
  if os.path.basename(b) == name:
@@ -58,15 +77,15 @@ def metadata_root(path: str, *, name: str = DEFAULT_NAME) -> bool:
58
77
  raise ValueError(f"Could not find '{name}' in {path}.")
59
78
 
60
79
 
61
- def load_metadata(path: str, *, supporting_arrays=False, name: str = DEFAULT_NAME) -> dict:
62
- """Load metadata from a checkpoint file
80
+ def load_metadata(path: str, *, supporting_arrays: bool = False, name: str = DEFAULT_NAME) -> dict:
81
+ """Load metadata from a checkpoint file.
63
82
 
64
83
  Parameters
65
84
  ----------
66
85
  path : str
67
86
  The path to the checkpoint file
68
87
 
69
- supporting_arrays: bool, optional
88
+ supporting_arrays : bool, optional
70
89
  If True, the function will return a dictionary with the supporting arrays
71
90
 
72
91
  name : str, optional
@@ -102,7 +121,21 @@ def load_metadata(path: str, *, supporting_arrays=False, name: str = DEFAULT_NAM
102
121
  raise ValueError(f"Could not find '{name}' in {path}.")
103
122
 
104
123
 
105
- def load_supporting_arrays(zipf, entries) -> dict:
124
+ def load_supporting_arrays(zipf: zipfile.ZipFile, entries: dict) -> dict:
125
+ """Load supporting arrays from a zip file.
126
+
127
+ Parameters
128
+ ----------
129
+ zipf : zipfile.ZipFile
130
+ The zip file
131
+ entries : dict
132
+ A dictionary of entries with paths, shapes, and dtypes
133
+
134
+ Returns
135
+ -------
136
+ dict
137
+ A dictionary of supporting arrays
138
+ """
106
139
  import numpy as np
107
140
 
108
141
  supporting_arrays = {}
@@ -114,16 +147,18 @@ def load_supporting_arrays(zipf, entries) -> dict:
114
147
  return supporting_arrays
115
148
 
116
149
 
117
- def save_metadata(path, metadata, *, supporting_arrays=None, name=DEFAULT_NAME, folder=DEFAULT_FOLDER) -> None:
118
- """Save metadata to a checkpoint file
150
+ def save_metadata(
151
+ path: str, metadata: dict, *, supporting_arrays: dict = None, name: str = DEFAULT_NAME, folder: str = DEFAULT_FOLDER
152
+ ) -> None:
153
+ """Save metadata to a checkpoint file.
119
154
 
120
155
  Parameters
121
156
  ----------
122
157
  path : str
123
158
  The path to the checkpoint file
124
- metadata : JSON
159
+ metadata : dict
125
160
  A JSON serializable object
126
- supporting_arrays: dict, optional
161
+ supporting_arrays : dict, optional
127
162
  A dictionary of supporting NumPy arrays
128
163
  name : str, optional
129
164
  The name of the metadata file in the zip archive
@@ -179,7 +214,20 @@ def save_metadata(path, metadata, *, supporting_arrays=None, name=DEFAULT_NAME,
179
214
  zipf.writestr(entry["path"], value.tobytes())
180
215
 
181
216
 
182
- def _edit_metadata(path, name, callback, supporting_arrays=None):
217
+ def _edit_metadata(path: str, name: str, callback: Callable, supporting_arrays: dict = None) -> None:
218
+ """Edit metadata in a checkpoint file.
219
+
220
+ Parameters
221
+ ----------
222
+ path : str
223
+ The path to the checkpoint file
224
+ name : str
225
+ The name of the metadata file in the zip archive
226
+ callback : Callable
227
+ A callback function to edit the metadata
228
+ supporting_arrays : dict, optional
229
+ A dictionary of supporting NumPy arrays
230
+ """
183
231
  new_path = f"{path}.anemoi-edit-{time.time()}-{os.getpid()}.tmp"
184
232
 
185
233
  found = False
@@ -223,8 +271,20 @@ def _edit_metadata(path, name, callback, supporting_arrays=None):
223
271
  LOG.info("Updated metadata in %s", path)
224
272
 
225
273
 
226
- def replace_metadata(path, metadata, supporting_arrays=None, *, name=DEFAULT_NAME):
274
+ def replace_metadata(path: str, metadata: dict, supporting_arrays: dict = None, *, name: str = DEFAULT_NAME) -> None:
275
+ """Replace metadata in a checkpoint file.
227
276
 
277
+ Parameters
278
+ ----------
279
+ path : str
280
+ The path to the checkpoint file
281
+ metadata : dict
282
+ A JSON serializable object
283
+ supporting_arrays : dict, optional
284
+ A dictionary of supporting NumPy arrays
285
+ name : str, optional
286
+ The name of the metadata file in the zip archive
287
+ """
228
288
  if not isinstance(metadata, dict):
229
289
  raise ValueError(f"metadata must be a dict, got {type(metadata)}")
230
290
 
@@ -238,8 +298,16 @@ def replace_metadata(path, metadata, supporting_arrays=None, *, name=DEFAULT_NAM
238
298
  return _edit_metadata(path, name, callback, supporting_arrays)
239
299
 
240
300
 
241
- def remove_metadata(path, *, name=DEFAULT_NAME):
301
+ def remove_metadata(path: str, *, name: str = DEFAULT_NAME) -> None:
302
+ """Remove metadata from a checkpoint file.
242
303
 
304
+ Parameters
305
+ ----------
306
+ path : str
307
+ The path to the checkpoint file
308
+ name : str, optional
309
+ The name of the metadata file in the zip archive
310
+ """
243
311
  LOG.info("Removing metadata '%s' from %s", name, path)
244
312
 
245
313
  def callback(full):
anemoi/utils/cli.py CHANGED
@@ -14,6 +14,7 @@ import logging
14
14
  import os
15
15
  import sys
16
16
  import traceback
17
+ from typing import Callable
17
18
 
18
19
  try:
19
20
  import argcomplete
@@ -24,13 +25,36 @@ LOG = logging.getLogger(__name__)
24
25
 
25
26
 
26
27
  class Command:
28
+ """Base class for commands."""
29
+
27
30
  accept_unknown_args = False
28
31
 
29
- def run(self, args):
32
+ def run(self, args: argparse.Namespace) -> None:
33
+ """Run the command.
34
+
35
+ Parameters
36
+ ----------
37
+ args : argparse.Namespace
38
+ The arguments for the command
39
+ """
30
40
  raise NotImplementedError(f"Command not implemented: {args.command}")
31
41
 
32
42
 
33
- def make_parser(description, commands):
43
+ def make_parser(description: str, commands: dict[str, Command]) -> argparse.ArgumentParser:
44
+ """Create an argument parser for the CLI.
45
+
46
+ Parameters
47
+ ----------
48
+ description : str
49
+ The description of the CLI
50
+ commands : dict[str, Command]
51
+ A dictionary of command names to Command instances
52
+
53
+ Returns
54
+ -------
55
+ argparse.ArgumentParser
56
+ The argument parser
57
+ """
34
58
  parser = argparse.ArgumentParser(
35
59
  description=description,
36
60
  formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -60,20 +84,61 @@ def make_parser(description, commands):
60
84
  class Failed(Command):
61
85
  """Command not available."""
62
86
 
63
- def __init__(self, name, error):
87
+ def __init__(self, name: str, error: ImportError):
88
+ """Initialize the Failed command.
89
+
90
+ Parameters
91
+ ----------
92
+ name : str
93
+ The name of the command
94
+ error : ImportError
95
+ The error that occurred
96
+ """
64
97
  self.name = name
65
98
  self.error = error
66
99
  traceback.print_tb(error.__traceback__)
67
100
 
68
- def add_arguments(self, command_parser):
101
+ def add_arguments(self, command_parser: argparse.ArgumentParser) -> None:
102
+ """Add arguments to the command parser.
103
+
104
+ Parameters
105
+ ----------
106
+ command_parser : argparse.ArgumentParser
107
+ The command parser
108
+ """
69
109
  command_parser.add_argument("x", nargs=argparse.REMAINDER)
70
110
 
71
- def run(self, args):
111
+ def run(self, args: argparse.Namespace) -> None:
112
+ """Run the command.
113
+
114
+ Parameters
115
+ ----------
116
+ args : argparse.Namespace
117
+ The arguments for the command
118
+ """
72
119
  print(f"Command '{self.name}' not available: {self.error}")
73
120
  sys.exit(1)
74
121
 
75
122
 
76
- def register_commands(here, package, select, fail=None):
123
+ def register_commands(here: str, package: str, select: Callable, fail: Callable = None) -> dict[str, Command]:
124
+ """Register commands from a package.
125
+
126
+ Parameters
127
+ ----------
128
+ here : str
129
+ The directory containing the commands
130
+ package : str
131
+ The package name
132
+ select : Callable
133
+ A function to select the command object from the module
134
+ fail : Callable, optional
135
+ A function to create a Failed command if a command cannot be imported
136
+
137
+ Returns
138
+ -------
139
+ dict[str, Command]
140
+ A dictionary of command names to Command instances
141
+ """
77
142
  result = {}
78
143
  not_available = {}
79
144
 
@@ -120,7 +185,18 @@ def register_commands(here, package, select, fail=None):
120
185
  return result
121
186
 
122
187
 
123
- def cli_main(version, description, commands):
188
+ def cli_main(version: str, description: str, commands: dict[str, Command]) -> None:
189
+ """Main entry point for the CLI.
190
+
191
+ Parameters
192
+ ----------
193
+ version : str
194
+ The version of the CLI
195
+ description : str
196
+ The description of the CLI
197
+ commands : dict[str, Command]
198
+ A dictionary of command names to Command instances
199
+ """
124
200
  parser = make_parser(description, commands)
125
201
  args, unknown = parser.parse_known_args()
126
202
  if argcomplete:
@@ -7,6 +7,10 @@
7
7
  # granted to it by virtue of its status as an intergovernmental organisation
8
8
  # nor does it submit to any jurisdiction.
9
9
 
10
+ """This module initializes and registers command-line interface (CLI) commands
11
+ for the Anemoi utilities.
12
+ """
13
+
10
14
  import os
11
15
 
12
16
  from anemoi.utils.cli import Command
@@ -9,6 +9,8 @@
9
9
 
10
10
 
11
11
  import json
12
+ from argparse import ArgumentParser
13
+ from argparse import Namespace
12
14
 
13
15
  from ..config import config_path
14
16
  from ..config import load_config
@@ -16,11 +18,26 @@ from . import Command
16
18
 
17
19
 
18
20
  class Config(Command):
21
+ """Handle configuration related commands."""
19
22
 
20
- def add_arguments(self, command_parser):
23
+ def add_arguments(self, command_parser: ArgumentParser) -> None:
24
+ """Add arguments to the command parser.
25
+
26
+ Parameters
27
+ ----------
28
+ command_parser : ArgumentParser
29
+ The argument parser to which the arguments will be added.
30
+ """
21
31
  command_parser.add_argument("--path", help="Print path to config file")
22
32
 
23
- def run(self, args):
33
+ def run(self, args: Namespace) -> None:
34
+ """Execute the command with the provided arguments.
35
+
36
+ Parameters
37
+ ----------
38
+ args : Namespace
39
+ The arguments passed to the command.
40
+ """
24
41
  if args.path:
25
42
  print(config_path())
26
43
  else:
@@ -7,6 +7,8 @@
7
7
 
8
8
  import json
9
9
  import sys
10
+ from argparse import ArgumentParser
11
+ from argparse import Namespace
10
12
 
11
13
  from anemoi.utils.mars.requests import print_request
12
14
 
@@ -16,13 +18,27 @@ from . import Command
16
18
  class Requests(Command):
17
19
  """Convert a JSON requests file to MARS format."""
18
20
 
19
- def add_arguments(self, command_parser):
21
+ def add_arguments(self, command_parser: ArgumentParser) -> None:
22
+ """Add arguments to the command parser.
23
+
24
+ Parameters
25
+ ----------
26
+ command_parser : ArgumentParser
27
+ The argument parser to which the arguments will be added.
28
+ """
20
29
  command_parser.add_argument("input")
21
30
  command_parser.add_argument("output")
22
31
  command_parser.add_argument("--verb", default="retrieve")
23
32
  command_parser.add_argument("--only-one-field", action="store_true")
24
33
 
25
- def run(self, args):
34
+ def run(self, args: Namespace) -> None:
35
+ """Execute the command with the provided arguments.
36
+
37
+ Parameters
38
+ ----------
39
+ args : Namespace
40
+ The arguments passed to the command.
41
+ """
26
42
  if args.input == "-":
27
43
  requests = json.load(sys.stdin)
28
44
  else: