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.
Files changed (52) hide show
  1. pythonwrench/__init__.py +490 -0
  2. pythonwrench/__main__.py +7 -0
  3. pythonwrench/_core.py +192 -0
  4. pythonwrench/abc.py +30 -0
  5. pythonwrench/argparse/__init__.py +81 -0
  6. pythonwrench/argparse/dataclass_.py +284 -0
  7. pythonwrench/argparse/parsers.py +619 -0
  8. pythonwrench/cast.py +247 -0
  9. pythonwrench/checksum.py +427 -0
  10. pythonwrench/collections/__init__.py +104 -0
  11. pythonwrench/collections/collections.py +900 -0
  12. pythonwrench/collections/prop.py +104 -0
  13. pythonwrench/collections/reducers.py +330 -0
  14. pythonwrench/concurrent.py +73 -0
  15. pythonwrench/csv.py +12 -0
  16. pythonwrench/dataclasses.py +117 -0
  17. pythonwrench/datetime.py +17 -0
  18. pythonwrench/difflib.py +39 -0
  19. pythonwrench/disk_cache.py +615 -0
  20. pythonwrench/entrypoints/info.py +44 -0
  21. pythonwrench/entrypoints/safe_rmdir.py +98 -0
  22. pythonwrench/entrypoints/tree.py +113 -0
  23. pythonwrench/enum.py +55 -0
  24. pythonwrench/functools.py +234 -0
  25. pythonwrench/hashlib.py +95 -0
  26. pythonwrench/importlib.py +243 -0
  27. pythonwrench/inspect.py +69 -0
  28. pythonwrench/json.py +12 -0
  29. pythonwrench/jsonl.py +12 -0
  30. pythonwrench/logging.py +252 -0
  31. pythonwrench/math.py +107 -0
  32. pythonwrench/os.py +226 -0
  33. pythonwrench/pickle.py +12 -0
  34. pythonwrench/random.py +60 -0
  35. pythonwrench/re.py +139 -0
  36. pythonwrench/semver.py +406 -0
  37. pythonwrench/serialization/__init__.py +70 -0
  38. pythonwrench/serialization/_core.py +70 -0
  39. pythonwrench/serialization/csv.py +493 -0
  40. pythonwrench/serialization/json.py +178 -0
  41. pythonwrench/serialization/jsonl.py +215 -0
  42. pythonwrench/serialization/pickle.py +186 -0
  43. pythonwrench/time.py +34 -0
  44. pythonwrench/typing/__init__.py +125 -0
  45. pythonwrench/typing/checks.py +551 -0
  46. pythonwrench/typing/classes.py +251 -0
  47. pythonwrench/warnings.py +118 -0
  48. pythonwrench-0.6.4.dist-info/METADATA +242 -0
  49. pythonwrench-0.6.4.dist-info/RECORD +52 -0
  50. pythonwrench-0.6.4.dist-info/WHEEL +4 -0
  51. pythonwrench-0.6.4.dist-info/entry_points.txt +10 -0
  52. 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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # for backward compatibility
5
+ from pythonwrench.serialization.pickle import ( # noqa: F401
6
+ dump_pickle,
7
+ dumps_pickle,
8
+ load_pickle,
9
+ loads_pickle,
10
+ read_pickle,
11
+ save_pickle,
12
+ )
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)