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.
- frametree/common/__init__.py +2 -0
- frametree/common/dirtree.py +447 -0
- frametree/common/space.py +42 -0
- frametree/core/__init__.py +6 -0
- frametree/core/_version.py +16 -0
- frametree/core/cell.py +76 -0
- frametree/core/cli/__init__.py +6 -0
- frametree/core/cli/apply.py +187 -0
- frametree/core/cli/base.py +9 -0
- frametree/core/cli/dataset.py +446 -0
- frametree/core/cli/derive.py +130 -0
- frametree/core/cli/ext.py +13 -0
- frametree/core/cli/store.py +160 -0
- frametree/core/cli/tests/test_cli_apply.py +47 -0
- frametree/core/cli/tests/test_cli_dataset.py +184 -0
- frametree/core/cli/tests/test_cli_derive.py +53 -0
- frametree/core/cli/tests/test_cli_ext.py +9 -0
- frametree/core/cli/tests/test_cli_install_licenses.py +93 -0
- frametree/core/cli/tests/test_cli_store.py +122 -0
- frametree/core/column.py +437 -0
- frametree/core/entry.py +161 -0
- frametree/core/exceptions.py +232 -0
- frametree/core/licence.py +62 -0
- frametree/core/packaging.py +183 -0
- frametree/core/pipeline.py +675 -0
- frametree/core/quality.py +29 -0
- frametree/core/row.py +260 -0
- frametree/core/salience.py +142 -0
- frametree/core/serialize.py +606 -0
- frametree/core/set/__init__.py +2 -0
- frametree/core/set/base.py +855 -0
- frametree/core/set/metadata.py +154 -0
- frametree/core/space.py +190 -0
- frametree/core/store/__init__.py +3 -0
- frametree/core/store/base.py +942 -0
- frametree/core/store/local.py +386 -0
- frametree/core/store/remote.py +590 -0
- frametree/core/store/tests/test_id_handling.py +104 -0
- frametree/core/store/tests/test_store.py +178 -0
- frametree/core/tests/test_clinical_space.py +75 -0
- frametree/core/tests/test_data_column.py +28 -0
- frametree/core/tests/test_dataset.py +51 -0
- frametree/core/tests/test_tree.py +282 -0
- frametree/core/tests/test_utils.py +26 -0
- frametree/core/tree.py +291 -0
- frametree/core/utils.py +626 -0
- frametree/testing/__init__.py +2 -0
- frametree/testing/blueprint.py +673 -0
- frametree/testing/space.py +31 -0
- frametree/testing/store.py +404 -0
- frametree/testing/tasks.py +284 -0
- frametree/testing/tests/test_blueprint.py +76 -0
- frametree-0.11.0.dist-info/METADATA +227 -0
- frametree-0.11.0.dist-info/RECORD +58 -0
- frametree-0.11.0.dist-info/WHEEL +4 -0
- frametree-0.11.0.dist-info/entry_points.txt +2 -0
- frametree-0.11.0.dist-info/licenses/AUTHORS +2 -0
- frametree-0.11.0.dist-info/licenses/LICENSE +110 -0
|
@@ -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,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
|
+
)
|