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/fnctools.py ADDED
@@ -0,0 +1,390 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses as dcl
4
+ from importlib import import_module
5
+ from types import ModuleType
6
+ from typing import Callable, get_type_hints
7
+
8
+ from . import as_list
9
+
10
+ __all__ = ['Namespace', 'Operator', 'O', 'comp']
11
+
12
+
13
+ class Namespace:
14
+ """
15
+ Create compound namespace from multiple sources.
16
+ Provides read-only dict-like access.
17
+
18
+ >>> n1 = Namespace({'x': abs}, {'y': 10, 'w': 20})
19
+ >>> n1
20
+ <Namespace> collected from [2]: {1+2}
21
+ >>> n2 = Namespace({'z': 'ok'}, n1)
22
+ >>> n2
23
+ <Namespace> collected from [3]: {1+1+2}
24
+ >>> n2.keys()
25
+ ['z', 'x', 'y', 'w']
26
+ >>> n1 + n2 # n2 contains n1, so only content of n1 (different order)
27
+ <Namespace> collected from [3]: {1+2+1}
28
+ >>> n2 += {'a': 100, 'b': 101, 'c': 102}; n2
29
+ <Namespace> collected from [4]: {1+1+2+3}
30
+ """
31
+
32
+ def __init__(self, *namespaces: dict | ModuleType | str, built=False):
33
+ """
34
+ Create compound namespace from sources of different kinds:
35
+ another namespaces (dict) or modules (as objects or their names)
36
+
37
+ :param namespaces: Can be dict, or module or importable module name
38
+ :param built: if True add also __builtin__ as a source
39
+ """
40
+
41
+ def flatter(collection):
42
+ for ns in collection:
43
+ if isinstance(ns, self.__class__):
44
+ yield from flatter(ns._namespaces)
45
+ else:
46
+ yield ns
47
+
48
+ self._namespaces = self._normalize([*flatter(namespaces)], built=built)
49
+
50
+ @classmethod
51
+ def _normalize(cls, namespaces, built=False):
52
+ """Remove duplicates and put builtins at the end"""
53
+ built = built or __builtins__ in namespaces
54
+
55
+ found = set() # if duplicated leave the first
56
+ namespaces = [found.add(id(ns)) or ns for ns in namespaces
57
+ if not (id(ns) in found or ns is __builtins__)] # exclude builtins
58
+ built and namespaces.append(__builtins__) # to ensure it is the last
59
+ return list(map(cls._to_mapping, namespaces))
60
+
61
+ @staticmethod
62
+ def _to_mapping(ns):
63
+ if isinstance(ns, str):
64
+ ns = import_module(ns)
65
+ if isinstance(ns, ModuleType):
66
+ ns = vars(ns)
67
+ if isinstance(ns, dict):
68
+ return ns
69
+ raise TypeError('Namespace must be provided as dict, module or mudule name')
70
+
71
+ def __getitem__(self, item):
72
+ for ns in self._namespaces:
73
+ if item in ns:
74
+ return ns[item]
75
+ raise KeyError(item)
76
+
77
+ def keys(self):
78
+ res = []
79
+ for ns in self._namespaces:
80
+ res.extend(ns)
81
+ return res
82
+
83
+ def __contains__(self, item):
84
+ for ns in self._namespaces:
85
+ if item in ns:
86
+ return True
87
+ return False
88
+
89
+ def __add__(self, other):
90
+ res = self.__class__(self)
91
+ res += other
92
+ return res
93
+
94
+ def __iadd__(self, other):
95
+ if isinstance(other, self.__class__):
96
+ self._namespaces.extend(other._namespaces)
97
+ else:
98
+ self._namespaces.append(self._to_mapping(other))
99
+ self._namespaces = self._normalize(self._namespaces)
100
+ return self
101
+
102
+ def __repr__(self):
103
+ represent = lambda ns: ns is __builtins__ and 'b' or str(len(ns))
104
+ return f"<Namespace> collected from [{len(self._namespaces)}]: " \
105
+ f"{{{'+'.join(map(represent, self._namespaces))}}}"
106
+
107
+ def __bool__(self):
108
+ return bool(self._namespaces)
109
+
110
+
111
+ class Operator:
112
+ """
113
+ Class Operator (alias O) allows building operators from functions and
114
+ use the operator notation to chain functions:
115
+ Instead of:
116
+ sin(cos(min(1, tan(x)))) -> (O(sin) * cos * O(min, 1) * tan)(x)
117
+ >>> inc_1 = O(lambda _: _ + 1, o_name='inc_1')
118
+ >>> f = O(min, 1, o_name='min_1') * inc_1
119
+ >>> f(2)
120
+ 1
121
+ >>> print(f)
122
+ O|min_1|inc_1|
123
+ >>> f = O(abs)
124
+ >>> f *= inc_1
125
+ >>> f(-2)
126
+ 1
127
+ >>> print(abs * inc_1)
128
+ O|abs|inc_1|
129
+ """
130
+
131
+ @dcl.dataclass
132
+ class Func:
133
+ fnc: Callable
134
+ name: str = None
135
+ args: tuple = ()
136
+ kws: dict = None
137
+ inp_key: str = None
138
+ inp_pos: int = None
139
+
140
+ def resolve(self, ns):
141
+ if isinstance(name := self.fnc, str):
142
+ if '.' in name:
143
+ module, name = name.rsplit('.', 1)
144
+ self.fnc = getattr(import_module(module), name)
145
+ else:
146
+ self.fnc = ns[name]
147
+ if not self.name:
148
+ self.name = name
149
+
150
+ def __post_init__(self):
151
+ if self.inp_key and self.inp_pos:
152
+ raise ValueError("Operand argument can't be described as both positional and keyword")
153
+
154
+ if self.inp_pos:
155
+ if not self.args:
156
+ raise ValueError("Operand's position > 0 assumes other positional arguments")
157
+ self.args = [*self.args[:self.inp_pos], None, *self.args[self.inp_pos:]]
158
+
159
+ if isinstance(self.fnc, self.__class__):
160
+ assert not (self.name or self.args or self.kws)
161
+ self.__dict__ = self.fnc.__dict__
162
+ return
163
+
164
+ if not self.name:
165
+ self.name = isinstance(self.fnc, str) and self.fnc.rsplit('.')[-1] or self.fnc.__name__
166
+ assert self.name and self.name != '<lambda>'
167
+ self.kws = self.kws or {}
168
+
169
+ # work around unresolved postponed types in the new python annotations mechanism
170
+ _func_arg_types = get_type_hints(Func) # resolved types of the fields
171
+
172
+ @classmethod
173
+ def comp(cls, *stages: Callable | str | Func | tuple, ns: dict = None, skip=None):
174
+ """
175
+ Creates an Operator object implementing a composition of functions
176
+ with specific signature, allowing the composition with next function.
177
+
178
+ The composition (sequential application) of two functions ``(f1, f2)``
179
+ ::
180
+ def f1(inp: T1, *args1, **kws1) -> T2
181
+ def f2(inp: T2, *args2, **kws2) -> T3
182
+ is performed from right to left, and ``y = comp(F1, F2)(x)`` stands for:
183
+ ::
184
+
185
+ t = f2(x, args2, **kws2)
186
+ y = f2(t, args1, **kws1)
187
+ where ``F`` is one of the forms to describe a function ``f``,
188
+ its `name` and `arguments`.
189
+
190
+ In the full form F is an instance of ``Operator.Func``:
191
+ ::
192
+ F = Operator.Func(f, 'func_name', args, kws)
193
+ However ``comp`` allows for additional forms, supported for convenience
194
+ by the ``Func`` constructor.
195
+
196
+ In addition to that, the function ``f`` may be provided as a string
197
+ referencing its implementation in the supplied `namespace` (dict-like) object.
198
+
199
+ Also stages can be `dict` in which case they are flattened into sequence of
200
+ tuples (key, value) with `key` for func, and `value` for its arguments.
201
+
202
+ Notice! None or empty elements passed as stages are filtered out.
203
+
204
+ >>> f = O.comp('sin', abs, 'cos', ns=Namespace('math'))
205
+ >>> f(10)
206
+ 0.7440230792707043
207
+
208
+ In particular ``Namespace`` can be used to combine different sources.
209
+
210
+ Note, that unless the ``ns`` argument is used,
211
+ the ``comp`` is equivalent to the ``Operators`` chaining
212
+ ::
213
+ comp(F1, F2) == O(F1) * O(F2)
214
+
215
+ Alternatively, in the dotted form, it may contain name of the package:
216
+ ``numpy.sin``, ``iotools.imread``.
217
+
218
+ The simplest form describes composition when neither function
219
+ arguments nor names are required:
220
+
221
+ >>> f = O.comp('math.sin', min, ('numpy.linalg.norm', {'axis':0}))
222
+ >>> print(f)
223
+ O|sin|min|norm|
224
+ >>> f([[1,2,3], [3,0,1]])
225
+ 0.9092974268256817
226
+
227
+ :param stages: dict of functions definitions in the required order
228
+ :param ns: Namespace with implementations of the functions
229
+ :param skip: a collection of hashable values to skip if found in stages,
230
+ if None - use all
231
+ :return:
232
+ """
233
+ from .datatools import map_by_type
234
+
235
+ ns = ns or Namespace()
236
+ skip = set(as_list(skip))
237
+
238
+ def normalize(seq):
239
+ for x in seq:
240
+ if isinstance(x, dict):
241
+ for items in x.items():
242
+ yield items
243
+ elif not hasattr(x, '__len__') and x in skip:
244
+ continue
245
+ else:
246
+ yield x
247
+
248
+ comp_func = O()
249
+
250
+ for func in normalize(stages):
251
+ if isinstance(func, str):
252
+ func = cls.Func(func)
253
+ elif isinstance(func, tuple):
254
+ fnc, *rest = func
255
+ func = cls.Func(fnc, **map_by_type(rest, cls._func_arg_types))
256
+ isinstance(func, cls.Func) and func.resolve(ns)
257
+ comp_func *= func
258
+ return comp_func
259
+
260
+ def __init__(self, fnc: Callable | str | Func | tuple[Func, Func] | Operator = None,
261
+ *args, o_name: str = '', o_key: str = None, o_pos: int = None,
262
+ ns: Namespace = None, **kws):
263
+ """
264
+ Create operator from the function description.
265
+
266
+ >>> f = O() # Without arguments creates a bypass operator.
267
+ >>> f(10) == 10
268
+ True
269
+
270
+ :param fnc: function performed by the operator
271
+ :param args: its additional positional arguments
272
+ :param o_name: name of the operator (uses fnc.__name__ unless fnc is lambda!)
273
+ :param o_key: key name of the operand argument - if not at pos 0
274
+ :param o_pos: position the operand argument - if not 0
275
+ :param ns: namespace in case function is described as a str
276
+ :param kws: its keyword arguments
277
+ """
278
+ if fnc is None:
279
+ self._stages = tuple()
280
+ return
281
+
282
+ if isinstance(fnc, tuple): # from stages, called from __mul__
283
+ assert all(map(lambda f: isinstance(f, Operator.Func), fnc))
284
+ self._stages = fnc
285
+ return
286
+
287
+ if isinstance(fnc, Operator): # COPY CONSTRUCTOR
288
+ assert not args and not kws
289
+ if not o_name:
290
+ self._stages = fnc._stages
291
+ return
292
+ # otherwise, encapsulate Operator as a function with new name
293
+
294
+ if not isinstance(fnc, Operator.Func):
295
+ fnc = Operator.Func(fnc, o_name, args, kws, inp_key=o_key)
296
+ ns and fnc.resolve(ns)
297
+ self._stages = (fnc,)
298
+
299
+ @property
300
+ def __name__(self):
301
+ return '<'.join(self._names())
302
+
303
+ def _names(self):
304
+ return map(lambda f: f.name, self._stages)
305
+
306
+ def __call__(self, x):
307
+ # print('Operator call')
308
+ ret = x
309
+ for f in reversed(self._stages):
310
+ if f.inp_key:
311
+ ret = f.fnc(*f.args, **({f.inp_key: ret} | f.kws))
312
+ elif f.inp_pos:
313
+ f.args[f.inp_pos] = ret
314
+ ret = f.fnc(*f.args, **f.kws)
315
+ else:
316
+ ret = f.fnc(ret, *f.args, **f.kws)
317
+ return ret
318
+
319
+ def __mul__(self, other):
320
+ if not isinstance(other, self.__class__):
321
+ other = self.__class__(other)
322
+ return self.__class__((*self._stages, *other._stages))
323
+
324
+ def __rmul__(self, other):
325
+ return O(other) * self
326
+
327
+ def __imul__(self, other):
328
+ if not isinstance(other, self.__class__):
329
+ other = self.__class__(other)
330
+ self._stages = (*self._stages, *other._stages)
331
+ return self
332
+
333
+ def __repr__(self):
334
+ return f"O|{'|'.join(self._names())}|"
335
+
336
+
337
+ O = Operator
338
+ comp = O.comp
339
+
340
+
341
+ def express_to_lambda(expression, arg='_'):
342
+ """Produces lambda function with a single argument input executing
343
+ given python expression.
344
+
345
+ :param arg: name of the input argument used in the exprerssion ('_')
346
+ :returns: function(arg) - a function expecting keywords arguments
347
+ Example:
348
+ >>> func = express_to_lambda('3 < _ < 2')
349
+ >>> func(4)
350
+ False
351
+ """
352
+ assert arg in expression, f"{arg} must be a variable name in the expression!"
353
+ code = compile(expression, '<string>', 'eval')
354
+
355
+ def func(_):
356
+ f"""Function: {expression}"""
357
+ return eval(code, __builtins__, {arg: _})
358
+
359
+ func.__qualname__ = f'`{expression}`'
360
+ return func
361
+
362
+
363
+ def express_to_kw_func(expression: str):
364
+ """
365
+ Produces a function executing given python expression with variables
366
+ passed into its namespace either as its keyword arguments or
367
+ a single dict argument.
368
+
369
+ Example:
370
+
371
+ >>> func = express_to_kw_func('x < 2 or y > 3')
372
+ >>> func(x=1, y=10)
373
+ True
374
+ >>> func(dict(x=1, y=10))
375
+ True
376
+
377
+ :param expression: string with python expression using variables names
378
+ to be passed to the resulting function as arguments
379
+ :returns: function(**kws) or function(kws: dict)
380
+ - a function with keywords arguments or one dict arguments
381
+ """
382
+ code = compile(expression, '<string>', 'eval')
383
+
384
+ def func(ns=None, **kws):
385
+ kws = kws if ns is None else {**ns, **kws}
386
+ f"""Function: {expression}"""
387
+ return eval(code, __builtins__, kws)
388
+
389
+ func.__qualname__ = f'`{expression}`'
390
+ return func
iad/core/label.py ADDED
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ['Array', 'Keys']
4
+
5
+ from collections import namedtuple
6
+ from typing import Dict, Sequence, Any, Collection, NamedTuple, Iterable
7
+
8
+ import pandas as pd
9
+
10
+ from . import as_list
11
+ from .nptools import Array
12
+ from .strings import compact_repr
13
+
14
+
15
+ class Keys(pd.core.indexes.frozen.FrozenList): # TODO: Merge Keys into Labeled?
16
+ def __init__(self, *keys):
17
+ n = len(keys)
18
+ if n == 1 and not isinstance(keys[0], str):
19
+ assert hasattr(keys[0], '__len__')
20
+ keys = keys[0]
21
+ super().__init__(keys)
22
+
23
+ @classmethod
24
+ def from_index(cls, index: pd.MultiIndex):
25
+ return cls(index.names)
26
+
27
+ def label(self, *vals, strict=False, **kvs):
28
+ """Construct Labels object by assigning values to the keys and verifying consistency.
29
+ Support two forms, explicit and implicit:
30
+ ::
31
+ keys.label(k1=v1, k2=v2)
32
+ keys.label(v1, v2)
33
+ keys.label([v1, v2]) # same as previous
34
+
35
+ They are equivalent if k1, k2 in keys, but explicit for is preferable
36
+ for visibility, and flexibility, as ``strict`` argument may relax
37
+ requirements for all the kw args being from keys.
38
+
39
+ Second form is useful when operating arrays of values.
40
+
41
+ :param strict: if strict is True and extra keys are used raise KeyError.
42
+ otherwise just checking that all the expected keys are assigned.
43
+ :param vals: sequence of values matching by the order keys in self,
44
+ as multiple positional arguments, or one of collection type
45
+ :param kvs: key: value pairs of the labels.
46
+ :return: Labels built from the keys
47
+ """
48
+ if vals:
49
+ if len(vals) == 1 and isinstance(vals[0], (list, tuple)):
50
+ vals = vals[0]
51
+ if kvs:
52
+ raise ValueError("Keyword and values only arguments can't be mixed!")
53
+ if len(vals) != len(self):
54
+ raise IndexError("Mismatch in length of values and keys")
55
+ kvs = dict(zip(self, vals))
56
+
57
+ if dif := (set(self).symmetric_difference(kvs) if strict else set(self).difference(kvs)):
58
+ raise KeyError(f"Mismatched keys: {dif} (valid: {self})")
59
+ return Labels(kvs)
60
+
61
+ def order_values(self, *, strict=False, fill_missing=None, **kws) -> tuple:
62
+ """Create tuple of values of all the keys in their order.
63
+
64
+ :param strict: if True rise KeyError if kws contain unknown keys
65
+ :param fill_missing: value to fill missing values or an exception class to raise
66
+ :param kws: keys-values to extract values from
67
+ :return: tuple of values of all the keys in their order
68
+ """
69
+ if issubclass(fill_missing, BaseException) and (dif := set(kws).difference(self)):
70
+ fill_missing(f"Unknown keys: {dif}")
71
+ if strict and (dif := set(self).difference(kws)):
72
+ KeyError(f"Missing keys to initialize {dif}")
73
+ return tuple(kws.get(k, fill_missing) for k in self)
74
+
75
+ def __eq__(self, other: Sequence):
76
+ return not set(self).symmetric_difference(other)
77
+
78
+
79
+ class Labels(dict):
80
+ """
81
+ Labels container class for labeled data with basic arithmetics
82
+ """
83
+
84
+ def __getattr__(self, item):
85
+ if item in self:
86
+ return self[item]
87
+ return self.get(item, super().__getattribute__(item))
88
+
89
+ def __setattr__(self, key, value):
90
+ if hasattr(self, key):
91
+ self[key] = value
92
+ else:
93
+ super().__setattr__(key, value)
94
+
95
+ # noinspection PyMissingConstructor
96
+ def __init__(self, *args: dict | NamedTuple | Iterable[tuple[str, Any]], **kws: Dict[str | Any]):
97
+ """
98
+ Construct labels in different forms:
99
+
100
+ - From mixed collection of dicts, named tuples and kws:
101
+ ::
102
+ Labels({'k':1}, named(x=2, z=4), (), a=10, b=20)
103
+ - From
104
+
105
+ :param args:
106
+ :param kws:
107
+ """
108
+ for d in args: # merge dict representations of different kinds
109
+ self.update(
110
+ d if isinstance(d, dict)
111
+ else d._asdict() if hasattr(d, '_asdict') # named tuple
112
+ else d # Iterator over (key, value) tuples
113
+ )
114
+ self.update(kws)
115
+
116
+ def __repr__(self):
117
+ sep = '@#'
118
+ items = sorted(self.items(), key=lambda kv: kv[0])
119
+ s = sep.join(f'{k}: {compact_repr(v)}' for k, v in items)
120
+ s = f"<{s.replace(sep, ', ')}>" if len(s) < 80 else s.replace(sep, '\n')
121
+ return s
122
+
123
+ __repr__.is_compact = True
124
+
125
+ def drop(self, keys: str | Collection[str], strict=False):
126
+ """Remove given key(s).
127
+
128
+ :param keys: keys to remove
129
+ :param strict: fail if requested key(s) not exist
130
+ """
131
+ keys = as_list(keys, collect=set)
132
+ if strict and (missing := keys.difference(self)):
133
+ raise ValueError(f"Some of the requested keys are {missing = }")
134
+ return self.__class__((k, v) for k, v in self.items() if k not in keys)
135
+
136
+ def select(self, *keys, strict=True):
137
+ """
138
+ Return subset of items in the given order.
139
+ :param keys: keys to select the items by
140
+ :param strict: raise KeyError if requested key does not exist
141
+ :return:
142
+ """
143
+ return self.__class__(
144
+ ((k, self[k]) for k in keys)
145
+ if strict else
146
+ ((k, self[k]) for k in keys if k in self)
147
+ )
148
+
149
+ def _from_set_operation(self, operation, other):
150
+ if isinstance(other, str):
151
+ other = {other}
152
+ elif hasattr(other, '_fields'):
153
+ other = other._fields
154
+ elif not hasattr(other, '__iter__'):
155
+ raise TypeError("Labels expect iterable or str or namedtuple")
156
+ return self.__class__((k, self[k]) for k in operation(set(self), other))
157
+
158
+ def __add__(self, other: dict | NamedTuple):
159
+ return self.__class__(self, other)
160
+
161
+ def __sub__(self, other: NamedTuple | Iterable | str):
162
+ return self._from_set_operation(set.difference, other)
163
+
164
+ def __and__(self, other: Iterable | NamedTuple | str):
165
+ return self._from_set_operation(set.intersection, other)
166
+
167
+ def __xor__(self, other: Iterable | NamedTuple | str):
168
+ return self._from_set_operation(set.symmetric_difference, other)
169
+
170
+ def __or__(self, other: dict):
171
+ """lb2 = lb | dict"""
172
+ return self.__class__(dict.__or__(self, other))
173
+
174
+ def __ior__(self, other: dict):
175
+ """lb |= dict"""
176
+ return dict.__ior__(self, other)
177
+
178
+ def to_keys(self):
179
+ return Keys(*self)
180
+
181
+ @property
182
+ def tuple(self):
183
+ return tuple(self.values())
184
+
185
+ @property
186
+ def namedtuple(self):
187
+ return namedtuple('LabelsValues', self)(*self.values())
188
+
189
+ def to_index(self):
190
+ """Convert labels into a single row of ``MultiIndex``.
191
+
192
+ If you need it for indexing, consider using ``tuple`` instead.
193
+ """
194
+ return pd.MultiIndex.from_tuples([tuple(self.values())], names=self)
195
+
196
+ @classmethod
197
+ def to_indices(cls, labels: list[Labels]):
198
+ return pd.MultiIndex.from_tuples(map(lambda x: x.values(), labels), names=labels[0])
199
+
200
+ @classmethod
201
+ def from_frame(cls, df, *, index=True, data=True, squeeze=False) -> Labels | list[Labels]:
202
+ """
203
+ Convert DataFrame into list of Labels, with control over
204
+ which data columns and levels of the index to use
205
+
206
+ :param df: DataFrame or Series
207
+ :param index: True to use all the levels of the Multi-index,
208
+ or specify required (False for none)
209
+ :param data: True to use all the columns, or specify required
210
+ :param squeeze: if list of labels contains only one Labels obj - return it
211
+ :return: list of Labels or Labels correspondingly
212
+ """
213
+ if isinstance(df, pd.Series):
214
+ df = df.to_frame()
215
+ if isinstance(df.columns, pd.MultiIndex):
216
+ df = df.T # series was inverted
217
+
218
+ data = [] if data is False else df.columns if data is True \
219
+ else [data] if isinstance(data, str) else data
220
+ df = df[data]
221
+ if isinstance(index, bool):
222
+ df = df.reset_index(drop=not index)
223
+ else:
224
+ df.reset_index(index).reset_index(drop=True)
225
+ labels = [*map(cls, df.to_dict('index').values())]
226
+ if squeeze and len(labels) == 1:
227
+ return labels[0]
228
+ return labels
229
+
230
+ @classmethod
231
+ def from_index(cls, index: pd.MultiIndex) -> list[Labels]:
232
+ """
233
+ Convert MultiIndex into Labels
234
+ :param index: Multi-index to convert from
235
+ :return: list of Labels
236
+ """
237
+ return Labels.from_frame(index.to_frame(index=False), index=False)
238
+
239
+ def copy(self) -> Labels:
240
+ return Labels(self)