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/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)
|