frametree 0.11.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 (58) hide show
  1. frametree/common/__init__.py +2 -0
  2. frametree/common/dirtree.py +447 -0
  3. frametree/common/space.py +42 -0
  4. frametree/core/__init__.py +6 -0
  5. frametree/core/_version.py +16 -0
  6. frametree/core/cell.py +76 -0
  7. frametree/core/cli/__init__.py +6 -0
  8. frametree/core/cli/apply.py +187 -0
  9. frametree/core/cli/base.py +9 -0
  10. frametree/core/cli/dataset.py +446 -0
  11. frametree/core/cli/derive.py +130 -0
  12. frametree/core/cli/ext.py +13 -0
  13. frametree/core/cli/store.py +160 -0
  14. frametree/core/cli/tests/test_cli_apply.py +47 -0
  15. frametree/core/cli/tests/test_cli_dataset.py +184 -0
  16. frametree/core/cli/tests/test_cli_derive.py +53 -0
  17. frametree/core/cli/tests/test_cli_ext.py +9 -0
  18. frametree/core/cli/tests/test_cli_install_licenses.py +93 -0
  19. frametree/core/cli/tests/test_cli_store.py +122 -0
  20. frametree/core/column.py +437 -0
  21. frametree/core/entry.py +161 -0
  22. frametree/core/exceptions.py +232 -0
  23. frametree/core/licence.py +62 -0
  24. frametree/core/packaging.py +183 -0
  25. frametree/core/pipeline.py +675 -0
  26. frametree/core/quality.py +29 -0
  27. frametree/core/row.py +260 -0
  28. frametree/core/salience.py +142 -0
  29. frametree/core/serialize.py +606 -0
  30. frametree/core/set/__init__.py +2 -0
  31. frametree/core/set/base.py +855 -0
  32. frametree/core/set/metadata.py +154 -0
  33. frametree/core/space.py +190 -0
  34. frametree/core/store/__init__.py +3 -0
  35. frametree/core/store/base.py +942 -0
  36. frametree/core/store/local.py +386 -0
  37. frametree/core/store/remote.py +590 -0
  38. frametree/core/store/tests/test_id_handling.py +104 -0
  39. frametree/core/store/tests/test_store.py +178 -0
  40. frametree/core/tests/test_clinical_space.py +75 -0
  41. frametree/core/tests/test_data_column.py +28 -0
  42. frametree/core/tests/test_dataset.py +51 -0
  43. frametree/core/tests/test_tree.py +282 -0
  44. frametree/core/tests/test_utils.py +26 -0
  45. frametree/core/tree.py +291 -0
  46. frametree/core/utils.py +626 -0
  47. frametree/testing/__init__.py +2 -0
  48. frametree/testing/blueprint.py +673 -0
  49. frametree/testing/space.py +31 -0
  50. frametree/testing/store.py +404 -0
  51. frametree/testing/tasks.py +284 -0
  52. frametree/testing/tests/test_blueprint.py +76 -0
  53. frametree-0.11.0.dist-info/METADATA +227 -0
  54. frametree-0.11.0.dist-info/RECORD +58 -0
  55. frametree-0.11.0.dist-info/WHEEL +4 -0
  56. frametree-0.11.0.dist-info/entry_points.txt +2 -0
  57. frametree-0.11.0.dist-info/licenses/AUTHORS +2 -0
  58. frametree-0.11.0.dist-info/licenses/LICENSE +110 -0
