ialdev-core 0.1.0__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.
- iad/core/__init__.py +9 -0
- iad/core/array.py +1961 -0
- iad/core/binary.py +377 -0
- iad/core/cache.py +903 -0
- iad/core/codetools.py +203 -0
- iad/core/datatools.py +671 -0
- iad/core/docs/locators.ipynb +754 -0
- iad/core/dotstyle.py +99 -0
- iad/core/env.py +271 -0
- iad/core/events.py +650 -0
- iad/core/filesproc.py +1046 -0
- iad/core/fnctools.py +390 -0
- iad/core/label.py +240 -0
- iad/core/logs.py +182 -0
- iad/core/nptools.py +449 -0
- iad/core/one_dark.puml +881 -0
- iad/core/param/__init__.py +17 -0
- iad/core/param/confargparse.py +55 -0
- iad/core/param/paramaze.py +339 -0
- iad/core/param/tbox.py +277 -0
- iad/core/paths.py +563 -0
- iad/core/pdtools.py +2570 -0
- iad/core/pydantools/__init__.py +5 -0
- iad/core/pydantools/fixed_pydantic_yaml/__init__.py +32 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/__init__.py +0 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/hacks.py +76 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/old_enums.py +37 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/representers.py +92 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/types.py +122 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/yaml_lib.py +104 -0
- iad/core/pydantools/fixed_pydantic_yaml/ext/__init__.py +1 -0
- iad/core/pydantools/fixed_pydantic_yaml/ext/semver.py +152 -0
- iad/core/pydantools/fixed_pydantic_yaml/ext/versioned_model.py +113 -0
- iad/core/pydantools/fixed_pydantic_yaml/main.py +30 -0
- iad/core/pydantools/fixed_pydantic_yaml/mixin.py +281 -0
- iad/core/pydantools/fixed_pydantic_yaml/model.py +20 -0
- iad/core/pydantools/fixed_pydantic_yaml/py.typed +1 -0
- iad/core/pydantools/fixed_pydantic_yaml/version.py +1 -0
- iad/core/pydantools/models.py +560 -0
- iad/core/regexp.py +348 -0
- iad/core/short.py +308 -0
- iad/core/strings.py +635 -0
- iad/core/unc_panda.py +270 -0
- iad/core/units.py +58 -0
- iad/core/wrap.py +420 -0
- ialdev_core-0.1.0.dist-info/METADATA +73 -0
- ialdev_core-0.1.0.dist-info/RECORD +48 -0
- ialdev_core-0.1.0.dist-info/WHEEL +4 -0
iad/core/regexp.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, Generator, Union
|
|
4
|
+
import regex as re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _gpt_strip_verbose(pattern):
|
|
8
|
+
"""
|
|
9
|
+
How it works:
|
|
10
|
+
1. Tracking State:
|
|
11
|
+
The code uses two boolean flags:
|
|
12
|
+
- in_char_class to know when it’s inside a character class (so that spaces
|
|
13
|
+
or # characters there are left intact).
|
|
14
|
+
- escaped to ensure that characters preceded by a backslash
|
|
15
|
+
are not processed as potential comment or whitespace markers.
|
|
16
|
+
|
|
17
|
+
2. Skipping Comments and Whitespace:
|
|
18
|
+
Outside a character class, when encountering a #,
|
|
19
|
+
the loop skips all characters until the next newline.
|
|
20
|
+
|
|
21
|
+
Similarly, whitespace characters outside character classes are not added to the output.
|
|
22
|
+
|
|
23
|
+
3. Preserving Important Characters:
|
|
24
|
+
All characters that are significant to the regex
|
|
25
|
+
(including escaped ones and those within character classes)
|
|
26
|
+
are appended to the result list.
|
|
27
|
+
|
|
28
|
+
**Caveats**
|
|
29
|
+
|
|
30
|
+
May fail on some edge cases, like nested constructs
|
|
31
|
+
|
|
32
|
+
:param pattern:
|
|
33
|
+
:return:
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
result = []
|
|
37
|
+
in_char_class = False
|
|
38
|
+
escaped = False
|
|
39
|
+
i = 0
|
|
40
|
+
while i < len(pattern):
|
|
41
|
+
char = pattern[i]
|
|
42
|
+
if escaped:
|
|
43
|
+
# If previous char was a backslash, keep this character.
|
|
44
|
+
result.append(char)
|
|
45
|
+
escaped = False
|
|
46
|
+
elif char == '\\':
|
|
47
|
+
# Mark the next character as escaped.
|
|
48
|
+
result.append(char)
|
|
49
|
+
escaped = True
|
|
50
|
+
elif char == '[':
|
|
51
|
+
in_char_class = True
|
|
52
|
+
result.append(char)
|
|
53
|
+
elif char == ']' and in_char_class:
|
|
54
|
+
in_char_class = False
|
|
55
|
+
result.append(char)
|
|
56
|
+
elif char == '#' and not in_char_class:
|
|
57
|
+
# Skip the comment: ignore characters until the end of the line.
|
|
58
|
+
while i < len(pattern) and pattern[i] != "\n":
|
|
59
|
+
i += 1
|
|
60
|
+
# Continue without appending the newline
|
|
61
|
+
# (or add it if you need to preserve line breaks)
|
|
62
|
+
elif char in (' ', '\n', '\t') and not in_char_class:
|
|
63
|
+
pass # Skip free-space when outside a character class.
|
|
64
|
+
else:
|
|
65
|
+
result.append(char)
|
|
66
|
+
i += 1
|
|
67
|
+
return ''.join(result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def strip_verbose_regex(pattern: str | re.Regex, *, validate=True, check=False):
|
|
71
|
+
"""
|
|
72
|
+
Given pattern or compiled Regex convert it from verbose into plain form
|
|
73
|
+
|
|
74
|
+
:param pattern: regex pattern to convert
|
|
75
|
+
:param check: check before stripping it is indeed verbose (is multiline)
|
|
76
|
+
:param validate: try to validate (compile) resulting expression
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(pattern, str):
|
|
79
|
+
pattern = pattern.pattern
|
|
80
|
+
|
|
81
|
+
if check and '\n' not in pattern:
|
|
82
|
+
return pattern
|
|
83
|
+
|
|
84
|
+
clean_pattern = _gpt_strip_verbose(pattern)
|
|
85
|
+
|
|
86
|
+
if validate:
|
|
87
|
+
import regex as _re
|
|
88
|
+
try:
|
|
89
|
+
_re.compile(clean_pattern)
|
|
90
|
+
except re.error:
|
|
91
|
+
try:
|
|
92
|
+
_re.compile(pattern, _re.MULTILINE | _re.VERBOSE)
|
|
93
|
+
except re.error:
|
|
94
|
+
raise ValueError(f"Invalid input {pattern=}")
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Attempt to strip verbose pattern '{pattern}'\n"
|
|
98
|
+
f"produced invalid pattern '{clean_pattern}'")
|
|
99
|
+
# In verbose pattern literal # and \s would be preceded by \ - remove it
|
|
100
|
+
return re.sub(r'\\([#\s])', r'\g<1>', clean_pattern)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _Patterns:
|
|
104
|
+
"""Helper class for compiling patterns on demand"""
|
|
105
|
+
def __init__(self):
|
|
106
|
+
self._patterns = {}
|
|
107
|
+
|
|
108
|
+
def _get(self, name, s, flags):
|
|
109
|
+
if not (pat := self._patterns.get(name, None)):
|
|
110
|
+
pat = self._patterns[name] = re.compile(s, flags)
|
|
111
|
+
return pat
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def named_rex(self):
|
|
115
|
+
"""Captures groups: 'p_name' """
|
|
116
|
+
return self._get('named_rex', r"""
|
|
117
|
+
\(\?P<
|
|
118
|
+
(?P<p_name> # named of the named group (captured as 'p_name')
|
|
119
|
+
\w+)>
|
|
120
|
+
(?P<REC> # matching pattern as REC in form: b(R1)a1...(R2)a2
|
|
121
|
+
[^)(]*+ # `b` (before) part - any not `)(` characters
|
|
122
|
+
(?: # non capturing group for repeated `(R)a`
|
|
123
|
+
\((?&REC)\) # `R` - reference to group 2 for possible nested braces
|
|
124
|
+
[^)(]*+ # 'a' (after) part - any not `)(` characters
|
|
125
|
+
)* # `(R)a` repetition is optional
|
|
126
|
+
) # end the group 2
|
|
127
|
+
\) # end of the named group to match
|
|
128
|
+
""", flags=re.FULLCASE | re.VERBOSE)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def named_fmt(self):
|
|
132
|
+
"""Captures groups: `f_name`, `spec`, `opt`"""
|
|
133
|
+
return self._get('named_fmt', r"""
|
|
134
|
+
{(?P<f_name>[a-zA-Z]\w*) # Group captured as 'f_name'
|
|
135
|
+
(?:
|
|
136
|
+
:(?P<pat> # Group : 'pat' - anything between `:` and `}`
|
|
137
|
+
(?: # explicitly describe repetitions \d{1,2}\w{3}
|
|
138
|
+
[^{}]+ # to allow correct parsing of its nested {}
|
|
139
|
+
{\d*(?:\,\d*)?}
|
|
140
|
+
)*
|
|
141
|
+
[^{}]*
|
|
142
|
+
)? # The 'pattern' part may be empty
|
|
143
|
+
)?}(?P<opt>\?)? # {}? - question mark indicating 'optional'
|
|
144
|
+
""", flags=re.FULLCASE | re.VERBOSE)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def named_rex_or_fmt(self):
|
|
148
|
+
"""Captures groups: `p_name` | (`f_name`, `spec`) """
|
|
149
|
+
rex, fmt = self.named_rex, self.named_fmt
|
|
150
|
+
return self._get('named_rex_or_fmt',
|
|
151
|
+
f"{rex.pattern}|{fmt.pattern}",
|
|
152
|
+
flags=rex.flags | fmt.flags)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_patterns = _Patterns() # use to access patterns
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def format_to_regex(pattern: str, *, pat=r'\w+?', dots=None, fspec=False):
|
|
159
|
+
r"""Convert format-like string into its regular expression form by replacing
|
|
160
|
+
named groups `{name:...}` into `(?P<name>{pattern})`.
|
|
161
|
+
|
|
162
|
+
Default `{pattern}` is provided by the `pat` argument.
|
|
163
|
+
|
|
164
|
+
The default `pat` is `\\w+?` (`?` - not greedy mainly to avoid interference
|
|
165
|
+
with `_` if used as a separator before the next group).
|
|
166
|
+
|
|
167
|
+
It also may be specified for every named group if `fspec=False`.
|
|
168
|
+
|
|
169
|
+
In this case, the part after `:` is interpreted as regex pattern:
|
|
170
|
+
"frame_{fid:00\\d{4}}" -> "frame_(?P<fid>00\\d{4})"
|
|
171
|
+
Otherwise, when `fspec=True`, format specifier is expected and the default
|
|
172
|
+
`pat` is used for the named group regex (if `pat='\\w+?'):
|
|
173
|
+
"very_{big:08.4g}/{fruit}" - > "very_(?P<big>\\w+?)/(?P<fruit>\\w+?)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
:param pattern: format string with items in curled braces:
|
|
177
|
+
no regular expression symbols are allowed, except of '.'
|
|
178
|
+
:param pat: re sub-pattern to use for groups, default is r'\w+?'.
|
|
179
|
+
:param fspec: interpret part after ':' in {name:fspec} as format specifier
|
|
180
|
+
:param dots: True if literal and require conversion ('.' -> '\.'), ``None`` - auto
|
|
181
|
+
:returns: a regular expression string
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
parts = list(partition(_patterns.named_rex_or_fmt, pattern))
|
|
185
|
+
dots = dots or (dots is None and not (
|
|
186
|
+
is_regex(''.join(_[0] for _ in parts))
|
|
187
|
+
)) # decide if its regex only based on non-group parts
|
|
188
|
+
|
|
189
|
+
s = ''
|
|
190
|
+
for before, match in parts:
|
|
191
|
+
if before:
|
|
192
|
+
s += before.replace('.', r'\.') if dots else before
|
|
193
|
+
if match:
|
|
194
|
+
groups = match.groupdict()
|
|
195
|
+
if name := groups['f_name']: # {name: pat}
|
|
196
|
+
_pat = groups['pat']
|
|
197
|
+
opt = groups['opt'] or '' # optional '?'
|
|
198
|
+
s += fr'(?P<{name}>{pat if fspec or not _pat else _pat}){opt}'
|
|
199
|
+
else: # (?P<name>...)?
|
|
200
|
+
s += match.group() # just copy the regular expression as is
|
|
201
|
+
return s
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def regex_to_format(pattern, verbose: bool | None = None):
|
|
205
|
+
r""" Convert regular expression named group parsing pattern (?P<key>...)
|
|
206
|
+
into an equivalent str.format substitutable string: {key}
|
|
207
|
+
Example::
|
|
208
|
+
|
|
209
|
+
regex_to_format(r'/some/fold_(?\P<id>)/(?P<name>\.tif') \
|
|
210
|
+
== '/some/fold_{id}/{name>}.tif'
|
|
211
|
+
|
|
212
|
+
:param pattern: regex with (?P<key>...) elements
|
|
213
|
+
:param verbose: consider pattern verbose, `None` - try guess from the pattern
|
|
214
|
+
:return: string with format {} elements.
|
|
215
|
+
"""
|
|
216
|
+
if verbose is not False:
|
|
217
|
+
assert verbose in (None, True)
|
|
218
|
+
pattern = strip_verbose_regex(pattern, validate=False, check=verbose is None)
|
|
219
|
+
# don't validate - could be mixed format-regex
|
|
220
|
+
fmt = _patterns.named_rex.sub(r'{\g<p_name>}', pattern)
|
|
221
|
+
fmt = fmt.replace(r'\\', '\\').replace(r'\.', '.').replace('(?:', '(')
|
|
222
|
+
|
|
223
|
+
# Remove from string groupings ...(...)... or ...(?:...)...
|
|
224
|
+
# with no additional effects like ()? or ()+.
|
|
225
|
+
fmt = re.sub(r"(?P<pair>\((?:[^\(\)]+|(?&pair))*\))(?![?+*]|\{\d})",
|
|
226
|
+
lambda _: _.group()[1:-1], fmt)
|
|
227
|
+
|
|
228
|
+
def optional_named(m):
|
|
229
|
+
grp = f"{{{m.group('f_name')}}}"
|
|
230
|
+
return f"({grp})?" if m.group('opt') else grp
|
|
231
|
+
|
|
232
|
+
fmt = _patterns.named_fmt.sub(optional_named, fmt)
|
|
233
|
+
return fmt
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def partition(rex, string: str, *, flags=None,
|
|
237
|
+
) -> Iterable[tuple[str, re.Match | None]]:
|
|
238
|
+
"""
|
|
239
|
+
Given string partition it into sections containing one substring matching
|
|
240
|
+
given regex.
|
|
241
|
+
|
|
242
|
+
[prefix]<match>|[prefix]<match>|[prefix]None
|
|
243
|
+
|
|
244
|
+
Yield every segment as tuple: `(substring preceding the match, match object)`
|
|
245
|
+
|
|
246
|
+
The last tuple *may* contain `None` as second element,
|
|
247
|
+
if the string does not end with a match.
|
|
248
|
+
|
|
249
|
+
Allows to provide compiled regular expression to control which regex package is used.
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
:param rex: regex as str or compiled
|
|
253
|
+
:param string: string to partition
|
|
254
|
+
:param flags: provide only if `rex` is `str`!
|
|
255
|
+
:return: generator
|
|
256
|
+
"""
|
|
257
|
+
if isinstance(rex, str):
|
|
258
|
+
rex = re.compile(rex, flags=flags or 0)
|
|
259
|
+
elif not (flags is None or int(flags) == rex.flags):
|
|
260
|
+
raise ValueError(f"Argument {flags=} conflicts with {rex.flags=}")
|
|
261
|
+
|
|
262
|
+
last = 0
|
|
263
|
+
for match in rex.finditer(string): # for every found optional group
|
|
264
|
+
start, stop = match.span()
|
|
265
|
+
yield string[last:start], match
|
|
266
|
+
last = stop
|
|
267
|
+
if last < len(string):
|
|
268
|
+
yield string[last:], None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def is_regex(s: str, compile_check=False) -> bool:
|
|
272
|
+
"""Check if string is a regular expression.
|
|
273
|
+
Looks for special symbols in the string.
|
|
274
|
+
|
|
275
|
+
If found, as an additional verification, the compilation can be requested.
|
|
276
|
+
In this case, if failed - return False, even if first test is passed.
|
|
277
|
+
|
|
278
|
+
:param s: string to check
|
|
279
|
+
:param compile_check: try to compile to confirm
|
|
280
|
+
|
|
281
|
+
:return: ``True`` if all the tests confirmed, otherwise ``False``
|
|
282
|
+
"""
|
|
283
|
+
if re.search(r'[$^?\+\*]|(\.[\?\+\*])|({\d+(,\d*)?})', s) is None:
|
|
284
|
+
return False
|
|
285
|
+
if compile_check:
|
|
286
|
+
try:
|
|
287
|
+
re.compile(s)
|
|
288
|
+
except:
|
|
289
|
+
return False
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def regex_parse(string: str, pattern, *, method='any') -> dict:
|
|
294
|
+
"""
|
|
295
|
+
Extract tags from string according to the NAMED groups in the regexp pattern.
|
|
296
|
+
:param string: file name
|
|
297
|
+
:param pattern: regular expression with NAMED groups to extract
|
|
298
|
+
:param method: which part of string to match:
|
|
299
|
+
- end - string ends with the pattern
|
|
300
|
+
- full - full match of the string to the pattern
|
|
301
|
+
- start - start of the string
|
|
302
|
+
- any - find anywhere in the string
|
|
303
|
+
|
|
304
|
+
Notice, that '$' is added to the pattern to process 'end' case.
|
|
305
|
+
You may prefer to do that by yourself (and select 'any') to speed it up.
|
|
306
|
+
|
|
307
|
+
For file names patterns ambiguity isn't probable and 'any' is enough.
|
|
308
|
+
"""
|
|
309
|
+
if method == 'end': # there is no such re function, so we alter pattern
|
|
310
|
+
current = pattern if isinstance(pattern, str) else pattern.pattern
|
|
311
|
+
if current[-1] != '$' or current[-2] == '\\': # or '...\$' -> '\$$'
|
|
312
|
+
pattern = current + '$'
|
|
313
|
+
|
|
314
|
+
match = _rp_method[method](pattern, string)
|
|
315
|
+
return match.groupdict() if match else {}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
_REX = Union[str, re.Pattern]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def filter_regex_matches(regex: _REX | Iterable[_REX], strings: str | Iterable[str], flags=0
|
|
322
|
+
) -> Generator[str]:
|
|
323
|
+
"""
|
|
324
|
+
Filter given iterable of strings for those matching at least one of provided regular expressions
|
|
325
|
+
|
|
326
|
+
:param regex: one or Iterable of regular expressions in object or string form
|
|
327
|
+
:param strings: one or Iterable
|
|
328
|
+
:param flags:
|
|
329
|
+
:return:
|
|
330
|
+
"""
|
|
331
|
+
from .short import as_list
|
|
332
|
+
|
|
333
|
+
regex = as_list(regex)
|
|
334
|
+
strings = as_list(strings)
|
|
335
|
+
|
|
336
|
+
for s in strings:
|
|
337
|
+
for rex in regex:
|
|
338
|
+
if re.fullmatch(rex, s, flags=flags):
|
|
339
|
+
yield s
|
|
340
|
+
break # at least one match found
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
_rp_method = dict(
|
|
344
|
+
full=re.fullmatch,
|
|
345
|
+
any=re.search,
|
|
346
|
+
end=re.search,
|
|
347
|
+
start=re.match
|
|
348
|
+
)
|
iad/core/short.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Iterable, Union, TypeVar, Collection, Type, Callable, get_args
|
|
5
|
+
|
|
6
|
+
Strings = Union[str, list[str]]
|
|
7
|
+
|
|
8
|
+
__all__ = ['as_list', 'drop_undef', 'as_iter', 'Strings', 'unless_subset']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def unless(condition, event, action: Union[Callable, BaseException] = ValueError):
|
|
12
|
+
"""one-liner expression for checking condition and acting if failed.
|
|
13
|
+
|
|
14
|
+
:param condition: an object which can be cased to boolean
|
|
15
|
+
:param event: could be Exception object or just a string
|
|
16
|
+
:param action: optional action:
|
|
17
|
+
- exception class - used if exception is string
|
|
18
|
+
- a callable object accepting event as argument
|
|
19
|
+
:return: the condition object
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
use_outcome = unless([], 'Empty list!', print) # == Empty list!
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
if not condition:
|
|
26
|
+
if isinstance(event, BaseException):
|
|
27
|
+
raise event
|
|
28
|
+
if isinstance(action, type) and issubclass(action, BaseException):
|
|
29
|
+
raise action(event)
|
|
30
|
+
action(event)
|
|
31
|
+
return condition
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def unless_subset(the_set: Iterable, sub_set: Iterable,
|
|
35
|
+
event="{inv} not found in the set: {the_set}",
|
|
36
|
+
action: Union[Callable, BaseException] = KeyError):
|
|
37
|
+
"""
|
|
38
|
+
Triggers event action unless `ss` is a subset of `s`.
|
|
39
|
+
:param the_set: the set
|
|
40
|
+
:param sub_set: expected subset
|
|
41
|
+
:param event: format(able) string with {inv} and {s} or exception to throw
|
|
42
|
+
:param action: optional action to perform (exception or callable)
|
|
43
|
+
:return: condition
|
|
44
|
+
"""
|
|
45
|
+
the_set = set(the_set)
|
|
46
|
+
inv = set(sub_set).difference(the_set)
|
|
47
|
+
event = event.format(**locals()) if isinstance(event, str) else event
|
|
48
|
+
return unless(not inv, event, action=action)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def issubset(set_a: Collection, set_b: Collection,
|
|
52
|
+
nonempty: bool = False, fail: bool = False):
|
|
53
|
+
"""
|
|
54
|
+
Checks whether set_a is a subset of set_b.
|
|
55
|
+
Overrides python's empty set native behaviour.
|
|
56
|
+
This behaviour relies on empty set - that is a subset of everything in set theory.
|
|
57
|
+
Although this holds, we want to know if one or both of the sets is empty.
|
|
58
|
+
|
|
59
|
+
python:
|
|
60
|
+
>>> set([]).issubset(['a']) == True
|
|
61
|
+
|
|
62
|
+
ours:
|
|
63
|
+
>>> set([]).issubset(['a']) == False
|
|
64
|
+
|
|
65
|
+
The user is in charge to pass set-cast friendly arguments.
|
|
66
|
+
The function gives the user descriptive message on non subset results.
|
|
67
|
+
|
|
68
|
+
function flow can be summarized as follows:
|
|
69
|
+
|
|
70
|
+
cast -> optional non empty check -> cardinality check -> subset check.
|
|
71
|
+
|
|
72
|
+
:param set_a: First collection.
|
|
73
|
+
:param set_b: Second collection.
|
|
74
|
+
:param nonempty: Whether empty sets results on error.
|
|
75
|
+
:param fail: Whether to raise errors, or return a boolean.
|
|
76
|
+
|
|
77
|
+
:raises KeyError: If set_a is not a subset of set_b.
|
|
78
|
+
ValueError: If a is empty.
|
|
79
|
+
|
|
80
|
+
:return: boolean indicates whether set_a is a subset of set_b
|
|
81
|
+
"""
|
|
82
|
+
set_a = set(set_a)
|
|
83
|
+
set_b = set(set_b)
|
|
84
|
+
if nonempty:
|
|
85
|
+
assert set_a, f'Given set_a is empty'
|
|
86
|
+
assert set_b, f'Given set_b is empty'
|
|
87
|
+
|
|
88
|
+
if not len(set_a) > 0:
|
|
89
|
+
if not len(set_b) > 0:
|
|
90
|
+
return True
|
|
91
|
+
else:
|
|
92
|
+
msg = f'Given set_a is empty, set_b {set_b} is not'
|
|
93
|
+
if fail:
|
|
94
|
+
raise ValueError(msg)
|
|
95
|
+
logging.warning(msg)
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
if set_a.issubset(set_b):
|
|
99
|
+
return True
|
|
100
|
+
else:
|
|
101
|
+
msg = f'The keys {set_a - set_b} of set_a are missing in set_b {set_b}'
|
|
102
|
+
if fail:
|
|
103
|
+
raise KeyError(msg)
|
|
104
|
+
else:
|
|
105
|
+
logging.warning(msg)
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def compare(set_a, set_b, fail):
|
|
110
|
+
set_a = set(set_a)
|
|
111
|
+
set_b = set(set_b)
|
|
112
|
+
diff1 = set_a.difference(set_b)
|
|
113
|
+
diff2 = set_b.difference(set_a)
|
|
114
|
+
|
|
115
|
+
if diff1 or diff2:
|
|
116
|
+
msg = ''
|
|
117
|
+
if diff1:
|
|
118
|
+
msg += f"\nKeys {diff1} are missing in set_b {set_b}"
|
|
119
|
+
if diff2:
|
|
120
|
+
msg += f"\nKeys {diff2} are missing in {set_a}"
|
|
121
|
+
if fail:
|
|
122
|
+
raise ValueError(msg)
|
|
123
|
+
return False
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def as_number(v):
|
|
128
|
+
"""
|
|
129
|
+
Return a number given number or string (int or float)
|
|
130
|
+
:param v: number | str
|
|
131
|
+
:return: number
|
|
132
|
+
"""
|
|
133
|
+
if isinstance(v, str):
|
|
134
|
+
try:
|
|
135
|
+
return int(v)
|
|
136
|
+
except ValueError:
|
|
137
|
+
return float(v)
|
|
138
|
+
return v
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def drop_undef(*keys: str, ns: dict = None, _undef=None, **kwargs) -> dict:
|
|
142
|
+
"""
|
|
143
|
+
Drop items with undefined values from a given dict.
|
|
144
|
+
|
|
145
|
+
(Useful to exclude undefined kw arguments when passing to functions.)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
Supported Forms
|
|
149
|
+
---------------
|
|
150
|
+
There are two forms of providing the dict.
|
|
151
|
+
|
|
152
|
+
Undefined object can be specified using `undef` argument, ``None`` by default.
|
|
153
|
+
|
|
154
|
+
Alternatively, undefined value can be defined per key and passed as kwargs
|
|
155
|
+
(if `ns` argument is defined and contains the dict to filter, otherwise `kwargs` are treated as `ns`)
|
|
156
|
+
|
|
157
|
+
Remove Items with `undef` values
|
|
158
|
+
=============================
|
|
159
|
+
|
|
160
|
+
The dict to filter can be passed to the function
|
|
161
|
+
1. as `keyword arguments` ``defined_kws(**kwargs)``
|
|
162
|
+
2. as a single argument `namespace` ``defined_kws(namespace=dct)``
|
|
163
|
+
3. selected keys from the `namespace` dict ``defined_kws(*keys, namespace=dct)``
|
|
164
|
+
|
|
165
|
+
So that form ``3`` is a generalized version of ``2``.
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
^^^^^^^^
|
|
169
|
+
::
|
|
170
|
+
|
|
171
|
+
dct = dict(x=False, y=None, z=0)
|
|
172
|
+
drop_undef(**dct) # drop all None from keyword arguments
|
|
173
|
+
{'x': False, 'z': 0}
|
|
174
|
+
|
|
175
|
+
drop_undef(ns=dct) # same, drop all None from namespace dict
|
|
176
|
+
{'x': False, 'z': 0}
|
|
177
|
+
|
|
178
|
+
drop_undef('x', 'y', ns=dct) # drop all except x, y, and them if None
|
|
179
|
+
{'x': False}
|
|
180
|
+
|
|
181
|
+
UNDEF = object() # define particular UNDEF object to keep Nones
|
|
182
|
+
drop_undef(undef=UNDEF, x=10, y=None, z=UNDEF)
|
|
183
|
+
{'x': 10, y: None}
|
|
184
|
+
|
|
185
|
+
Remove items with custom values
|
|
186
|
+
===============================
|
|
187
|
+
Forms ``2`` and ``3`` allow to describe `undefined` for everey key:
|
|
188
|
+
- as a value, alternative to ``None``
|
|
189
|
+
- as a callble evaluated to ``False`` indicating "undefined"
|
|
190
|
+
|
|
191
|
+
If a key is not mentioned it assumes the usual undefined value of ``None``.
|
|
192
|
+
|
|
193
|
+
drop_undef(ns=dct, y=0) # drop y if 0, others if None
|
|
194
|
+
{'x': False, 'y': None, 'z': 0}
|
|
195
|
+
|
|
196
|
+
As in ``3`` additional selection by `keys` may be applied:
|
|
197
|
+
|
|
198
|
+
drop_undef('x', 'y', ns=dct, x=False, y=0) # drop z regardless
|
|
199
|
+
{'y': None}
|
|
200
|
+
|
|
201
|
+
drop_undef(ns=dct, x={False, 0}.__contains__)
|
|
202
|
+
{'z': 0}
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def defined(k, v):
|
|
207
|
+
if keys and k not in keys:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
cond = kwargs.get(k, None)
|
|
211
|
+
return not (
|
|
212
|
+
v is _undef if cond is None
|
|
213
|
+
else cond(v) if callable(cond)
|
|
214
|
+
else cond == v
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
keys = keys and set(keys)
|
|
218
|
+
if ns:
|
|
219
|
+
return {k: v for k, v in ns.items() if defined(k, v)}
|
|
220
|
+
else:
|
|
221
|
+
return {k: v for k, v in kwargs.items() if v is not _undef}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
T = TypeVar('T', bound=Collection)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def as_list(v: Iterable | object, empty_none=True, *, collect: Type[T] = list,
|
|
228
|
+
no_iter: Type[Iterable] | tuple[Type[Iterable]] = None) -> T:
|
|
229
|
+
""" Converts iterable and scalar objects to list (or requested collection).
|
|
230
|
+
|
|
231
|
+
By *scalar* we understand any object which is neither of:
|
|
232
|
+
- instance of ``Iterable``
|
|
233
|
+
- string
|
|
234
|
+
- has attribute ``.shape`` (like ``ndarray``)
|
|
235
|
+
- of type (or tuple of types) provided in ``no_iter`` argument
|
|
236
|
+
|
|
237
|
+
`None` is converted to [] unless `empty_none` is False
|
|
238
|
+
|
|
239
|
+
:param v: an object or iterable to convert.
|
|
240
|
+
:param empty_none: convert ``None`` into empty collection, otherwise treat as value
|
|
241
|
+
:param collect: type of the resulting collection.
|
|
242
|
+
:param no_iter: type of tuple of types to regard as scalar, inspire they are ``Iterable``.
|
|
243
|
+
"""
|
|
244
|
+
if v is None:
|
|
245
|
+
return collect() if empty_none else collect([None, ])
|
|
246
|
+
|
|
247
|
+
if isinstance(collect, type) and isinstance(v, collect):
|
|
248
|
+
if no_iter and isinstance(v, no_iter):
|
|
249
|
+
return collect([v])
|
|
250
|
+
return v
|
|
251
|
+
|
|
252
|
+
if (isinstance(v, str) or
|
|
253
|
+
hasattr(v, 'shape') or
|
|
254
|
+
not hasattr(v, '__iter__') or
|
|
255
|
+
no_iter and isinstance(v, no_iter)):
|
|
256
|
+
v = [v]
|
|
257
|
+
|
|
258
|
+
return collect(v)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def as_iter(v: Iterable | object, empty_none=True,
|
|
262
|
+
no_iter: Type[Iterable] | tuple[Type[Iterable]] = None) -> Iterable:
|
|
263
|
+
"""
|
|
264
|
+
For any input ensure the output is ``Iterable``:
|
|
265
|
+
- *scalar* input ``v`` convert to tuple ``(v, )``
|
|
266
|
+
- ``Iterable`` return unchanged
|
|
267
|
+
- ``None`` convert to ``()`` if ``empty_none is True``, otherwise to ``(None, )``
|
|
268
|
+
|
|
269
|
+
By *scalar* we understand any object which is neither of:
|
|
270
|
+
- instance of ``Iterable``
|
|
271
|
+
- string
|
|
272
|
+
- has attribute ``.shape`` (like ``ndarray``)
|
|
273
|
+
- of type (or tuple of types) provided in ``no_iter`` argument
|
|
274
|
+
|
|
275
|
+
:param v: an object or iterable to convert.
|
|
276
|
+
:param empty_none: convert ``None`` into empty collection, otherwise treat as value.
|
|
277
|
+
:param no_iter: type of tuple of types to regard as scalar, inspire they are ``Iterable``.
|
|
278
|
+
"""
|
|
279
|
+
if v is None:
|
|
280
|
+
return () if empty_none else (None,)
|
|
281
|
+
|
|
282
|
+
if (isinstance(v, str) or
|
|
283
|
+
hasattr(v, 'shape') or
|
|
284
|
+
not hasattr(v, '__iter__') or
|
|
285
|
+
no_iter and isinstance(v, no_iter)):
|
|
286
|
+
return (v, )
|
|
287
|
+
|
|
288
|
+
return v
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def validate_literal(val, literal_type, *, msg="literal value") -> set:
|
|
292
|
+
"""
|
|
293
|
+
Validate that given value matches given literal type.
|
|
294
|
+
|
|
295
|
+
:param val: Value(s) to check
|
|
296
|
+
:param literal_type: Type created using `Literal[...]
|
|
297
|
+
:param msg: description of the type
|
|
298
|
+
:raise: `ValueError` if not.
|
|
299
|
+
|
|
300
|
+
return - validated set of literals
|
|
301
|
+
"""
|
|
302
|
+
valid = set(get_args(literal_type))
|
|
303
|
+
val = as_list(val, collect=set)
|
|
304
|
+
|
|
305
|
+
if inv := val - valid:
|
|
306
|
+
raise ValueError(f"Invalid {msg} {inv}. Allowed: {valid}")
|
|
307
|
+
|
|
308
|
+
return val
|