anemoi-utils 0.4.11__py3-none-any.whl → 0.4.13__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.
- anemoi/utils/__init__.py +1 -0
- anemoi/utils/__main__.py +12 -2
- anemoi/utils/_version.py +9 -4
- anemoi/utils/caching.py +138 -13
- anemoi/utils/checkpoints.py +81 -13
- anemoi/utils/cli.py +83 -7
- anemoi/utils/commands/__init__.py +4 -0
- anemoi/utils/commands/config.py +19 -2
- anemoi/utils/commands/requests.py +24 -4
- anemoi/utils/compatibility.py +6 -5
- anemoi/utils/config.py +254 -23
- anemoi/utils/dates.py +216 -55
- anemoi/utils/devtools.py +68 -7
- anemoi/utils/grib.py +30 -9
- anemoi/utils/grids.py +85 -8
- anemoi/utils/hindcasts.py +25 -8
- anemoi/utils/humanize.py +357 -52
- anemoi/utils/logs.py +31 -3
- anemoi/utils/mars/__init__.py +46 -12
- anemoi/utils/mars/requests.py +15 -1
- anemoi/utils/provenance.py +185 -28
- anemoi/utils/registry.py +122 -13
- anemoi/utils/remote/__init__.py +386 -38
- anemoi/utils/remote/s3.py +252 -29
- anemoi/utils/remote/ssh.py +140 -8
- anemoi/utils/s3.py +77 -4
- anemoi/utils/sanitise.py +52 -7
- anemoi/utils/text.py +218 -54
- anemoi/utils/timer.py +91 -15
- {anemoi_utils-0.4.11.dist-info → anemoi_utils-0.4.13.dist-info}/LICENSE +1 -1
- {anemoi_utils-0.4.11.dist-info → anemoi_utils-0.4.13.dist-info}/METADATA +7 -4
- anemoi_utils-0.4.13.dist-info/RECORD +37 -0
- {anemoi_utils-0.4.11.dist-info → anemoi_utils-0.4.13.dist-info}/WHEEL +1 -1
- anemoi_utils-0.4.11.dist-info/RECORD +0 -37
- {anemoi_utils-0.4.11.dist-info → anemoi_utils-0.4.13.dist-info}/entry_points.txt +0 -0
- {anemoi_utils-0.4.11.dist-info → anemoi_utils-0.4.13.dist-info}/top_level.txt +0 -0
anemoi/utils/s3.py
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
# nor does it submit to any jurisdiction.
|
|
9
9
|
|
|
10
10
|
import warnings
|
|
11
|
+
from typing import Any
|
|
12
|
+
from typing import Callable
|
|
13
|
+
from typing import Optional
|
|
11
14
|
|
|
12
15
|
from .remote import transfer
|
|
13
16
|
from .remote.s3 import delete as delete_
|
|
@@ -21,7 +24,21 @@ warnings.warn(
|
|
|
21
24
|
)
|
|
22
25
|
|
|
23
26
|
|
|
24
|
-
def s3_client(*args, **kwargs):
|
|
27
|
+
def s3_client(*args: Any, **kwargs: Any) -> Any:
|
|
28
|
+
"""Create an S3 client.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
*args : Any
|
|
33
|
+
Positional arguments for the S3 client.
|
|
34
|
+
**kwargs : Any
|
|
35
|
+
Keyword arguments for the S3 client.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
Any
|
|
40
|
+
The S3 client.
|
|
41
|
+
"""
|
|
25
42
|
warnings.warn(
|
|
26
43
|
"The 's3_client' function (from anemoi.utils.s3 import s3_client) function is deprecated and will be removed in a future release. "
|
|
27
44
|
"Please use the 's3_client' function (from anemoi.utils.remote.s3 import s3_client) instead.",
|
|
@@ -31,7 +48,35 @@ def s3_client(*args, **kwargs):
|
|
|
31
48
|
return s3_client_(*args, **kwargs)
|
|
32
49
|
|
|
33
50
|
|
|
34
|
-
def upload(
|
|
51
|
+
def upload(
|
|
52
|
+
source: str,
|
|
53
|
+
target: str,
|
|
54
|
+
*,
|
|
55
|
+
overwrite: bool = False,
|
|
56
|
+
resume: bool = False,
|
|
57
|
+
verbosity: int = 1,
|
|
58
|
+
progress: Optional[Callable] = None,
|
|
59
|
+
threads: int = 1,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Upload a file to S3.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
source : str
|
|
66
|
+
The source file path.
|
|
67
|
+
target : str
|
|
68
|
+
The target S3 path.
|
|
69
|
+
overwrite : bool, optional
|
|
70
|
+
Whether to overwrite the target file, by default False.
|
|
71
|
+
resume : bool, optional
|
|
72
|
+
Whether to resume a previous upload, by default False.
|
|
73
|
+
verbosity : int, optional
|
|
74
|
+
The verbosity level, by default 1.
|
|
75
|
+
progress : Callable, optional
|
|
76
|
+
A callback function for progress updates, by default None.
|
|
77
|
+
threads : int, optional
|
|
78
|
+
The number of threads to use, by default 1.
|
|
79
|
+
"""
|
|
35
80
|
warnings.warn(
|
|
36
81
|
"The 'upload' function (from anemoi.utils.s3 import upload) function is deprecated and will be removed in a future release. "
|
|
37
82
|
"Please use the 'transfer' function (from anemoi.utils.remote import transfer) instead.",
|
|
@@ -43,7 +88,21 @@ def upload(source, target, *, overwrite=False, resume=False, verbosity=1, progre
|
|
|
43
88
|
)
|
|
44
89
|
|
|
45
90
|
|
|
46
|
-
def download(*args, **kwargs):
|
|
91
|
+
def download(*args: Any, **kwargs: Any) -> Any:
|
|
92
|
+
"""Download a file from S3.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
*args : Any
|
|
97
|
+
Positional arguments for the download.
|
|
98
|
+
**kwargs : Any
|
|
99
|
+
Keyword arguments for the download.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
Any
|
|
104
|
+
The result of the download.
|
|
105
|
+
"""
|
|
47
106
|
warnings.warn(
|
|
48
107
|
"The 'download' function (from anemoi.utils.s3 import download) function is deprecated and will be removed in a future release. "
|
|
49
108
|
"Please use the 'transfer' function (from anemoi.utils.remote import transfer) instead.",
|
|
@@ -53,7 +112,21 @@ def download(*args, **kwargs):
|
|
|
53
112
|
return transfer(*args, **kwargs)
|
|
54
113
|
|
|
55
114
|
|
|
56
|
-
def delete(*args, **kwargs):
|
|
115
|
+
def delete(*args: Any, **kwargs: Any) -> Any:
|
|
116
|
+
"""Delete a file from S3.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
*args : Any
|
|
121
|
+
Positional arguments for the delete.
|
|
122
|
+
**kwargs : Any
|
|
123
|
+
Keyword arguments for the delete.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
Any
|
|
128
|
+
The result of the delete.
|
|
129
|
+
"""
|
|
57
130
|
warnings.warn(
|
|
58
131
|
"The 'delete' function (from anemoi.utils.s3 import delete) function is deprecated and will be removed in a future release. "
|
|
59
132
|
"Please use the 'transfer' function (from anemoi.utils.remote.s3 import delete) instead.",
|
anemoi/utils/sanitise.py
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
13
|
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
14
15
|
from urllib.parse import parse_qs
|
|
15
16
|
from urllib.parse import urlencode
|
|
16
17
|
from urllib.parse import urlparse
|
|
@@ -22,10 +23,18 @@ RE1 = re.compile(r"{([^}]*)}")
|
|
|
22
23
|
RE2 = re.compile(r"\(([^}]*)\)")
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def sanitise(obj):
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
def sanitise(obj: Any) -> Any:
|
|
27
|
+
"""Sanitise an object by replacing all full paths with shortened versions and URL passwords with '***'.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
obj : Any
|
|
32
|
+
The object to sanitise.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
Any
|
|
37
|
+
The sanitised object.
|
|
29
38
|
"""
|
|
30
39
|
|
|
31
40
|
if isinstance(obj, dict):
|
|
@@ -43,7 +52,19 @@ def sanitise(obj):
|
|
|
43
52
|
return obj
|
|
44
53
|
|
|
45
54
|
|
|
46
|
-
def _sanitise_string(obj):
|
|
55
|
+
def _sanitise_string(obj: str) -> str:
|
|
56
|
+
"""Sanitise a string by replacing full paths and URL passwords.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
obj : str
|
|
61
|
+
The string to sanitise.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
str
|
|
66
|
+
The sanitised string.
|
|
67
|
+
"""
|
|
47
68
|
|
|
48
69
|
parsed = urlparse(obj, allow_fragments=True)
|
|
49
70
|
|
|
@@ -56,7 +77,19 @@ def _sanitise_string(obj):
|
|
|
56
77
|
return obj
|
|
57
78
|
|
|
58
79
|
|
|
59
|
-
def _sanitise_url(parsed):
|
|
80
|
+
def _sanitise_url(parsed: Any) -> str:
|
|
81
|
+
"""Sanitise a URL by replacing passwords with '***'.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
parsed : Any
|
|
86
|
+
The parsed URL.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
str
|
|
91
|
+
The sanitised URL.
|
|
92
|
+
"""
|
|
60
93
|
|
|
61
94
|
LIST = [
|
|
62
95
|
"pass",
|
|
@@ -100,7 +133,19 @@ def _sanitise_url(parsed):
|
|
|
100
133
|
return urlunparse([scheme, netloc, path, params, query, fragment])
|
|
101
134
|
|
|
102
135
|
|
|
103
|
-
def _sanitise_path(path):
|
|
136
|
+
def _sanitise_path(path: str) -> str:
|
|
137
|
+
"""Sanitise a file path by shortening it.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
path : str
|
|
142
|
+
The file path to sanitise.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
str
|
|
147
|
+
The sanitised file path.
|
|
148
|
+
"""
|
|
104
149
|
bits = list(reversed(Path(path).parts))
|
|
105
150
|
result = [bits.pop(0)]
|
|
106
151
|
for bit in bits:
|
anemoi/utils/text.py
CHANGED
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
# nor does it submit to any jurisdiction.
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
"""Text utilities"""
|
|
11
|
+
"""Text utilities."""
|
|
12
12
|
|
|
13
13
|
import re
|
|
14
14
|
from collections import defaultdict
|
|
15
|
+
from typing import Any
|
|
16
|
+
from typing import List
|
|
17
|
+
from typing import Optional
|
|
18
|
+
from typing import Tuple
|
|
19
|
+
from typing import Union
|
|
15
20
|
|
|
16
21
|
# https://en.wikipedia.org/wiki/Box-drawing_character
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
def dotted_line(width=84) -> str:
|
|
20
|
-
"""Return a dotted line using '┈'
|
|
25
|
+
"""Return a dotted line using '┈'.
|
|
21
26
|
|
|
22
27
|
>>> dotted_line(40)
|
|
23
28
|
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
|
|
@@ -40,12 +45,35 @@ def dotted_line(width=84) -> str:
|
|
|
40
45
|
_ansi_escape = re.compile(r"\x1b\[([0-9;]*[mGKH])")
|
|
41
46
|
|
|
42
47
|
|
|
43
|
-
def _has_ansi_escape(s):
|
|
48
|
+
def _has_ansi_escape(s: str) -> bool:
|
|
49
|
+
"""Check if a string contains ANSI escape codes.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
s : str
|
|
54
|
+
The string to check.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
bool
|
|
59
|
+
True if the string contains ANSI escape codes, False otherwise.
|
|
60
|
+
"""
|
|
44
61
|
return _ansi_escape.search(s) is not None
|
|
45
62
|
|
|
46
63
|
|
|
47
|
-
def _split_tokens(s):
|
|
48
|
-
"""Split a string into a list of visual characters with their
|
|
64
|
+
def _split_tokens(s: str) -> List[Tuple[str, int]]:
|
|
65
|
+
"""Split a string into a list of visual characters with their lengths.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
s : str
|
|
70
|
+
The string to split.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
list of tuple
|
|
75
|
+
A list of tuples where each tuple contains a visual character and its length.
|
|
76
|
+
"""
|
|
49
77
|
from wcwidth import wcswidth
|
|
50
78
|
|
|
51
79
|
initial = s
|
|
@@ -76,8 +104,19 @@ def _split_tokens(s):
|
|
|
76
104
|
return out
|
|
77
105
|
|
|
78
106
|
|
|
79
|
-
def visual_len(s):
|
|
80
|
-
"""Compute the length of a string as it appears on the terminal.
|
|
107
|
+
def visual_len(s: Union[str, List[Tuple[str, int]]]) -> int:
|
|
108
|
+
"""Compute the length of a string as it appears on the terminal.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
s : str or list of tuple
|
|
113
|
+
The string or list of visual characters with their lengths.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
int
|
|
118
|
+
The visual length of the string.
|
|
119
|
+
"""
|
|
81
120
|
if isinstance(s, str):
|
|
82
121
|
s = _split_tokens(s)
|
|
83
122
|
assert isinstance(s, (tuple, list)), (type(s), s)
|
|
@@ -92,8 +131,8 @@ def visual_len(s):
|
|
|
92
131
|
return n
|
|
93
132
|
|
|
94
133
|
|
|
95
|
-
def boxed(text, min_width=80, max_width=80) -> str:
|
|
96
|
-
"""Put a box around a text
|
|
134
|
+
def boxed(text: str, min_width: int = 80, max_width: int = 80) -> str:
|
|
135
|
+
"""Put a box around a text.
|
|
97
136
|
|
|
98
137
|
>>> boxed("Hello,\\nWorld!", max_width=40)
|
|
99
138
|
┌──────────────────────────────────────────┐
|
|
@@ -114,12 +153,11 @@ def boxed(text, min_width=80, max_width=80) -> str:
|
|
|
114
153
|
-------
|
|
115
154
|
str
|
|
116
155
|
A boxed version of the input text
|
|
117
|
-
|
|
118
156
|
"""
|
|
119
157
|
|
|
120
158
|
lines = []
|
|
121
159
|
for line in text.split("\n"):
|
|
122
|
-
line = line.
|
|
160
|
+
line = line.rstrip()
|
|
123
161
|
line = _split_tokens(line)
|
|
124
162
|
lines.append(line)
|
|
125
163
|
|
|
@@ -158,68 +196,157 @@ def boxed(text, min_width=80, max_width=80) -> str:
|
|
|
158
196
|
return "\n".join(box)
|
|
159
197
|
|
|
160
198
|
|
|
161
|
-
def bold(text):
|
|
199
|
+
def bold(text: str) -> str:
|
|
200
|
+
"""Make the text bold.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
text : str
|
|
205
|
+
The text to make bold.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
str
|
|
210
|
+
The bold text.
|
|
211
|
+
"""
|
|
162
212
|
from termcolor import colored
|
|
163
213
|
|
|
164
214
|
return colored(text, attrs=["bold"])
|
|
165
215
|
|
|
166
216
|
|
|
167
|
-
def red(text):
|
|
217
|
+
def red(text: str) -> str:
|
|
218
|
+
"""Make the text red.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
text : str
|
|
223
|
+
The text to make red.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
str
|
|
228
|
+
The red text.
|
|
229
|
+
"""
|
|
168
230
|
from termcolor import colored
|
|
169
231
|
|
|
170
232
|
return colored(text, "red")
|
|
171
233
|
|
|
172
234
|
|
|
173
|
-
def green(text):
|
|
235
|
+
def green(text: str) -> str:
|
|
236
|
+
"""Make the text green.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
text : str
|
|
241
|
+
The text to make green.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
str
|
|
246
|
+
The green text.
|
|
247
|
+
"""
|
|
174
248
|
from termcolor import colored
|
|
175
249
|
|
|
176
250
|
return colored(text, "green")
|
|
177
251
|
|
|
178
252
|
|
|
179
|
-
def blue(text):
|
|
253
|
+
def blue(text: str) -> str:
|
|
254
|
+
"""Make the text blue.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
text : str
|
|
259
|
+
The text to make blue.
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
str
|
|
264
|
+
The blue text.
|
|
265
|
+
"""
|
|
180
266
|
from termcolor import colored
|
|
181
267
|
|
|
182
268
|
return colored(text, "blue")
|
|
183
269
|
|
|
184
270
|
|
|
185
271
|
class Tree:
|
|
186
|
-
"""Tree data structure.
|
|
272
|
+
"""Tree data structure.
|
|
187
273
|
|
|
188
|
-
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
actor : Any
|
|
277
|
+
The actor associated with the tree node.
|
|
278
|
+
parent : Tree, optional
|
|
279
|
+
The parent tree node, by default None.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(self, actor: Any, parent: Optional["Tree"] = None):
|
|
189
283
|
self._actor = actor
|
|
190
284
|
self._kids = []
|
|
191
285
|
self._parent = parent
|
|
192
286
|
|
|
193
|
-
def adopt(self, kid):
|
|
287
|
+
def adopt(self, kid: "Tree") -> None:
|
|
288
|
+
"""Adopt a child tree node.
|
|
289
|
+
|
|
290
|
+
Parameters
|
|
291
|
+
----------
|
|
292
|
+
kid : Tree
|
|
293
|
+
The child tree node to adopt.
|
|
294
|
+
"""
|
|
194
295
|
kid._parent._kids.remove(kid)
|
|
195
296
|
self._kids.append(kid)
|
|
196
297
|
kid._parent = self
|
|
197
298
|
# assert False
|
|
198
299
|
|
|
199
|
-
def forget(self):
|
|
300
|
+
def forget(self) -> None:
|
|
301
|
+
"""Forget the current tree node."""
|
|
200
302
|
self._parent._kids.remove(self)
|
|
201
303
|
self._parent = None
|
|
202
304
|
|
|
203
305
|
@property
|
|
204
|
-
def is_leaf(self):
|
|
306
|
+
def is_leaf(self) -> bool:
|
|
307
|
+
"""Bool: True if the tree node is a leaf, False otherwise."""
|
|
205
308
|
return len(self._kids) == 0
|
|
206
309
|
|
|
207
310
|
@property
|
|
208
|
-
def key(self):
|
|
311
|
+
def key(self) -> Tuple:
|
|
312
|
+
"""Tuple: The key of the tree node."""
|
|
209
313
|
return tuple(sorted(self._actor.as_dict().items()))
|
|
210
314
|
|
|
211
315
|
@property
|
|
212
|
-
def _text(self):
|
|
316
|
+
def _text(self) -> str:
|
|
317
|
+
"""Str: The text representation of the tree node."""
|
|
213
318
|
return self._actor.summary
|
|
214
319
|
|
|
215
320
|
@property
|
|
216
|
-
def summary(self):
|
|
321
|
+
def summary(self) -> str:
|
|
322
|
+
"""Str: The summary of the tree node."""
|
|
217
323
|
return self._actor.summary
|
|
218
324
|
|
|
219
|
-
def as_dict(self):
|
|
325
|
+
def as_dict(self) -> dict:
|
|
326
|
+
"""Convert the tree node to a dictionary.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
dict
|
|
331
|
+
The dictionary representation of the tree node.
|
|
332
|
+
"""
|
|
220
333
|
return self._actor.as_dict()
|
|
221
334
|
|
|
222
|
-
def node(self, actor, insert=False):
|
|
335
|
+
def node(self, actor: Any, insert: bool = False) -> "Tree":
|
|
336
|
+
"""Create a new tree node.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
actor : Any
|
|
341
|
+
The actor associated with the new tree node.
|
|
342
|
+
insert : bool, optional
|
|
343
|
+
Whether to insert the new tree node at the beginning, by default False.
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
Tree
|
|
348
|
+
The new tree node.
|
|
349
|
+
"""
|
|
223
350
|
node = Tree(actor, self)
|
|
224
351
|
if insert:
|
|
225
352
|
self._kids.insert(0, node)
|
|
@@ -227,7 +354,8 @@ class Tree:
|
|
|
227
354
|
self._kids.append(node)
|
|
228
355
|
return node
|
|
229
356
|
|
|
230
|
-
def print(self):
|
|
357
|
+
def print(self) -> None:
|
|
358
|
+
"""Print the tree."""
|
|
231
359
|
padding = []
|
|
232
360
|
|
|
233
361
|
while self._factorise():
|
|
@@ -235,14 +363,28 @@ class Tree:
|
|
|
235
363
|
|
|
236
364
|
self._print(padding)
|
|
237
365
|
|
|
238
|
-
def _leaves(self, result):
|
|
366
|
+
def _leaves(self, result: List["Tree"]) -> None:
|
|
367
|
+
"""Collect all leaf nodes.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
result : list of Tree
|
|
372
|
+
The list to collect the leaf nodes.
|
|
373
|
+
"""
|
|
239
374
|
if self.is_leaf:
|
|
240
375
|
result.append(self)
|
|
241
376
|
else:
|
|
242
377
|
for kid in self._kids:
|
|
243
378
|
kid._leaves(result)
|
|
244
379
|
|
|
245
|
-
def _factorise(self):
|
|
380
|
+
def _factorise(self) -> bool:
|
|
381
|
+
"""Factorise the tree.
|
|
382
|
+
|
|
383
|
+
Returns
|
|
384
|
+
-------
|
|
385
|
+
bool
|
|
386
|
+
True if the tree was factorised, False otherwise.
|
|
387
|
+
"""
|
|
246
388
|
if len(self._kids) == 0:
|
|
247
389
|
return False
|
|
248
390
|
|
|
@@ -290,7 +432,14 @@ class Tree:
|
|
|
290
432
|
|
|
291
433
|
return result
|
|
292
434
|
|
|
293
|
-
def _print(self, padding):
|
|
435
|
+
def _print(self, padding: List[str]) -> None:
|
|
436
|
+
"""Print the tree with padding.
|
|
437
|
+
|
|
438
|
+
Parameters
|
|
439
|
+
----------
|
|
440
|
+
padding : list of str
|
|
441
|
+
The padding for each level of the tree.
|
|
442
|
+
"""
|
|
294
443
|
for i, p in enumerate(padding[:-1]):
|
|
295
444
|
if p == " └":
|
|
296
445
|
padding[i] = " "
|
|
@@ -308,7 +457,19 @@ class Tree:
|
|
|
308
457
|
|
|
309
458
|
padding.pop()
|
|
310
459
|
|
|
311
|
-
def to_json(self, depth=0):
|
|
460
|
+
def to_json(self, depth: int = 0) -> dict:
|
|
461
|
+
"""Convert the tree to a JSON serializable dictionary.
|
|
462
|
+
|
|
463
|
+
Parameters
|
|
464
|
+
----------
|
|
465
|
+
depth : int, optional
|
|
466
|
+
The depth of the tree, by default 0.
|
|
467
|
+
|
|
468
|
+
Returns
|
|
469
|
+
-------
|
|
470
|
+
dict
|
|
471
|
+
The JSON serializable dictionary representation of the tree.
|
|
472
|
+
"""
|
|
312
473
|
while self._factorise():
|
|
313
474
|
pass
|
|
314
475
|
|
|
@@ -319,20 +480,20 @@ class Tree:
|
|
|
319
480
|
}
|
|
320
481
|
|
|
321
482
|
|
|
322
|
-
def table(rows, header, align, margin=0) -> str:
|
|
323
|
-
"""Format a table
|
|
483
|
+
def table(rows: List[List[Any]], header: List[str], align: List[str], margin: int = 0) -> str:
|
|
484
|
+
"""Format a table.
|
|
324
485
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
486
|
+
>>> table([['Aa', 12, 5],
|
|
487
|
+
['B', 120, 1],
|
|
488
|
+
['C', 9, 123]],
|
|
489
|
+
['C1', 'C2', 'C3'],
|
|
490
|
+
['<', '>', '>'])
|
|
491
|
+
C1 │ C2 │ C3
|
|
492
|
+
───┼─────┼────
|
|
493
|
+
Aa │ 12 │ 5
|
|
494
|
+
B │ 120 │ 1
|
|
495
|
+
C │ 9 │ 123
|
|
496
|
+
───┴─────┴────
|
|
336
497
|
|
|
337
498
|
Parameters
|
|
338
499
|
----------
|
|
@@ -397,26 +558,29 @@ def table(rows, header, align, margin=0) -> str:
|
|
|
397
558
|
return "\n".join(result)
|
|
398
559
|
|
|
399
560
|
|
|
400
|
-
def progress(done, todo, width=80) -> str:
|
|
401
|
-
"""
|
|
402
|
-
|
|
403
|
-
>>> print(progress(10, 100,width=50))
|
|
404
|
-
█████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
|
561
|
+
def progress(done: int, todo: int, width: int = 80) -> str:
|
|
562
|
+
"""Generates a progress bar string.
|
|
405
563
|
|
|
406
564
|
Parameters
|
|
407
565
|
----------
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
566
|
+
|
|
567
|
+
done : int
|
|
568
|
+
The number of tasks completed.
|
|
569
|
+
todo : int
|
|
570
|
+
The total number of tasks.
|
|
412
571
|
width : int, optional
|
|
413
|
-
|
|
572
|
+
The width of the progress bar, by default 80.
|
|
414
573
|
|
|
415
574
|
Returns
|
|
416
575
|
-------
|
|
417
576
|
str
|
|
418
|
-
|
|
577
|
+
A string representing the progress bar.
|
|
419
578
|
|
|
579
|
+
Example
|
|
580
|
+
-------
|
|
581
|
+
|
|
582
|
+
>>> print(progress(10, 100,width=50))
|
|
583
|
+
█████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
|
420
584
|
"""
|
|
421
585
|
done = min(int(done / todo * width + 0.5), width)
|
|
422
586
|
return green("█" * done) + red("█" * (width - done))
|