ialdev-dataman 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.
@@ -0,0 +1,608 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Union, Callable, Iterable, Literal, Any, Collection, get_args
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ from iad.core import as_list, pdtools as pdt
11
+ from iad.core.fnctools import O, Namespace
12
+ from iad.core.label import Labels
13
+ from iad.core.wrap import name_tuple
14
+
15
+ EMPTY_CELL = (None, pd.NA, np.nan)
16
+
17
+
18
+ def transforms() -> dict[str, Callable]:
19
+ """Return namespace of transforms defined in transform module."""
20
+
21
+ from . import transforms
22
+ _ns = vars(transforms)
23
+ return {name: _ns[name] for name in transforms.__all__}
24
+
25
+
26
+ def applicable(fnc, *pos_names, **kws_map):
27
+ """
28
+ Given a function creates its wrap usable in pd.DataFrame.apply
29
+ Some of its arguments to be associated to the pd.DataFrame columns,
30
+ the rest must be either default or passed as `**kwargs` in `apply`.
31
+
32
+ Example:
33
+ >>> def f(x, y=0, *, z, w=None): pass
34
+ >>> af = applicable(f, 'data', z='weight') # data as first arg
35
+ >>> df = pd.DataFrame(dict(data=[1,2,3], weight=[0,1,2]))
36
+ >>> df['new'] = df.apply(af, axis=1, y=2, w=100)
37
+
38
+ :param fnc: function to be wrapped into the new interface
39
+ :param pos_names: column names of the positinal parameters
40
+ :param kws_map: mappings between the kw arguments and columns names
41
+ :return: function used for apply
42
+ """
43
+ assert pos_names or kws_map, "At least one argument is required"
44
+
45
+ def apply_func(dct, **kwargs):
46
+ if hasattr(dct, '__getitem__'):
47
+ args = (dct[p] for p in pos_names)
48
+ kws = {par: dct[field] for par, field in kws_map.items()}
49
+ return fnc(*args, **kws, **kwargs)
50
+ else:
51
+ return fnc(dct, **kwargs)
52
+
53
+ return apply_func
54
+
55
+
56
+ def apply_to(df: pd.DataFrame, func: Callable, out, *args, **kwargs):
57
+ """Create new dataframe with Apply func to the data-frame into `out` column.
58
+
59
+ :param df: the data-frame
60
+ :param func: function to be passed to `df.apply`
61
+ :param out: column where the result is placed
62
+ :param args: arguments passed to `func`
63
+ :param kwargs: keyword arguments passed to `func`
64
+ :return The modified input `df`
65
+ """
66
+ import warnings
67
+ with warnings.catch_warnings():
68
+ warnings.filterwarnings('error')
69
+ ndf = df.copy()
70
+ ndf[out] = df.apply(func, *args, axis=1, **kwargs)
71
+ return df
72
+
73
+
74
+ @dataclass
75
+ class Col:
76
+ """Standard columns names"""
77
+ path: str = 'path'
78
+ data: str = 'data'
79
+ ext: str = 'ext'
80
+ desc: str = 'desc'
81
+ description: str = 'description'
82
+ transforms: str = 'transforms'
83
+ transformed: str = 'transformed'
84
+ align_trans: str = 'align_trans'
85
+ read_trans: str = 'read_trans'
86
+
87
+
88
+ def apply_seq(df: pd.DataFrame, *trans, inp=Col.data, out=Col.data):
89
+ """Apply sequence of transformations to the `df` columns
90
+ :param df:
91
+ :param trans: sequence of transformation stage instructions:
92
+ `(fnc, inp, out), (fnc, inp), (fnc, )` or `fnc`
93
+ :param inp: default inp column for the first stage. Others if their
94
+ `inp` is not specified use `out` from the previous stage.
95
+ :param out: default output column of any stage with unspecified `out`
96
+
97
+ Example:
98
+ >>> apply(df, [f1, f2, f3], inp=x, out=y)
99
+ means: `df[x] -> f1 -> df[y] -> f2 -> df[y] ->f3 -> df[y]`
100
+ >>> apply(df, [(f1, y1, x), (f2, y2), f3], out=y)
101
+ means: `df[x] -> f1 -> df[y1] -> f2 -> df[y2] -> f3 -> df[y]`,
102
+ So that in this case all the intermediate results are kept in y1, y2
103
+ """
104
+ for t in trans:
105
+ fnc, _out, _inp = (*t, *(out, inp)[:3 - len(t)]) \
106
+ if isinstance(t, (list, tuple)) else (t, out, inp)
107
+ inp = _out # default inp for next stage is previous out
108
+ if not fnc:
109
+ continue
110
+ df[_out] = df[_inp].apply(applicable(fnc, _inp), axis=1)
111
+
112
+
113
+ def apply_transforms(df: pd.DataFrame, *, inp=Col.path, out=Col.data,
114
+ trans=Col.transforms, ns=None,
115
+ keep: Union[str, Iterable] = ()) -> pd.DataFrame:
116
+ """
117
+ Apply sequence of data initialization transforms on the df.
118
+ Full sequence is:
119
+ [inp] -> read -> [out] -> {transforms} -> [out]
120
+
121
+ Stage `read` is applied only if defined (default is `imread`)
122
+
123
+ :param df: columns will be processed in this data frame
124
+ :param inp: name of first stage input column (if read then path)
125
+ :param out: name of output column
126
+ :param keep: `trans`, `inp` are dropped unless any is listed here
127
+ :param trans: name of column with transforms
128
+ :param ns: module or dict-like container with transform functions
129
+ :return:
130
+ """
131
+ if ns is None:
132
+ ns = transforms()
133
+
134
+ trans = as_list(trans)
135
+
136
+ def row_transform(row: pd.Series):
137
+ return O.comp(*map(row.get, trans), ns=ns, skip=EMPTY_CELL)(row.get(inp))
138
+
139
+ drop = {*trans, inp}.difference({out}.union(as_list(keep)))
140
+ df = df.copy() # if df is a view, then df[out] = assigns to cached copy
141
+
142
+ if set(trans).issubset(df.columns):
143
+ df[out] = df.apply(row_transform, axis=1)
144
+ df.drop(drop, axis=1, inplace=True, errors='ignore')
145
+ return df
146
+
147
+
148
+ def apply_column_transform(df: pd.Series | pd.DataFrame, *,
149
+ inp: str | Collection[str] = Col.path,
150
+ out: str = Col.data,
151
+ trans: str = Col.read_trans,
152
+ recover: bool = False,
153
+ args: Literal["series", "pos", "kws"] = 'pos',
154
+ fallback: Any = pd.NA,
155
+ drop: str | Collection[str] | bool = True,
156
+ inplace=False,
157
+ **kwargs) -> pd.DataFrame:
158
+ """
159
+ Apply (by row) transforms stored in ``trans`` column on the ``inp`` column.
160
+
161
+ Default configuration is set for `read_trans` transformation to load data.
162
+
163
+ **See also**:
164
+ ``compile_read_transforms`` to create ``read_trans`` with functions.
165
+
166
+ ``apply_transforms`` as more general version of this function
167
+
168
+ By default, drop (True), all `used` columns (``trans`` and ``inp``).
169
+ Otherwise, specific columns to drop may be also specified, or None to keep.
170
+
171
+ Transform may receive arguments in three different forms:
172
+ - `pos` - as positional arguments ordered as in `inp` collection. `trans(*pos)`.
173
+ - `series` - row series as produced by the pandas `apply`. `trans(series)`
174
+ - `kws` - as keyword arguments with columns as keys: `trans(**kws)`
175
+
176
+ Transform failed on specific rows can be recovered by re-applying on the item
177
+ provided "fallback" function, or just producing given "error" value.
178
+
179
+ :param df: group (Series or Frame) usually produced
180
+ :param inp: name of the input column,
181
+ :param trans: column with transform functions objects
182
+ :param out: output column name
183
+ :param args: from in which arguments are passed into the `trans`
184
+ :param recover: if True - recover per row from failed transform using `fallback`
185
+ :param fallback: alternative callable or fallback value used to recover
186
+ :param drop: True to drop used columns, or specific columns to drop
187
+ :param kwargs: passed to `trans` function as additional kw args
188
+ :param inplace: update given ``DataFrame``, and return it
189
+ :return: transformed table
190
+ """
191
+ # first make sure we work with frame
192
+ if isinstance(df, pd.Series):
193
+ if inplace:
194
+ raise TypeError("Can't inplace transform Series!")
195
+ df_out = df.to_frame()
196
+ elif not inplace:
197
+ df_out = df.copy()
198
+ else:
199
+ df_out = df
200
+
201
+ inputs = as_list(inp) or df_out.columns
202
+
203
+ if args == 'pos':
204
+ func = lambda g: g[trans](*(g[attr] for attr in inputs), **kwargs)
205
+ elif args == 'kws':
206
+ func = lambda g: g[trans](**{attr: g[attr] for attr in inputs}, **kwargs)
207
+ elif args == 'series':
208
+ func = lambda g: g[trans](g if inp is None else g[inputs], **kwargs)
209
+ else:
210
+ raise ValueError(f'Unknown {args=}')
211
+
212
+ if recover: # wrap func into (try - except) to ensure row-based resilience
213
+ if not isinstance(fallback, Callable):
214
+ error_value = fallback
215
+ fallback = lambda _: error_value
216
+ _func = func
217
+
218
+ def func(g):
219
+ try:
220
+ return _func(g)
221
+ except:
222
+ return fallback(g)
223
+
224
+ df_out[out] = df_out.apply(func, axis=1)
225
+
226
+ if drop is True:
227
+ drop = set(inputs) - {out} | {trans}
228
+ if drop:
229
+ if not inplace:
230
+ df.drop(drop, axis=1, inplace=True) # drop used columns in the original df, if returning new df
231
+ return df_out.drop(drop, axis=1)
232
+ df_out.drop(drop, axis=1, inplace=True) # drop used columns in the inplace df
233
+ return df_out
234
+
235
+
236
+ def row_func_prepend_transform(fnc: Callable, trans=Col.read_trans, inp=Col.path, out=Col.data):
237
+ """Prepend transform to the given row function
238
+
239
+ :param fnc: function to wrap with pre-transform
240
+ :param trans: name of the column with transform
241
+ :param inp: name of the columns used as input to the transform
242
+ :param out: name of the column to write transform results
243
+ """
244
+ if not trans:
245
+ return fnc
246
+
247
+ def trans_fnc(row: pd.Series, *args, **kws):
248
+ row[out] = row[trans](row[inp])
249
+ return fnc(row, *args, **kws)
250
+
251
+ return trans_fnc
252
+
253
+
254
+ def _encode_transforms(series: pd.Series, invert=False) -> tuple[list[tuple[str]], dict[tuple[str], dict]]:
255
+ """
256
+ Encode series with transforms (dict of dict or nan) in the cells into a two components data structure:
257
+ - series-size array of tuples with names of transforms, and
258
+ - map from such a tuple into the original transforms of the corresponding cell, or their inverse
259
+
260
+ More specifically:
261
+ - every cell of the series may contain dict of transforms ``C = {tr_name: tr_dict}``
262
+ - aggregated transform merges of all the cells dicts into one ``A``
263
+ - inverse cell transform ``IC`` is all the transforms in ``A`` except of those in ``C``: ``IC = A - C``
264
+
265
+ **ASSUMPTION**
266
+
267
+ Inversion implementation assumes that transforms are uniquely identified by their ``tr_name``
268
+
269
+ Return array of such `code names` and a map from name to the actual transforms, which
270
+ contains reference to cells with no transforms in the entry: ``"EMPTY": {}``.
271
+
272
+ :param series: a Series with transforms as dict of dict (or NONE)
273
+ :param invert: if TRue - request to construct map with inverse transforms
274
+ :return: array of code names per original cell, and map from the names to associated transforms
275
+ """
276
+ series = series.reset_index(drop=True) # same index is used by the all_names np.array!
277
+ defined = series[~series.isna()] # work with cells actually containing transforms
278
+
279
+ if not all(filter(lambda x: isinstance(x, dict), defined)):
280
+ raise TypeError("Transforms must be dictionaries")
281
+ empty = 'EMPTY'
282
+ all_names = np.full(series.size, empty, dtype=object) # results initialized by empty names
283
+ cells_map = {empty: {}} # cell hash:tuple(names) -> transforms:dict
284
+ trans_map = {} # trans hash:str -> item:(func, {par}) - needed for INVERT case
285
+
286
+ for idx, cell_trans in defined.items():
287
+ items = [*cell_trans.items()] # list transform items in this cell
288
+ cell_names = tuple(map(str, items)) # tuple of items str (hashable!) in this cell
289
+ if cell_names not in cells_map: # new type of cell is found
290
+ cells_map[cell_names] = cell_trans # update mappings of cells and
291
+ invert and trans_map.update(zip(cell_names, items))
292
+ all_names[idx] = cell_names
293
+
294
+ if invert: # notice, cells names remain unconverted but still unique!
295
+ all_trans_names = set(trans_map) # crashes all transforms names together, ignores arguments!
296
+ cells_map = {cell: dict(map(trans_map.get, all_trans_names.difference(cell)))
297
+ for cell in cells_map}
298
+ return name_tuple(names=all_names, map=cells_map)
299
+
300
+
301
+ def compile_read_transforms(df: pd.DataFrame, *, col=Col, ns=None):
302
+ """
303
+ From collection (usually a transformed column pd.Series) with dicts
304
+ describing transforms which have been applied on the data (in this row)
305
+ build a collection of transforms required to align the corresponding data
306
+ to same total transformation for entire collection.
307
+
308
+ Namely, in set terms:
309
+ ``t_i -> T - t_i``, (where ``T = sum[ t_i ]``)
310
+
311
+ Assumes `commutability` of all the transformations!
312
+
313
+ If namespace with functions is provided, use it to substitute
314
+ dicts describing transforms to Callables from this namespace.
315
+ Otherwise, return ``np.ndarray`` with dicts or nans
316
+
317
+ :param df:
318
+ :param col: has attributes: transforms, transformed with names of those columns
319
+ :param ns: namespace with function to substitute from
320
+ :return: align_trans
321
+ """
322
+
323
+ ns = ns and Namespace(ns) or {}
324
+ reader = (read_by_format, 'read')
325
+
326
+ # Prepare only transforms available in the columns
327
+ has_tm, has_td = map(df.columns.__contains__, [col.transforms, col.transformed])
328
+ if not (has_tm or has_td): # NO transforms columns - just use reader for all the cells
329
+ return np.full(len(df), O.comp(reader, ns=ns))
330
+
331
+ coded = dict( # hash only existing columns
332
+ td=has_td and _encode_transforms(df[col.transformed], invert=True),
333
+ tm=has_tm and _encode_transforms(df[col.transforms])
334
+ )
335
+
336
+ if has_td and has_tm: # BOTH transform types are defined
337
+ iterator = zip(coded['td'].names, coded['tm'].names) # iter over codes: (td-name, tm-name)
338
+ td_map, tm_map = coded['td'].map, coded['tm'].map
339
+ trans = lambda k: td_map[k[0]] | tm_map[k[1]] # combine from transforms and inversed transformed
340
+ else: # ONLY ONE of transform columns is defined
341
+ iterator, trans_map = coded[has_td and 'td' or 'tm'] # iter over codes: t[d|m]-name
342
+ trans = lambda k: trans_map[k] # function return transform given code
343
+
344
+ def trans_gen():
345
+ """ Generator of transforms for all the rows."""
346
+ cache = {} # Cache previously seen transforms to avoid expensive creations
347
+ for key in iterator:
348
+ if (t := cache.get(key, None)) is None:
349
+ t = O.comp(trans(key), reader, ns=ns)
350
+ cache[key] = t
351
+ yield t
352
+
353
+ return np.array([*trans_gen()])
354
+
355
+
356
+ def is_data(item):
357
+ """Check if item could be any data object:
358
+
359
+ `isinstance(item, Iterable) or not pd.isna(item)`
360
+ """
361
+ return isinstance(item, Iterable) or not pd.isna(item)
362
+
363
+
364
+ # noinspection PyTypeChecker
365
+ def cond_read_trans(s: pd.Series, check_data: bool | Callable = is_data, check_file=True):
366
+ """
367
+ Conditional variant of ``read_trans`` function `applicable` to DataFrame with
368
+ columns: `Col.data`, `Col.path`, `Col.read_trans`.
369
+
370
+ Calls the original `s[Col.read_trans]` if two conditions are satisfied:
371
+ - `check_data(s[Col.data]) is False` AND
372
+ - `check_file` is False OR `s[Col.path].is_file`
373
+
374
+ :param s: pd.Series
375
+ :param check_data:
376
+ :param check_file: return ``pd.NA`` instead raising on not existing files
377
+ :return:
378
+ """
379
+ if check_data and 'data' in s:
380
+ if (is_data if check_data is True else check_data)(data := s.data):
381
+ return data # return existing data
382
+
383
+ if path := s[Col.path]:
384
+ if check_file and not Path(path).is_file():
385
+ return pd.NA # file not found
386
+ read_trans: Callable = s[Col.read_trans]
387
+ return read_trans(path) # read file
388
+
389
+ return pd.NA # path is not defined
390
+
391
+
392
+ def read_by_format(path):
393
+ """
394
+ Read data from file by determining its format from its name or labels.
395
+ :param path:
396
+ :return: data from the file if format is found or None
397
+ """
398
+ from iad.io.imread import imread, SUPPORT_READ
399
+ from iad.io.inu.utils.imread import imread_stereo
400
+ path = Path(path)
401
+ ext = path.suffix.lower()
402
+ if "StereoImage__Channel_" in path.name and ext == '.tif': # combined L and R from NU40
403
+ return imread_stereo(path, cam_info=True) # raise exception if there is no cam_info
404
+ elif ext in SUPPORT_READ:
405
+ return imread(path)
406
+
407
+ if path.name.lower() == 'calib.txt':
408
+ from iad.io.special import middlebury_calib
409
+ return middlebury_calib(path)
410
+ raise NotImplementedError(f"File's {str(path)} {ext=} is not among supported ({SUPPORT_READ=})")
411
+
412
+
413
+ # TODO: remove all the references, including docs
414
+ # def align_transformed(df, *, inp=Col.data, out=None,
415
+ # align_trans=Col.align_trans, transformed=Col.transformed,
416
+ # keep=(), lib=None) -> pd.DataFrame:
417
+ # """
418
+ # Align all the data items in the column to ensure that same
419
+ # transforms are applied to all of them.
420
+ #
421
+ # Note: transforms must commute (applied in arbitrary order)
422
+ #
423
+ # :param df: a pd.DataFrame containing the data column to align
424
+ # :param inp: name of the column to align
425
+ # :param out: name of the columns with aligned data (default=`inp`)
426
+ # :param keep: temporal columns to keep, otherwise `transformed`, 'align_trans', `inp`
427
+ # :param align_trans: column name with alignment transforms
428
+ # :param transformed: column describing transforms been applied
429
+ # to the `inp` by now
430
+ # :param lib: dictionary of transforms functions, by default from transforms()
431
+ # :return: pd.DataFrame with aligned column
432
+ # """
433
+ # from functools import reduce
434
+ # from box import Box # to make dicts hashable for sets
435
+ # out = out or inp
436
+ # keep = as_list(keep)
437
+ # if transformed in df:
438
+ # tran_set = lambda t: {*Box(t, frozen_box=True).items()} \
439
+ # if isinstance(t, dict) else set() # as set of tuples {(func, kwargs), }
440
+ # combined = reduce(set.union, map(tran_set, df[transformed])) # all transforms
441
+ #
442
+ # dif = lambda t: dict(combined.difference(tran_set(t))) # create column with
443
+ # df[align_trans] = df[transformed].apply(dif) # missing transforms
444
+ #
445
+ # df = apply_transforms(df, trans=align_trans, read=None, keep=keep,
446
+ # inp=inp, out=out, ns=lib or transforms()) # apply missing transforms
447
+ # if transformed not in keep:
448
+ # df.drop(transformed, axis=1, inplace=True) # drop temporary column
449
+ # else:
450
+ # if out != inp: # trivial inp -> out transform
451
+ # df[out] = df[inp]
452
+ # if inp not in keep:
453
+ # df.drop(inp, axis=1, inplace=True)
454
+ # return df
455
+ #
456
+ #
457
+ # def grp_code_regions(g: pd.DataFrame, codes='rgn_codes', data='data', out='series') -> pd.Series:
458
+ # """Transform group by splitting `data` column coded image into Regions
459
+ # object according to mapping in `code` column (usually 'rgn_code').
460
+ # If such column is not found - return the input group
461
+ #
462
+ # :param g: group with 'data' and codes columns
463
+ # :param codes: name of the codes mapping column (dict content)
464
+ # :param data: name of the data column (usually 'data')
465
+ # :param out: 'series' | 'frame' | None
466
+ # :return: pd.DataFrame with Regions in decoded rows of data column
467
+ # and with `codes` column dropped
468
+ # """
469
+ # if 'rgn_codes' in g.columns:
470
+ # from iad.core.image import Regions
471
+ # data_cols = ['data', codes]
472
+ # is_rgn = g.rgn_codes.notna()
473
+ # g.loc[is_rgn, data] = g.loc[is_rgn, data_cols].apply(lambda p: Regions(*p), axis=1)
474
+ # g = g.drop(codes, 1)
475
+ # return g[data] if out == 'series' else g
476
+
477
+ _FETCH_RES = Literal['data', 'all', 'essen']
478
+
479
+
480
+ class Fetchable:
481
+ # ToDo: @Ilya - Create Task: g.qix().fetch(), g.select().fetch() keep data in g!
482
+
483
+ def fetch(self: pdt.DTable, *, check_file=False,
484
+ out: _FETCH_RES = 'data',
485
+ reuse: bool | Callable = True,
486
+ inplace=True) -> CollectTable | CollectSeries:
487
+ """
488
+ Return table with data fetched according to the read transform of every item.
489
+
490
+ Resulted table may be a data-only `series` or `frame` with columns depending
491
+ on the ``out`` argument:
492
+ - 'data' - `series` with only data,
493
+ - 'all' - `frame` with all the original + data,
494
+ - 'essen' - `frame` with essential columns: `all` except those used by transform.
495
+ (`frame` will be produced also in special case when source is series with only `data`)
496
+
497
+ If source already has valid data, data is not reloaded if ``reuse is True``.
498
+
499
+ The default condition of *validity* (``datacast.transtools.is_data``)
500
+ can be replaced by providing alternative function: ``reuse=new_data_cond``.
501
+
502
+ If not ``inplace``, fetch returns a new table without changing the source,
503
+ so that subsequent fetching from same source requires loading data again.
504
+
505
+ Using ``inplace=True`` allows to avoid reloading in certain scenarios.
506
+
507
+ In this case data column is added to the source table.
508
+ Then, only if requested format of the output table is different, a new table is produced.
509
+
510
+ It modifies self by loading into 'data' column, AND ALSO returns the data in the requested form.
511
+
512
+ Therefore, in this example only first ``fetch()`` will require loading:
513
+
514
+ >>> g2 = g1.fetch() # loading
515
+ >>> g3 = g2.qix(**labels).fetch() # no loading
516
+ >>> g1.loc[index].fetch() # no loading
517
+ ```
518
+ However, that does not work if used AFTER querying:
519
+
520
+ >>> g2 = g1.qix(**labels).fetch() # loading
521
+ >>> g2.fetch() # no loading
522
+ >>> g1.loc[index].fetch() # loading again
523
+
524
+ :param self: table
525
+ :param out: columns to return,
526
+ :param check_file: if file in `Col.path` not exists don't raise just return ``pd.NA``
527
+ :param reuse: don't fetch data if already available in `Col.data`
528
+ :param inplace: update self ``DataFrame`` instead of fetching into a copy.
529
+ :return: with selected rows and loaded data as values or a data object
530
+ """
531
+ assert out in get_args(_FETCH_RES)
532
+ trans_labels = [Col.path, Col.read_trans]
533
+
534
+ if self.empty:
535
+ self['data'] = None
536
+ return self
537
+
538
+ if not isinstance(self, pd.DataFrame): # Series, must be already with data
539
+ if reuse and self.name == 'data':
540
+ return self if out == 'data' else pdt.as_table(self, series=False)
541
+ raise IndexError(f"Fetch requires {trans_labels} or 'data' columns!")
542
+
543
+ if not set(trans_labels).issubset(self.columns):
544
+ if reuse and 'data' in self.columns:
545
+ return self.data if out == 'data' else self
546
+ raise IndexError(f"Fetch requires {trans_labels} or 'data' columns")
547
+
548
+ # Consider: use apply_column_transform for cases above to verify data?
549
+
550
+ if reuse or check_file: # change to conditional read if required
551
+ kws = {'check_file': check_file} | (reuse and {'check_data': reuse} or {})
552
+ trans = '__cond_trans__'
553
+ self[trans] = O(cond_read_trans, **kws)
554
+ trans_args = dict(drop=trans, args='series', inp=None) # series with all columns
555
+ else:
556
+ trans = Col.read_trans
557
+ trans_args = dict(drop=False)
558
+
559
+ res = apply_column_transform(self, out=Col.data, trans=trans, inplace=inplace, **trans_args)
560
+ match out:
561
+ case 'data': return res.data
562
+ case 'essen': return res.drop(trans_labels)
563
+ case 'all': return res
564
+
565
+
566
+ class CollectSeries(pdt.DataSeries, Fetchable):
567
+ _metadata = pdt.DataSeries._metadata + ["collection"]
568
+
569
+ @property
570
+ def _constructor(self):
571
+ return CollectSeries
572
+
573
+ @property
574
+ def _constructor_expanddim(self):
575
+ return CollectTable
576
+
577
+
578
+ class CollectTable(pdt.DataTable, Fetchable):
579
+ _metadata = pdt.DataTable._metadata + ['collection']
580
+
581
+ @property
582
+ def _constructor(self):
583
+ return CollectTable
584
+
585
+ @property
586
+ def _constructor_sliced(self):
587
+ return CollectSeries
588
+
589
+
590
+ class DataItem(Labels):
591
+
592
+ def fetch(self, data='data', trans='read_trans', inp='path',
593
+ reuse=True, check_file=True,
594
+ drop: str | Collection[str] | bool = True):
595
+ if missing := {trans, inp}.difference(self.keys()):
596
+ raise KeyError(f"Fetch requires keys which are {missing=}")
597
+
598
+ if reuse or check_file:
599
+ from .transtools import cond_read_trans
600
+ trans = cond_read_trans()
601
+
602
+ res = self.copy()
603
+ if not (reuse or data in res):
604
+ res[data] = res[trans](res[inp])
605
+ if drop is True:
606
+ drop = [trans, inp]
607
+ if drop:
608
+ res.drop(drop)
iad/dataman/env.py ADDED
@@ -0,0 +1,10 @@
1
+ from iad.core.env import EnvLoc as CoreEnvLoc, Locator, env_log
2
+
3
+
4
+ class EnvLoc(CoreEnvLoc):
5
+ """Environment locators for dataman package."""
6
+ DATA = Locator(envar='ALG_DATA', alarm=env_log.error)
7
+ DATASETS = Locator(envar='ALG_DATASETS', alarm=env_log.error)
8
+ RESOURCES = Locator(envar='ALG_RESOURCES', alarm=env_log.error)
9
+
10
+ EnvLoc.reset(validate=False)