ialdev-core 0.2.5__tar.gz → 0.2.6__tar.gz
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.
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/PKG-INFO +1 -1
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/pyproject.toml +1 -1
- ialdev_core-0.2.6/src/iad/core/data/labeled.py +249 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/data/pdtools.py +123 -9
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/fs/filesproc.py +26 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/wrap.py +2 -2
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_filesproc.py +30 -1
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_pdtools.py +30 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/.gitignore +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/.pixi/config.toml +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/README.md +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/__init__.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/cache.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/codetools.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/data/__init__.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/data/array.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/data/binary.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/data/label.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/data/nptools.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/data/unc_panda.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/data/units.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/datatools.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/docs/locators.ipynb +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/events.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/fnctools.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/fs/__init__.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/fs/env.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/fs/paths.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/logs.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/one_dark.puml +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/param/__init__.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/param/confargparse.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/param/paramaze.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/regexp.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/short.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/strings.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/src/iad/core/tbox.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/array.ipynb +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/conftest.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/data/.env +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/dot_styles_examples.ipynb +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_array.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_binar.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_cachepipe.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_codetools.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_coltools.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_datatools.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_events.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_nptools.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_paths.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_regexp.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_shorts.py +0 -0
- {ialdev_core-0.2.5 → ialdev_core-0.2.6}/tests/test_tbox.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ialdev-core"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
description = "iad.core — algorithmic utilities: data tools, parameters, paths, caching, and more"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .label import Labels
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .pdtools import DTable
|
|
11
|
+
|
|
12
|
+
__all__ = ['Labeled', 'LabeledType']
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Labeled(Labels):
|
|
16
|
+
"""
|
|
17
|
+
Base ABSTRACT class for Labeled data types - an extended dict containing data
|
|
18
|
+
in one field and descriptive labels in the others.
|
|
19
|
+
|
|
20
|
+
Actual Labeled Data types are created for specific collection of labels
|
|
21
|
+
(by ``LabeledType`` metaclass) using ``labeled_type()`` or ``LT`` alias
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def type_keys(cls) -> tuple:
|
|
26
|
+
return *cls._defaults, *cls._undefined
|
|
27
|
+
|
|
28
|
+
UNDEF = object()
|
|
29
|
+
|
|
30
|
+
def __eq__(self, other):
|
|
31
|
+
if not (isinstance(other, dict) and len(self) == len(other)):
|
|
32
|
+
return False
|
|
33
|
+
no_key = object()
|
|
34
|
+
try:
|
|
35
|
+
for k, vo in other.items():
|
|
36
|
+
if (v := self.get(k, no_key)) is no_key:
|
|
37
|
+
return False
|
|
38
|
+
na = isinstance(v, np.ndarray) + isinstance(vo, np.ndarray)
|
|
39
|
+
if na == 1 or na == 0 and vo != v or na == 2 and not np.array_equal(vo, v):
|
|
40
|
+
return False
|
|
41
|
+
except Exception as ex: # various not comparable types
|
|
42
|
+
print(ex)
|
|
43
|
+
return False
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def __init__(self, data=UNDEF, **kws):
|
|
47
|
+
"""
|
|
48
|
+
Support dict-like initializations:
|
|
49
|
+
A(data=2, b=3) and A({'data':2, 'y':3})
|
|
50
|
+
and silent data field:
|
|
51
|
+
A(2, b=3) == A(data=2, b=3)
|
|
52
|
+
:param data: not defined or data or dict with labels (like kws)
|
|
53
|
+
:param kws: labels key, values
|
|
54
|
+
"""
|
|
55
|
+
cls = type(self)
|
|
56
|
+
if cls is Labeled:
|
|
57
|
+
raise RuntimeError(f"Class {cls} is a mixin class not intended for instantiation.")
|
|
58
|
+
|
|
59
|
+
# ---- STEP 1 - convert inputs into homogeneous form
|
|
60
|
+
if data is not self.UNDEF:
|
|
61
|
+
# support Labeled(dict) like initialization
|
|
62
|
+
if isinstance(data, dict) and self._all_keys.issuperset(data):
|
|
63
|
+
kws.update(data)
|
|
64
|
+
else:
|
|
65
|
+
assert 'data' in self._all_keys, "unnamed argument allowed only if 'data' field defined!"
|
|
66
|
+
kws['data'] = data
|
|
67
|
+
|
|
68
|
+
# ---- STEP 2 - check all the conditions are met
|
|
69
|
+
if missing := set(self._undefined).difference(kws):
|
|
70
|
+
raise KeyError(f"labels {missing} not initialized in {cls}!")
|
|
71
|
+
|
|
72
|
+
if violated := '\n\t'.join(
|
|
73
|
+
f"{k}={assigned} ({expected=})"
|
|
74
|
+
for k in self._frozen_defaults.intersection(kws)
|
|
75
|
+
if (expected := self._defaults[k]) != (assigned := kws[k])
|
|
76
|
+
): raise KeyError(f"Initialization of {cls} violates frozen defaults:\n {violated}")
|
|
77
|
+
|
|
78
|
+
if self._frozen_keys and not self._all_keys.issuperset(kws):
|
|
79
|
+
raise KeyError(f"Undefined keys {set(kws).difference(self._all_keys)} in frozen {cls}")
|
|
80
|
+
|
|
81
|
+
# ---- STEP 3 - initialize the items
|
|
82
|
+
super().__init__(self._defaults) # first fill with defaults
|
|
83
|
+
# then assign from the arguments
|
|
84
|
+
for k, v in kws.items():
|
|
85
|
+
if self._validate_type and \
|
|
86
|
+
(tp := self._undefined.get(k, False)) and \
|
|
87
|
+
type(tp) is type and not isinstance(v, tp):
|
|
88
|
+
v = tp(v)
|
|
89
|
+
self[k] = v
|
|
90
|
+
|
|
91
|
+
def flat(self, *, sep='_', exclude='data', data='data', fmt=None) -> tuple[str, np.ndarray]:
|
|
92
|
+
"""
|
|
93
|
+
Merge labels into a single string and produce a tuple (flatten_label, data)
|
|
94
|
+
|
|
95
|
+
:param sep: separator when joining
|
|
96
|
+
:param exclude: keys exclude from the join
|
|
97
|
+
:param data: key of the data field returned as second item in the tuple
|
|
98
|
+
:param fmt: optional str.format sting, if provided ignores arguments:
|
|
99
|
+
sep, exclude, keys
|
|
100
|
+
:return: tuple(joint_label, data_item)
|
|
101
|
+
"""
|
|
102
|
+
from iad.core import as_iter
|
|
103
|
+
data = self[data]
|
|
104
|
+
if fmt:
|
|
105
|
+
return fmt.format(**self), data
|
|
106
|
+
|
|
107
|
+
excluded = set(as_iter(exclude))
|
|
108
|
+
included = lambda _: _[0] not in excluded
|
|
109
|
+
str_val = lambda _: str(_[1])
|
|
110
|
+
return sep.join(map(str_val, filter(included, self.items()))), data
|
|
111
|
+
|
|
112
|
+
def __setattr__(self, item, val):
|
|
113
|
+
if item in self:
|
|
114
|
+
self[item] = val
|
|
115
|
+
else:
|
|
116
|
+
super().__setattr__(val)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_table(cls, db: DTable, *, missing=KeyError, undef=()):
|
|
120
|
+
"""
|
|
121
|
+
Create instance of this Labeled type from :cls:`iad.core.data.pdtools.DataTable`
|
|
122
|
+
All the pre-defined keys are used to query the index.
|
|
123
|
+
All the undefined keys must be in the table and are initialized
|
|
124
|
+
|
|
125
|
+
If table does not contain given label, then depending on the value of ``missing``:
|
|
126
|
+
- `issubclass(missing, BaseException)` - raise `missing(f"{type(ex)}({ex.args})")`
|
|
127
|
+
- `isinstance(missing, BaseException)` - raise `missing`
|
|
128
|
+
- otherwise, return `missing`
|
|
129
|
+
|
|
130
|
+
:param db:
|
|
131
|
+
:param missing: Exception class, instance, or value to return
|
|
132
|
+
:param undef: keys listed here are forced to be considered as undefined
|
|
133
|
+
:return:
|
|
134
|
+
"""
|
|
135
|
+
if undef:
|
|
136
|
+
undef = set(undef)
|
|
137
|
+
if undef := undef.difference(cls._undefined): # undef not already undefined
|
|
138
|
+
if not_defined := undef.difference(cls._defaults): # must be defined!
|
|
139
|
+
raise KeyError(f"Unknown keys: {not_defined}")
|
|
140
|
+
# move defined undef from defined into undefined
|
|
141
|
+
defined = {k: v for k, v in cls._defaults.items() if k not in undef}
|
|
142
|
+
undefined = [*undef, cls._undefined]
|
|
143
|
+
else:
|
|
144
|
+
defined = cls._defaults
|
|
145
|
+
undefined = [*cls._undefined]
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
res = db.select(defined)
|
|
149
|
+
except (KeyError, LookupError) as ex:
|
|
150
|
+
if isinstance(missing, BaseException):
|
|
151
|
+
raise missing
|
|
152
|
+
elif isinstance(missing, type) and issubclass(missing, BaseException):
|
|
153
|
+
raise missing(f"{type(ex)}({ex.args})")
|
|
154
|
+
return missing
|
|
155
|
+
if hasattr(res, 'columns'):
|
|
156
|
+
res = res[undefined]
|
|
157
|
+
elif not (len(undefined) == 1 and res.name == undefined[0]):
|
|
158
|
+
raise KeyError(f"Table does not contain required keys {undefined}")
|
|
159
|
+
return cls(**res.reset_index().iloc[0].to_dict())
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class LabeledType(type):
|
|
163
|
+
""" Metaclass to create Labeled Data classes extending ``dict``,
|
|
164
|
+
but built around predefined keys with optional default values.
|
|
165
|
+
|
|
166
|
+
Main purpose of such classes to ensure that during instantiation all
|
|
167
|
+
those keys are initialized either by their defaults or as arguments.
|
|
168
|
+
|
|
169
|
+
Note: additional keys except those defined in the class may be added.
|
|
170
|
+
|
|
171
|
+
Classes are supposed to be created by directly calling this meta:
|
|
172
|
+
|
|
173
|
+
>>> LabeledXY = LabeledType(x=10, y=int)
|
|
174
|
+
>>> d = LabeledXY(y=2) # LT(x=2) raise exception for undefined y!
|
|
175
|
+
>>> assert d['y'] == 10
|
|
176
|
+
>>> assert type(d.__name__) == 'Labeled' # default class name
|
|
177
|
+
>>> LabeledData = LabeledType('LabeledData', x=10, y=int)
|
|
178
|
+
>>> assert LabeledData.__name__ == 'LabeledData' # custom class name
|
|
179
|
+
|
|
180
|
+
To encorage usage of 'data' key for data its positional initialization
|
|
181
|
+
is supported if data key is introduced explicitely or using `LD` function
|
|
182
|
+
>>> LabeledData = LabeledType(data=NDArray, y=int) # equivalent = LD(y=int)
|
|
183
|
+
>>> d = LabeledData([1,2,3], y=20)
|
|
184
|
+
>>> assert d['data'] == [1,2,3]
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __repr__(cls):
|
|
188
|
+
undefined = (f"{k}: {v.__name__}" for k, v in cls._undefined.items())
|
|
189
|
+
defaults = (f"{k}={v}" for k, v in cls._defaults.items())
|
|
190
|
+
return f"{cls.__qualname__}<{', '.join((*undefined, *defaults))}>"
|
|
191
|
+
|
|
192
|
+
def __new__(mcs, _name=None, *, validate_type=False,
|
|
193
|
+
frozen_defaults=False, frozen_keys=False,
|
|
194
|
+
**labels):
|
|
195
|
+
"""
|
|
196
|
+
:param _name: Name of the created class
|
|
197
|
+
:param validate_type: perform casting on keys initialization if possible
|
|
198
|
+
:param frozen_defaults: allow change default values (False)
|
|
199
|
+
use True to define templates
|
|
200
|
+
:param frozen_keys: allow not defined keys
|
|
201
|
+
:param labels: dict with {key: default_value} and {key: type} items
|
|
202
|
+
"""
|
|
203
|
+
_name = _name or f"Labeled"
|
|
204
|
+
# 'undefined' are keys assigned by types, not
|
|
205
|
+
# default values which would be called 'defaults'.
|
|
206
|
+
cls = super().__new__(mcs, _name, (Labeled,), {})
|
|
207
|
+
|
|
208
|
+
cls._undefined = {k: labels.pop(k) for k in
|
|
209
|
+
[_ for _, v in labels.items() if type(v) is type]}
|
|
210
|
+
cls._defaults = labels
|
|
211
|
+
cls._all_keys = {*cls._undefined, *cls._defaults}
|
|
212
|
+
|
|
213
|
+
frozen_defaults = cls._defaults if frozen_defaults is True else \
|
|
214
|
+
[] if frozen_defaults is False else frozen_defaults
|
|
215
|
+
cls._frozen_defaults = {*frozen_defaults}
|
|
216
|
+
cls._frozen_keys = frozen_keys
|
|
217
|
+
cls._validate_type = validate_type
|
|
218
|
+
return cls
|
|
219
|
+
|
|
220
|
+
def __init__(mcs, _name=None, *, validate_type=False,
|
|
221
|
+
frozen_defaults=False, frozen_keys=False,
|
|
222
|
+
**labels):
|
|
223
|
+
"""
|
|
224
|
+
:param _name: Name of the created class
|
|
225
|
+
:param validate_type: perform casting on keys initialization if possible
|
|
226
|
+
:param frozen_defaults: allow change default values (False)
|
|
227
|
+
use True to define templates
|
|
228
|
+
:param frozen_keys: allow not defined keys
|
|
229
|
+
:param labels: dict with {key: default_value} and {key: type} items
|
|
230
|
+
"""
|
|
231
|
+
pass # to avoid calling type.__init__ with unusual arguments
|
|
232
|
+
|
|
233
|
+
def __contains__(cls, item):
|
|
234
|
+
return item in cls._defaults or item in cls._undefined
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def undefined(cls):
|
|
238
|
+
"""keys with undefined default values"""
|
|
239
|
+
return cls._undefined.keys()
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def default(cls):
|
|
243
|
+
"""keys with defined default values"""
|
|
244
|
+
return cls._defaults.keys()
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def defined(cls):
|
|
248
|
+
"""set of ALL the keys with undefined AND default values"""
|
|
249
|
+
return cls._all_keys
|
|
@@ -1050,6 +1050,56 @@ def _expand_vec_values(pairs: list[tuple[Scalar, Scalar | Vector]], prev_expande
|
|
|
1050
1050
|
yield prev_expanded # return as received - start aggregation
|
|
1051
1051
|
|
|
1052
1052
|
|
|
1053
|
+
def pivot_flat(df, base: str | list[str], *, pivot: str | list[str],
|
|
1054
|
+
index: bool | list[str] | str | None = None, sep=','):
|
|
1055
|
+
"""
|
|
1056
|
+
In the given dataframe creates multi-index columns by adding `pivot` levels to the `base` columns`
|
|
1057
|
+
and flattens them into str using sep.
|
|
1058
|
+
|
|
1059
|
+
All the operations are done using on both df.columns and df.index.names.
|
|
1060
|
+
|
|
1061
|
+
Sets `index` columns as multi-index levels.
|
|
1062
|
+
- if `index` is True, use all the remaining columns not mentioned in `pivot`.
|
|
1063
|
+
- if `index` is False, index is reset completely
|
|
1064
|
+
- if `index` is None, leaves original index levels (except of pivoted).
|
|
1065
|
+
"""
|
|
1066
|
+
remove = lambda x, c : [_ for _ in x if _ not in c]
|
|
1067
|
+
|
|
1068
|
+
org_index = df.index.names if isinstance(df.index, pd.MultiIndex) else []
|
|
1069
|
+
org_columns = df.columns
|
|
1070
|
+
all_labels = [*org_index, *org_columns]
|
|
1071
|
+
|
|
1072
|
+
invalid = list(filter(lambda _: not isinstance(_, (str, type(None))), org_columns))
|
|
1073
|
+
assert not invalid, f"Some of columns/ index levels are not strings: {invalid}"
|
|
1074
|
+
|
|
1075
|
+
def check(x, name):
|
|
1076
|
+
x = as_list(x)
|
|
1077
|
+
assert x, f"At least one {x} needed"
|
|
1078
|
+
assert not (_:= set(x).difference(all_labels)), f"Unknown {name}: {_}"
|
|
1079
|
+
return x
|
|
1080
|
+
|
|
1081
|
+
base, pivot = check(base, 'base'), check(pivot, 'pivot')
|
|
1082
|
+
|
|
1083
|
+
# leave only base as columns, and then pivot reuired levels
|
|
1084
|
+
df = df.set_index(remove(all_labels, base)).unstack(pivot)
|
|
1085
|
+
df.columns = [sep.join(col) for col in df.columns] # flatten columns names into str
|
|
1086
|
+
|
|
1087
|
+
if index is True:
|
|
1088
|
+
index = df.index.names
|
|
1089
|
+
elif index is False:
|
|
1090
|
+
index = []
|
|
1091
|
+
elif index is None:
|
|
1092
|
+
index = re|move(org_index, pivot)
|
|
1093
|
+
else:
|
|
1094
|
+
index = as_list(index)
|
|
1095
|
+
assert not (_:= set(index).difference(df.index.names)), f"Invaid index levels: {_}"
|
|
1096
|
+
|
|
1097
|
+
pivot_columns = list(df.columns)
|
|
1098
|
+
to_columns = remove(df.index.names, index)
|
|
1099
|
+
df = df.reset_index(to_columns)
|
|
1100
|
+
return df, pivot_columns
|
|
1101
|
+
|
|
1102
|
+
|
|
1053
1103
|
def _row_queries_gen(labels, levels, levels_map) -> Generator[list[tuple[int, Scalar]]]:
|
|
1054
1104
|
"""
|
|
1055
1105
|
Generator of mult-index row queries in form of:
|
|
@@ -1790,18 +1840,32 @@ class _TableMixIn:
|
|
|
1790
1840
|
|
|
1791
1841
|
def qix(self: PTable, *anonymous,
|
|
1792
1842
|
drop_level: bool | Collection = False, keep: Collection = None,
|
|
1793
|
-
axis: AxisT = None, key_err=True, **named) -> PTable:
|
|
1843
|
+
axis: AxisT = None, key_err=True, reorder=False, **named) -> PTable:
|
|
1794
1844
|
"""
|
|
1795
1845
|
Filter a dataframe or series using fuzzy query of its multi-index.
|
|
1796
|
-
Arguments in different forms describe the index values to be searched for.
|
|
1797
|
-
|
|
1798
|
-
Some index levels in the output can be dropped by specifying either
|
|
1799
|
-
``drop_level`` or ``keep`` (mutually exclusive) arguments.
|
|
1800
1846
|
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1847
|
+
``qix`` is a FILTER, not a selector: each requested value may match
|
|
1848
|
+
zero, one, or many rows, and results follow the table's storage
|
|
1849
|
+
order. For structured, ordered, one-row-per-label retrieval (e.g.
|
|
1850
|
+
``img, gt = ...``) use :meth:`select`.
|
|
1851
|
+
|
|
1852
|
+
Capabilities, from simple to advanced:
|
|
1853
|
+
|
|
1854
|
+
1. One level by name: ``dt.qix(w='a')``
|
|
1855
|
+
2. Several levels (AND): ``dt.qix(w='a', x='1')``
|
|
1856
|
+
3. Several values per level: ``dt.qix(w=['b', 'a'])``
|
|
1857
|
+
(membership; rows stay in storage order)
|
|
1858
|
+
4. Both axes auto-matched: ``dt.qix(w='a', y='4')``
|
|
1859
|
+
- restrict with ``axis='index'``/``'columns'`` (or 0/1)
|
|
1860
|
+
5. Anonymous values: ``dt.qix('a', '3')``
|
|
1861
|
+
- search a value without naming its level (error if ambiguous)
|
|
1862
|
+
6. Collapse redundant levels with ``drop_level`` / ``keep``:
|
|
1863
|
+
``dt.qix(w='a', drop_level=True)`` (drops only when one value remains)
|
|
1864
|
+
7. Tolerate misses with ``key_err=False`` (returns empty table)
|
|
1865
|
+
8. Opt-in ordering: ``dt.qix(w=['b', 'a'], reorder=True)``
|
|
1866
|
+
- with ``reorder=True`` the listed values define each level's
|
|
1867
|
+
output order (first keyword = primary key; unqueried levels keep
|
|
1868
|
+
storage order), and duplicate listed values raise ``ValueError``.
|
|
1805
1869
|
|
|
1806
1870
|
:param self: The table to filter
|
|
1807
1871
|
:param anonymous: list of values from one of the index levels
|
|
@@ -1811,6 +1875,10 @@ class _TableMixIn:
|
|
|
1811
1875
|
:param keep: a collection of levels to keep - excludes using drop_level
|
|
1812
1876
|
:param axis: if specified query only this axis
|
|
1813
1877
|
:param key_err: raise KeyError if index not found or return empty
|
|
1878
|
+
:param reorder: if True, reorder results to the order values are listed
|
|
1879
|
+
per level (kwargs order = precedence) and raise ValueError
|
|
1880
|
+
on duplicate listed values. Default False (pure filter,
|
|
1881
|
+
storage order).
|
|
1814
1882
|
:param named: {level: value} - specific levels to find,
|
|
1815
1883
|
eliminates exhaustive search in all levels
|
|
1816
1884
|
:return: Filtered table
|
|
@@ -1862,6 +1930,17 @@ class _TableMixIn:
|
|
|
1862
1930
|
x
|
|
1863
1931
|
1 1 5
|
|
1864
1932
|
2 1 5
|
|
1933
|
+
|
|
1934
|
+
By default qix is a filter and keeps storage order. Pass
|
|
1935
|
+
``reorder=True`` to order results by the values you list.
|
|
1936
|
+
Example 4 - opt-in ordering (note y follows the listed [5, 3]):
|
|
1937
|
+
|
|
1938
|
+
>>> dt.qix(x='1', y=['5','3'], drop_level=True, reorder=True) # doctest: +NORMALIZE_WHITESPACE
|
|
1939
|
+
z data1 data2
|
|
1940
|
+
w a b c d a b c d
|
|
1941
|
+
y
|
|
1942
|
+
5 1 2 3 4 5 6 7 8
|
|
1943
|
+
3 1 2 3 4 5 6 7 8
|
|
1865
1944
|
"""
|
|
1866
1945
|
# ToDo: add option to search not only in the index
|
|
1867
1946
|
# FixMe: self.empty check twice!
|
|
@@ -1908,6 +1987,17 @@ class _TableMixIn:
|
|
|
1908
1987
|
logging.debug(f'qix:\n{err}')
|
|
1909
1988
|
return self.iloc[0:0]
|
|
1910
1989
|
|
|
1990
|
+
if reorder:
|
|
1991
|
+
for lvl, val in named.items():
|
|
1992
|
+
vals = as_list(val)
|
|
1993
|
+
if len(vals) != len(set(vals)):
|
|
1994
|
+
raise ValueError(f"Duplicate values with reorder=True: {lvl=}, {vals=}")
|
|
1995
|
+
for ax, axis_kws in enumerate(axs_kws):
|
|
1996
|
+
# follow kwargs (named) order so the first listed level is primary
|
|
1997
|
+
ordered = {lvl: named[lvl] for lvl in named if lvl in axis_kws}
|
|
1998
|
+
if ordered and (keys := order_keys(res.axes[ax], ordered)):
|
|
1999
|
+
res = res.take(np.lexsort(keys[::-1]), axis=ax)
|
|
2000
|
+
|
|
1911
2001
|
if drop_level:
|
|
1912
2002
|
to_set = lambda x: {x} if isinstance(x, (str, int)) else set(x)
|
|
1913
2003
|
for ax, (kws, axis) in enumerate(zip(axs_kws, res.axes)):
|
|
@@ -2073,6 +2163,30 @@ class _TableMixIn:
|
|
|
2073
2163
|
return hash_value.hexdigest()[-n:]
|
|
2074
2164
|
|
|
2075
2165
|
|
|
2166
|
+
def order_keys(axis_index: pd.Index | pd.MultiIndex, kws) -> list[np.ndarray]:
|
|
2167
|
+
""" Positional sort keys to reorder rows by the order values are listed.
|
|
2168
|
+
|
|
2169
|
+
For each level whose requested value is a list of two or more values,
|
|
2170
|
+
map every axis entry to the position of its value in that list. Levels
|
|
2171
|
+
with a scalar / single value are skipped (no reordering needed).
|
|
2172
|
+
|
|
2173
|
+
Args:
|
|
2174
|
+
axis_index: the (multi-)index of the already-filtered axis
|
|
2175
|
+
kws: {level: value | [values]} requested for this axis
|
|
2176
|
+
|
|
2177
|
+
Returns: list of int key arrays, one per listed level (kwargs order)
|
|
2178
|
+
"""
|
|
2179
|
+
keys = []
|
|
2180
|
+
for lvl, val in kws.items():
|
|
2181
|
+
vals = as_list(val)
|
|
2182
|
+
if len(vals) < 2:
|
|
2183
|
+
continue
|
|
2184
|
+
pos = {v: i for i, v in enumerate(vals)}
|
|
2185
|
+
level_vals = axis_index.get_level_values(lvl) if axis_index.nlevels > 1 else axis_index
|
|
2186
|
+
keys.append(level_vals.map(pos).to_numpy())
|
|
2187
|
+
return keys
|
|
2188
|
+
|
|
2189
|
+
|
|
2076
2190
|
def slicer(index: pd.Index | pd.MultiIndex, kws):
|
|
2077
2191
|
""" Slicing the index with kws requested
|
|
2078
2192
|
Args:
|
|
@@ -215,6 +215,32 @@ def rename_files_by_ref(files: Union[Iterable, str], ref_files: Union[Iterable,
|
|
|
215
215
|
return old_new_pairs
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
def inc_file_sfx(path, sep='_'):
|
|
219
|
+
"""Given full path (<dir>/<stem>.<ext>) find if there are any files with such name
|
|
220
|
+
or with additional suffix (<dir>/<stem><sep><num>.<ext>'),
|
|
221
|
+
and create a new name with suffix incremented over the maximal found.
|
|
222
|
+
|
|
223
|
+
- if only `path` start with '_1'
|
|
224
|
+
- if `path` is not found:
|
|
225
|
+
- found with suffixes - increment max suffix
|
|
226
|
+
- otherwise return the path
|
|
227
|
+
"""
|
|
228
|
+
path = Path(path).expanduser()
|
|
229
|
+
base = path.stem + sep
|
|
230
|
+
pat = re.compile(re.escape(base) + r'(\d+)')
|
|
231
|
+
last = max((
|
|
232
|
+
int(m.group(1))
|
|
233
|
+
for p in path.parent.iterdir()
|
|
234
|
+
if p.is_file() and (m := pat.fullmatch(p.stem))
|
|
235
|
+
), default=None)
|
|
236
|
+
|
|
237
|
+
if last is None:
|
|
238
|
+
stem = base + '1' if path.exists() else path.stem
|
|
239
|
+
else:
|
|
240
|
+
stem = base + str(last+1)
|
|
241
|
+
return path.with_stem(stem)
|
|
242
|
+
|
|
243
|
+
|
|
218
244
|
def normalize(path: PathT, out: Type[Path] | Type[str] | None = Path):
|
|
219
245
|
"""
|
|
220
246
|
Translate path into canonical form expanding user but not resolving links.
|
|
@@ -150,7 +150,6 @@ class NamedTupleI:
|
|
|
150
150
|
::
|
|
151
151
|
isinstance(type(obj), NamedTupleMeta)
|
|
152
152
|
"""
|
|
153
|
-
from .data.nptools import array_info_str
|
|
154
153
|
|
|
155
154
|
_fields: list[str]
|
|
156
155
|
__iter__: Callable
|
|
@@ -158,7 +157,8 @@ class NamedTupleI:
|
|
|
158
157
|
def __init__(self, *args, **kws): ...
|
|
159
158
|
|
|
160
159
|
def __repr__(self):
|
|
161
|
-
|
|
160
|
+
from .data.nptools import array_info_str
|
|
161
|
+
val_str = lambda v: (array_info_str(v, stats=False)
|
|
162
162
|
if hasattr(v, 'shape') and hasattr(v, 'ndim') else str(v))
|
|
163
163
|
fields_str = (f'{k}: {val_str(v)}' for k, v in zip(self._fields, self))
|
|
164
164
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
-
from iad.core.fs.filesproc import Locator, Path
|
|
3
|
+
from iad.core.fs.filesproc import Locator, Path, inc_file_sfx
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def test_locator(tmp_path):
|
|
@@ -95,3 +95,32 @@ def test_locator_not_responsive(tmp_path):
|
|
|
95
95
|
loc.validate(alarm=True) # removes the "invalid" folder
|
|
96
96
|
assert not any(loc.defined()), "Must have failed to validate in 0 time"
|
|
97
97
|
|
|
98
|
+
|
|
99
|
+
def test_inc_file_sfx_returns_unused_path(tmp_path):
|
|
100
|
+
path = tmp_path / 'result.txt'
|
|
101
|
+
|
|
102
|
+
assert inc_file_sfx(path) == path
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_inc_file_sfx_starts_suffix_when_file_exists(tmp_path):
|
|
106
|
+
path = tmp_path / 'result.txt'
|
|
107
|
+
path.touch()
|
|
108
|
+
|
|
109
|
+
assert inc_file_sfx(path) == tmp_path / 'result_1.txt'
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_inc_file_sfx_increments_max_suffix(tmp_path):
|
|
113
|
+
path = tmp_path / 'result.txt'
|
|
114
|
+
(tmp_path / 'result_1.txt').touch()
|
|
115
|
+
(tmp_path / 'result_3.txt').touch()
|
|
116
|
+
(tmp_path / 'result_other.txt').touch()
|
|
117
|
+
|
|
118
|
+
assert inc_file_sfx(path) == tmp_path / 'result_4.txt'
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_inc_file_sfx_supports_custom_separator(tmp_path):
|
|
122
|
+
path = tmp_path / 'result.txt'
|
|
123
|
+
(tmp_path / 'result-2.txt').touch()
|
|
124
|
+
|
|
125
|
+
assert inc_file_sfx(path, sep='-') == tmp_path / 'result-3.txt'
|
|
126
|
+
|
|
@@ -326,6 +326,34 @@ def test_qix_anonymous(sdt):
|
|
|
326
326
|
sdt.qix('Im not there')
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
def test_qix_reorder(sdt):
|
|
330
|
+
# default is a pure filter: storage order, not requested order
|
|
331
|
+
qixed = sdt.qix(x='1', y=['5', '3'])
|
|
332
|
+
assert list(qixed.index.get_level_values('y')) == ['3', '5']
|
|
333
|
+
# opt-in reorder follows the listed order, single index level
|
|
334
|
+
qixed = sdt.qix(x='1', y=['5', '3'], reorder=True)
|
|
335
|
+
assert list(qixed.index.get_level_values('y')) == ['5', '3']
|
|
336
|
+
# reorder on the columns axis
|
|
337
|
+
qixed = sdt.qix(z='data1', w=['c', 'a'], reorder=True)
|
|
338
|
+
assert list(qixed.columns.get_level_values('w')) == ['c', 'a']
|
|
339
|
+
# multi-level: first kwarg ('y') is the primary key
|
|
340
|
+
qixed = sdt.qix(y=['5', '3'], x=['2', '1'], reorder=True)
|
|
341
|
+
assert list(qixed.index.get_level_values('y'))[:2] == ['5', '5']
|
|
342
|
+
assert list(qixed.index.get_level_values('x'))[:2] == ['2', '1']
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_qix_reorder_duplicates_raise(sdt):
|
|
346
|
+
# duplicates are rejected only under reorder=True
|
|
347
|
+
with pytest.raises(ValueError):
|
|
348
|
+
sdt.qix(y=['3', '3'], reorder=True)
|
|
349
|
+
with pytest.raises(ValueError):
|
|
350
|
+
sdt.qix(w=['a', 'a'], reorder=True)
|
|
351
|
+
with pytest.raises(ValueError):
|
|
352
|
+
sdt.qix('a', w='a', reorder=True)
|
|
353
|
+
# without reorder the same duplicates are tolerated (no raise)
|
|
354
|
+
assert sdt.qix(y=['3', '3']) is not None
|
|
355
|
+
|
|
356
|
+
|
|
329
357
|
def test_freeze(multi_label_data_table):
|
|
330
358
|
dt = multi_label_data_table
|
|
331
359
|
initial_id = id(dt)
|
|
@@ -618,6 +646,8 @@ if __name__ == '__main__':
|
|
|
618
646
|
test_qix_basic(sdt)
|
|
619
647
|
test_qix_specified(sdt)
|
|
620
648
|
test_qix_anonymous(sdt)
|
|
649
|
+
test_qix_reorder(sdt)
|
|
650
|
+
test_qix_reorder_duplicates_raise(sdt)
|
|
621
651
|
test_freeze()
|
|
622
652
|
test_kron()
|
|
623
653
|
test_outer()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|