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.
Files changed (48) hide show
  1. iad/core/__init__.py +9 -0
  2. iad/core/array.py +1961 -0
  3. iad/core/binary.py +377 -0
  4. iad/core/cache.py +903 -0
  5. iad/core/codetools.py +203 -0
  6. iad/core/datatools.py +671 -0
  7. iad/core/docs/locators.ipynb +754 -0
  8. iad/core/dotstyle.py +99 -0
  9. iad/core/env.py +271 -0
  10. iad/core/events.py +650 -0
  11. iad/core/filesproc.py +1046 -0
  12. iad/core/fnctools.py +390 -0
  13. iad/core/label.py +240 -0
  14. iad/core/logs.py +182 -0
  15. iad/core/nptools.py +449 -0
  16. iad/core/one_dark.puml +881 -0
  17. iad/core/param/__init__.py +17 -0
  18. iad/core/param/confargparse.py +55 -0
  19. iad/core/param/paramaze.py +339 -0
  20. iad/core/param/tbox.py +277 -0
  21. iad/core/paths.py +563 -0
  22. iad/core/pdtools.py +2570 -0
  23. iad/core/pydantools/__init__.py +5 -0
  24. iad/core/pydantools/fixed_pydantic_yaml/__init__.py +32 -0
  25. iad/core/pydantools/fixed_pydantic_yaml/compat/__init__.py +0 -0
  26. iad/core/pydantools/fixed_pydantic_yaml/compat/hacks.py +76 -0
  27. iad/core/pydantools/fixed_pydantic_yaml/compat/old_enums.py +37 -0
  28. iad/core/pydantools/fixed_pydantic_yaml/compat/representers.py +92 -0
  29. iad/core/pydantools/fixed_pydantic_yaml/compat/types.py +122 -0
  30. iad/core/pydantools/fixed_pydantic_yaml/compat/yaml_lib.py +104 -0
  31. iad/core/pydantools/fixed_pydantic_yaml/ext/__init__.py +1 -0
  32. iad/core/pydantools/fixed_pydantic_yaml/ext/semver.py +152 -0
  33. iad/core/pydantools/fixed_pydantic_yaml/ext/versioned_model.py +113 -0
  34. iad/core/pydantools/fixed_pydantic_yaml/main.py +30 -0
  35. iad/core/pydantools/fixed_pydantic_yaml/mixin.py +281 -0
  36. iad/core/pydantools/fixed_pydantic_yaml/model.py +20 -0
  37. iad/core/pydantools/fixed_pydantic_yaml/py.typed +1 -0
  38. iad/core/pydantools/fixed_pydantic_yaml/version.py +1 -0
  39. iad/core/pydantools/models.py +560 -0
  40. iad/core/regexp.py +348 -0
  41. iad/core/short.py +308 -0
  42. iad/core/strings.py +635 -0
  43. iad/core/unc_panda.py +270 -0
  44. iad/core/units.py +58 -0
  45. iad/core/wrap.py +420 -0
  46. ialdev_core-0.1.0.dist-info/METADATA +73 -0
  47. ialdev_core-0.1.0.dist-info/RECORD +48 -0
  48. 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