@@ -0,0 +1,2 @@
1
+ from .dirtree import DirTree
2
+ from .space import Clinical, Samples
@@ -0,0 +1,447 @@
1
+ from __future__ import annotations
2
+ import os
3
+ from pathlib import Path
4
+ import re
5
+ import typing as ty
6
+ import logging
7
+ import json
8
+ import attrs
9
+ from fileformats.core import FileSet, Field
10
+ from frametree.core.exceptions import FrameTreeUsageError
11
+ from frametree.core.set.base import DataTree
12
+ from frametree.core.row import DataRow
13
+ from frametree.core.entry import DataEntry
14
+ from frametree.core.store import LocalStore
15
+ from frametree.core.utils import full_path
16
+
17
+
18
+ logger = logging.getLogger("frametree")
19
+
20
+
21
+ # Matches directory names used for summary rows with dunder beginning and
22
+ # end (e.g. '__visit_01__') and hidden directories (i.e. starting with '.' or
23
+ # '~')
24
+ special_dir_re = re.compile(r"(__.*__$|\..*|~.*)")
25
+
26
+
27
+ @attrs.define
28
+ class DirTree(LocalStore):
29
+ """
30
+ A Repository class for data stored hierarchically within sub-directories
31
+ of a file-system directory. The depth and which layer in the data tree
32
+ the sub-directories correspond to is defined by the `hierarchy` argument.
33
+
34
+ Parameters
35
+ ----------
36
+ base_dir : str
37
+ Path to the base directory of the "store", i.e. datasets are
38
+ arranged by name as sub-directories of the base dir.
39
+
40
+ """
41
+
42
+ PROV_SUFFIX = ".provenance"
43
+ FIELDS_FNAME = "__fields__"
44
+ FIELDS_PROV_FNAME = "__fields_provenance__"
45
+
46
+ # Note this name will be constant, as there is only ever one store,
47
+ # which covers whole FS
48
+ name: str = "dirtree"
49
+
50
+ #################################
51
+ # Abstract-method implementations
52
+ #################################
53
+
54
+ def populate_tree(self, tree: DataTree):
55
+ """
56
+ Scans the data present in the dataset and populates the data tree with nodes
57
+
58
+ Parameters
59
+ ----------
60
+ dataset : Dataset
61
+ The dataset to construct the tree dimensions for
62
+ """
63
+ if not os.path.exists(tree.dataset_id):
64
+ raise FrameTreeUsageError(
65
+ f"Could not find a directory at '{tree.dataset_id}' to be the "
66
+ "root row of the dataset"
67
+ )
68
+ dpaths = sorted(d for d, _, _ in os.walk(tree.dataset_id))
69
+ for dpath in dpaths:
70
+ tree_path = tuple(Path(dpath).relative_to(tree.dataset_id).parts)
71
+ if len(tree_path) != len(tree.hierarchy):
72
+ continue
73
+ if self.ARCANA_DIR in tree_path:
74
+ continue
75
+ tree.add_leaf(tree_path)
76
+
77
+ def populate_row(self, row: DataRow):
78
+ """Scans the node in the data tree corresponding to the data row and populates
79
+ the row with data entries found in the tree node
80
+
81
+ Parameters
82
+ ----------
83
+ row : DataRow
84
+ the data row to populate
85
+ """
86
+
87
+ def filter_entry_dir(entry_dir):
88
+ for subpath in entry_dir.iterdir():
89
+ entry_name = subpath.name
90
+ if (
91
+ not entry_name.startswith(".")
92
+ and entry_name != self.ARCANA_DIR
93
+ and entry_name
94
+ not in (
95
+ self.FIELDS_FNAME,
96
+ self.FIELDS_PROV_FNAME,
97
+ self.FIELDS_FNAME + self.LOCK_SUFFIX,
98
+ )
99
+ and not entry_name.endswith(self.PROV_SUFFIX)
100
+ ):
101
+ yield subpath
102
+
103
+ root_dir = full_path(row.dataset.id)
104
+
105
+ # Iterate through all directories saved for the source and dataset derivatives
106
+ for dataset_name in self._row_dataset_names(row):
107
+ row_dir = root_dir / self._row_relpath(row, dataset_name=dataset_name)
108
+ if row_dir.exists():
109
+ # Filter contents of directory to omit fields JSON and provenance and
110
+ # add file-set entries
111
+ for entry_fspath in filter_entry_dir(row_dir):
112
+ path = str(entry_fspath.relative_to(row_dir))
113
+ if dataset_name is not None:
114
+ path += "@" + dataset_name
115
+ row.add_entry(
116
+ path=path,
117
+ datatype=FileSet,
118
+ uri=str(entry_fspath.relative_to(root_dir)),
119
+ )
120
+ # Add field entries
121
+ fields_json = row_dir / self.FIELDS_FNAME
122
+ try:
123
+ with open(fields_json) as f:
124
+ fields_dict = json.load(f)
125
+ except FileNotFoundError:
126
+ pass
127
+ else:
128
+ for name in fields_dict:
129
+ path = (
130
+ f"{name}@{dataset_name}"
131
+ if dataset_name is not None
132
+ else name
133
+ )
134
+ row.add_entry(
135
+ path=path,
136
+ datatype=Field,
137
+ uri=str(fields_json.relative_to(root_dir)) + "::" + name,
138
+ )
139
+
140
+ def get_field(self, entry: DataEntry, datatype: type) -> Field:
141
+ """Retrieve the field associated with the given entry and return it cast
142
+ to the specified datatype
143
+
144
+ Parameters
145
+ ----------
146
+ entry : DataEntry
147
+ the entry to retrieve the field for
148
+ datatype : type (subclass DataType)
149
+ the datatype to return the field as
150
+
151
+ Returns
152
+ -------
153
+ Field
154
+ the retrieved field
155
+ """
156
+ fspath, key = self._fields_fspath_and_key(entry)
157
+ return datatype(self.read_from_json(fspath, key))
158
+
159
+ def get_fileset(self, entry: DataEntry, datatype: type) -> FileSet:
160
+ """Retrieve the file-set associated with the given entry and return it cast
161
+ to the specified datatype
162
+
163
+ Parameters
164
+ ----------
165
+ entry : DataEntry
166
+ the entry to retrieve the file-set for
167
+ datatype : type (subclass DataType)
168
+ the datatype to return the file-set as
169
+
170
+ Returns
171
+ -------
172
+ FileSet
173
+ the retrieved file-set
174
+ """
175
+ return datatype(self._fileset_fspath(entry))
176
+
177
+ def put_fileset(self, fileset: FileSet, entry: DataEntry) -> FileSet:
178
+ """Put a file-set into the specified data entry
179
+
180
+ Parameters
181
+ ----------
182
+ fileset : FileSet
183
+ the file-set to store
184
+ entry : DataEntry
185
+ the entry to store the file-set in
186
+
187
+ Returns
188
+ -------
189
+ FileSet
190
+ the copy of the file-set that has been stored within the data entry
191
+ """
192
+ fspath = self._fileset_fspath(entry)
193
+ _, new_stem, new_ext = FileSet.decompose_fspath(entry.path.split("@")[0])
194
+ if new_ext:
195
+ if len(fileset.fspaths) > 1:
196
+ raise FrameTreeUsageError(
197
+ "Cannot store file-set with multiple files in dirtree store "
198
+ "when extension is specified"
199
+ )
200
+ if new_ext != FileSet.decompose_fspath(fileset.fspath)[2]:
201
+ raise FrameTreeUsageError(
202
+ "Cannot change extension of file-set when copying to dirtree store"
203
+ )
204
+ # Create target directory if it doesn't exist already
205
+ copied_fileset = fileset.copy(
206
+ dest_dir=fspath.parent,
207
+ collation=fileset.CopyCollation.adjacent,
208
+ new_stem=new_stem,
209
+ make_dirs=True,
210
+ overwrite=True,
211
+ )
212
+ return copied_fileset
213
+
214
+ def put_field(self, field: Field, entry: DataEntry):
215
+ """Put a field into the specified data entry
216
+
217
+ Parameters
218
+ ----------
219
+ field : Field
220
+ the field to store
221
+ entry : DataEntry
222
+ the entry to store the field in
223
+ """
224
+ fspath, key = self._fields_fspath_and_key(entry)
225
+ self.update_json(fspath, key, field.primitive(field))
226
+
227
+ def get_fileset_provenance(
228
+ self, entry: DataEntry
229
+ ) -> ty.Union[ty.Dict[str, ty.Any], None]:
230
+ """Retrieves provenance associated with a file-set data entry
231
+
232
+ Parameters
233
+ ----------
234
+ entry : DataEntry
235
+ the entry of the file-set to retrieve the provenance for
236
+
237
+ Returns
238
+ -------
239
+ ty.Dict[str, ty.Any] or None
240
+ the retrieved provenance or None if it doesn't exist
241
+ """
242
+ with open(self._fileset_prov_fspath(entry)) as f:
243
+ provenance = json.load(f)
244
+ return provenance
245
+
246
+ def put_fileset_provenance(
247
+ self, provenance: ty.Dict[str, ty.Any], entry: DataEntry
248
+ ):
249
+ """Puts provenance associated with a file-set data entry into the store
250
+
251
+ Parameters
252
+ ----------
253
+ provenance : dict[str, ty.Any]
254
+ the provenance to store
255
+ entry : DataEntry
256
+ the entry to associate the provenance with
257
+ """
258
+ with open(self._fileset_prov_fspath(entry), "w") as f:
259
+ json.dump(provenance, f)
260
+
261
+ def get_field_provenance(
262
+ self, entry: DataEntry
263
+ ) -> ty.Union[ty.Dict[str, ty.Any], None]:
264
+ """Retrieves provenance associated with a field data entry
265
+
266
+ Parameters
267
+ ----------
268
+ entry : DataEntry
269
+ the entry of the field to retrieve the provenance for
270
+
271
+ Returns
272
+ -------
273
+ ty.Dict[str, ty.Any] or None
274
+ the retrieved provenance or None if it doesn't exist
275
+ """
276
+ fspath, key = self._fields_prov_fspath_and_key(entry)
277
+ with open(fspath) as f:
278
+ fields_provenance = json.load(f)
279
+ return fields_provenance[key]
280
+
281
+ def put_field_provenance(self, provenance: ty.Dict[str, ty.Any], entry: DataEntry):
282
+ """Puts provenance associated with a field data entry into the store
283
+
284
+ Parameters
285
+ ----------
286
+ provenance : dict[str, ty.Any]
287
+ the provenance to store
288
+ entry : DataEntry
289
+ the entry to associate the provenance with
290
+ """
291
+ fspath, key = self._fields_prov_fspath_and_key(entry)
292
+ self.update_json(fspath, key, provenance)
293
+
294
+ def fileset_uri(self, path: str, datatype: type, row: DataRow) -> str:
295
+ """Returns the "uri" (e.g. file-system path relative to root dir) of a file-set
296
+ entry at the given path relative to the given row
297
+
298
+ Parameters
299
+ ----------
300
+ path : str
301
+ path to the entry relative to the row
302
+ datatype : type
303
+ the datatype of the entry
304
+ row : DataRow
305
+ the row of the entry
306
+
307
+ Returns
308
+ -------
309
+ uri : str
310
+ the "uri" to the file-set entry relative to the data store
311
+ """
312
+ path, dataset_name = DataEntry.split_dataset_name_from_path(path)
313
+ row_dir = self._row_relpath(row, dataset_name=dataset_name)
314
+ return str(row_dir.joinpath(*path.split("/"))) + datatype.strext
315
+
316
+ def field_uri(self, path: str, datatype: type, row: DataRow) -> str:
317
+ """Returns the "uri" (e.g. file-system path relative to root dir) of a field
318
+ entry at the given path relative to the given row
319
+
320
+ Parameters
321
+ ----------
322
+ path : str
323
+ path to the entry relative to the row
324
+ datatype : type
325
+ the datatype of the entry
326
+ row : DataRow
327
+ the row of the entry
328
+
329
+ Returns
330
+ -------
331
+ uri : str
332
+ the "uri" to the field entry relative to the data store
333
+ """
334
+ path, dataset_name = DataEntry.split_dataset_name_from_path(path)
335
+ row_dir = self._row_relpath(row, dataset_name=dataset_name)
336
+ return str(row_dir / self.FIELDS_FNAME) + "::" + path
337
+
338
+ def create_data_tree(self, id: str, leaves: ty.List[ty.Tuple[str, ...]], **kwargs):
339
+ """creates a new empty dataset within in the store. Used in test routines and
340
+ importing/exporting datasets between stores
341
+
342
+ Parameters
343
+ ----------
344
+ id : str
345
+ ID for the newly created dataset
346
+ leaves : list[tuple[str, ...]]
347
+ list of IDs for each leaf node to be added to the dataset. The IDs for each
348
+ leaf should be a tuple with an ID for each level in the tree's hierarchy, e.g.
349
+ for a hierarchy of [subject, timepoint] ->
350
+ [("SUBJ01", "TIMEPOINT01"), ("SUBJ01", "TIMEPOINT02"), ....]
351
+ hierarchy: ty.List[str]
352
+ the hierarchy of the dataset to be created
353
+ space : type(DataSpace)
354
+ the data space of the dataset
355
+ """
356
+ root_dir = Path(id)
357
+ root_dir.mkdir(parents=True)
358
+ # Create sub-directories corresponding to rows of the dataset
359
+ for ids_tuple in leaves:
360
+ root_dir.joinpath(*ids_tuple).mkdir(parents=True)
361
+
362
+ ##################
363
+ # Helper functions
364
+ ##################
365
+
366
+ def _row_relpath(self, row: DataRow, dataset_name=None):
367
+ """Get the file-system path to the dataset root for the given row, taking into
368
+ account non-leaf rows
369
+
370
+ Parameters
371
+ ----------
372
+ row : DataRow
373
+ the row to get the relative path for
374
+ derivatives : bool
375
+ whether to return the directory containing derivatives or originals
376
+
377
+ Returns
378
+ -------
379
+ relpath : Path
380
+ the relative path to the row directory
381
+ """
382
+ relpath = Path()
383
+ if row.frequency is max(row.dataset.space): # leaf node
384
+ for freq in row.dataset.hierarchy:
385
+ relpath /= row.frequency_id(freq)
386
+ if dataset_name is not None:
387
+ relpath /= self.ARCANA_DIR
388
+ if dataset_name:
389
+ relpath /= dataset_name
390
+ else:
391
+ relpath /= self.EMPTY_DATASET_NAME
392
+ else:
393
+ relpath = relpath.joinpath(
394
+ self.ARCANA_DIR,
395
+ str(row.frequency),
396
+ )
397
+ if isinstance(row.id, tuple):
398
+ relpath /= ".".join(row.id)
399
+ elif row.id:
400
+ relpath /= row.id
401
+ if dataset_name is None:
402
+ relpath /= self.ARCANA_DIR
403
+ elif not dataset_name:
404
+ relpath /= self.EMPTY_DATASET_NAME
405
+ else:
406
+ relpath /= dataset_name
407
+ return relpath
408
+
409
+ def _row_dataset_names(self, row: DataRow):
410
+ """list all dataset names stored in the given row
411
+
412
+ Parameters
413
+ ----------
414
+ row : DataRow
415
+ row to return the dataset names for
416
+
417
+ Returns
418
+ -------
419
+ dataset_names : list[str]
420
+ list of dataset names stored in the given row
421
+ """
422
+ dataset_names = [None] # The source data
423
+ derivs_dir = (
424
+ Path(row.dataset.id) / self._row_relpath(row, dataset_name="").parent
425
+ )
426
+ if derivs_dir.exists():
427
+ dataset_names.extend(
428
+ ("" if d.name == self.EMPTY_DATASET_NAME else d.name)
429
+ for d in derivs_dir.iterdir()
430
+ if d.name != self.ARCANA_DIR
431
+ )
432
+ return dataset_names
433
+
434
+ def _fileset_fspath(self, entry):
435
+ return Path(entry.row.dataset.id) / entry.uri
436
+
437
+ def _fields_fspath_and_key(self, entry):
438
+ relpath, key = entry.uri.split("::")
439
+ fspath = Path(entry.row.dataset.id) / relpath
440
+ return fspath, key
441
+
442
+ def _fileset_prov_fspath(self, entry):
443
+ return self._fileset_fspath(entry).with_suffix(self.PROV_SUFFIX)
444
+
445
+ def _fields_prov_fspath_and_key(self, entry):
446
+ fields_fspath, key = self._fields_fspath_and_key(entry)
447
+ return fields_fspath.parent / self.FIELDS_PROV_FNAME, key
@@ -0,0 +1,42 @@
1
+ from frametree.core.space import DataSpace
2
+
3
+
4
+ class Clinical(DataSpace):
5
+ """
6
+ An enum used to specify the hierarchy of data trees and the "frequencies" of items
7
+ within dataset typical of medimage research, i.e. subjects split into groups scanned
8
+ at different timepoints (in longitudinal studies).
9
+ """
10
+
11
+ # Root row of the dataset
12
+ dataset = 0b000 # singular within the dataset
13
+
14
+ # Axes of the data space
15
+ member = 0b001 # subjects relative to their group membership, i.e.
16
+ # matched pairs of test and control subjects should share
17
+ # the same member IDs.
18
+ group = 0b010 # subject groups (e.g. test & control)
19
+ timepoint = 0b100 # timepoints in longitudinal studies
20
+
21
+ # Combinations
22
+ session = 0b111 # a single session (i.e. a single timepoint of a subject)
23
+ subject = 0b011 # uniquely identified subject within in the dataset.
24
+ # As opposed to 'member', which specifies a subject in
25
+ # relation to its group (i.e. one subject for each member
26
+ # in each group). For datasets with only one study group,
27
+ # then subject and member are equivalent
28
+ batch = 0b110 # data from separate groups at separate timepoints
29
+ matchedpoint = 0b101 # matched members (e.g. test & control) across
30
+ # all groups and timepoints
31
+
32
+
33
+ class Samples(DataSpace):
34
+ """
35
+ The most basic data space within only one dimension
36
+ """
37
+
38
+ # Root row of the dataset
39
+ dataset = 0b0 # singular within the dataset
40
+
41
+ # Axes of the data space
42
+ sample = 0b1
@@ -0,0 +1,6 @@
1
+ from ._version import __version__ # noqa: F401
2
+
3
+ PACKAGE_NAME = "frametree"
4
+ CODE_URL = f"https://github.com/ArcanaFramework/{PACKAGE_NAME}"
5
+
6
+ __authors__ = [("Thomas G. Close", "tom.g.close@gmail.com")]
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.11.0'
16
+ __version_tuple__ = version_tuple = (0, 11, 0)
frametree/core/cell.py ADDED
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+ import typing as ty
3
+ import attrs
4
+ from fileformats.core import DataType
5
+ from frametree.core.exceptions import FrameTreeError
6
+
7
+ if ty.TYPE_CHECKING: # pragma: no cover
8
+ from .row import DataRow
9
+ from .column import DataColumn
10
+ from .entry import DataEntry
11
+
12
+
13
+ @attrs.define
14
+ class DataCell:
15
+ """
16
+ An abstract container representing the intersection between a row and a column,
17
+ which points to an item within a dataset.
18
+
19
+ Parameters
20
+ ----------
21
+ row : DataRow
22
+ the row the cell belongs to
23
+ column : DataColumn
24
+ the column the cell belongs to
25
+ entry : DataEntry or None
26
+ the entry in the row that matches the column
27
+ provenance : Provenance | None
28
+ The recorded provenance for the item stored within the data cell,
29
+ if applicable
30
+ """
31
+
32
+ row: DataRow
33
+ column: DataColumn
34
+ entry: DataEntry
35
+ provenance: ty.Dict[str, ty.Any] = None
36
+
37
+ @property
38
+ def datatype(self):
39
+ return self.column.datatype
40
+
41
+ @property
42
+ def is_empty(self):
43
+ return self.entry is None
44
+
45
+ @property
46
+ def item(self) -> DataType:
47
+ if self.is_empty:
48
+ raise FrameTreeError(f"Cannot access item of empty cell, {self}")
49
+ return self.entry.get_item(self.column.datatype)
50
+
51
+ @item.setter
52
+ def item(self, item: DataType):
53
+ if not self.column.is_sink:
54
+ raise FrameTreeError(
55
+ f"Cannot set data items ({item}) into source column cell {self}"
56
+ )
57
+ item = self.datatype(item)
58
+ if self.is_empty:
59
+ entry = self.row.dataset.store.post(
60
+ item=item, path=self.column.path, datatype=self.datatype, row=self.row
61
+ )
62
+ self.entry = entry
63
+ else:
64
+ self.entry.item = item
65
+
66
+ @classmethod
67
+ def intersection(
68
+ cls, column: DataColumn, row: DataRow, allow_empty: ty.Optional[bool] = None
69
+ ) -> DataCell:
70
+ if allow_empty is None:
71
+ allow_empty = column.is_sink
72
+ return cls(
73
+ row=row,
74
+ column=column,
75
+ entry=column.match_entry(row, allow_none=allow_empty),
76
+ )
@@ -0,0 +1,6 @@
1
+ from .base import cli
2
+ from .ext import ext
3
+ from .store import store
4
+ from .dataset import dataset
5
+ from .apply import apply
6
+ from .derive import derive