pythonwrench 0.6.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.
- pythonwrench/__init__.py +490 -0
- pythonwrench/__main__.py +7 -0
- pythonwrench/_core.py +192 -0
- pythonwrench/abc.py +30 -0
- pythonwrench/argparse/__init__.py +81 -0
- pythonwrench/argparse/dataclass_.py +284 -0
- pythonwrench/argparse/parsers.py +619 -0
- pythonwrench/cast.py +247 -0
- pythonwrench/checksum.py +427 -0
- pythonwrench/collections/__init__.py +104 -0
- pythonwrench/collections/collections.py +900 -0
- pythonwrench/collections/prop.py +104 -0
- pythonwrench/collections/reducers.py +330 -0
- pythonwrench/concurrent.py +73 -0
- pythonwrench/csv.py +12 -0
- pythonwrench/dataclasses.py +117 -0
- pythonwrench/datetime.py +17 -0
- pythonwrench/difflib.py +39 -0
- pythonwrench/disk_cache.py +615 -0
- pythonwrench/entrypoints/info.py +44 -0
- pythonwrench/entrypoints/safe_rmdir.py +98 -0
- pythonwrench/entrypoints/tree.py +113 -0
- pythonwrench/enum.py +55 -0
- pythonwrench/functools.py +234 -0
- pythonwrench/hashlib.py +95 -0
- pythonwrench/importlib.py +243 -0
- pythonwrench/inspect.py +69 -0
- pythonwrench/json.py +12 -0
- pythonwrench/jsonl.py +12 -0
- pythonwrench/logging.py +252 -0
- pythonwrench/math.py +107 -0
- pythonwrench/os.py +226 -0
- pythonwrench/pickle.py +12 -0
- pythonwrench/random.py +60 -0
- pythonwrench/re.py +139 -0
- pythonwrench/semver.py +406 -0
- pythonwrench/serialization/__init__.py +70 -0
- pythonwrench/serialization/_core.py +70 -0
- pythonwrench/serialization/csv.py +493 -0
- pythonwrench/serialization/json.py +178 -0
- pythonwrench/serialization/jsonl.py +215 -0
- pythonwrench/serialization/pickle.py +186 -0
- pythonwrench/time.py +34 -0
- pythonwrench/typing/__init__.py +125 -0
- pythonwrench/typing/checks.py +551 -0
- pythonwrench/typing/classes.py +251 -0
- pythonwrench/warnings.py +118 -0
- pythonwrench-0.6.4.dist-info/METADATA +242 -0
- pythonwrench-0.6.4.dist-info/RECORD +52 -0
- pythonwrench-0.6.4.dist-info/WHEEL +4 -0
- pythonwrench-0.6.4.dist-info/entry_points.txt +10 -0
- pythonwrench-0.6.4.dist-info/licenses/LICENSE +21 -0
pythonwrench/os.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import os.path as osp
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from re import Pattern
|
|
10
|
+
from typing import Any, Generator, Iterable, List, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from pythonwrench.re import PatternLike, compile_patterns, match_patterns
|
|
13
|
+
from pythonwrench.warnings import warn_once
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_num_cpus_available() -> int:
|
|
19
|
+
"""Returns the number of CPUs available for the current process on Linux-based platforms.
|
|
20
|
+
|
|
21
|
+
On Windows and MAC OS, this will just return the number of logical CPUs on this machine.
|
|
22
|
+
If the number of CPUs cannot be detected, returns 0.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
num_cpus = len(os.sched_getaffinity(0)) # type: ignore
|
|
26
|
+
except AttributeError:
|
|
27
|
+
msg = "Cannot detect number of CPUs available for the current process. Function 'get_num_cpus_available' will just returns the number of CPUs on this machine."
|
|
28
|
+
warn_once(msg)
|
|
29
|
+
|
|
30
|
+
num_cpus = os.cpu_count()
|
|
31
|
+
if num_cpus is None:
|
|
32
|
+
num_cpus = 0
|
|
33
|
+
return num_cpus
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def safe_rmdir(
|
|
37
|
+
root: Union[str, Path],
|
|
38
|
+
*,
|
|
39
|
+
rm_root: bool = True,
|
|
40
|
+
error_on_non_empty_dir: bool = True,
|
|
41
|
+
followlinks: bool = False,
|
|
42
|
+
dry_run: bool = False,
|
|
43
|
+
verbose: int = 0,
|
|
44
|
+
) -> Tuple[List[str], List[str]]:
|
|
45
|
+
"""Remove all empty sub-directories.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
root: Root directory path.
|
|
49
|
+
rm_root: If True, remove the root directory too if it is empty at the end. defaults to True.
|
|
50
|
+
error_on_non_empty_dir: If True, raises a RuntimeError if a subdirectory contains at least 1 file. Otherwise it will ignore non-empty directories. defaults to True.
|
|
51
|
+
followlinks: Indicates whether or not symbolic links shound be followed. defaults to False.
|
|
52
|
+
dry_run: If True, does not remove any directory and just output the list of directories which could be deleted. defaults to False.
|
|
53
|
+
verbose: Verbose level. defaults to 0.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A tuple containing the list of directories paths deleted and the list of directories paths reviewed.
|
|
57
|
+
"""
|
|
58
|
+
root = str(root)
|
|
59
|
+
if not osp.isdir(root):
|
|
60
|
+
msg = f"Target root directory does not exists. (with {root=})"
|
|
61
|
+
raise FileNotFoundError(msg)
|
|
62
|
+
|
|
63
|
+
to_delete = {}
|
|
64
|
+
reviewed = []
|
|
65
|
+
walker = os.walk(root, topdown=False, followlinks=followlinks)
|
|
66
|
+
|
|
67
|
+
for dpath, dnames, fnames in walker:
|
|
68
|
+
reviewed.append(dpath)
|
|
69
|
+
|
|
70
|
+
if not rm_root and dpath == root:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
elif len(fnames) == 0 and (
|
|
74
|
+
all(osp.join(dpath, dname) in to_delete for dname in dnames)
|
|
75
|
+
):
|
|
76
|
+
to_delete[dpath] = None
|
|
77
|
+
|
|
78
|
+
elif error_on_non_empty_dir:
|
|
79
|
+
raise RuntimeError(f"Cannot remove non-empty directory '{dpath}'.")
|
|
80
|
+
elif verbose >= 2:
|
|
81
|
+
logger.debug(f"Ignoring non-empty directory '{dpath}'...")
|
|
82
|
+
|
|
83
|
+
if not dry_run:
|
|
84
|
+
for dpath in to_delete:
|
|
85
|
+
os.rmdir(dpath)
|
|
86
|
+
|
|
87
|
+
return list(to_delete), reviewed
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def tree_iter(
|
|
91
|
+
root: Union[str, Path],
|
|
92
|
+
*,
|
|
93
|
+
include: Union[PatternLike, Iterable[PatternLike]] = ".*",
|
|
94
|
+
exclude: Union[PatternLike, Iterable[PatternLike]] = (),
|
|
95
|
+
space: str = " ",
|
|
96
|
+
branch: str = "│ ",
|
|
97
|
+
tee: str = "├── ",
|
|
98
|
+
last: str = "└── ",
|
|
99
|
+
max_depth: int = sys.maxsize,
|
|
100
|
+
followlinks: bool = False,
|
|
101
|
+
skipfiles: bool = False,
|
|
102
|
+
sort: bool = False,
|
|
103
|
+
) -> Generator[str, Any, None]:
|
|
104
|
+
"""A recursive generator, given a directory Path object will yield a visual tree structure line by line with each line prefixed by the same characters
|
|
105
|
+
|
|
106
|
+
Based on: https://stackoverflow.com/questions/9727673/list-directory-tree-structure-in-python
|
|
107
|
+
"""
|
|
108
|
+
root = Path(root)
|
|
109
|
+
if not root.is_dir():
|
|
110
|
+
msg = f"Invalid argument path '{root}'. (not a directory)"
|
|
111
|
+
raise ValueError(msg)
|
|
112
|
+
|
|
113
|
+
if not followlinks and root.is_symlink():
|
|
114
|
+
yield from ()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
include = compile_patterns(include)
|
|
118
|
+
exclude = compile_patterns(exclude)
|
|
119
|
+
if not match_patterns(str(root), include, exclude=exclude):
|
|
120
|
+
yield from ()
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
yield root.resolve().name + "/"
|
|
124
|
+
|
|
125
|
+
if max_depth <= 0:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
yield from _tree_impl(
|
|
129
|
+
root,
|
|
130
|
+
include=include,
|
|
131
|
+
exclude=exclude,
|
|
132
|
+
prefix="",
|
|
133
|
+
space=space,
|
|
134
|
+
branch=branch,
|
|
135
|
+
tee=tee,
|
|
136
|
+
last=last,
|
|
137
|
+
max_depth=max_depth,
|
|
138
|
+
followlinks=followlinks,
|
|
139
|
+
skipfiles=skipfiles,
|
|
140
|
+
sort=sort,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _tree_impl(
|
|
145
|
+
root: Path,
|
|
146
|
+
*,
|
|
147
|
+
include: List[Pattern],
|
|
148
|
+
exclude: List[Pattern],
|
|
149
|
+
max_depth: int,
|
|
150
|
+
followlinks: bool,
|
|
151
|
+
skipfiles: bool,
|
|
152
|
+
sort: bool,
|
|
153
|
+
prefix: str,
|
|
154
|
+
space: str,
|
|
155
|
+
branch: str,
|
|
156
|
+
tee: str,
|
|
157
|
+
last: str,
|
|
158
|
+
) -> Generator[str, Any, None]:
|
|
159
|
+
"""Perform the tree impl operation."""
|
|
160
|
+
walker = _walker_impl(
|
|
161
|
+
root,
|
|
162
|
+
[],
|
|
163
|
+
include=include,
|
|
164
|
+
exclude=exclude,
|
|
165
|
+
max_depth=max_depth,
|
|
166
|
+
followlinks=followlinks,
|
|
167
|
+
skipfiles=skipfiles,
|
|
168
|
+
sort=sort,
|
|
169
|
+
)
|
|
170
|
+
for path, is_dir, locs in walker:
|
|
171
|
+
prefix = "".join((branch if i < num - 1 else space) for i, num in locs[:-1])
|
|
172
|
+
index_in_parent, num_files_in_parent = locs[-1]
|
|
173
|
+
pointer = tee if index_in_parent < num_files_in_parent - 1 else last
|
|
174
|
+
suffix = "/" if is_dir else ""
|
|
175
|
+
|
|
176
|
+
yield prefix + pointer + path.name + suffix
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _walker_impl(
|
|
180
|
+
root: Path,
|
|
181
|
+
locs: List[Tuple[int, int]],
|
|
182
|
+
*,
|
|
183
|
+
include: List[Pattern],
|
|
184
|
+
exclude: List[Pattern],
|
|
185
|
+
max_depth: int,
|
|
186
|
+
followlinks: bool,
|
|
187
|
+
skipfiles: bool,
|
|
188
|
+
sort: bool,
|
|
189
|
+
) -> Generator[Tuple[Path, bool, List[Tuple[int, int]]], Any, None]:
|
|
190
|
+
"""Perform the walker impl operation."""
|
|
191
|
+
candidates_paths = root.iterdir()
|
|
192
|
+
|
|
193
|
+
if sort:
|
|
194
|
+
candidates_paths = sorted(candidates_paths)
|
|
195
|
+
|
|
196
|
+
paths: List[Path] = []
|
|
197
|
+
for path in candidates_paths:
|
|
198
|
+
if not match_patterns(str(path), include, exclude=exclude):
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
if not followlinks and path.is_symlink():
|
|
203
|
+
continue
|
|
204
|
+
if skipfiles and path.is_file():
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
paths.append(path)
|
|
208
|
+
except PermissionError:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
for i, path in enumerate(paths):
|
|
212
|
+
is_dir = path.is_dir()
|
|
213
|
+
locs_i = locs + [(i, len(paths))]
|
|
214
|
+
yield path, is_dir, locs_i
|
|
215
|
+
|
|
216
|
+
if is_dir and len(locs_i) < max_depth:
|
|
217
|
+
yield from _walker_impl(
|
|
218
|
+
path,
|
|
219
|
+
locs=locs_i,
|
|
220
|
+
include=include,
|
|
221
|
+
exclude=exclude,
|
|
222
|
+
max_depth=max_depth,
|
|
223
|
+
followlinks=followlinks,
|
|
224
|
+
skipfiles=skipfiles,
|
|
225
|
+
sort=sort,
|
|
226
|
+
)
|
pythonwrench/pickle.py
ADDED
pythonwrench/random.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
import string
|
|
6
|
+
from typing import Iterable, Optional, overload
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@overload
|
|
10
|
+
def randstr(
|
|
11
|
+
size: int = 10,
|
|
12
|
+
high: None = None,
|
|
13
|
+
/,
|
|
14
|
+
*,
|
|
15
|
+
letters: Iterable[str] = string.ascii_letters,
|
|
16
|
+
seed: Optional[int] = None,
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Perform the randstr operation."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
def randstr(
|
|
24
|
+
low: int,
|
|
25
|
+
high: int,
|
|
26
|
+
/,
|
|
27
|
+
*,
|
|
28
|
+
letters: Iterable[str] = string.ascii_letters,
|
|
29
|
+
seed: Optional[int] = None,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Perform the randstr operation."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def randstr(
|
|
36
|
+
low_or_size: int = 10,
|
|
37
|
+
high: Optional[int] = None,
|
|
38
|
+
/,
|
|
39
|
+
*,
|
|
40
|
+
letters: Iterable[str] = string.ascii_letters,
|
|
41
|
+
seed: Optional[int] = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Returns a randomly generated string of a random range length."""
|
|
44
|
+
assert low_or_size >= 0
|
|
45
|
+
|
|
46
|
+
initial_state = random.getstate()
|
|
47
|
+
if high is None:
|
|
48
|
+
size = low_or_size
|
|
49
|
+
random.seed(seed)
|
|
50
|
+
else:
|
|
51
|
+
assert low_or_size < high
|
|
52
|
+
random.seed(seed)
|
|
53
|
+
size = random.randint(low_or_size, high - 1)
|
|
54
|
+
|
|
55
|
+
letters = list(letters)
|
|
56
|
+
result = "".join(random.choice(letters) for _ in range(size))
|
|
57
|
+
|
|
58
|
+
if seed is not None:
|
|
59
|
+
random.setstate(initial_state)
|
|
60
|
+
return result
|
pythonwrench/re.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from functools import partial
|
|
7
|
+
from re import Pattern
|
|
8
|
+
from typing import Any, Callable, Iterable, List, Literal, Optional, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
from typing_extensions import TypeAlias
|
|
11
|
+
|
|
12
|
+
from pythonwrench.collections import find
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
PatternLike: TypeAlias = Union[str, Pattern]
|
|
17
|
+
PatternListLike: TypeAlias = Union[PatternLike, Iterable[PatternLike]]
|
|
18
|
+
|
|
19
|
+
MatchFn = Callable[[PatternLike, str], Any]
|
|
20
|
+
MatchName = Literal["search", "match"]
|
|
21
|
+
MatchLike = Union[MatchFn, MatchName]
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def filter_with_patterns(
|
|
27
|
+
x: Iterable[str],
|
|
28
|
+
include: Optional[PatternListLike] = ".*",
|
|
29
|
+
*,
|
|
30
|
+
exclude: Optional[PatternListLike] = (),
|
|
31
|
+
match_fn: MatchLike = re.search,
|
|
32
|
+
) -> List[str]:
|
|
33
|
+
"""Perform the filter with patterns operation."""
|
|
34
|
+
if include is None:
|
|
35
|
+
include = ".*"
|
|
36
|
+
if exclude is None:
|
|
37
|
+
exclude = ()
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
xi
|
|
41
|
+
for xi in x
|
|
42
|
+
if match_patterns(xi, include, exclude=exclude, match_fn=match_fn)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def match_patterns(
|
|
47
|
+
x: str,
|
|
48
|
+
include: Optional[PatternListLike] = ".*",
|
|
49
|
+
*,
|
|
50
|
+
exclude: Optional[PatternListLike] = (),
|
|
51
|
+
match_fn: MatchLike = re.search,
|
|
52
|
+
) -> bool:
|
|
53
|
+
"""Returns True if the first argument match at least 1 included pattern and does not match any excluded pattern.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
x: String to check.
|
|
57
|
+
include: Acceptable pattern(s) for x. If None, match all patterns with '.*'. defaults to '.*'.
|
|
58
|
+
exclude: Forbidden pattern(s) for x. If None, match no patterns with value (). defaults to ().
|
|
59
|
+
match_fn: Match function use to compare a pattern with argument x. defaults to re.search.
|
|
60
|
+
"""
|
|
61
|
+
if include is None:
|
|
62
|
+
include = ".*"
|
|
63
|
+
if exclude is None:
|
|
64
|
+
exclude = ()
|
|
65
|
+
|
|
66
|
+
include_index = find_patterns(x, include, match_fn=match_fn, default=-1)
|
|
67
|
+
exclude_index = find_patterns(x, exclude, match_fn=match_fn, default=-1)
|
|
68
|
+
return include_index != -1 and exclude_index == -1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def sort_with_patterns(
|
|
72
|
+
x: Iterable[str],
|
|
73
|
+
patterns: PatternListLike,
|
|
74
|
+
*,
|
|
75
|
+
match_fn: MatchLike = re.search,
|
|
76
|
+
reverse: bool = False,
|
|
77
|
+
) -> List[str]:
|
|
78
|
+
"""Perform the sort with patterns operation."""
|
|
79
|
+
key_fn = get_key_fn(patterns, match_fn=match_fn)
|
|
80
|
+
x = sorted(x, key=key_fn, reverse=reverse)
|
|
81
|
+
return x
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_key_fn(
|
|
85
|
+
patterns: PatternListLike,
|
|
86
|
+
*,
|
|
87
|
+
match_fn: MatchLike = re.search,
|
|
88
|
+
) -> Callable[[str], int]:
|
|
89
|
+
"""Generate key_fn to sorted list of string using multiple patterns.
|
|
90
|
+
|
|
91
|
+
Example
|
|
92
|
+
-------
|
|
93
|
+
>>> lst = ["a", "abc", "aa", "abcd"]
|
|
94
|
+
>>> patterns = ["^ab"] # sort list with elements starting with 'ab' first
|
|
95
|
+
>>> list(sorted(lst, key=get_key_fn(patterns)))
|
|
96
|
+
... ["abc", "abcd", "a", "aa"]
|
|
97
|
+
"""
|
|
98
|
+
patterns = compile_patterns(patterns)
|
|
99
|
+
key_fn = partial(
|
|
100
|
+
find_patterns,
|
|
101
|
+
patterns=patterns,
|
|
102
|
+
match_fn=match_fn,
|
|
103
|
+
default=len(patterns),
|
|
104
|
+
)
|
|
105
|
+
return key_fn # type: ignore
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def find_patterns(
|
|
109
|
+
x: str,
|
|
110
|
+
patterns: PatternListLike,
|
|
111
|
+
*,
|
|
112
|
+
match_fn: MatchLike = re.search,
|
|
113
|
+
default: T = -1,
|
|
114
|
+
) -> Union[int, T]:
|
|
115
|
+
"""Find index of a pattern that match the first argument. If no pattern matches, returns the default value (-1)."""
|
|
116
|
+
patterns = compile_patterns(patterns)
|
|
117
|
+
match_fn = _get_match_fn(match_fn)
|
|
118
|
+
index = find(x, patterns, match_fn=match_fn, order="right", default=default)
|
|
119
|
+
return index
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def compile_patterns(patterns: PatternListLike) -> List[Pattern]:
|
|
123
|
+
"""Compile patterns-like to a list."""
|
|
124
|
+
if isinstance(patterns, (str, Pattern)):
|
|
125
|
+
patterns = [patterns]
|
|
126
|
+
patterns = [re.compile(pattern) for pattern in patterns]
|
|
127
|
+
return patterns
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _get_match_fn(match_fn: MatchLike) -> MatchFn:
|
|
131
|
+
if callable(match_fn):
|
|
132
|
+
return match_fn
|
|
133
|
+
elif match_fn == "search":
|
|
134
|
+
return re.search
|
|
135
|
+
elif match_fn == "match":
|
|
136
|
+
return re.match
|
|
137
|
+
else:
|
|
138
|
+
msg = f"Invalid argument {match_fn=}. (expected callable, 'search' or 'match')"
|
|
139
|
+
raise ValueError(msg)
|