ONE-api 3.0b3__py3-none-any.whl → 3.0b5__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.
- {ONE_api-3.0b3.dist-info → ONE_api-3.0b5.dist-info}/LICENSE +21 -21
- {ONE_api-3.0b3.dist-info → ONE_api-3.0b5.dist-info}/METADATA +115 -115
- ONE_api-3.0b5.dist-info/RECORD +37 -0
- one/__init__.py +2 -2
- one/alf/__init__.py +1 -1
- one/alf/cache.py +640 -653
- one/alf/exceptions.py +105 -105
- one/alf/io.py +876 -876
- one/alf/path.py +1450 -1450
- one/alf/spec.py +519 -519
- one/api.py +2979 -2973
- one/converters.py +850 -850
- one/params.py +414 -414
- one/registration.py +845 -845
- one/remote/__init__.py +1 -1
- one/remote/aws.py +313 -313
- one/remote/base.py +142 -142
- one/remote/globus.py +1254 -1254
- one/tests/fixtures/params/.caches +6 -6
- one/tests/fixtures/params/.test.alyx.internationalbrainlab.org +8 -8
- one/tests/fixtures/rest_responses/1f187d80fd59677b395fcdb18e68e4401bfa1cc9 +1 -1
- one/tests/fixtures/rest_responses/47893cf67c985e6361cdee009334963f49fb0746 +1 -1
- one/tests/fixtures/rest_responses/535d0e9a1e2c1efbdeba0d673b131e00361a2edb +1 -1
- one/tests/fixtures/rest_responses/6dc96f7e9bcc6ac2e7581489b9580a6cd3f28293 +1 -1
- one/tests/fixtures/rest_responses/db1731fb8df0208944ae85f76718430813a8bf50 +1 -1
- one/tests/fixtures/rest_responses/dcce48259bb929661f60a02a48563f70aa6185b3 +1 -1
- one/tests/fixtures/rest_responses/f530d6022f61cdc9e38cc66beb3cb71f3003c9a1 +1 -1
- one/tests/fixtures/test_dbs.json +14 -14
- one/util.py +524 -524
- one/webclient.py +1368 -1354
- ONE_api-3.0b3.dist-info/RECORD +0 -37
- {ONE_api-3.0b3.dist-info → ONE_api-3.0b5.dist-info}/WHEEL +0 -0
- {ONE_api-3.0b3.dist-info → ONE_api-3.0b5.dist-info}/top_level.txt +0 -0
one/alf/path.py
CHANGED
|
@@ -1,1450 +1,1450 @@
|
|
|
1
|
-
"""Module for identifying and parsing ALF file names.
|
|
2
|
-
|
|
3
|
-
An ALF file has the following components (those in brackets are optional):
|
|
4
|
-
(_namespace_)object.attribute(_timescale)(.extra.parts).ext
|
|
5
|
-
|
|
6
|
-
Note the following:
|
|
7
|
-
Object attributes may not contain an underscore unless followed by 'times' or 'intervals'.
|
|
8
|
-
A namespace must not contain extra underscores (i.e. `name_space` and `__namespace__` are not
|
|
9
|
-
valid).
|
|
10
|
-
ALF files must always have an extension.
|
|
11
|
-
|
|
12
|
-
For more information, see the following documentation:
|
|
13
|
-
https://int-brain-lab.github.io/ONE/alf_intro.html
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
ALFPath differences
|
|
17
|
-
-------------------
|
|
18
|
-
ALFPath.iter_datasets returns full paths (close the pathlib.Path.iterdir), whereas
|
|
19
|
-
alf.io.iter_datasets returns relative paths as POSIX strings (TODO).
|
|
20
|
-
|
|
21
|
-
ALFPath.parse_* methods return a dict by default, whereas parse_* functions return
|
|
22
|
-
tuples by default. Additionally, the parse_* functions raise ALFInvalid errors by
|
|
23
|
-
default if the path can't be parsed. ALFPath.parse_* methods have no validation
|
|
24
|
-
option.
|
|
25
|
-
|
|
26
|
-
ALFPath properties return empty str instead of None if ALF part isn't present..
|
|
27
|
-
"""
|
|
28
|
-
import os
|
|
29
|
-
import pathlib
|
|
30
|
-
from collections import OrderedDict
|
|
31
|
-
from datetime import datetime
|
|
32
|
-
from typing import Union, Optional, Iterable
|
|
33
|
-
import logging
|
|
34
|
-
|
|
35
|
-
from iblutil.util import Listable
|
|
36
|
-
|
|
37
|
-
from .exceptions import ALFInvalid
|
|
38
|
-
from . import spec
|
|
39
|
-
from .spec import SESSION_SPEC, COLLECTION_SPEC, FILE_SPEC, REL_PATH_SPEC
|
|
40
|
-
|
|
41
|
-
_logger = logging.getLogger(__name__)
|
|
42
|
-
__all__ = [
|
|
43
|
-
'ALFPath', 'PureALFPath', 'WindowsALFPath', 'PosixALFPath',
|
|
44
|
-
'PureWindowsALFPath', 'PurePosixALFPath'
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def rel_path_parts(rel_path, as_dict=False, assert_valid=True):
|
|
49
|
-
"""Parse a relative path into the relevant parts.
|
|
50
|
-
|
|
51
|
-
A relative path follows the pattern
|
|
52
|
-
(collection/)(#revision#/)_namespace_object.attribute_timescale.extra.extension
|
|
53
|
-
|
|
54
|
-
Parameters
|
|
55
|
-
----------
|
|
56
|
-
rel_path : str, pathlib.Path
|
|
57
|
-
A relative path string.
|
|
58
|
-
as_dict : bool
|
|
59
|
-
If true, an OrderedDict of parts are returned with the keys ('lab', 'subject', 'date',
|
|
60
|
-
'number'), otherwise a tuple of values are returned.
|
|
61
|
-
assert_valid : bool
|
|
62
|
-
If true an ALFInvalid is raised when the session cannot be parsed, otherwise an empty
|
|
63
|
-
dict of tuple of Nones is returned.
|
|
64
|
-
|
|
65
|
-
Returns
|
|
66
|
-
-------
|
|
67
|
-
OrderedDict, tuple
|
|
68
|
-
A dict if as_dict is true, or a tuple of parsed values.
|
|
69
|
-
|
|
70
|
-
"""
|
|
71
|
-
return _path_parts(rel_path, REL_PATH_SPEC, True, as_dict, assert_valid)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def session_path_parts(session_path, as_dict=False, assert_valid=True):
|
|
75
|
-
"""Parse a session path into the relevant parts.
|
|
76
|
-
|
|
77
|
-
Return keys:
|
|
78
|
-
- lab
|
|
79
|
-
- subject
|
|
80
|
-
- date
|
|
81
|
-
- number
|
|
82
|
-
|
|
83
|
-
Parameters
|
|
84
|
-
----------
|
|
85
|
-
session_path : str, pathlib.Path
|
|
86
|
-
A session path string.
|
|
87
|
-
as_dict : bool
|
|
88
|
-
If true, an OrderedDict of parts are returned with the keys ('lab', 'subject', 'date',
|
|
89
|
-
'number'), otherwise a tuple of values are returned.
|
|
90
|
-
assert_valid : bool
|
|
91
|
-
If true an ALFInvalid is raised when the session cannot be parsed, otherwise an empty
|
|
92
|
-
dict of tuple of Nones is returned.
|
|
93
|
-
|
|
94
|
-
Returns
|
|
95
|
-
-------
|
|
96
|
-
OrderedDict, tuple
|
|
97
|
-
A dict if as_dict is true, or a tuple of parsed values.
|
|
98
|
-
|
|
99
|
-
Raises
|
|
100
|
-
------
|
|
101
|
-
ALFInvalid
|
|
102
|
-
Invalid ALF session path (assert_valid is True).
|
|
103
|
-
|
|
104
|
-
"""
|
|
105
|
-
return _path_parts(session_path, SESSION_SPEC, False, as_dict, assert_valid)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _path_parts(path, spec_str, match=True, as_dict=False, assert_valid=True):
|
|
109
|
-
"""Given a ALF and a spec string, parse into parts.
|
|
110
|
-
|
|
111
|
-
Parameters
|
|
112
|
-
----------
|
|
113
|
-
path : str, pathlib.Path
|
|
114
|
-
An ALF path or dataset.
|
|
115
|
-
match : bool
|
|
116
|
-
If True, string must match exactly, otherwise search for expression within path.
|
|
117
|
-
as_dict : bool
|
|
118
|
-
When true a dict of matches is returned.
|
|
119
|
-
assert_valid : bool
|
|
120
|
-
When true an exception is raised when the filename cannot be parsed.
|
|
121
|
-
|
|
122
|
-
Returns
|
|
123
|
-
-------
|
|
124
|
-
OrderedDict, tuple
|
|
125
|
-
A dict if as_dict is true, or a tuple of parsed values.
|
|
126
|
-
|
|
127
|
-
Raises
|
|
128
|
-
------
|
|
129
|
-
ALFInvalid
|
|
130
|
-
Invalid ALF path (assert_valid is True).
|
|
131
|
-
|
|
132
|
-
"""
|
|
133
|
-
if hasattr(path, 'as_posix'):
|
|
134
|
-
path = path.as_posix()
|
|
135
|
-
pattern = spec.regex(spec_str)
|
|
136
|
-
empty = OrderedDict.fromkeys(pattern.groupindex.keys())
|
|
137
|
-
parsed = (pattern.match if match else pattern.search)(path)
|
|
138
|
-
if parsed: # py3.8
|
|
139
|
-
parsed_dict = parsed.groupdict()
|
|
140
|
-
return OrderedDict(parsed_dict) if as_dict else tuple(parsed_dict.values())
|
|
141
|
-
elif assert_valid:
|
|
142
|
-
raise ALFInvalid(path)
|
|
143
|
-
else:
|
|
144
|
-
return empty if as_dict else tuple(empty.values())
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def filename_parts(filename, as_dict=False, assert_valid=True) -> Union[dict, tuple]:
|
|
148
|
-
"""Return the parsed elements of a given ALF filename.
|
|
149
|
-
|
|
150
|
-
Parameters
|
|
151
|
-
----------
|
|
152
|
-
filename : str
|
|
153
|
-
The name of the file.
|
|
154
|
-
as_dict : bool
|
|
155
|
-
When true a dict of matches is returned.
|
|
156
|
-
assert_valid : bool
|
|
157
|
-
When true an exception is raised when the filename cannot be parsed.
|
|
158
|
-
|
|
159
|
-
Returns
|
|
160
|
-
-------
|
|
161
|
-
namespace : str
|
|
162
|
-
The _namespace_ or None if not present.
|
|
163
|
-
object : str
|
|
164
|
-
ALF object.
|
|
165
|
-
attribute : str
|
|
166
|
-
The ALF attribute.
|
|
167
|
-
timescale : str
|
|
168
|
-
The ALF _timescale or None if not present.
|
|
169
|
-
extra : str
|
|
170
|
-
Any extra parts to the filename, or None if not present.
|
|
171
|
-
extension : str
|
|
172
|
-
The file extension.
|
|
173
|
-
|
|
174
|
-
Examples
|
|
175
|
-
--------
|
|
176
|
-
>>> filename_parts('_namespace_obj.times_timescale.extra.foo.ext')
|
|
177
|
-
('namespace', 'obj', 'times', 'timescale', 'extra.foo', 'ext')
|
|
178
|
-
>>> filename_parts('spikes.clusters.npy', as_dict=True)
|
|
179
|
-
{'namespace': None,
|
|
180
|
-
'object': 'spikes',
|
|
181
|
-
'attribute': 'clusters',
|
|
182
|
-
'timescale': None,
|
|
183
|
-
'extra': None,
|
|
184
|
-
'extension': 'npy'}
|
|
185
|
-
>>> filename_parts('spikes.times_ephysClock.npy')
|
|
186
|
-
(None, 'spikes', 'times', 'ephysClock', None, 'npy')
|
|
187
|
-
>>> filename_parts('_iblmic_audioSpectrogram.frequencies.npy')
|
|
188
|
-
('iblmic', 'audioSpectrogram', 'frequencies', None, None, 'npy')
|
|
189
|
-
>>> filename_parts('_spikeglx_ephysData_g0_t0.imec.wiring.json')
|
|
190
|
-
('spikeglx', 'ephysData_g0_t0', 'imec', None, 'wiring', 'json')
|
|
191
|
-
>>> filename_parts('_spikeglx_ephysData_g0_t0.imec0.lf.bin')
|
|
192
|
-
('spikeglx', 'ephysData_g0_t0', 'imec0', None, 'lf', 'bin')
|
|
193
|
-
>>> filename_parts('_ibl_trials.goCue_times_bpod.csv')
|
|
194
|
-
('ibl', 'trials', 'goCue_times', 'bpod', None, 'csv')
|
|
195
|
-
|
|
196
|
-
Raises
|
|
197
|
-
------
|
|
198
|
-
ALFInvalid
|
|
199
|
-
Invalid ALF dataset (assert_valid is True).
|
|
200
|
-
|
|
201
|
-
"""
|
|
202
|
-
return _path_parts(filename, FILE_SPEC, True, as_dict, assert_valid)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def full_path_parts(path, as_dict=False, assert_valid=True) -> Union[dict, tuple]:
|
|
206
|
-
"""Parse all filename and folder parts.
|
|
207
|
-
|
|
208
|
-
Parameters
|
|
209
|
-
----------
|
|
210
|
-
path : str, pathlib.Path.
|
|
211
|
-
The ALF path
|
|
212
|
-
as_dict : bool
|
|
213
|
-
When true a dict of matches is returned.
|
|
214
|
-
assert_valid : bool
|
|
215
|
-
When true an exception is raised when the filename cannot be parsed.
|
|
216
|
-
|
|
217
|
-
Returns
|
|
218
|
-
-------
|
|
219
|
-
OrderedDict, tuple
|
|
220
|
-
A dict if as_dict is true, or a tuple of parsed values.
|
|
221
|
-
|
|
222
|
-
Examples
|
|
223
|
-
--------
|
|
224
|
-
>>> full_path_parts(
|
|
225
|
-
... 'lab/Subjects/subject/2020-01-01/001/collection/#revision#/'
|
|
226
|
-
... '_namespace_obj.times_timescale.extra.foo.ext')
|
|
227
|
-
('lab', 'subject', '2020-01-01', '001', 'collection', 'revision',
|
|
228
|
-
'namespace', 'obj', 'times','timescale', 'extra.foo', 'ext')
|
|
229
|
-
>>> full_path_parts('spikes.clusters.npy', as_dict=True)
|
|
230
|
-
{'lab': None,
|
|
231
|
-
'subject': None,
|
|
232
|
-
'date': None,
|
|
233
|
-
'number': None,
|
|
234
|
-
'collection': None,
|
|
235
|
-
'revision': None,
|
|
236
|
-
'namespace': None,
|
|
237
|
-
'object': 'spikes',
|
|
238
|
-
'attribute': 'clusters',
|
|
239
|
-
'timescale': None,
|
|
240
|
-
'extra': None,
|
|
241
|
-
'extension': 'npy'}
|
|
242
|
-
|
|
243
|
-
Raises
|
|
244
|
-
------
|
|
245
|
-
ALFInvalid
|
|
246
|
-
Invalid ALF path (assert_valid is True).
|
|
247
|
-
|
|
248
|
-
"""
|
|
249
|
-
path = pathlib.Path(path)
|
|
250
|
-
# NB We try to determine whether we have a folder or filename path. Filenames contain at
|
|
251
|
-
# least two periods, however it is currently permitted to have any number of periods in a
|
|
252
|
-
# collection, making the ALF path ambiguous.
|
|
253
|
-
if sum(x == '.' for x in path.name) < 2: # folder only
|
|
254
|
-
folders = folder_parts(path, as_dict, assert_valid)
|
|
255
|
-
if assert_valid:
|
|
256
|
-
# Edge case: ensure is indeed folder by checking that name is in parts
|
|
257
|
-
invalid_file = path.name not in (folders.values() if as_dict else folders)
|
|
258
|
-
is_revision = f'#{folders["revision"] if as_dict else folders[-1]}#' == path.name
|
|
259
|
-
if not is_revision and invalid_file:
|
|
260
|
-
raise ALFInvalid(path)
|
|
261
|
-
dataset = filename_parts('', as_dict, assert_valid=False)
|
|
262
|
-
elif '/' not in path.as_posix(): # filename only
|
|
263
|
-
folders = folder_parts('', as_dict, assert_valid=False)
|
|
264
|
-
dataset = filename_parts(path.name, as_dict, assert_valid)
|
|
265
|
-
else: # full filepath
|
|
266
|
-
folders = folder_parts(path.parent, as_dict, assert_valid)
|
|
267
|
-
dataset = filename_parts(path.name, as_dict, assert_valid)
|
|
268
|
-
if as_dict:
|
|
269
|
-
return OrderedDict(**folders, **dataset)
|
|
270
|
-
else:
|
|
271
|
-
return folders + dataset
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def folder_parts(folder_path, as_dict=False, assert_valid=True) -> Union[dict, tuple]:
|
|
275
|
-
"""Parse all folder parts, including session, collection and revision.
|
|
276
|
-
|
|
277
|
-
Parameters
|
|
278
|
-
----------
|
|
279
|
-
folder_path : str, pathlib.Path
|
|
280
|
-
The ALF folder path.
|
|
281
|
-
as_dict : bool
|
|
282
|
-
When true a dict of matches is returned.
|
|
283
|
-
assert_valid : bool
|
|
284
|
-
When true an exception is raised when the filename cannot be parsed.
|
|
285
|
-
|
|
286
|
-
Returns
|
|
287
|
-
-------
|
|
288
|
-
OrderedDict, tuple
|
|
289
|
-
A dict if as_dict is true, or a tuple of parsed values.
|
|
290
|
-
|
|
291
|
-
Examples
|
|
292
|
-
--------
|
|
293
|
-
>>> folder_parts('lab/Subjects/subject/2020-01-01/001/collection/#revision#')
|
|
294
|
-
('lab', 'subject', '2020-01-01', '001', 'collection', 'revision')
|
|
295
|
-
>>> folder_parts(Path('lab/Subjects/subject/2020-01-01/001'), as_dict=True)
|
|
296
|
-
{'lab': 'lab',
|
|
297
|
-
'subject': 'subject',
|
|
298
|
-
'date': '2020-01-01',
|
|
299
|
-
'number': '001',
|
|
300
|
-
'collection': None,
|
|
301
|
-
'revision': None}
|
|
302
|
-
|
|
303
|
-
Raises
|
|
304
|
-
------
|
|
305
|
-
ALFInvalid
|
|
306
|
-
Invalid ALF path (assert_valid is True).
|
|
307
|
-
|
|
308
|
-
"""
|
|
309
|
-
if hasattr(folder_path, 'as_posix'):
|
|
310
|
-
folder_path = folder_path.as_posix()
|
|
311
|
-
if folder_path and folder_path[-1] != '/': # Slash required for regex pattern
|
|
312
|
-
folder_path = folder_path + '/'
|
|
313
|
-
spec_str = f'{SESSION_SPEC}/{COLLECTION_SPEC}'
|
|
314
|
-
return _path_parts(folder_path, spec_str, False, as_dict, assert_valid)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _isdatetime(s: str) -> bool:
|
|
318
|
-
"""Returns True if input is valid ISO date string."""
|
|
319
|
-
try:
|
|
320
|
-
datetime.strptime(s, '%Y-%m-%d')
|
|
321
|
-
return True
|
|
322
|
-
except ValueError:
|
|
323
|
-
return False
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def get_session_path(path: Union[str, pathlib.Path]) -> Optional[pathlib.Path]:
|
|
327
|
-
"""Return full session path from any file path if the date/number pattern is found.
|
|
328
|
-
|
|
329
|
-
Returns
|
|
330
|
-
-------
|
|
331
|
-
pathlib.Path
|
|
332
|
-
The session path part of the input path or None if path invalid.
|
|
333
|
-
|
|
334
|
-
Examples
|
|
335
|
-
--------
|
|
336
|
-
>>> get_session_path('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001')
|
|
337
|
-
Path('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001')
|
|
338
|
-
|
|
339
|
-
>>> get_session_path('C:\\Data\\subject\\2020-01-01\\1\\trials.intervals.npy')
|
|
340
|
-
Path('C:/Data/subject/2020-01-01/1')
|
|
341
|
-
|
|
342
|
-
"""
|
|
343
|
-
if path is None:
|
|
344
|
-
return
|
|
345
|
-
if isinstance(path, str):
|
|
346
|
-
path = pathlib.Path(path)
|
|
347
|
-
for i, p in enumerate(path.parts):
|
|
348
|
-
if p.isdigit() and _isdatetime(path.parts[i - 1]):
|
|
349
|
-
return path.__class__().joinpath(*path.parts[:i + 1])
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def get_alf_path(path: Union[str, pathlib.Path]) -> str:
|
|
353
|
-
"""Returns the ALF part of a path or filename.
|
|
354
|
-
|
|
355
|
-
Attempts to return the first valid part of the path, first searching for a session path,
|
|
356
|
-
then relative path (collection/revision/filename), then just the filename. If all invalid,
|
|
357
|
-
None is returned.
|
|
358
|
-
|
|
359
|
-
Parameters
|
|
360
|
-
----------
|
|
361
|
-
path : str, pathlib.Path
|
|
362
|
-
A path to parse.
|
|
363
|
-
|
|
364
|
-
Returns
|
|
365
|
-
-------
|
|
366
|
-
str
|
|
367
|
-
A string containing the full ALF path, session path, relative path or filename.
|
|
368
|
-
|
|
369
|
-
Examples
|
|
370
|
-
--------
|
|
371
|
-
>>> get_alf_path('etc/etc/lab/Subjects/subj/2021-01-21/001')
|
|
372
|
-
'lab/Subjects/subj/2021-01-21/001/collection/file.attr.ext'
|
|
373
|
-
|
|
374
|
-
>>> get_alf_path('etc/etc/subj/2021-01-21/001/collection/file.attr.ext')
|
|
375
|
-
'subj/2021-01-21/001/collection/file.attr.ext'
|
|
376
|
-
|
|
377
|
-
>>> get_alf_path('collection/file.attr.ext')
|
|
378
|
-
'collection/file.attr.ext'
|
|
379
|
-
|
|
380
|
-
"""
|
|
381
|
-
if not isinstance(path, str):
|
|
382
|
-
path = pathlib.Path(path).as_posix()
|
|
383
|
-
path = path.strip('/')
|
|
384
|
-
|
|
385
|
-
# Check if session path
|
|
386
|
-
if match_session := spec.regex(SESSION_SPEC).search(path):
|
|
387
|
-
return path[match_session.start():]
|
|
388
|
-
|
|
389
|
-
# Check if filename / relative path (i.e. collection + filename)
|
|
390
|
-
parts = path.rsplit('/', 1)
|
|
391
|
-
if spec.regex(FILE_SPEC).match(parts[-1]):
|
|
392
|
-
return path if spec.regex(f'{COLLECTION_SPEC}{FILE_SPEC}').match(path) else parts[-1]
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
def add_uuid_string(file_path, uuid):
|
|
396
|
-
"""Add a UUID to the filename of an ALF path.
|
|
397
|
-
|
|
398
|
-
Adds a UUID to an ALF filename as an extra part, e.g.
|
|
399
|
-
'obj.attr.ext' -> 'obj.attr.a976e418-c8b8-4d24-be47-d05120b18341.ext'.
|
|
400
|
-
|
|
401
|
-
Parameters
|
|
402
|
-
----------
|
|
403
|
-
file_path : str, pathlib.Path, pathlib.PurePath
|
|
404
|
-
An ALF path to add the UUID to.
|
|
405
|
-
uuid : str, uuid.UUID
|
|
406
|
-
The UUID to add.
|
|
407
|
-
|
|
408
|
-
Returns
|
|
409
|
-
-------
|
|
410
|
-
pathlib.Path, pathlib.PurePath
|
|
411
|
-
A new Path or PurePath object with a UUID in the filename.
|
|
412
|
-
|
|
413
|
-
Examples
|
|
414
|
-
--------
|
|
415
|
-
>>> add_uuid_string('/path/to/trials.intervals.npy', 'a976e418-c8b8-4d24-be47-d05120b18341')
|
|
416
|
-
Path('/path/to/trials.intervals.a976e418-c8b8-4d24-be47-d05120b18341.npy')
|
|
417
|
-
|
|
418
|
-
Raises
|
|
419
|
-
------
|
|
420
|
-
ValueError
|
|
421
|
-
`uuid` must be a valid hyphen-separated hexadecimal UUID.
|
|
422
|
-
|
|
423
|
-
See Also
|
|
424
|
-
--------
|
|
425
|
-
one.alf.path.ALFPath.with_uuid
|
|
426
|
-
one.alf.path.remove_uuid_string
|
|
427
|
-
one.alf.spec.is_uuid
|
|
428
|
-
|
|
429
|
-
"""
|
|
430
|
-
if isinstance(uuid, str) and not spec.is_uuid_string(uuid):
|
|
431
|
-
raise ValueError('Should provide a valid UUID v4')
|
|
432
|
-
uuid = str(uuid)
|
|
433
|
-
# NB: Only instantiate as Path if not already a Path, otherwise we risk changing the class
|
|
434
|
-
if isinstance(file_path, str):
|
|
435
|
-
file_path = pathlib.Path(file_path)
|
|
436
|
-
name_parts = file_path.stem.split('.')
|
|
437
|
-
if spec.is_uuid(name_parts[-1]):
|
|
438
|
-
*name_parts, old_uuid = name_parts
|
|
439
|
-
if old_uuid == uuid:
|
|
440
|
-
_logger.warning(f'UUID already found in file name: {file_path.name}: IGNORE')
|
|
441
|
-
return file_path
|
|
442
|
-
else:
|
|
443
|
-
_logger.debug('Replacing %s with %s in %s', old_uuid, uuid, file_path)
|
|
444
|
-
return file_path.parent.joinpath(f"{'.'.join(name_parts)}.{uuid}{file_path.suffix}")
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
def remove_uuid_string(file_path):
|
|
448
|
-
"""Remove UUID from a filename of an ALF path.
|
|
449
|
-
|
|
450
|
-
Parameters
|
|
451
|
-
----------
|
|
452
|
-
file_path : str, pathlib.Path, pathlib.PurePath
|
|
453
|
-
An ALF path to add the UUID to.
|
|
454
|
-
|
|
455
|
-
Returns
|
|
456
|
-
-------
|
|
457
|
-
ALFPath, PureALFPath, pathlib.Path, pathlib.PurePath
|
|
458
|
-
A new Path or PurePath object without a UUID in the filename.
|
|
459
|
-
|
|
460
|
-
Examples
|
|
461
|
-
--------
|
|
462
|
-
>>> add_uuid_string('/path/to/trials.intervals.a976e418-c8b8-4d24-be47-d05120b18341.npy')
|
|
463
|
-
Path('/path/to/trials.intervals.npy')
|
|
464
|
-
|
|
465
|
-
>>> add_uuid_string('/path/to/trials.intervals.npy')
|
|
466
|
-
Path('/path/to/trials.intervals.npy')
|
|
467
|
-
|
|
468
|
-
See Also
|
|
469
|
-
--------
|
|
470
|
-
one.alf.path.ALFPath.without_uuid
|
|
471
|
-
one.alf.path.add_uuid_string
|
|
472
|
-
|
|
473
|
-
"""
|
|
474
|
-
if isinstance(file_path, str):
|
|
475
|
-
file_path = pathlib.Path(file_path)
|
|
476
|
-
name_parts = file_path.stem.split('.')
|
|
477
|
-
|
|
478
|
-
if spec.is_uuid_string(name_parts[-1]):
|
|
479
|
-
file_path = file_path.with_name('.'.join(name_parts[:-1]) + file_path.suffix)
|
|
480
|
-
return file_path
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
def padded_sequence(file_path):
|
|
484
|
-
"""Ensures a file path contains a zero-padded experiment sequence folder.
|
|
485
|
-
|
|
486
|
-
Parameters
|
|
487
|
-
----------
|
|
488
|
-
file_path : str, pathlib.Path, pathlib.PurePath
|
|
489
|
-
A session or file path to convert.
|
|
490
|
-
|
|
491
|
-
Returns
|
|
492
|
-
-------
|
|
493
|
-
ALFPath, PureALFPath
|
|
494
|
-
The same path but with the experiment sequence folder zero-padded. If a PurePath was
|
|
495
|
-
passed, a PurePath will be returned, otherwise a Path object is returned.
|
|
496
|
-
|
|
497
|
-
Examples
|
|
498
|
-
--------
|
|
499
|
-
>>> file_path = '/iblrigdata/subject/2023-01-01/1/_ibl_experiment.description.yaml'
|
|
500
|
-
>>> padded_sequence(file_path)
|
|
501
|
-
pathlib.Path('/iblrigdata/subject/2023-01-01/001/_ibl_experiment.description.yaml')
|
|
502
|
-
|
|
503
|
-
Supports folders and will not affect already padded paths
|
|
504
|
-
|
|
505
|
-
>>> session_path = pathlib.PurePosixPath('subject/2023-01-01/001')
|
|
506
|
-
>>> padded_sequence(file_path)
|
|
507
|
-
pathlib.PurePosixPath('subject/2023-01-01/001')
|
|
508
|
-
|
|
509
|
-
"""
|
|
510
|
-
file_path = ensure_alf_path(file_path)
|
|
511
|
-
if (session_path := get_session_path(file_path)) is None:
|
|
512
|
-
raise ValueError('path must include a valid ALF session path, e.g. subject/YYYY-MM-DD/N')
|
|
513
|
-
idx = len(file_path.parts) - len(session_path.parts)
|
|
514
|
-
sequence = str(int(session_path.parts[-1])).zfill(3) # zero-pad if necessary
|
|
515
|
-
return file_path.parents[idx].joinpath(sequence, file_path.relative_to(session_path))
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
def without_revision(file_path):
|
|
519
|
-
"""Return file path without a revision folder.
|
|
520
|
-
|
|
521
|
-
Parameters
|
|
522
|
-
----------
|
|
523
|
-
file_path : str, pathlib.Path
|
|
524
|
-
A valid ALF dataset path.
|
|
525
|
-
|
|
526
|
-
Returns
|
|
527
|
-
-------
|
|
528
|
-
pathlib.Path
|
|
529
|
-
The input file path without a revision folder.
|
|
530
|
-
|
|
531
|
-
Examples
|
|
532
|
-
--------
|
|
533
|
-
>>> without_revision('/lab/Subjects/subject/2023-01-01/001/collection/#revision#/obj.attr.ext')
|
|
534
|
-
Path('/lab/Subjects/subject/2023-01-01/001/collection/obj.attr.ext')
|
|
535
|
-
|
|
536
|
-
"""
|
|
537
|
-
if isinstance(file_path, str):
|
|
538
|
-
file_path = pathlib.Path(file_path)
|
|
539
|
-
*_, collection, revision = folder_parts(file_path.parent)
|
|
540
|
-
return get_session_path(file_path).joinpath(*filter(None, (collection, file_path.name)))
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
class PureALFPath(pathlib.PurePath): # py3.12 supports direct subclassing
|
|
544
|
-
"""Base class for manipulating Alyx file (ALF) paths without I/O.
|
|
545
|
-
|
|
546
|
-
Similar to a pathlib PurePath object but with methods for validating, parsing, and replacing
|
|
547
|
-
ALF path parts.
|
|
548
|
-
|
|
549
|
-
Parameters
|
|
550
|
-
----------
|
|
551
|
-
args : str, pathlib.PurePath
|
|
552
|
-
One or more pathlike objects to combine into an ALF path object.
|
|
553
|
-
|
|
554
|
-
"""
|
|
555
|
-
|
|
556
|
-
def __new__(cls, *args):
|
|
557
|
-
"""Construct a ALFPurePath from one or several strings and or existing PurePath objects.
|
|
558
|
-
|
|
559
|
-
The strings and path objects are combined so as to yield a canonicalized path, which is
|
|
560
|
-
incorporated into the new PurePath object.
|
|
561
|
-
"""
|
|
562
|
-
if cls is PureALFPath:
|
|
563
|
-
cls = PureWindowsALFPath if os.name == 'nt' else PurePosixALFPath
|
|
564
|
-
return super().__new__(cls, *args)
|
|
565
|
-
|
|
566
|
-
def is_dataset(self):
|
|
567
|
-
"""Determine if path is an ALF dataset, rather than a folder.
|
|
568
|
-
|
|
569
|
-
Returns
|
|
570
|
-
-------
|
|
571
|
-
bool
|
|
572
|
-
True if filename is ALF dataset.
|
|
573
|
-
|
|
574
|
-
"""
|
|
575
|
-
return spec.is_valid(self.name)
|
|
576
|
-
|
|
577
|
-
def is_valid_alf(path) -> bool:
|
|
578
|
-
"""Check if path is a valid ALF path.
|
|
579
|
-
|
|
580
|
-
This returns true if the input path matches any part of the ALF path specification.
|
|
581
|
-
This method can be used as a static method with any pathlike input, or as an instance
|
|
582
|
-
method. This will validate both directory paths and file paths.
|
|
583
|
-
|
|
584
|
-
Parameters
|
|
585
|
-
----------
|
|
586
|
-
path : str, pathlib.PurePath
|
|
587
|
-
A path to check the validity of.
|
|
588
|
-
|
|
589
|
-
Returns
|
|
590
|
-
-------
|
|
591
|
-
bool
|
|
592
|
-
True if the path is recognized as a valid ALF path.
|
|
593
|
-
|
|
594
|
-
Examples
|
|
595
|
-
--------
|
|
596
|
-
>>> ALFPath('/home/foo/2020-01-01/001').is_valid_alf()
|
|
597
|
-
True
|
|
598
|
-
|
|
599
|
-
>>> ALFPath('/home/foo/2020-01-01/001/alf/spikes.times.npy').is_valid_alf()
|
|
600
|
-
True
|
|
601
|
-
|
|
602
|
-
>>> ALFPath.is_valid_alf('_ibl_wheel.timestamps.npy')
|
|
603
|
-
True
|
|
604
|
-
|
|
605
|
-
>>> ALFPath.is_valid_alf('foo.bar')
|
|
606
|
-
False
|
|
607
|
-
|
|
608
|
-
See Also
|
|
609
|
-
--------
|
|
610
|
-
PureALFPath.is_dataset - Test whether file name is valid as well as directory path.
|
|
611
|
-
full_path_parts - Validates path and returns the parsed ALF path parts.
|
|
612
|
-
|
|
613
|
-
"""
|
|
614
|
-
try:
|
|
615
|
-
return any(full_path_parts(path))
|
|
616
|
-
except ALFInvalid:
|
|
617
|
-
return False
|
|
618
|
-
|
|
619
|
-
def is_session_path(path) -> bool:
|
|
620
|
-
"""Check if path is a valid ALF session path.
|
|
621
|
-
|
|
622
|
-
This returns true if the input path matches the ALF session path specification.
|
|
623
|
-
This method can be used as a static method with any pathlike input, or as an instance
|
|
624
|
-
method.
|
|
625
|
-
|
|
626
|
-
Parameters
|
|
627
|
-
----------
|
|
628
|
-
path : str, pathlib.PurePath
|
|
629
|
-
A session path to check the validity of.
|
|
630
|
-
|
|
631
|
-
Returns
|
|
632
|
-
-------
|
|
633
|
-
bool
|
|
634
|
-
True if the path is recognized as a valid ALF session path.
|
|
635
|
-
|
|
636
|
-
Examples
|
|
637
|
-
--------
|
|
638
|
-
>>> ALFPath('/home/foo/2020-01-01/001').is_session_path()
|
|
639
|
-
True
|
|
640
|
-
|
|
641
|
-
>>> ALFPath('/home/foo/2020-01-01/001/alf/spikes.times.npy').is_session_path()
|
|
642
|
-
False
|
|
643
|
-
|
|
644
|
-
>>> ALFPath.is_session_path('_ibl_wheel.timestamps.npy')
|
|
645
|
-
False
|
|
646
|
-
|
|
647
|
-
>>> ALFPath.is_valid_alf('lab/Subjects/foo/2020-01-01/001')
|
|
648
|
-
True
|
|
649
|
-
|
|
650
|
-
See Also
|
|
651
|
-
--------
|
|
652
|
-
PureALFPath.is_valid_alf - Test whether path is generally valid a valid ALF path.
|
|
653
|
-
PureALFPath.session_path_parts - Returns parsed session path parts as tuple of str.
|
|
654
|
-
|
|
655
|
-
"""
|
|
656
|
-
return spec.is_session_path(path)
|
|
657
|
-
|
|
658
|
-
def session_path(self):
|
|
659
|
-
"""Extract the full session path.
|
|
660
|
-
|
|
661
|
-
Returns the session path from the filepath if the date/number pattern is found,
|
|
662
|
-
including the root directory.
|
|
663
|
-
|
|
664
|
-
Returns
|
|
665
|
-
-------
|
|
666
|
-
PureALFPath
|
|
667
|
-
The session path part of the input path or None if path invalid.
|
|
668
|
-
|
|
669
|
-
Examples
|
|
670
|
-
--------
|
|
671
|
-
>>> ALFPath('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001').session_path()
|
|
672
|
-
ALFPath('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001')
|
|
673
|
-
|
|
674
|
-
>>> ALFPath('C:\\Data\\subject\\2020-01-01\\1\\trials.intervals.npy').session_path()
|
|
675
|
-
ALFPath('C:/Data/subject/2020-01-01/1')
|
|
676
|
-
|
|
677
|
-
"""
|
|
678
|
-
return get_session_path(self)
|
|
679
|
-
|
|
680
|
-
def session_path_short(self, include_lab=False) -> str:
|
|
681
|
-
"""Return only the ALF session path as a posix str.
|
|
682
|
-
|
|
683
|
-
Params
|
|
684
|
-
------
|
|
685
|
-
include_lab : bool
|
|
686
|
-
If true, the lab/subject/date/number is returned, otherwise the lab part is dropped.
|
|
687
|
-
|
|
688
|
-
Returns
|
|
689
|
-
-------
|
|
690
|
-
str
|
|
691
|
-
The session path part of the input path or None if path invalid.
|
|
692
|
-
|
|
693
|
-
Examples
|
|
694
|
-
--------
|
|
695
|
-
>>> ALFPath('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001').session_path_short()
|
|
696
|
-
'subject/2020-01-01/001'
|
|
697
|
-
|
|
698
|
-
>>> alfpath = ALFPath('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001')
|
|
699
|
-
>>> alfpath.session_path_short(include_lab=True)
|
|
700
|
-
'lab/subject/2020-01-01/001'
|
|
701
|
-
|
|
702
|
-
>>> ALFPath('C:\\Data\\subject\\2020-01-01\\1\\trials.intervals.npy').session_path_short()
|
|
703
|
-
'subject/2020-01-01/1'
|
|
704
|
-
|
|
705
|
-
"""
|
|
706
|
-
idx = 0 if include_lab else 1
|
|
707
|
-
if any(parts := self.session_parts[idx:]):
|
|
708
|
-
return '/'.join(parts)
|
|
709
|
-
|
|
710
|
-
def without_lab(self) -> 'PureALFPath':
|
|
711
|
-
"""Return path without the <lab>/Subjects/ part.
|
|
712
|
-
|
|
713
|
-
If the <lab>/Subjects pattern is not found, the same path is returned.
|
|
714
|
-
|
|
715
|
-
Returns
|
|
716
|
-
-------
|
|
717
|
-
PureALFPath
|
|
718
|
-
The same path without the <lab>/Subjects part.
|
|
719
|
-
|
|
720
|
-
"""
|
|
721
|
-
p = self.as_posix()
|
|
722
|
-
if m := spec.regex('{lab}/Subjects/').search(p):
|
|
723
|
-
return self.__class__(p[:m.start()], p[m.end():])
|
|
724
|
-
else:
|
|
725
|
-
return self
|
|
726
|
-
|
|
727
|
-
def relative_to_lab(self) -> 'PureALFPath':
|
|
728
|
-
"""Return path relative to <lab>/Subjects/ part.
|
|
729
|
-
|
|
730
|
-
Returns
|
|
731
|
-
-------
|
|
732
|
-
PureALFPath
|
|
733
|
-
The same path, relative to the <lab>/Subjects/ part.
|
|
734
|
-
|
|
735
|
-
Raises
|
|
736
|
-
------
|
|
737
|
-
ValueError
|
|
738
|
-
The path doesn't contain a <lab>/Subjects/ pattern.
|
|
739
|
-
|
|
740
|
-
"""
|
|
741
|
-
p = self.as_posix()
|
|
742
|
-
if m := spec.regex('{lab}/Subjects/').search(p):
|
|
743
|
-
return self.__class__(p[m.end():])
|
|
744
|
-
else:
|
|
745
|
-
raise ValueError(f'{self} does not contain <lab>/Subjects pattern')
|
|
746
|
-
|
|
747
|
-
def relative_to_session(self):
|
|
748
|
-
"""Return path relative to session part.
|
|
749
|
-
|
|
750
|
-
Returns
|
|
751
|
-
-------
|
|
752
|
-
PureALFPath
|
|
753
|
-
The same path, relative to the <lab>/Subjects/<subject>/<date>/<number> part.
|
|
754
|
-
|
|
755
|
-
Raises
|
|
756
|
-
------
|
|
757
|
-
ValueError
|
|
758
|
-
The path doesn't contain a <lab>/Subjects/ pattern.
|
|
759
|
-
|
|
760
|
-
"""
|
|
761
|
-
if (session_path := self.session_path()):
|
|
762
|
-
return self.relative_to(session_path)
|
|
763
|
-
else:
|
|
764
|
-
raise ValueError(f'{self} does not contain session path pattern')
|
|
765
|
-
|
|
766
|
-
def parse_alf_path(self, as_dict=True):
|
|
767
|
-
"""Parse all filename and folder parts.
|
|
768
|
-
|
|
769
|
-
Parameters
|
|
770
|
-
----------
|
|
771
|
-
as_dict : bool
|
|
772
|
-
When true a dict of matches is returned.
|
|
773
|
-
|
|
774
|
-
Returns
|
|
775
|
-
-------
|
|
776
|
-
OrderedDict, tuple
|
|
777
|
-
A dict if as_dict is true, or a tuple of parsed values.
|
|
778
|
-
|
|
779
|
-
Examples
|
|
780
|
-
--------
|
|
781
|
-
>>> alfpath = PureALFPath(
|
|
782
|
-
... 'lab/Subjects/subject/2020-01-01/001/collection/#revision#/'
|
|
783
|
-
... '_namespace_obj.times_timescale.extra.foo.ext')
|
|
784
|
-
>>> alfpath.parse_alf_path()
|
|
785
|
-
{'lab': 'lab',
|
|
786
|
-
'subject': 'subject',
|
|
787
|
-
'date': '2020-01-01',
|
|
788
|
-
'number': '001',
|
|
789
|
-
'collection': 'collection',
|
|
790
|
-
'revision': 'revision',
|
|
791
|
-
'namespace': 'namespace',
|
|
792
|
-
'object': 'obj',
|
|
793
|
-
'attribute': 'times',
|
|
794
|
-
'timescale': 'timescale',
|
|
795
|
-
'extra': 'extra.foo',
|
|
796
|
-
'extension': 'ext'}
|
|
797
|
-
|
|
798
|
-
>>> PureALFPath('_namespace_obj.times_timescale.extra.foo.ext').parse_alf_path()
|
|
799
|
-
(None, None, None, None, None, None, 'namespace',
|
|
800
|
-
'obj', 'times','timescale', 'extra.foo', 'ext')
|
|
801
|
-
|
|
802
|
-
"""
|
|
803
|
-
return full_path_parts(self, assert_valid=False, as_dict=as_dict)
|
|
804
|
-
|
|
805
|
-
def parse_alf_name(self, as_dict=True):
|
|
806
|
-
"""Return the parsed elements of a given ALF filename.
|
|
807
|
-
|
|
808
|
-
Parameters
|
|
809
|
-
----------
|
|
810
|
-
as_dict : bool
|
|
811
|
-
When true a dict of matches is returned.
|
|
812
|
-
|
|
813
|
-
Returns
|
|
814
|
-
-------
|
|
815
|
-
namespace : str
|
|
816
|
-
The _namespace_ or None if not present.
|
|
817
|
-
object : str
|
|
818
|
-
ALF object.
|
|
819
|
-
attribute : str
|
|
820
|
-
The ALF attribute.
|
|
821
|
-
timescale : str
|
|
822
|
-
The ALF _timescale or None if not present.
|
|
823
|
-
extra : str
|
|
824
|
-
Any extra parts to the filename, or None if not present.
|
|
825
|
-
extension : str
|
|
826
|
-
The file extension.
|
|
827
|
-
|
|
828
|
-
Examples
|
|
829
|
-
--------
|
|
830
|
-
>>> alfpath = PureALFPath(
|
|
831
|
-
... 'lab/Subjects/subject/2020-01-01/001/collection/#revision#/'
|
|
832
|
-
... '_namespace_obj.times_timescale.extra.foo.ext')
|
|
833
|
-
>>> alfpath.parse_alf_name()
|
|
834
|
-
{'namespace': 'namespace',
|
|
835
|
-
'object': 'obj',
|
|
836
|
-
'attribute': 'times',
|
|
837
|
-
'timescale': 'timescale',
|
|
838
|
-
'extra': 'extra.foo',
|
|
839
|
-
'extension': 'ext'}
|
|
840
|
-
|
|
841
|
-
>>> PureALFPath('spikes.clusters.npy', as_dict=False)
|
|
842
|
-
(None, 'spikes', 'clusters', None, None, npy)
|
|
843
|
-
|
|
844
|
-
"""
|
|
845
|
-
return filename_parts(self.name, assert_valid=False, as_dict=as_dict)
|
|
846
|
-
|
|
847
|
-
@property
|
|
848
|
-
def dataset_name_parts(self):
|
|
849
|
-
"""tuple of str: the dataset name parts, with empty strings for missing parts."""
|
|
850
|
-
return tuple(p or '' for p in self.parse_alf_name(as_dict=False))
|
|
851
|
-
|
|
852
|
-
@property
|
|
853
|
-
def session_parts(self):
|
|
854
|
-
"""tuple of str: the session path parts, with empty strings for missing parts."""
|
|
855
|
-
return tuple(p or '' for p in session_path_parts(self, assert_valid=False))
|
|
856
|
-
|
|
857
|
-
@property
|
|
858
|
-
def alf_parts(self):
|
|
859
|
-
"""tuple of str: the full ALF path parts, with empty strings for missing parts."""
|
|
860
|
-
return tuple(p or '' for p in self.parse_alf_path(as_dict=False))
|
|
861
|
-
|
|
862
|
-
@property
|
|
863
|
-
def namespace(self):
|
|
864
|
-
"""str: The namespace part of the ALF name, or and empty str if not present."""
|
|
865
|
-
return self.dataset_name_parts[0]
|
|
866
|
-
|
|
867
|
-
@property
|
|
868
|
-
def object(self):
|
|
869
|
-
"""str: The object part of the ALF name, or and empty str if not present."""
|
|
870
|
-
return self.dataset_name_parts[1]
|
|
871
|
-
|
|
872
|
-
@property
|
|
873
|
-
def attribute(self):
|
|
874
|
-
"""str: The attribute part of the ALF name, or and empty str if not present."""
|
|
875
|
-
return self.dataset_name_parts[2]
|
|
876
|
-
|
|
877
|
-
@property
|
|
878
|
-
def timescale(self):
|
|
879
|
-
"""str: The timescale part of the ALF name, or and empty str if not present."""
|
|
880
|
-
return self.dataset_name_parts[3]
|
|
881
|
-
|
|
882
|
-
@property
|
|
883
|
-
def extra(self):
|
|
884
|
-
"""str: The extra part of the ALF name, or and empty str if not present."""
|
|
885
|
-
return self.dataset_name_parts[4]
|
|
886
|
-
|
|
887
|
-
def with_object(self, obj):
|
|
888
|
-
"""Return a new path with the ALF object changed.
|
|
889
|
-
|
|
890
|
-
Parameters
|
|
891
|
-
----------
|
|
892
|
-
obj : str
|
|
893
|
-
An ALF object name part to use.
|
|
894
|
-
|
|
895
|
-
Returns
|
|
896
|
-
-------
|
|
897
|
-
PureALFPath
|
|
898
|
-
The same file path but with the object part replaced with the input.
|
|
899
|
-
|
|
900
|
-
Raises
|
|
901
|
-
------
|
|
902
|
-
ALFInvalid
|
|
903
|
-
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
904
|
-
contains invalid characters).
|
|
905
|
-
|
|
906
|
-
"""
|
|
907
|
-
if not self.is_dataset():
|
|
908
|
-
raise ALFInvalid(str(self))
|
|
909
|
-
ns_obj, rest = self.name.split('.', 1)
|
|
910
|
-
ns, _ = spec.regex(FILE_SPEC.split('\\.')[0]).match(ns_obj).groups()
|
|
911
|
-
ns = f'_{ns}_' if ns else ''
|
|
912
|
-
return self.with_name(f'{ns}{obj}.{rest}')
|
|
913
|
-
|
|
914
|
-
def with_namespace(self, ns):
|
|
915
|
-
"""Return a new path with the ALF namespace added or changed.
|
|
916
|
-
|
|
917
|
-
Parameters
|
|
918
|
-
----------
|
|
919
|
-
namespace : str
|
|
920
|
-
An ALF namespace part to use.
|
|
921
|
-
|
|
922
|
-
Returns
|
|
923
|
-
-------
|
|
924
|
-
PureALFPath
|
|
925
|
-
The same file path but with the namespace part added/replaced with the input.
|
|
926
|
-
|
|
927
|
-
Raises
|
|
928
|
-
------
|
|
929
|
-
ALFInvalid
|
|
930
|
-
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
931
|
-
contains invalid characters).
|
|
932
|
-
|
|
933
|
-
"""
|
|
934
|
-
if not self.is_dataset():
|
|
935
|
-
raise ALFInvalid(self)
|
|
936
|
-
ns_obj, rest = self.name.split('.', 1)
|
|
937
|
-
_, obj = spec.regex(FILE_SPEC.split('\\.')[0]).match(ns_obj).groups()
|
|
938
|
-
ns = f'_{ns}_' if ns else ''
|
|
939
|
-
return self.with_name(f'{ns}{obj}.{rest}')
|
|
940
|
-
|
|
941
|
-
def with_attribute(self, attr):
|
|
942
|
-
"""Return a new path with the ALF attribute changed.
|
|
943
|
-
|
|
944
|
-
Parameters
|
|
945
|
-
----------
|
|
946
|
-
attribute : str
|
|
947
|
-
An ALF attribute part to use.
|
|
948
|
-
|
|
949
|
-
Returns
|
|
950
|
-
-------
|
|
951
|
-
PureALFPath
|
|
952
|
-
The same file path but with the attribute part replaced with the input.
|
|
953
|
-
|
|
954
|
-
Raises
|
|
955
|
-
------
|
|
956
|
-
ALFInvalid
|
|
957
|
-
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
958
|
-
contains invalid characters).
|
|
959
|
-
|
|
960
|
-
"""
|
|
961
|
-
if not self.is_dataset():
|
|
962
|
-
raise ALFInvalid(self)
|
|
963
|
-
ns_obj, attr_ts, rest = self.name.split('.', 2)
|
|
964
|
-
_, ts = spec.regex('{attribute}(?:_{timescale})?').match(attr_ts).groups()
|
|
965
|
-
ts = f'_{ts}' if ts else ''
|
|
966
|
-
return self.with_name(f'{ns_obj}.{attr}{ts}.{rest}')
|
|
967
|
-
|
|
968
|
-
def with_timescale(self, timescale):
|
|
969
|
-
"""Return a new path with the ALF timescale added or changed.
|
|
970
|
-
|
|
971
|
-
Parameters
|
|
972
|
-
----------
|
|
973
|
-
timescale : str
|
|
974
|
-
An ALF timescale part to use.
|
|
975
|
-
|
|
976
|
-
Returns
|
|
977
|
-
-------
|
|
978
|
-
PureALFPath
|
|
979
|
-
The same file path but with the timescale part added/replaced with the input.
|
|
980
|
-
|
|
981
|
-
Raises
|
|
982
|
-
------
|
|
983
|
-
ALFInvalid
|
|
984
|
-
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
985
|
-
contains invalid characters).
|
|
986
|
-
|
|
987
|
-
"""
|
|
988
|
-
if not self.is_dataset():
|
|
989
|
-
raise ALFInvalid(self)
|
|
990
|
-
ns_obj, attr_ts, rest = self.name.split('.', 2)
|
|
991
|
-
attr, _ = spec.regex('{attribute}(?:_{timescale})?').match(attr_ts).groups()
|
|
992
|
-
ts = f'_{timescale}' if timescale else ''
|
|
993
|
-
return self.with_name(f'{ns_obj}.{attr}{ts}.{rest}')
|
|
994
|
-
|
|
995
|
-
def with_extra(self, extra, append=False):
|
|
996
|
-
"""Return a new path with extra ALF parts added or changed.
|
|
997
|
-
|
|
998
|
-
Parameters
|
|
999
|
-
----------
|
|
1000
|
-
extra : str, list of str
|
|
1001
|
-
Extra ALF parts to add/replace.
|
|
1002
|
-
append : bool
|
|
1003
|
-
When false (default) any existing extra parts are replaced instead of added to.
|
|
1004
|
-
|
|
1005
|
-
Returns
|
|
1006
|
-
-------
|
|
1007
|
-
PureALFPath
|
|
1008
|
-
The same file path but with the extra part(s) replaced or appended to with the input.
|
|
1009
|
-
|
|
1010
|
-
Raises
|
|
1011
|
-
------
|
|
1012
|
-
ALFInvalid
|
|
1013
|
-
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
1014
|
-
contains invalid characters).
|
|
1015
|
-
|
|
1016
|
-
"""
|
|
1017
|
-
if not self.is_dataset():
|
|
1018
|
-
raise ALFInvalid(self)
|
|
1019
|
-
parts = self.stem.split('.', 2)
|
|
1020
|
-
if isinstance(extra, str):
|
|
1021
|
-
extra = extra.strip('.').split('.')
|
|
1022
|
-
if (prev := parts.pop() if len(parts) > 2 else None) and append:
|
|
1023
|
-
extra = (prev, *extra)
|
|
1024
|
-
obj_attr = '.'.join(parts)
|
|
1025
|
-
if extra := '.'.join(filter(None, extra)):
|
|
1026
|
-
return self.with_stem(f'{obj_attr}.{extra}')
|
|
1027
|
-
else:
|
|
1028
|
-
return self.with_stem(obj_attr)
|
|
1029
|
-
|
|
1030
|
-
def with_extension(self, ext):
|
|
1031
|
-
"""Return a new path with the ALF extension (suffix) changed.
|
|
1032
|
-
|
|
1033
|
-
Note that unlike PurePath's `with_suffix` method, this asserts that the filename is a valid
|
|
1034
|
-
ALF dataset and the `ext` argument should be without the period.
|
|
1035
|
-
|
|
1036
|
-
Parameters
|
|
1037
|
-
----------
|
|
1038
|
-
ext : str
|
|
1039
|
-
An ALF extension part to use (sans period).
|
|
1040
|
-
|
|
1041
|
-
Returns
|
|
1042
|
-
-------
|
|
1043
|
-
PureALFPath
|
|
1044
|
-
The same file path but with the extension part replaced with the input.
|
|
1045
|
-
|
|
1046
|
-
Raises
|
|
1047
|
-
------
|
|
1048
|
-
ALFInvalid
|
|
1049
|
-
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
1050
|
-
contains invalid characters).
|
|
1051
|
-
|
|
1052
|
-
"""
|
|
1053
|
-
if not self.is_dataset():
|
|
1054
|
-
raise ALFInvalid(str(self))
|
|
1055
|
-
return self.with_suffix(f'.{ext}')
|
|
1056
|
-
|
|
1057
|
-
def with_padded_sequence(path):
|
|
1058
|
-
"""Ensures a file path contains a zero-padded experiment sequence folder.
|
|
1059
|
-
|
|
1060
|
-
Parameters
|
|
1061
|
-
----------
|
|
1062
|
-
path : str pathlib.PurePath
|
|
1063
|
-
A session or file path to convert.
|
|
1064
|
-
|
|
1065
|
-
Returns
|
|
1066
|
-
-------
|
|
1067
|
-
ALFPath, PureALFPath
|
|
1068
|
-
The same path but with the experiment sequence folder zero-padded. If a PurePath was
|
|
1069
|
-
passed, a PurePath will be returned, otherwise a Path object is returned.
|
|
1070
|
-
|
|
1071
|
-
Examples
|
|
1072
|
-
--------
|
|
1073
|
-
Supports calling as static function
|
|
1074
|
-
|
|
1075
|
-
>>> file_path = '/iblrigdata/subject/2023-01-01/1/_ibl_experiment.description.yaml'
|
|
1076
|
-
>>> ALFPath.with_padded_sequence(file_path)
|
|
1077
|
-
ALFPath('/iblrigdata/subject/2023-01-01/001/_ibl_experiment.description.yaml')
|
|
1078
|
-
|
|
1079
|
-
Supports folders and will not affect already padded paths
|
|
1080
|
-
|
|
1081
|
-
>>> ALFPath('subject/2023-01-01/001').with_padded_sequence(file_path)
|
|
1082
|
-
ALFPath('subject/2023-01-01/001')
|
|
1083
|
-
|
|
1084
|
-
"""
|
|
1085
|
-
return padded_sequence(path)
|
|
1086
|
-
|
|
1087
|
-
def with_revision(self, revision):
|
|
1088
|
-
"""Return a new path with the ALF revision part added/changed.
|
|
1089
|
-
|
|
1090
|
-
Parameters
|
|
1091
|
-
----------
|
|
1092
|
-
revision : str
|
|
1093
|
-
An ALF revision part to use (NB: do not include the pound sign '#').
|
|
1094
|
-
|
|
1095
|
-
Returns
|
|
1096
|
-
-------
|
|
1097
|
-
PureALFPath
|
|
1098
|
-
The same file path but with the revision part added or replaced with the input.
|
|
1099
|
-
|
|
1100
|
-
Examples
|
|
1101
|
-
--------
|
|
1102
|
-
If not in the ALF path, one will be added
|
|
1103
|
-
|
|
1104
|
-
>>> ALFPath('/subject/2023-01-01/1/alf/obj.attr.ext').with_revision('revision')
|
|
1105
|
-
ALFPath('/subject/2023-01-01/1/alf/#xxx#/obj.attr.ext')
|
|
1106
|
-
|
|
1107
|
-
If a revision is already in the ALF path it will be replaced
|
|
1108
|
-
|
|
1109
|
-
>>> ALFPath('/subject/2023-01-01/1/alf/#revision#/obj.attr.ext').with_revision('xxx')
|
|
1110
|
-
ALFPath('/subject/2023-01-01/1/alf/#xxx#/obj.attr.ext')
|
|
1111
|
-
|
|
1112
|
-
Raises
|
|
1113
|
-
------
|
|
1114
|
-
ALFInvalid
|
|
1115
|
-
The ALF path is not valid or is relative to the session path. The path must include
|
|
1116
|
-
the session parts otherwise the path is too ambiguous to determine validity.
|
|
1117
|
-
ALFInvalid
|
|
1118
|
-
The revision provided does not match the ALF specification pattern.
|
|
1119
|
-
|
|
1120
|
-
See Also
|
|
1121
|
-
--------
|
|
1122
|
-
PureALFPath.without_revision
|
|
1123
|
-
|
|
1124
|
-
"""
|
|
1125
|
-
# Validate the revision input
|
|
1126
|
-
revision, = _path_parts(revision, '^{revision}$', match=True, assert_valid=True)
|
|
1127
|
-
if PureALFPath.is_dataset(self):
|
|
1128
|
-
return self.without_revision().parent / f'#{revision}#' / self.name
|
|
1129
|
-
else:
|
|
1130
|
-
return self.without_revision() / f'#{revision}#'
|
|
1131
|
-
|
|
1132
|
-
def without_revision(self):
|
|
1133
|
-
"""Return a new path with the ALF revision part removed.
|
|
1134
|
-
|
|
1135
|
-
Returns
|
|
1136
|
-
-------
|
|
1137
|
-
PureALFPath
|
|
1138
|
-
The same file path but with the revision part removed.
|
|
1139
|
-
|
|
1140
|
-
Examples
|
|
1141
|
-
--------
|
|
1142
|
-
If not in the ALF path, no change occurs
|
|
1143
|
-
|
|
1144
|
-
>>> ALFPath('/subject/2023-01-01/1/alf/obj.attr.ext').with_revision('revision')
|
|
1145
|
-
ALFPath('/subject/2023-01-01/1/alf/obj.attr.ext')
|
|
1146
|
-
|
|
1147
|
-
If a revision is in the ALF path it will be removed
|
|
1148
|
-
|
|
1149
|
-
>>> ALFPath('/subject/2023-01-01/1/alf/#revision#/obj.attr.ext').without_revision()
|
|
1150
|
-
ALFPath('/subject/2023-01-01/1/alf/obj.attr.ext')
|
|
1151
|
-
|
|
1152
|
-
Raises
|
|
1153
|
-
------
|
|
1154
|
-
ALFInvalid
|
|
1155
|
-
The ALF path is not valid or is relative to the session path. The path must include
|
|
1156
|
-
the session parts otherwise the path is too ambiguous to determine validity.
|
|
1157
|
-
|
|
1158
|
-
See Also
|
|
1159
|
-
--------
|
|
1160
|
-
PureALFPath.with_revision
|
|
1161
|
-
|
|
1162
|
-
"""
|
|
1163
|
-
if PureALFPath.is_dataset(self):
|
|
1164
|
-
# Is a file path (rather than folder path)
|
|
1165
|
-
return without_revision(self)
|
|
1166
|
-
if not self.is_valid_alf():
|
|
1167
|
-
raise ALFInvalid(f'{self} not a valid ALF path or is relative to session')
|
|
1168
|
-
elif spec.regex('^#{revision}#$').match(self.name):
|
|
1169
|
-
# Includes revision
|
|
1170
|
-
return self.parent
|
|
1171
|
-
else:
|
|
1172
|
-
# Does not include revision
|
|
1173
|
-
return self
|
|
1174
|
-
|
|
1175
|
-
def with_uuid(self, uuid):
|
|
1176
|
-
"""Return a new path with the ALF UUID part added/changed.
|
|
1177
|
-
|
|
1178
|
-
Parameters
|
|
1179
|
-
----------
|
|
1180
|
-
uuid : str, uuid.UUID
|
|
1181
|
-
The UUID to add.
|
|
1182
|
-
|
|
1183
|
-
Returns
|
|
1184
|
-
-------
|
|
1185
|
-
PureALFPath
|
|
1186
|
-
A new ALFPath object with a UUID in the filename.
|
|
1187
|
-
|
|
1188
|
-
Examples
|
|
1189
|
-
--------
|
|
1190
|
-
>>> uuid = 'a976e418-c8b8-4d24-be47-d05120b18341'
|
|
1191
|
-
>>> ALFPath('/path/to/trials.intervals.npy').with_uuid(uuid)
|
|
1192
|
-
ALFPath('/path/to/trials.intervals.a976e418-c8b8-4d24-be47-d05120b18341.npy')
|
|
1193
|
-
|
|
1194
|
-
Raises
|
|
1195
|
-
------
|
|
1196
|
-
ValueError
|
|
1197
|
-
`uuid` must be a valid hyphen-separated hexadecimal UUID.
|
|
1198
|
-
ALFInvalid
|
|
1199
|
-
Path is not a valid ALF file path.
|
|
1200
|
-
|
|
1201
|
-
"""
|
|
1202
|
-
if not self.is_dataset():
|
|
1203
|
-
raise ALFInvalid(f'{self} is not a valid ALF dataset file path')
|
|
1204
|
-
return add_uuid_string(self, uuid)
|
|
1205
|
-
|
|
1206
|
-
def without_uuid(self):
|
|
1207
|
-
"""Return a new path with the ALF UUID part removed.
|
|
1208
|
-
|
|
1209
|
-
Returns
|
|
1210
|
-
-------
|
|
1211
|
-
PureALFPath
|
|
1212
|
-
A new ALFPath object with a UUID removed from the filename, if present.
|
|
1213
|
-
|
|
1214
|
-
Examples
|
|
1215
|
-
--------
|
|
1216
|
-
>>> alfpath = ALFPath('/path/to/trials.intervals.a976e418-c8b8-4d24-be47-d05120b18341.npy')
|
|
1217
|
-
>>> alfpath.without_uuid(uuid)
|
|
1218
|
-
ALFPath('/path/to/trials.intervals.npy')
|
|
1219
|
-
|
|
1220
|
-
>>> ALFPath('/path/to/trials.intervals.npy').without_uuid(uuid)
|
|
1221
|
-
ALFPath('/path/to/trials.intervals.npy')
|
|
1222
|
-
|
|
1223
|
-
"""
|
|
1224
|
-
return remove_uuid_string(self) if self.is_dataset() else self
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
class ALFPath(PureALFPath):
|
|
1228
|
-
"""Base class for manipulating Alyx file (ALF) paths with system calls.
|
|
1229
|
-
|
|
1230
|
-
Similar to a pathlib Path object but with methods for validating, parsing, and replacing ALF
|
|
1231
|
-
path parts. This class also contains methods that work on system files.
|
|
1232
|
-
|
|
1233
|
-
Parameters
|
|
1234
|
-
----------
|
|
1235
|
-
args : str, pathlib.PurePath
|
|
1236
|
-
One or more pathlike objects to combine into an ALF path object.
|
|
1237
|
-
|
|
1238
|
-
"""
|
|
1239
|
-
|
|
1240
|
-
def __new__(cls, *args):
|
|
1241
|
-
"""Construct a ALFPurePath from one or several strings and or existing PurePath objects.
|
|
1242
|
-
|
|
1243
|
-
The strings and path objects are combined so as to yield a canonicalized path, which is
|
|
1244
|
-
incorporated into the new PurePath object.
|
|
1245
|
-
"""
|
|
1246
|
-
return super().__new__(WindowsALFPath if os.name == 'nt' else PosixALFPath, *args)
|
|
1247
|
-
|
|
1248
|
-
def is_dataset(self) -> bool:
|
|
1249
|
-
"""Determine if path is an ALF dataset, rather than a folder.
|
|
1250
|
-
|
|
1251
|
-
Unlike pathlib and PureALFPath methods, this will return False if the path exists but
|
|
1252
|
-
is a folder, otherwise this simply tests the path name, whether it exists or not.
|
|
1253
|
-
|
|
1254
|
-
Returns
|
|
1255
|
-
-------
|
|
1256
|
-
bool
|
|
1257
|
-
True if filename is ALF dataset.
|
|
1258
|
-
|
|
1259
|
-
"""
|
|
1260
|
-
return not self.is_dir() and spec.is_valid(self.name)
|
|
1261
|
-
|
|
1262
|
-
def is_session_path(self) -> bool:
|
|
1263
|
-
"""Check if path is a valid ALF session path.
|
|
1264
|
-
|
|
1265
|
-
This returns true if the input path matches the ALF session path specification.
|
|
1266
|
-
This method can be used as a static method with any pathlike input, or as an instance
|
|
1267
|
-
method.
|
|
1268
|
-
|
|
1269
|
-
Unlike the PureALFPath method, this will return false if the path matches but is in fact
|
|
1270
|
-
a file on disk.
|
|
1271
|
-
|
|
1272
|
-
Parameters
|
|
1273
|
-
----------
|
|
1274
|
-
path : str, pathlib.PurePath
|
|
1275
|
-
A session path to check the validity of.
|
|
1276
|
-
|
|
1277
|
-
Returns
|
|
1278
|
-
-------
|
|
1279
|
-
bool
|
|
1280
|
-
True if the path is recognized as a valid ALF session path.
|
|
1281
|
-
|
|
1282
|
-
Examples
|
|
1283
|
-
--------
|
|
1284
|
-
>>> ALFPath('/home/foo/2020-01-01/001').is_session_path()
|
|
1285
|
-
True
|
|
1286
|
-
|
|
1287
|
-
>>> ALFPath('/home/foo/2020-01-01/001/alf/spikes.times.npy').is_session_path()
|
|
1288
|
-
False
|
|
1289
|
-
|
|
1290
|
-
>>> ALFPath.is_session_path('_ibl_wheel.timestamps.npy')
|
|
1291
|
-
False
|
|
1292
|
-
|
|
1293
|
-
>>> ALFPath.is_valid_alf('lab/Subjects/foo/2020-01-01/001')
|
|
1294
|
-
True
|
|
1295
|
-
|
|
1296
|
-
See Also
|
|
1297
|
-
--------
|
|
1298
|
-
PureALFPath.is_valid_alf - Test whether path is generally valid a valid ALF path.
|
|
1299
|
-
PureALFPath.session_path_parts - Returns parsed session path parts as tuple of str.
|
|
1300
|
-
|
|
1301
|
-
"""
|
|
1302
|
-
return not self.is_file() and spec.is_session_path(self)
|
|
1303
|
-
|
|
1304
|
-
def is_valid_alf(path) -> bool:
|
|
1305
|
-
"""Check if path is a valid ALF path.
|
|
1306
|
-
|
|
1307
|
-
This returns true if the input path matches any part of the ALF path specification.
|
|
1308
|
-
This method can be used as a static method with any pathlike input, or as an instance
|
|
1309
|
-
method. This will validate both directory paths and file paths.
|
|
1310
|
-
|
|
1311
|
-
Unlike the PureALFPath method, this one will return false if the path matches a dataset
|
|
1312
|
-
file pattern but is actually a folder on disk, or if the path matches as a file but is
|
|
1313
|
-
is a folder on disk.
|
|
1314
|
-
|
|
1315
|
-
Parameters
|
|
1316
|
-
----------
|
|
1317
|
-
path : str, pathlib.PurePath
|
|
1318
|
-
A path to check the validity of.
|
|
1319
|
-
|
|
1320
|
-
Returns
|
|
1321
|
-
-------
|
|
1322
|
-
bool
|
|
1323
|
-
True if the path is recognized as a valid ALF path.
|
|
1324
|
-
|
|
1325
|
-
Examples
|
|
1326
|
-
--------
|
|
1327
|
-
>>> ALFPath('/home/foo/2020-01-01/001').is_valid_alf()
|
|
1328
|
-
True
|
|
1329
|
-
|
|
1330
|
-
>>> ALFPath('/home/foo/2020-01-01/001/alf/spikes.times.npy').is_valid_alf()
|
|
1331
|
-
True
|
|
1332
|
-
|
|
1333
|
-
>>> ALFPath.is_valid_alf('_ibl_wheel.timestamps.npy')
|
|
1334
|
-
True
|
|
1335
|
-
|
|
1336
|
-
>>> ALFPath.is_valid_alf('foo.bar')
|
|
1337
|
-
False
|
|
1338
|
-
|
|
1339
|
-
See Also
|
|
1340
|
-
--------
|
|
1341
|
-
PureALFPath.is_dataset - Test whether file name is valid as well as directory path.
|
|
1342
|
-
full_path_parts - Validates path and returns the parsed ALF path parts.
|
|
1343
|
-
|
|
1344
|
-
"""
|
|
1345
|
-
try:
|
|
1346
|
-
parsed = full_path_parts(path, as_dict=True)
|
|
1347
|
-
except ALFInvalid:
|
|
1348
|
-
return False
|
|
1349
|
-
is_dataset = parsed['object'] is not None
|
|
1350
|
-
if isinstance(path, str):
|
|
1351
|
-
path = ALFPath(path)
|
|
1352
|
-
if hasattr(path, 'is_file') and path.is_file():
|
|
1353
|
-
return is_dataset
|
|
1354
|
-
elif hasattr(path, 'is_dir') and path.is_dir():
|
|
1355
|
-
return not is_dataset
|
|
1356
|
-
return True
|
|
1357
|
-
|
|
1358
|
-
def iter_datasets(self, recursive=False):
|
|
1359
|
-
"""Iterate over all files in path, and yield relative dataset paths.
|
|
1360
|
-
|
|
1361
|
-
Parameters
|
|
1362
|
-
----------
|
|
1363
|
-
recursive : bool
|
|
1364
|
-
If true, yield datasets in subdirectories.
|
|
1365
|
-
|
|
1366
|
-
Yields
|
|
1367
|
-
------
|
|
1368
|
-
ALFPath
|
|
1369
|
-
The next valid dataset path in lexicographical order.
|
|
1370
|
-
|
|
1371
|
-
See Also
|
|
1372
|
-
--------
|
|
1373
|
-
one.alf.io.iter_datasets - Equivalent function that can take any pathlike input and returns
|
|
1374
|
-
paths relative to the input path.
|
|
1375
|
-
|
|
1376
|
-
"""
|
|
1377
|
-
glob = self.rglob if recursive else self.glob
|
|
1378
|
-
for p in sorted(glob('*.*.*')):
|
|
1379
|
-
if not p.is_dir() and p.is_dataset:
|
|
1380
|
-
yield p
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
class PureWindowsALFPath(pathlib.PureWindowsPath, PureALFPath):
|
|
1384
|
-
"""PureALFPath subclass for Windows systems."""
|
|
1385
|
-
|
|
1386
|
-
pass
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
class PurePosixALFPath(pathlib.PurePosixPath, PureALFPath):
|
|
1390
|
-
"""PureALFPath subclass for non-Windows systems."""
|
|
1391
|
-
|
|
1392
|
-
pass
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
class WindowsALFPath(pathlib.WindowsPath, ALFPath):
|
|
1396
|
-
"""ALFPath subclass for Windows systems."""
|
|
1397
|
-
|
|
1398
|
-
pass
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
class PosixALFPath(pathlib.PosixPath, ALFPath):
|
|
1402
|
-
"""ALFPath subclass for non-Windows systems."""
|
|
1403
|
-
|
|
1404
|
-
pass
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
def ensure_alf_path(path) -> Listable(PureALFPath):
|
|
1408
|
-
"""Ensure path is a PureALFPath instance.
|
|
1409
|
-
|
|
1410
|
-
Ensures the path entered is cast to a PureALFPath instance. If input class is PureALFPath or
|
|
1411
|
-
pathlib.PurePath, a PureALFPath instance is returned, otherwise an ALFPath instance is
|
|
1412
|
-
returned.
|
|
1413
|
-
|
|
1414
|
-
Parameters
|
|
1415
|
-
----------
|
|
1416
|
-
path : str, pathlib.PurePath, ALFPath, iterable
|
|
1417
|
-
One or more path-like objects.
|
|
1418
|
-
|
|
1419
|
-
Returns
|
|
1420
|
-
-------
|
|
1421
|
-
ALFPath, PureALFPath, list of ALFPath, list of PureALFPath
|
|
1422
|
-
One or more ALFPath objects.
|
|
1423
|
-
|
|
1424
|
-
Raises
|
|
1425
|
-
------
|
|
1426
|
-
TypeError
|
|
1427
|
-
Unexpected path instance; input must be a str or pathlib.PurePath instance, or an
|
|
1428
|
-
iterable thereof.
|
|
1429
|
-
|
|
1430
|
-
"""
|
|
1431
|
-
if isinstance(path, PureALFPath):
|
|
1432
|
-
# Already an ALFPath instance
|
|
1433
|
-
return path
|
|
1434
|
-
if isinstance(path, pathlib.PurePath):
|
|
1435
|
-
# Cast pathlib instance to equivalent ALFPath
|
|
1436
|
-
if isinstance(path, pathlib.Path):
|
|
1437
|
-
return ALFPath(path)
|
|
1438
|
-
elif isinstance(path, pathlib.PurePosixPath):
|
|
1439
|
-
return PurePosixALFPath(path)
|
|
1440
|
-
elif isinstance(path, pathlib.PureWindowsPath):
|
|
1441
|
-
return PureWindowsALFPath(path)
|
|
1442
|
-
else:
|
|
1443
|
-
return PureALFPath(path)
|
|
1444
|
-
if isinstance(path, str):
|
|
1445
|
-
# Cast str to ALFPath
|
|
1446
|
-
return ALFPath(path)
|
|
1447
|
-
if isinstance(path, Iterable):
|
|
1448
|
-
# Cast list, generator, tuple, etc. to list of ALFPath
|
|
1449
|
-
return list(map(ensure_alf_path, path))
|
|
1450
|
-
raise TypeError(f'expected os.PathLike type, got {type(path)} instead')
|
|
1
|
+
"""Module for identifying and parsing ALF file names.
|
|
2
|
+
|
|
3
|
+
An ALF file has the following components (those in brackets are optional):
|
|
4
|
+
(_namespace_)object.attribute(_timescale)(.extra.parts).ext
|
|
5
|
+
|
|
6
|
+
Note the following:
|
|
7
|
+
Object attributes may not contain an underscore unless followed by 'times' or 'intervals'.
|
|
8
|
+
A namespace must not contain extra underscores (i.e. `name_space` and `__namespace__` are not
|
|
9
|
+
valid).
|
|
10
|
+
ALF files must always have an extension.
|
|
11
|
+
|
|
12
|
+
For more information, see the following documentation:
|
|
13
|
+
https://int-brain-lab.github.io/ONE/alf_intro.html
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ALFPath differences
|
|
17
|
+
-------------------
|
|
18
|
+
ALFPath.iter_datasets returns full paths (close the pathlib.Path.iterdir), whereas
|
|
19
|
+
alf.io.iter_datasets returns relative paths as POSIX strings (TODO).
|
|
20
|
+
|
|
21
|
+
ALFPath.parse_* methods return a dict by default, whereas parse_* functions return
|
|
22
|
+
tuples by default. Additionally, the parse_* functions raise ALFInvalid errors by
|
|
23
|
+
default if the path can't be parsed. ALFPath.parse_* methods have no validation
|
|
24
|
+
option.
|
|
25
|
+
|
|
26
|
+
ALFPath properties return empty str instead of None if ALF part isn't present..
|
|
27
|
+
"""
|
|
28
|
+
import os
|
|
29
|
+
import pathlib
|
|
30
|
+
from collections import OrderedDict
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
from typing import Union, Optional, Iterable
|
|
33
|
+
import logging
|
|
34
|
+
|
|
35
|
+
from iblutil.util import Listable
|
|
36
|
+
|
|
37
|
+
from .exceptions import ALFInvalid
|
|
38
|
+
from . import spec
|
|
39
|
+
from .spec import SESSION_SPEC, COLLECTION_SPEC, FILE_SPEC, REL_PATH_SPEC
|
|
40
|
+
|
|
41
|
+
_logger = logging.getLogger(__name__)
|
|
42
|
+
__all__ = [
|
|
43
|
+
'ALFPath', 'PureALFPath', 'WindowsALFPath', 'PosixALFPath',
|
|
44
|
+
'PureWindowsALFPath', 'PurePosixALFPath'
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def rel_path_parts(rel_path, as_dict=False, assert_valid=True):
|
|
49
|
+
"""Parse a relative path into the relevant parts.
|
|
50
|
+
|
|
51
|
+
A relative path follows the pattern
|
|
52
|
+
(collection/)(#revision#/)_namespace_object.attribute_timescale.extra.extension
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
rel_path : str, pathlib.Path
|
|
57
|
+
A relative path string.
|
|
58
|
+
as_dict : bool
|
|
59
|
+
If true, an OrderedDict of parts are returned with the keys ('lab', 'subject', 'date',
|
|
60
|
+
'number'), otherwise a tuple of values are returned.
|
|
61
|
+
assert_valid : bool
|
|
62
|
+
If true an ALFInvalid is raised when the session cannot be parsed, otherwise an empty
|
|
63
|
+
dict of tuple of Nones is returned.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
OrderedDict, tuple
|
|
68
|
+
A dict if as_dict is true, or a tuple of parsed values.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
return _path_parts(rel_path, REL_PATH_SPEC, True, as_dict, assert_valid)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def session_path_parts(session_path, as_dict=False, assert_valid=True):
|
|
75
|
+
"""Parse a session path into the relevant parts.
|
|
76
|
+
|
|
77
|
+
Return keys:
|
|
78
|
+
- lab
|
|
79
|
+
- subject
|
|
80
|
+
- date
|
|
81
|
+
- number
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
session_path : str, pathlib.Path
|
|
86
|
+
A session path string.
|
|
87
|
+
as_dict : bool
|
|
88
|
+
If true, an OrderedDict of parts are returned with the keys ('lab', 'subject', 'date',
|
|
89
|
+
'number'), otherwise a tuple of values are returned.
|
|
90
|
+
assert_valid : bool
|
|
91
|
+
If true an ALFInvalid is raised when the session cannot be parsed, otherwise an empty
|
|
92
|
+
dict of tuple of Nones is returned.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
OrderedDict, tuple
|
|
97
|
+
A dict if as_dict is true, or a tuple of parsed values.
|
|
98
|
+
|
|
99
|
+
Raises
|
|
100
|
+
------
|
|
101
|
+
ALFInvalid
|
|
102
|
+
Invalid ALF session path (assert_valid is True).
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
return _path_parts(session_path, SESSION_SPEC, False, as_dict, assert_valid)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _path_parts(path, spec_str, match=True, as_dict=False, assert_valid=True):
|
|
109
|
+
"""Given a ALF and a spec string, parse into parts.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
path : str, pathlib.Path
|
|
114
|
+
An ALF path or dataset.
|
|
115
|
+
match : bool
|
|
116
|
+
If True, string must match exactly, otherwise search for expression within path.
|
|
117
|
+
as_dict : bool
|
|
118
|
+
When true a dict of matches is returned.
|
|
119
|
+
assert_valid : bool
|
|
120
|
+
When true an exception is raised when the filename cannot be parsed.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
OrderedDict, tuple
|
|
125
|
+
A dict if as_dict is true, or a tuple of parsed values.
|
|
126
|
+
|
|
127
|
+
Raises
|
|
128
|
+
------
|
|
129
|
+
ALFInvalid
|
|
130
|
+
Invalid ALF path (assert_valid is True).
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
if hasattr(path, 'as_posix'):
|
|
134
|
+
path = path.as_posix()
|
|
135
|
+
pattern = spec.regex(spec_str)
|
|
136
|
+
empty = OrderedDict.fromkeys(pattern.groupindex.keys())
|
|
137
|
+
parsed = (pattern.match if match else pattern.search)(path)
|
|
138
|
+
if parsed: # py3.8
|
|
139
|
+
parsed_dict = parsed.groupdict()
|
|
140
|
+
return OrderedDict(parsed_dict) if as_dict else tuple(parsed_dict.values())
|
|
141
|
+
elif assert_valid:
|
|
142
|
+
raise ALFInvalid(path)
|
|
143
|
+
else:
|
|
144
|
+
return empty if as_dict else tuple(empty.values())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def filename_parts(filename, as_dict=False, assert_valid=True) -> Union[dict, tuple]:
|
|
148
|
+
"""Return the parsed elements of a given ALF filename.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
filename : str
|
|
153
|
+
The name of the file.
|
|
154
|
+
as_dict : bool
|
|
155
|
+
When true a dict of matches is returned.
|
|
156
|
+
assert_valid : bool
|
|
157
|
+
When true an exception is raised when the filename cannot be parsed.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
namespace : str
|
|
162
|
+
The _namespace_ or None if not present.
|
|
163
|
+
object : str
|
|
164
|
+
ALF object.
|
|
165
|
+
attribute : str
|
|
166
|
+
The ALF attribute.
|
|
167
|
+
timescale : str
|
|
168
|
+
The ALF _timescale or None if not present.
|
|
169
|
+
extra : str
|
|
170
|
+
Any extra parts to the filename, or None if not present.
|
|
171
|
+
extension : str
|
|
172
|
+
The file extension.
|
|
173
|
+
|
|
174
|
+
Examples
|
|
175
|
+
--------
|
|
176
|
+
>>> filename_parts('_namespace_obj.times_timescale.extra.foo.ext')
|
|
177
|
+
('namespace', 'obj', 'times', 'timescale', 'extra.foo', 'ext')
|
|
178
|
+
>>> filename_parts('spikes.clusters.npy', as_dict=True)
|
|
179
|
+
{'namespace': None,
|
|
180
|
+
'object': 'spikes',
|
|
181
|
+
'attribute': 'clusters',
|
|
182
|
+
'timescale': None,
|
|
183
|
+
'extra': None,
|
|
184
|
+
'extension': 'npy'}
|
|
185
|
+
>>> filename_parts('spikes.times_ephysClock.npy')
|
|
186
|
+
(None, 'spikes', 'times', 'ephysClock', None, 'npy')
|
|
187
|
+
>>> filename_parts('_iblmic_audioSpectrogram.frequencies.npy')
|
|
188
|
+
('iblmic', 'audioSpectrogram', 'frequencies', None, None, 'npy')
|
|
189
|
+
>>> filename_parts('_spikeglx_ephysData_g0_t0.imec.wiring.json')
|
|
190
|
+
('spikeglx', 'ephysData_g0_t0', 'imec', None, 'wiring', 'json')
|
|
191
|
+
>>> filename_parts('_spikeglx_ephysData_g0_t0.imec0.lf.bin')
|
|
192
|
+
('spikeglx', 'ephysData_g0_t0', 'imec0', None, 'lf', 'bin')
|
|
193
|
+
>>> filename_parts('_ibl_trials.goCue_times_bpod.csv')
|
|
194
|
+
('ibl', 'trials', 'goCue_times', 'bpod', None, 'csv')
|
|
195
|
+
|
|
196
|
+
Raises
|
|
197
|
+
------
|
|
198
|
+
ALFInvalid
|
|
199
|
+
Invalid ALF dataset (assert_valid is True).
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
return _path_parts(filename, FILE_SPEC, True, as_dict, assert_valid)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def full_path_parts(path, as_dict=False, assert_valid=True) -> Union[dict, tuple]:
|
|
206
|
+
"""Parse all filename and folder parts.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
path : str, pathlib.Path.
|
|
211
|
+
The ALF path
|
|
212
|
+
as_dict : bool
|
|
213
|
+
When true a dict of matches is returned.
|
|
214
|
+
assert_valid : bool
|
|
215
|
+
When true an exception is raised when the filename cannot be parsed.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
OrderedDict, tuple
|
|
220
|
+
A dict if as_dict is true, or a tuple of parsed values.
|
|
221
|
+
|
|
222
|
+
Examples
|
|
223
|
+
--------
|
|
224
|
+
>>> full_path_parts(
|
|
225
|
+
... 'lab/Subjects/subject/2020-01-01/001/collection/#revision#/'
|
|
226
|
+
... '_namespace_obj.times_timescale.extra.foo.ext')
|
|
227
|
+
('lab', 'subject', '2020-01-01', '001', 'collection', 'revision',
|
|
228
|
+
'namespace', 'obj', 'times','timescale', 'extra.foo', 'ext')
|
|
229
|
+
>>> full_path_parts('spikes.clusters.npy', as_dict=True)
|
|
230
|
+
{'lab': None,
|
|
231
|
+
'subject': None,
|
|
232
|
+
'date': None,
|
|
233
|
+
'number': None,
|
|
234
|
+
'collection': None,
|
|
235
|
+
'revision': None,
|
|
236
|
+
'namespace': None,
|
|
237
|
+
'object': 'spikes',
|
|
238
|
+
'attribute': 'clusters',
|
|
239
|
+
'timescale': None,
|
|
240
|
+
'extra': None,
|
|
241
|
+
'extension': 'npy'}
|
|
242
|
+
|
|
243
|
+
Raises
|
|
244
|
+
------
|
|
245
|
+
ALFInvalid
|
|
246
|
+
Invalid ALF path (assert_valid is True).
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
path = pathlib.Path(path)
|
|
250
|
+
# NB We try to determine whether we have a folder or filename path. Filenames contain at
|
|
251
|
+
# least two periods, however it is currently permitted to have any number of periods in a
|
|
252
|
+
# collection, making the ALF path ambiguous.
|
|
253
|
+
if sum(x == '.' for x in path.name) < 2: # folder only
|
|
254
|
+
folders = folder_parts(path, as_dict, assert_valid)
|
|
255
|
+
if assert_valid:
|
|
256
|
+
# Edge case: ensure is indeed folder by checking that name is in parts
|
|
257
|
+
invalid_file = path.name not in (folders.values() if as_dict else folders)
|
|
258
|
+
is_revision = f'#{folders["revision"] if as_dict else folders[-1]}#' == path.name
|
|
259
|
+
if not is_revision and invalid_file:
|
|
260
|
+
raise ALFInvalid(path)
|
|
261
|
+
dataset = filename_parts('', as_dict, assert_valid=False)
|
|
262
|
+
elif '/' not in path.as_posix(): # filename only
|
|
263
|
+
folders = folder_parts('', as_dict, assert_valid=False)
|
|
264
|
+
dataset = filename_parts(path.name, as_dict, assert_valid)
|
|
265
|
+
else: # full filepath
|
|
266
|
+
folders = folder_parts(path.parent, as_dict, assert_valid)
|
|
267
|
+
dataset = filename_parts(path.name, as_dict, assert_valid)
|
|
268
|
+
if as_dict:
|
|
269
|
+
return OrderedDict(**folders, **dataset)
|
|
270
|
+
else:
|
|
271
|
+
return folders + dataset
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def folder_parts(folder_path, as_dict=False, assert_valid=True) -> Union[dict, tuple]:
|
|
275
|
+
"""Parse all folder parts, including session, collection and revision.
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
folder_path : str, pathlib.Path
|
|
280
|
+
The ALF folder path.
|
|
281
|
+
as_dict : bool
|
|
282
|
+
When true a dict of matches is returned.
|
|
283
|
+
assert_valid : bool
|
|
284
|
+
When true an exception is raised when the filename cannot be parsed.
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
OrderedDict, tuple
|
|
289
|
+
A dict if as_dict is true, or a tuple of parsed values.
|
|
290
|
+
|
|
291
|
+
Examples
|
|
292
|
+
--------
|
|
293
|
+
>>> folder_parts('lab/Subjects/subject/2020-01-01/001/collection/#revision#')
|
|
294
|
+
('lab', 'subject', '2020-01-01', '001', 'collection', 'revision')
|
|
295
|
+
>>> folder_parts(Path('lab/Subjects/subject/2020-01-01/001'), as_dict=True)
|
|
296
|
+
{'lab': 'lab',
|
|
297
|
+
'subject': 'subject',
|
|
298
|
+
'date': '2020-01-01',
|
|
299
|
+
'number': '001',
|
|
300
|
+
'collection': None,
|
|
301
|
+
'revision': None}
|
|
302
|
+
|
|
303
|
+
Raises
|
|
304
|
+
------
|
|
305
|
+
ALFInvalid
|
|
306
|
+
Invalid ALF path (assert_valid is True).
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
if hasattr(folder_path, 'as_posix'):
|
|
310
|
+
folder_path = folder_path.as_posix()
|
|
311
|
+
if folder_path and folder_path[-1] != '/': # Slash required for regex pattern
|
|
312
|
+
folder_path = folder_path + '/'
|
|
313
|
+
spec_str = f'{SESSION_SPEC}/{COLLECTION_SPEC}'
|
|
314
|
+
return _path_parts(folder_path, spec_str, False, as_dict, assert_valid)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _isdatetime(s: str) -> bool:
|
|
318
|
+
"""Returns True if input is valid ISO date string."""
|
|
319
|
+
try:
|
|
320
|
+
datetime.strptime(s, '%Y-%m-%d')
|
|
321
|
+
return True
|
|
322
|
+
except ValueError:
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_session_path(path: Union[str, pathlib.Path]) -> Optional[pathlib.Path]:
|
|
327
|
+
"""Return full session path from any file path if the date/number pattern is found.
|
|
328
|
+
|
|
329
|
+
Returns
|
|
330
|
+
-------
|
|
331
|
+
pathlib.Path
|
|
332
|
+
The session path part of the input path or None if path invalid.
|
|
333
|
+
|
|
334
|
+
Examples
|
|
335
|
+
--------
|
|
336
|
+
>>> get_session_path('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001')
|
|
337
|
+
Path('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001')
|
|
338
|
+
|
|
339
|
+
>>> get_session_path('C:\\Data\\subject\\2020-01-01\\1\\trials.intervals.npy')
|
|
340
|
+
Path('C:/Data/subject/2020-01-01/1')
|
|
341
|
+
|
|
342
|
+
"""
|
|
343
|
+
if path is None:
|
|
344
|
+
return
|
|
345
|
+
if isinstance(path, str):
|
|
346
|
+
path = pathlib.Path(path)
|
|
347
|
+
for i, p in enumerate(path.parts):
|
|
348
|
+
if p.isdigit() and _isdatetime(path.parts[i - 1]):
|
|
349
|
+
return path.__class__().joinpath(*path.parts[:i + 1])
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def get_alf_path(path: Union[str, pathlib.Path]) -> str:
|
|
353
|
+
"""Returns the ALF part of a path or filename.
|
|
354
|
+
|
|
355
|
+
Attempts to return the first valid part of the path, first searching for a session path,
|
|
356
|
+
then relative path (collection/revision/filename), then just the filename. If all invalid,
|
|
357
|
+
None is returned.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
path : str, pathlib.Path
|
|
362
|
+
A path to parse.
|
|
363
|
+
|
|
364
|
+
Returns
|
|
365
|
+
-------
|
|
366
|
+
str
|
|
367
|
+
A string containing the full ALF path, session path, relative path or filename.
|
|
368
|
+
|
|
369
|
+
Examples
|
|
370
|
+
--------
|
|
371
|
+
>>> get_alf_path('etc/etc/lab/Subjects/subj/2021-01-21/001')
|
|
372
|
+
'lab/Subjects/subj/2021-01-21/001/collection/file.attr.ext'
|
|
373
|
+
|
|
374
|
+
>>> get_alf_path('etc/etc/subj/2021-01-21/001/collection/file.attr.ext')
|
|
375
|
+
'subj/2021-01-21/001/collection/file.attr.ext'
|
|
376
|
+
|
|
377
|
+
>>> get_alf_path('collection/file.attr.ext')
|
|
378
|
+
'collection/file.attr.ext'
|
|
379
|
+
|
|
380
|
+
"""
|
|
381
|
+
if not isinstance(path, str):
|
|
382
|
+
path = pathlib.Path(path).as_posix()
|
|
383
|
+
path = path.strip('/')
|
|
384
|
+
|
|
385
|
+
# Check if session path
|
|
386
|
+
if match_session := spec.regex(SESSION_SPEC).search(path):
|
|
387
|
+
return path[match_session.start():]
|
|
388
|
+
|
|
389
|
+
# Check if filename / relative path (i.e. collection + filename)
|
|
390
|
+
parts = path.rsplit('/', 1)
|
|
391
|
+
if spec.regex(FILE_SPEC).match(parts[-1]):
|
|
392
|
+
return path if spec.regex(f'{COLLECTION_SPEC}{FILE_SPEC}').match(path) else parts[-1]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def add_uuid_string(file_path, uuid):
|
|
396
|
+
"""Add a UUID to the filename of an ALF path.
|
|
397
|
+
|
|
398
|
+
Adds a UUID to an ALF filename as an extra part, e.g.
|
|
399
|
+
'obj.attr.ext' -> 'obj.attr.a976e418-c8b8-4d24-be47-d05120b18341.ext'.
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
file_path : str, pathlib.Path, pathlib.PurePath
|
|
404
|
+
An ALF path to add the UUID to.
|
|
405
|
+
uuid : str, uuid.UUID
|
|
406
|
+
The UUID to add.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
pathlib.Path, pathlib.PurePath
|
|
411
|
+
A new Path or PurePath object with a UUID in the filename.
|
|
412
|
+
|
|
413
|
+
Examples
|
|
414
|
+
--------
|
|
415
|
+
>>> add_uuid_string('/path/to/trials.intervals.npy', 'a976e418-c8b8-4d24-be47-d05120b18341')
|
|
416
|
+
Path('/path/to/trials.intervals.a976e418-c8b8-4d24-be47-d05120b18341.npy')
|
|
417
|
+
|
|
418
|
+
Raises
|
|
419
|
+
------
|
|
420
|
+
ValueError
|
|
421
|
+
`uuid` must be a valid hyphen-separated hexadecimal UUID.
|
|
422
|
+
|
|
423
|
+
See Also
|
|
424
|
+
--------
|
|
425
|
+
one.alf.path.ALFPath.with_uuid
|
|
426
|
+
one.alf.path.remove_uuid_string
|
|
427
|
+
one.alf.spec.is_uuid
|
|
428
|
+
|
|
429
|
+
"""
|
|
430
|
+
if isinstance(uuid, str) and not spec.is_uuid_string(uuid):
|
|
431
|
+
raise ValueError('Should provide a valid UUID v4')
|
|
432
|
+
uuid = str(uuid)
|
|
433
|
+
# NB: Only instantiate as Path if not already a Path, otherwise we risk changing the class
|
|
434
|
+
if isinstance(file_path, str):
|
|
435
|
+
file_path = pathlib.Path(file_path)
|
|
436
|
+
name_parts = file_path.stem.split('.')
|
|
437
|
+
if spec.is_uuid(name_parts[-1]):
|
|
438
|
+
*name_parts, old_uuid = name_parts
|
|
439
|
+
if old_uuid == uuid:
|
|
440
|
+
_logger.warning(f'UUID already found in file name: {file_path.name}: IGNORE')
|
|
441
|
+
return file_path
|
|
442
|
+
else:
|
|
443
|
+
_logger.debug('Replacing %s with %s in %s', old_uuid, uuid, file_path)
|
|
444
|
+
return file_path.parent.joinpath(f"{'.'.join(name_parts)}.{uuid}{file_path.suffix}")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def remove_uuid_string(file_path):
|
|
448
|
+
"""Remove UUID from a filename of an ALF path.
|
|
449
|
+
|
|
450
|
+
Parameters
|
|
451
|
+
----------
|
|
452
|
+
file_path : str, pathlib.Path, pathlib.PurePath
|
|
453
|
+
An ALF path to add the UUID to.
|
|
454
|
+
|
|
455
|
+
Returns
|
|
456
|
+
-------
|
|
457
|
+
ALFPath, PureALFPath, pathlib.Path, pathlib.PurePath
|
|
458
|
+
A new Path or PurePath object without a UUID in the filename.
|
|
459
|
+
|
|
460
|
+
Examples
|
|
461
|
+
--------
|
|
462
|
+
>>> add_uuid_string('/path/to/trials.intervals.a976e418-c8b8-4d24-be47-d05120b18341.npy')
|
|
463
|
+
Path('/path/to/trials.intervals.npy')
|
|
464
|
+
|
|
465
|
+
>>> add_uuid_string('/path/to/trials.intervals.npy')
|
|
466
|
+
Path('/path/to/trials.intervals.npy')
|
|
467
|
+
|
|
468
|
+
See Also
|
|
469
|
+
--------
|
|
470
|
+
one.alf.path.ALFPath.without_uuid
|
|
471
|
+
one.alf.path.add_uuid_string
|
|
472
|
+
|
|
473
|
+
"""
|
|
474
|
+
if isinstance(file_path, str):
|
|
475
|
+
file_path = pathlib.Path(file_path)
|
|
476
|
+
name_parts = file_path.stem.split('.')
|
|
477
|
+
|
|
478
|
+
if spec.is_uuid_string(name_parts[-1]):
|
|
479
|
+
file_path = file_path.with_name('.'.join(name_parts[:-1]) + file_path.suffix)
|
|
480
|
+
return file_path
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def padded_sequence(file_path):
|
|
484
|
+
"""Ensures a file path contains a zero-padded experiment sequence folder.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
file_path : str, pathlib.Path, pathlib.PurePath
|
|
489
|
+
A session or file path to convert.
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
ALFPath, PureALFPath
|
|
494
|
+
The same path but with the experiment sequence folder zero-padded. If a PurePath was
|
|
495
|
+
passed, a PurePath will be returned, otherwise a Path object is returned.
|
|
496
|
+
|
|
497
|
+
Examples
|
|
498
|
+
--------
|
|
499
|
+
>>> file_path = '/iblrigdata/subject/2023-01-01/1/_ibl_experiment.description.yaml'
|
|
500
|
+
>>> padded_sequence(file_path)
|
|
501
|
+
pathlib.Path('/iblrigdata/subject/2023-01-01/001/_ibl_experiment.description.yaml')
|
|
502
|
+
|
|
503
|
+
Supports folders and will not affect already padded paths
|
|
504
|
+
|
|
505
|
+
>>> session_path = pathlib.PurePosixPath('subject/2023-01-01/001')
|
|
506
|
+
>>> padded_sequence(file_path)
|
|
507
|
+
pathlib.PurePosixPath('subject/2023-01-01/001')
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
file_path = ensure_alf_path(file_path)
|
|
511
|
+
if (session_path := get_session_path(file_path)) is None:
|
|
512
|
+
raise ValueError('path must include a valid ALF session path, e.g. subject/YYYY-MM-DD/N')
|
|
513
|
+
idx = len(file_path.parts) - len(session_path.parts)
|
|
514
|
+
sequence = str(int(session_path.parts[-1])).zfill(3) # zero-pad if necessary
|
|
515
|
+
return file_path.parents[idx].joinpath(sequence, file_path.relative_to(session_path))
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def without_revision(file_path):
|
|
519
|
+
"""Return file path without a revision folder.
|
|
520
|
+
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
file_path : str, pathlib.Path
|
|
524
|
+
A valid ALF dataset path.
|
|
525
|
+
|
|
526
|
+
Returns
|
|
527
|
+
-------
|
|
528
|
+
pathlib.Path
|
|
529
|
+
The input file path without a revision folder.
|
|
530
|
+
|
|
531
|
+
Examples
|
|
532
|
+
--------
|
|
533
|
+
>>> without_revision('/lab/Subjects/subject/2023-01-01/001/collection/#revision#/obj.attr.ext')
|
|
534
|
+
Path('/lab/Subjects/subject/2023-01-01/001/collection/obj.attr.ext')
|
|
535
|
+
|
|
536
|
+
"""
|
|
537
|
+
if isinstance(file_path, str):
|
|
538
|
+
file_path = pathlib.Path(file_path)
|
|
539
|
+
*_, collection, revision = folder_parts(file_path.parent)
|
|
540
|
+
return get_session_path(file_path).joinpath(*filter(None, (collection, file_path.name)))
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class PureALFPath(pathlib.PurePath): # py3.12 supports direct subclassing
|
|
544
|
+
"""Base class for manipulating Alyx file (ALF) paths without I/O.
|
|
545
|
+
|
|
546
|
+
Similar to a pathlib PurePath object but with methods for validating, parsing, and replacing
|
|
547
|
+
ALF path parts.
|
|
548
|
+
|
|
549
|
+
Parameters
|
|
550
|
+
----------
|
|
551
|
+
args : str, pathlib.PurePath
|
|
552
|
+
One or more pathlike objects to combine into an ALF path object.
|
|
553
|
+
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
def __new__(cls, *args):
|
|
557
|
+
"""Construct a ALFPurePath from one or several strings and or existing PurePath objects.
|
|
558
|
+
|
|
559
|
+
The strings and path objects are combined so as to yield a canonicalized path, which is
|
|
560
|
+
incorporated into the new PurePath object.
|
|
561
|
+
"""
|
|
562
|
+
if cls is PureALFPath:
|
|
563
|
+
cls = PureWindowsALFPath if os.name == 'nt' else PurePosixALFPath
|
|
564
|
+
return super().__new__(cls, *args)
|
|
565
|
+
|
|
566
|
+
def is_dataset(self):
|
|
567
|
+
"""Determine if path is an ALF dataset, rather than a folder.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
bool
|
|
572
|
+
True if filename is ALF dataset.
|
|
573
|
+
|
|
574
|
+
"""
|
|
575
|
+
return spec.is_valid(self.name)
|
|
576
|
+
|
|
577
|
+
def is_valid_alf(path) -> bool:
|
|
578
|
+
"""Check if path is a valid ALF path.
|
|
579
|
+
|
|
580
|
+
This returns true if the input path matches any part of the ALF path specification.
|
|
581
|
+
This method can be used as a static method with any pathlike input, or as an instance
|
|
582
|
+
method. This will validate both directory paths and file paths.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
path : str, pathlib.PurePath
|
|
587
|
+
A path to check the validity of.
|
|
588
|
+
|
|
589
|
+
Returns
|
|
590
|
+
-------
|
|
591
|
+
bool
|
|
592
|
+
True if the path is recognized as a valid ALF path.
|
|
593
|
+
|
|
594
|
+
Examples
|
|
595
|
+
--------
|
|
596
|
+
>>> ALFPath('/home/foo/2020-01-01/001').is_valid_alf()
|
|
597
|
+
True
|
|
598
|
+
|
|
599
|
+
>>> ALFPath('/home/foo/2020-01-01/001/alf/spikes.times.npy').is_valid_alf()
|
|
600
|
+
True
|
|
601
|
+
|
|
602
|
+
>>> ALFPath.is_valid_alf('_ibl_wheel.timestamps.npy')
|
|
603
|
+
True
|
|
604
|
+
|
|
605
|
+
>>> ALFPath.is_valid_alf('foo.bar')
|
|
606
|
+
False
|
|
607
|
+
|
|
608
|
+
See Also
|
|
609
|
+
--------
|
|
610
|
+
PureALFPath.is_dataset - Test whether file name is valid as well as directory path.
|
|
611
|
+
full_path_parts - Validates path and returns the parsed ALF path parts.
|
|
612
|
+
|
|
613
|
+
"""
|
|
614
|
+
try:
|
|
615
|
+
return any(full_path_parts(path))
|
|
616
|
+
except ALFInvalid:
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
def is_session_path(path) -> bool:
|
|
620
|
+
"""Check if path is a valid ALF session path.
|
|
621
|
+
|
|
622
|
+
This returns true if the input path matches the ALF session path specification.
|
|
623
|
+
This method can be used as a static method with any pathlike input, or as an instance
|
|
624
|
+
method.
|
|
625
|
+
|
|
626
|
+
Parameters
|
|
627
|
+
----------
|
|
628
|
+
path : str, pathlib.PurePath
|
|
629
|
+
A session path to check the validity of.
|
|
630
|
+
|
|
631
|
+
Returns
|
|
632
|
+
-------
|
|
633
|
+
bool
|
|
634
|
+
True if the path is recognized as a valid ALF session path.
|
|
635
|
+
|
|
636
|
+
Examples
|
|
637
|
+
--------
|
|
638
|
+
>>> ALFPath('/home/foo/2020-01-01/001').is_session_path()
|
|
639
|
+
True
|
|
640
|
+
|
|
641
|
+
>>> ALFPath('/home/foo/2020-01-01/001/alf/spikes.times.npy').is_session_path()
|
|
642
|
+
False
|
|
643
|
+
|
|
644
|
+
>>> ALFPath.is_session_path('_ibl_wheel.timestamps.npy')
|
|
645
|
+
False
|
|
646
|
+
|
|
647
|
+
>>> ALFPath.is_valid_alf('lab/Subjects/foo/2020-01-01/001')
|
|
648
|
+
True
|
|
649
|
+
|
|
650
|
+
See Also
|
|
651
|
+
--------
|
|
652
|
+
PureALFPath.is_valid_alf - Test whether path is generally valid a valid ALF path.
|
|
653
|
+
PureALFPath.session_path_parts - Returns parsed session path parts as tuple of str.
|
|
654
|
+
|
|
655
|
+
"""
|
|
656
|
+
return spec.is_session_path(path)
|
|
657
|
+
|
|
658
|
+
def session_path(self):
|
|
659
|
+
"""Extract the full session path.
|
|
660
|
+
|
|
661
|
+
Returns the session path from the filepath if the date/number pattern is found,
|
|
662
|
+
including the root directory.
|
|
663
|
+
|
|
664
|
+
Returns
|
|
665
|
+
-------
|
|
666
|
+
PureALFPath
|
|
667
|
+
The session path part of the input path or None if path invalid.
|
|
668
|
+
|
|
669
|
+
Examples
|
|
670
|
+
--------
|
|
671
|
+
>>> ALFPath('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001').session_path()
|
|
672
|
+
ALFPath('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001')
|
|
673
|
+
|
|
674
|
+
>>> ALFPath('C:\\Data\\subject\\2020-01-01\\1\\trials.intervals.npy').session_path()
|
|
675
|
+
ALFPath('C:/Data/subject/2020-01-01/1')
|
|
676
|
+
|
|
677
|
+
"""
|
|
678
|
+
return get_session_path(self)
|
|
679
|
+
|
|
680
|
+
def session_path_short(self, include_lab=False) -> str:
|
|
681
|
+
"""Return only the ALF session path as a posix str.
|
|
682
|
+
|
|
683
|
+
Params
|
|
684
|
+
------
|
|
685
|
+
include_lab : bool
|
|
686
|
+
If true, the lab/subject/date/number is returned, otherwise the lab part is dropped.
|
|
687
|
+
|
|
688
|
+
Returns
|
|
689
|
+
-------
|
|
690
|
+
str
|
|
691
|
+
The session path part of the input path or None if path invalid.
|
|
692
|
+
|
|
693
|
+
Examples
|
|
694
|
+
--------
|
|
695
|
+
>>> ALFPath('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001').session_path_short()
|
|
696
|
+
'subject/2020-01-01/001'
|
|
697
|
+
|
|
698
|
+
>>> alfpath = ALFPath('/mnt/sd0/Data/lab/Subjects/subject/2020-01-01/001')
|
|
699
|
+
>>> alfpath.session_path_short(include_lab=True)
|
|
700
|
+
'lab/subject/2020-01-01/001'
|
|
701
|
+
|
|
702
|
+
>>> ALFPath('C:\\Data\\subject\\2020-01-01\\1\\trials.intervals.npy').session_path_short()
|
|
703
|
+
'subject/2020-01-01/1'
|
|
704
|
+
|
|
705
|
+
"""
|
|
706
|
+
idx = 0 if include_lab else 1
|
|
707
|
+
if any(parts := self.session_parts[idx:]):
|
|
708
|
+
return '/'.join(parts)
|
|
709
|
+
|
|
710
|
+
def without_lab(self) -> 'PureALFPath':
|
|
711
|
+
"""Return path without the <lab>/Subjects/ part.
|
|
712
|
+
|
|
713
|
+
If the <lab>/Subjects pattern is not found, the same path is returned.
|
|
714
|
+
|
|
715
|
+
Returns
|
|
716
|
+
-------
|
|
717
|
+
PureALFPath
|
|
718
|
+
The same path without the <lab>/Subjects part.
|
|
719
|
+
|
|
720
|
+
"""
|
|
721
|
+
p = self.as_posix()
|
|
722
|
+
if m := spec.regex('{lab}/Subjects/').search(p):
|
|
723
|
+
return self.__class__(p[:m.start()], p[m.end():])
|
|
724
|
+
else:
|
|
725
|
+
return self
|
|
726
|
+
|
|
727
|
+
def relative_to_lab(self) -> 'PureALFPath':
|
|
728
|
+
"""Return path relative to <lab>/Subjects/ part.
|
|
729
|
+
|
|
730
|
+
Returns
|
|
731
|
+
-------
|
|
732
|
+
PureALFPath
|
|
733
|
+
The same path, relative to the <lab>/Subjects/ part.
|
|
734
|
+
|
|
735
|
+
Raises
|
|
736
|
+
------
|
|
737
|
+
ValueError
|
|
738
|
+
The path doesn't contain a <lab>/Subjects/ pattern.
|
|
739
|
+
|
|
740
|
+
"""
|
|
741
|
+
p = self.as_posix()
|
|
742
|
+
if m := spec.regex('{lab}/Subjects/').search(p):
|
|
743
|
+
return self.__class__(p[m.end():])
|
|
744
|
+
else:
|
|
745
|
+
raise ValueError(f'{self} does not contain <lab>/Subjects pattern')
|
|
746
|
+
|
|
747
|
+
def relative_to_session(self):
|
|
748
|
+
"""Return path relative to session part.
|
|
749
|
+
|
|
750
|
+
Returns
|
|
751
|
+
-------
|
|
752
|
+
PureALFPath
|
|
753
|
+
The same path, relative to the <lab>/Subjects/<subject>/<date>/<number> part.
|
|
754
|
+
|
|
755
|
+
Raises
|
|
756
|
+
------
|
|
757
|
+
ValueError
|
|
758
|
+
The path doesn't contain a <lab>/Subjects/ pattern.
|
|
759
|
+
|
|
760
|
+
"""
|
|
761
|
+
if (session_path := self.session_path()):
|
|
762
|
+
return self.relative_to(session_path)
|
|
763
|
+
else:
|
|
764
|
+
raise ValueError(f'{self} does not contain session path pattern')
|
|
765
|
+
|
|
766
|
+
def parse_alf_path(self, as_dict=True):
|
|
767
|
+
"""Parse all filename and folder parts.
|
|
768
|
+
|
|
769
|
+
Parameters
|
|
770
|
+
----------
|
|
771
|
+
as_dict : bool
|
|
772
|
+
When true a dict of matches is returned.
|
|
773
|
+
|
|
774
|
+
Returns
|
|
775
|
+
-------
|
|
776
|
+
OrderedDict, tuple
|
|
777
|
+
A dict if as_dict is true, or a tuple of parsed values.
|
|
778
|
+
|
|
779
|
+
Examples
|
|
780
|
+
--------
|
|
781
|
+
>>> alfpath = PureALFPath(
|
|
782
|
+
... 'lab/Subjects/subject/2020-01-01/001/collection/#revision#/'
|
|
783
|
+
... '_namespace_obj.times_timescale.extra.foo.ext')
|
|
784
|
+
>>> alfpath.parse_alf_path()
|
|
785
|
+
{'lab': 'lab',
|
|
786
|
+
'subject': 'subject',
|
|
787
|
+
'date': '2020-01-01',
|
|
788
|
+
'number': '001',
|
|
789
|
+
'collection': 'collection',
|
|
790
|
+
'revision': 'revision',
|
|
791
|
+
'namespace': 'namespace',
|
|
792
|
+
'object': 'obj',
|
|
793
|
+
'attribute': 'times',
|
|
794
|
+
'timescale': 'timescale',
|
|
795
|
+
'extra': 'extra.foo',
|
|
796
|
+
'extension': 'ext'}
|
|
797
|
+
|
|
798
|
+
>>> PureALFPath('_namespace_obj.times_timescale.extra.foo.ext').parse_alf_path()
|
|
799
|
+
(None, None, None, None, None, None, 'namespace',
|
|
800
|
+
'obj', 'times','timescale', 'extra.foo', 'ext')
|
|
801
|
+
|
|
802
|
+
"""
|
|
803
|
+
return full_path_parts(self, assert_valid=False, as_dict=as_dict)
|
|
804
|
+
|
|
805
|
+
def parse_alf_name(self, as_dict=True):
|
|
806
|
+
"""Return the parsed elements of a given ALF filename.
|
|
807
|
+
|
|
808
|
+
Parameters
|
|
809
|
+
----------
|
|
810
|
+
as_dict : bool
|
|
811
|
+
When true a dict of matches is returned.
|
|
812
|
+
|
|
813
|
+
Returns
|
|
814
|
+
-------
|
|
815
|
+
namespace : str
|
|
816
|
+
The _namespace_ or None if not present.
|
|
817
|
+
object : str
|
|
818
|
+
ALF object.
|
|
819
|
+
attribute : str
|
|
820
|
+
The ALF attribute.
|
|
821
|
+
timescale : str
|
|
822
|
+
The ALF _timescale or None if not present.
|
|
823
|
+
extra : str
|
|
824
|
+
Any extra parts to the filename, or None if not present.
|
|
825
|
+
extension : str
|
|
826
|
+
The file extension.
|
|
827
|
+
|
|
828
|
+
Examples
|
|
829
|
+
--------
|
|
830
|
+
>>> alfpath = PureALFPath(
|
|
831
|
+
... 'lab/Subjects/subject/2020-01-01/001/collection/#revision#/'
|
|
832
|
+
... '_namespace_obj.times_timescale.extra.foo.ext')
|
|
833
|
+
>>> alfpath.parse_alf_name()
|
|
834
|
+
{'namespace': 'namespace',
|
|
835
|
+
'object': 'obj',
|
|
836
|
+
'attribute': 'times',
|
|
837
|
+
'timescale': 'timescale',
|
|
838
|
+
'extra': 'extra.foo',
|
|
839
|
+
'extension': 'ext'}
|
|
840
|
+
|
|
841
|
+
>>> PureALFPath('spikes.clusters.npy', as_dict=False)
|
|
842
|
+
(None, 'spikes', 'clusters', None, None, npy)
|
|
843
|
+
|
|
844
|
+
"""
|
|
845
|
+
return filename_parts(self.name, assert_valid=False, as_dict=as_dict)
|
|
846
|
+
|
|
847
|
+
@property
|
|
848
|
+
def dataset_name_parts(self):
|
|
849
|
+
"""tuple of str: the dataset name parts, with empty strings for missing parts."""
|
|
850
|
+
return tuple(p or '' for p in self.parse_alf_name(as_dict=False))
|
|
851
|
+
|
|
852
|
+
@property
|
|
853
|
+
def session_parts(self):
|
|
854
|
+
"""tuple of str: the session path parts, with empty strings for missing parts."""
|
|
855
|
+
return tuple(p or '' for p in session_path_parts(self, assert_valid=False))
|
|
856
|
+
|
|
857
|
+
@property
|
|
858
|
+
def alf_parts(self):
|
|
859
|
+
"""tuple of str: the full ALF path parts, with empty strings for missing parts."""
|
|
860
|
+
return tuple(p or '' for p in self.parse_alf_path(as_dict=False))
|
|
861
|
+
|
|
862
|
+
@property
|
|
863
|
+
def namespace(self):
|
|
864
|
+
"""str: The namespace part of the ALF name, or and empty str if not present."""
|
|
865
|
+
return self.dataset_name_parts[0]
|
|
866
|
+
|
|
867
|
+
@property
|
|
868
|
+
def object(self):
|
|
869
|
+
"""str: The object part of the ALF name, or and empty str if not present."""
|
|
870
|
+
return self.dataset_name_parts[1]
|
|
871
|
+
|
|
872
|
+
@property
|
|
873
|
+
def attribute(self):
|
|
874
|
+
"""str: The attribute part of the ALF name, or and empty str if not present."""
|
|
875
|
+
return self.dataset_name_parts[2]
|
|
876
|
+
|
|
877
|
+
@property
|
|
878
|
+
def timescale(self):
|
|
879
|
+
"""str: The timescale part of the ALF name, or and empty str if not present."""
|
|
880
|
+
return self.dataset_name_parts[3]
|
|
881
|
+
|
|
882
|
+
@property
|
|
883
|
+
def extra(self):
|
|
884
|
+
"""str: The extra part of the ALF name, or and empty str if not present."""
|
|
885
|
+
return self.dataset_name_parts[4]
|
|
886
|
+
|
|
887
|
+
def with_object(self, obj):
|
|
888
|
+
"""Return a new path with the ALF object changed.
|
|
889
|
+
|
|
890
|
+
Parameters
|
|
891
|
+
----------
|
|
892
|
+
obj : str
|
|
893
|
+
An ALF object name part to use.
|
|
894
|
+
|
|
895
|
+
Returns
|
|
896
|
+
-------
|
|
897
|
+
PureALFPath
|
|
898
|
+
The same file path but with the object part replaced with the input.
|
|
899
|
+
|
|
900
|
+
Raises
|
|
901
|
+
------
|
|
902
|
+
ALFInvalid
|
|
903
|
+
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
904
|
+
contains invalid characters).
|
|
905
|
+
|
|
906
|
+
"""
|
|
907
|
+
if not self.is_dataset():
|
|
908
|
+
raise ALFInvalid(str(self))
|
|
909
|
+
ns_obj, rest = self.name.split('.', 1)
|
|
910
|
+
ns, _ = spec.regex(FILE_SPEC.split('\\.')[0]).match(ns_obj).groups()
|
|
911
|
+
ns = f'_{ns}_' if ns else ''
|
|
912
|
+
return self.with_name(f'{ns}{obj}.{rest}')
|
|
913
|
+
|
|
914
|
+
def with_namespace(self, ns):
|
|
915
|
+
"""Return a new path with the ALF namespace added or changed.
|
|
916
|
+
|
|
917
|
+
Parameters
|
|
918
|
+
----------
|
|
919
|
+
namespace : str
|
|
920
|
+
An ALF namespace part to use.
|
|
921
|
+
|
|
922
|
+
Returns
|
|
923
|
+
-------
|
|
924
|
+
PureALFPath
|
|
925
|
+
The same file path but with the namespace part added/replaced with the input.
|
|
926
|
+
|
|
927
|
+
Raises
|
|
928
|
+
------
|
|
929
|
+
ALFInvalid
|
|
930
|
+
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
931
|
+
contains invalid characters).
|
|
932
|
+
|
|
933
|
+
"""
|
|
934
|
+
if not self.is_dataset():
|
|
935
|
+
raise ALFInvalid(self)
|
|
936
|
+
ns_obj, rest = self.name.split('.', 1)
|
|
937
|
+
_, obj = spec.regex(FILE_SPEC.split('\\.')[0]).match(ns_obj).groups()
|
|
938
|
+
ns = f'_{ns}_' if ns else ''
|
|
939
|
+
return self.with_name(f'{ns}{obj}.{rest}')
|
|
940
|
+
|
|
941
|
+
def with_attribute(self, attr):
|
|
942
|
+
"""Return a new path with the ALF attribute changed.
|
|
943
|
+
|
|
944
|
+
Parameters
|
|
945
|
+
----------
|
|
946
|
+
attribute : str
|
|
947
|
+
An ALF attribute part to use.
|
|
948
|
+
|
|
949
|
+
Returns
|
|
950
|
+
-------
|
|
951
|
+
PureALFPath
|
|
952
|
+
The same file path but with the attribute part replaced with the input.
|
|
953
|
+
|
|
954
|
+
Raises
|
|
955
|
+
------
|
|
956
|
+
ALFInvalid
|
|
957
|
+
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
958
|
+
contains invalid characters).
|
|
959
|
+
|
|
960
|
+
"""
|
|
961
|
+
if not self.is_dataset():
|
|
962
|
+
raise ALFInvalid(self)
|
|
963
|
+
ns_obj, attr_ts, rest = self.name.split('.', 2)
|
|
964
|
+
_, ts = spec.regex('{attribute}(?:_{timescale})?').match(attr_ts).groups()
|
|
965
|
+
ts = f'_{ts}' if ts else ''
|
|
966
|
+
return self.with_name(f'{ns_obj}.{attr}{ts}.{rest}')
|
|
967
|
+
|
|
968
|
+
def with_timescale(self, timescale):
|
|
969
|
+
"""Return a new path with the ALF timescale added or changed.
|
|
970
|
+
|
|
971
|
+
Parameters
|
|
972
|
+
----------
|
|
973
|
+
timescale : str
|
|
974
|
+
An ALF timescale part to use.
|
|
975
|
+
|
|
976
|
+
Returns
|
|
977
|
+
-------
|
|
978
|
+
PureALFPath
|
|
979
|
+
The same file path but with the timescale part added/replaced with the input.
|
|
980
|
+
|
|
981
|
+
Raises
|
|
982
|
+
------
|
|
983
|
+
ALFInvalid
|
|
984
|
+
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
985
|
+
contains invalid characters).
|
|
986
|
+
|
|
987
|
+
"""
|
|
988
|
+
if not self.is_dataset():
|
|
989
|
+
raise ALFInvalid(self)
|
|
990
|
+
ns_obj, attr_ts, rest = self.name.split('.', 2)
|
|
991
|
+
attr, _ = spec.regex('{attribute}(?:_{timescale})?').match(attr_ts).groups()
|
|
992
|
+
ts = f'_{timescale}' if timescale else ''
|
|
993
|
+
return self.with_name(f'{ns_obj}.{attr}{ts}.{rest}')
|
|
994
|
+
|
|
995
|
+
def with_extra(self, extra, append=False):
|
|
996
|
+
"""Return a new path with extra ALF parts added or changed.
|
|
997
|
+
|
|
998
|
+
Parameters
|
|
999
|
+
----------
|
|
1000
|
+
extra : str, list of str
|
|
1001
|
+
Extra ALF parts to add/replace.
|
|
1002
|
+
append : bool
|
|
1003
|
+
When false (default) any existing extra parts are replaced instead of added to.
|
|
1004
|
+
|
|
1005
|
+
Returns
|
|
1006
|
+
-------
|
|
1007
|
+
PureALFPath
|
|
1008
|
+
The same file path but with the extra part(s) replaced or appended to with the input.
|
|
1009
|
+
|
|
1010
|
+
Raises
|
|
1011
|
+
------
|
|
1012
|
+
ALFInvalid
|
|
1013
|
+
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
1014
|
+
contains invalid characters).
|
|
1015
|
+
|
|
1016
|
+
"""
|
|
1017
|
+
if not self.is_dataset():
|
|
1018
|
+
raise ALFInvalid(self)
|
|
1019
|
+
parts = self.stem.split('.', 2)
|
|
1020
|
+
if isinstance(extra, str):
|
|
1021
|
+
extra = extra.strip('.').split('.')
|
|
1022
|
+
if (prev := parts.pop() if len(parts) > 2 else None) and append:
|
|
1023
|
+
extra = (prev, *extra)
|
|
1024
|
+
obj_attr = '.'.join(parts)
|
|
1025
|
+
if extra := '.'.join(filter(None, extra)):
|
|
1026
|
+
return self.with_stem(f'{obj_attr}.{extra}')
|
|
1027
|
+
else:
|
|
1028
|
+
return self.with_stem(obj_attr)
|
|
1029
|
+
|
|
1030
|
+
def with_extension(self, ext):
|
|
1031
|
+
"""Return a new path with the ALF extension (suffix) changed.
|
|
1032
|
+
|
|
1033
|
+
Note that unlike PurePath's `with_suffix` method, this asserts that the filename is a valid
|
|
1034
|
+
ALF dataset and the `ext` argument should be without the period.
|
|
1035
|
+
|
|
1036
|
+
Parameters
|
|
1037
|
+
----------
|
|
1038
|
+
ext : str
|
|
1039
|
+
An ALF extension part to use (sans period).
|
|
1040
|
+
|
|
1041
|
+
Returns
|
|
1042
|
+
-------
|
|
1043
|
+
PureALFPath
|
|
1044
|
+
The same file path but with the extension part replaced with the input.
|
|
1045
|
+
|
|
1046
|
+
Raises
|
|
1047
|
+
------
|
|
1048
|
+
ALFInvalid
|
|
1049
|
+
The path is not a valid ALF dataset (e.g. doesn't have a three-part filename, or
|
|
1050
|
+
contains invalid characters).
|
|
1051
|
+
|
|
1052
|
+
"""
|
|
1053
|
+
if not self.is_dataset():
|
|
1054
|
+
raise ALFInvalid(str(self))
|
|
1055
|
+
return self.with_suffix(f'.{ext}')
|
|
1056
|
+
|
|
1057
|
+
def with_padded_sequence(path):
|
|
1058
|
+
"""Ensures a file path contains a zero-padded experiment sequence folder.
|
|
1059
|
+
|
|
1060
|
+
Parameters
|
|
1061
|
+
----------
|
|
1062
|
+
path : str pathlib.PurePath
|
|
1063
|
+
A session or file path to convert.
|
|
1064
|
+
|
|
1065
|
+
Returns
|
|
1066
|
+
-------
|
|
1067
|
+
ALFPath, PureALFPath
|
|
1068
|
+
The same path but with the experiment sequence folder zero-padded. If a PurePath was
|
|
1069
|
+
passed, a PurePath will be returned, otherwise a Path object is returned.
|
|
1070
|
+
|
|
1071
|
+
Examples
|
|
1072
|
+
--------
|
|
1073
|
+
Supports calling as static function
|
|
1074
|
+
|
|
1075
|
+
>>> file_path = '/iblrigdata/subject/2023-01-01/1/_ibl_experiment.description.yaml'
|
|
1076
|
+
>>> ALFPath.with_padded_sequence(file_path)
|
|
1077
|
+
ALFPath('/iblrigdata/subject/2023-01-01/001/_ibl_experiment.description.yaml')
|
|
1078
|
+
|
|
1079
|
+
Supports folders and will not affect already padded paths
|
|
1080
|
+
|
|
1081
|
+
>>> ALFPath('subject/2023-01-01/001').with_padded_sequence(file_path)
|
|
1082
|
+
ALFPath('subject/2023-01-01/001')
|
|
1083
|
+
|
|
1084
|
+
"""
|
|
1085
|
+
return padded_sequence(path)
|
|
1086
|
+
|
|
1087
|
+
def with_revision(self, revision):
|
|
1088
|
+
"""Return a new path with the ALF revision part added/changed.
|
|
1089
|
+
|
|
1090
|
+
Parameters
|
|
1091
|
+
----------
|
|
1092
|
+
revision : str
|
|
1093
|
+
An ALF revision part to use (NB: do not include the pound sign '#').
|
|
1094
|
+
|
|
1095
|
+
Returns
|
|
1096
|
+
-------
|
|
1097
|
+
PureALFPath
|
|
1098
|
+
The same file path but with the revision part added or replaced with the input.
|
|
1099
|
+
|
|
1100
|
+
Examples
|
|
1101
|
+
--------
|
|
1102
|
+
If not in the ALF path, one will be added
|
|
1103
|
+
|
|
1104
|
+
>>> ALFPath('/subject/2023-01-01/1/alf/obj.attr.ext').with_revision('revision')
|
|
1105
|
+
ALFPath('/subject/2023-01-01/1/alf/#xxx#/obj.attr.ext')
|
|
1106
|
+
|
|
1107
|
+
If a revision is already in the ALF path it will be replaced
|
|
1108
|
+
|
|
1109
|
+
>>> ALFPath('/subject/2023-01-01/1/alf/#revision#/obj.attr.ext').with_revision('xxx')
|
|
1110
|
+
ALFPath('/subject/2023-01-01/1/alf/#xxx#/obj.attr.ext')
|
|
1111
|
+
|
|
1112
|
+
Raises
|
|
1113
|
+
------
|
|
1114
|
+
ALFInvalid
|
|
1115
|
+
The ALF path is not valid or is relative to the session path. The path must include
|
|
1116
|
+
the session parts otherwise the path is too ambiguous to determine validity.
|
|
1117
|
+
ALFInvalid
|
|
1118
|
+
The revision provided does not match the ALF specification pattern.
|
|
1119
|
+
|
|
1120
|
+
See Also
|
|
1121
|
+
--------
|
|
1122
|
+
PureALFPath.without_revision
|
|
1123
|
+
|
|
1124
|
+
"""
|
|
1125
|
+
# Validate the revision input
|
|
1126
|
+
revision, = _path_parts(revision, '^{revision}$', match=True, assert_valid=True)
|
|
1127
|
+
if PureALFPath.is_dataset(self):
|
|
1128
|
+
return self.without_revision().parent / f'#{revision}#' / self.name
|
|
1129
|
+
else:
|
|
1130
|
+
return self.without_revision() / f'#{revision}#'
|
|
1131
|
+
|
|
1132
|
+
def without_revision(self):
|
|
1133
|
+
"""Return a new path with the ALF revision part removed.
|
|
1134
|
+
|
|
1135
|
+
Returns
|
|
1136
|
+
-------
|
|
1137
|
+
PureALFPath
|
|
1138
|
+
The same file path but with the revision part removed.
|
|
1139
|
+
|
|
1140
|
+
Examples
|
|
1141
|
+
--------
|
|
1142
|
+
If not in the ALF path, no change occurs
|
|
1143
|
+
|
|
1144
|
+
>>> ALFPath('/subject/2023-01-01/1/alf/obj.attr.ext').with_revision('revision')
|
|
1145
|
+
ALFPath('/subject/2023-01-01/1/alf/obj.attr.ext')
|
|
1146
|
+
|
|
1147
|
+
If a revision is in the ALF path it will be removed
|
|
1148
|
+
|
|
1149
|
+
>>> ALFPath('/subject/2023-01-01/1/alf/#revision#/obj.attr.ext').without_revision()
|
|
1150
|
+
ALFPath('/subject/2023-01-01/1/alf/obj.attr.ext')
|
|
1151
|
+
|
|
1152
|
+
Raises
|
|
1153
|
+
------
|
|
1154
|
+
ALFInvalid
|
|
1155
|
+
The ALF path is not valid or is relative to the session path. The path must include
|
|
1156
|
+
the session parts otherwise the path is too ambiguous to determine validity.
|
|
1157
|
+
|
|
1158
|
+
See Also
|
|
1159
|
+
--------
|
|
1160
|
+
PureALFPath.with_revision
|
|
1161
|
+
|
|
1162
|
+
"""
|
|
1163
|
+
if PureALFPath.is_dataset(self):
|
|
1164
|
+
# Is a file path (rather than folder path)
|
|
1165
|
+
return without_revision(self)
|
|
1166
|
+
if not self.is_valid_alf():
|
|
1167
|
+
raise ALFInvalid(f'{self} not a valid ALF path or is relative to session')
|
|
1168
|
+
elif spec.regex('^#{revision}#$').match(self.name):
|
|
1169
|
+
# Includes revision
|
|
1170
|
+
return self.parent
|
|
1171
|
+
else:
|
|
1172
|
+
# Does not include revision
|
|
1173
|
+
return self
|
|
1174
|
+
|
|
1175
|
+
def with_uuid(self, uuid):
|
|
1176
|
+
"""Return a new path with the ALF UUID part added/changed.
|
|
1177
|
+
|
|
1178
|
+
Parameters
|
|
1179
|
+
----------
|
|
1180
|
+
uuid : str, uuid.UUID
|
|
1181
|
+
The UUID to add.
|
|
1182
|
+
|
|
1183
|
+
Returns
|
|
1184
|
+
-------
|
|
1185
|
+
PureALFPath
|
|
1186
|
+
A new ALFPath object with a UUID in the filename.
|
|
1187
|
+
|
|
1188
|
+
Examples
|
|
1189
|
+
--------
|
|
1190
|
+
>>> uuid = 'a976e418-c8b8-4d24-be47-d05120b18341'
|
|
1191
|
+
>>> ALFPath('/path/to/trials.intervals.npy').with_uuid(uuid)
|
|
1192
|
+
ALFPath('/path/to/trials.intervals.a976e418-c8b8-4d24-be47-d05120b18341.npy')
|
|
1193
|
+
|
|
1194
|
+
Raises
|
|
1195
|
+
------
|
|
1196
|
+
ValueError
|
|
1197
|
+
`uuid` must be a valid hyphen-separated hexadecimal UUID.
|
|
1198
|
+
ALFInvalid
|
|
1199
|
+
Path is not a valid ALF file path.
|
|
1200
|
+
|
|
1201
|
+
"""
|
|
1202
|
+
if not self.is_dataset():
|
|
1203
|
+
raise ALFInvalid(f'{self} is not a valid ALF dataset file path')
|
|
1204
|
+
return add_uuid_string(self, uuid)
|
|
1205
|
+
|
|
1206
|
+
def without_uuid(self):
|
|
1207
|
+
"""Return a new path with the ALF UUID part removed.
|
|
1208
|
+
|
|
1209
|
+
Returns
|
|
1210
|
+
-------
|
|
1211
|
+
PureALFPath
|
|
1212
|
+
A new ALFPath object with a UUID removed from the filename, if present.
|
|
1213
|
+
|
|
1214
|
+
Examples
|
|
1215
|
+
--------
|
|
1216
|
+
>>> alfpath = ALFPath('/path/to/trials.intervals.a976e418-c8b8-4d24-be47-d05120b18341.npy')
|
|
1217
|
+
>>> alfpath.without_uuid(uuid)
|
|
1218
|
+
ALFPath('/path/to/trials.intervals.npy')
|
|
1219
|
+
|
|
1220
|
+
>>> ALFPath('/path/to/trials.intervals.npy').without_uuid(uuid)
|
|
1221
|
+
ALFPath('/path/to/trials.intervals.npy')
|
|
1222
|
+
|
|
1223
|
+
"""
|
|
1224
|
+
return remove_uuid_string(self) if self.is_dataset() else self
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
class ALFPath(PureALFPath):
|
|
1228
|
+
"""Base class for manipulating Alyx file (ALF) paths with system calls.
|
|
1229
|
+
|
|
1230
|
+
Similar to a pathlib Path object but with methods for validating, parsing, and replacing ALF
|
|
1231
|
+
path parts. This class also contains methods that work on system files.
|
|
1232
|
+
|
|
1233
|
+
Parameters
|
|
1234
|
+
----------
|
|
1235
|
+
args : str, pathlib.PurePath
|
|
1236
|
+
One or more pathlike objects to combine into an ALF path object.
|
|
1237
|
+
|
|
1238
|
+
"""
|
|
1239
|
+
|
|
1240
|
+
def __new__(cls, *args):
|
|
1241
|
+
"""Construct a ALFPurePath from one or several strings and or existing PurePath objects.
|
|
1242
|
+
|
|
1243
|
+
The strings and path objects are combined so as to yield a canonicalized path, which is
|
|
1244
|
+
incorporated into the new PurePath object.
|
|
1245
|
+
"""
|
|
1246
|
+
return super().__new__(WindowsALFPath if os.name == 'nt' else PosixALFPath, *args)
|
|
1247
|
+
|
|
1248
|
+
def is_dataset(self) -> bool:
|
|
1249
|
+
"""Determine if path is an ALF dataset, rather than a folder.
|
|
1250
|
+
|
|
1251
|
+
Unlike pathlib and PureALFPath methods, this will return False if the path exists but
|
|
1252
|
+
is a folder, otherwise this simply tests the path name, whether it exists or not.
|
|
1253
|
+
|
|
1254
|
+
Returns
|
|
1255
|
+
-------
|
|
1256
|
+
bool
|
|
1257
|
+
True if filename is ALF dataset.
|
|
1258
|
+
|
|
1259
|
+
"""
|
|
1260
|
+
return not self.is_dir() and spec.is_valid(self.name)
|
|
1261
|
+
|
|
1262
|
+
def is_session_path(self) -> bool:
|
|
1263
|
+
"""Check if path is a valid ALF session path.
|
|
1264
|
+
|
|
1265
|
+
This returns true if the input path matches the ALF session path specification.
|
|
1266
|
+
This method can be used as a static method with any pathlike input, or as an instance
|
|
1267
|
+
method.
|
|
1268
|
+
|
|
1269
|
+
Unlike the PureALFPath method, this will return false if the path matches but is in fact
|
|
1270
|
+
a file on disk.
|
|
1271
|
+
|
|
1272
|
+
Parameters
|
|
1273
|
+
----------
|
|
1274
|
+
path : str, pathlib.PurePath
|
|
1275
|
+
A session path to check the validity of.
|
|
1276
|
+
|
|
1277
|
+
Returns
|
|
1278
|
+
-------
|
|
1279
|
+
bool
|
|
1280
|
+
True if the path is recognized as a valid ALF session path.
|
|
1281
|
+
|
|
1282
|
+
Examples
|
|
1283
|
+
--------
|
|
1284
|
+
>>> ALFPath('/home/foo/2020-01-01/001').is_session_path()
|
|
1285
|
+
True
|
|
1286
|
+
|
|
1287
|
+
>>> ALFPath('/home/foo/2020-01-01/001/alf/spikes.times.npy').is_session_path()
|
|
1288
|
+
False
|
|
1289
|
+
|
|
1290
|
+
>>> ALFPath.is_session_path('_ibl_wheel.timestamps.npy')
|
|
1291
|
+
False
|
|
1292
|
+
|
|
1293
|
+
>>> ALFPath.is_valid_alf('lab/Subjects/foo/2020-01-01/001')
|
|
1294
|
+
True
|
|
1295
|
+
|
|
1296
|
+
See Also
|
|
1297
|
+
--------
|
|
1298
|
+
PureALFPath.is_valid_alf - Test whether path is generally valid a valid ALF path.
|
|
1299
|
+
PureALFPath.session_path_parts - Returns parsed session path parts as tuple of str.
|
|
1300
|
+
|
|
1301
|
+
"""
|
|
1302
|
+
return not self.is_file() and spec.is_session_path(self)
|
|
1303
|
+
|
|
1304
|
+
def is_valid_alf(path) -> bool:
|
|
1305
|
+
"""Check if path is a valid ALF path.
|
|
1306
|
+
|
|
1307
|
+
This returns true if the input path matches any part of the ALF path specification.
|
|
1308
|
+
This method can be used as a static method with any pathlike input, or as an instance
|
|
1309
|
+
method. This will validate both directory paths and file paths.
|
|
1310
|
+
|
|
1311
|
+
Unlike the PureALFPath method, this one will return false if the path matches a dataset
|
|
1312
|
+
file pattern but is actually a folder on disk, or if the path matches as a file but is
|
|
1313
|
+
is a folder on disk.
|
|
1314
|
+
|
|
1315
|
+
Parameters
|
|
1316
|
+
----------
|
|
1317
|
+
path : str, pathlib.PurePath
|
|
1318
|
+
A path to check the validity of.
|
|
1319
|
+
|
|
1320
|
+
Returns
|
|
1321
|
+
-------
|
|
1322
|
+
bool
|
|
1323
|
+
True if the path is recognized as a valid ALF path.
|
|
1324
|
+
|
|
1325
|
+
Examples
|
|
1326
|
+
--------
|
|
1327
|
+
>>> ALFPath('/home/foo/2020-01-01/001').is_valid_alf()
|
|
1328
|
+
True
|
|
1329
|
+
|
|
1330
|
+
>>> ALFPath('/home/foo/2020-01-01/001/alf/spikes.times.npy').is_valid_alf()
|
|
1331
|
+
True
|
|
1332
|
+
|
|
1333
|
+
>>> ALFPath.is_valid_alf('_ibl_wheel.timestamps.npy')
|
|
1334
|
+
True
|
|
1335
|
+
|
|
1336
|
+
>>> ALFPath.is_valid_alf('foo.bar')
|
|
1337
|
+
False
|
|
1338
|
+
|
|
1339
|
+
See Also
|
|
1340
|
+
--------
|
|
1341
|
+
PureALFPath.is_dataset - Test whether file name is valid as well as directory path.
|
|
1342
|
+
full_path_parts - Validates path and returns the parsed ALF path parts.
|
|
1343
|
+
|
|
1344
|
+
"""
|
|
1345
|
+
try:
|
|
1346
|
+
parsed = full_path_parts(path, as_dict=True)
|
|
1347
|
+
except ALFInvalid:
|
|
1348
|
+
return False
|
|
1349
|
+
is_dataset = parsed['object'] is not None
|
|
1350
|
+
if isinstance(path, str):
|
|
1351
|
+
path = ALFPath(path)
|
|
1352
|
+
if hasattr(path, 'is_file') and path.is_file():
|
|
1353
|
+
return is_dataset
|
|
1354
|
+
elif hasattr(path, 'is_dir') and path.is_dir():
|
|
1355
|
+
return not is_dataset
|
|
1356
|
+
return True
|
|
1357
|
+
|
|
1358
|
+
def iter_datasets(self, recursive=False):
|
|
1359
|
+
"""Iterate over all files in path, and yield relative dataset paths.
|
|
1360
|
+
|
|
1361
|
+
Parameters
|
|
1362
|
+
----------
|
|
1363
|
+
recursive : bool
|
|
1364
|
+
If true, yield datasets in subdirectories.
|
|
1365
|
+
|
|
1366
|
+
Yields
|
|
1367
|
+
------
|
|
1368
|
+
ALFPath
|
|
1369
|
+
The next valid dataset path in lexicographical order.
|
|
1370
|
+
|
|
1371
|
+
See Also
|
|
1372
|
+
--------
|
|
1373
|
+
one.alf.io.iter_datasets - Equivalent function that can take any pathlike input and returns
|
|
1374
|
+
paths relative to the input path.
|
|
1375
|
+
|
|
1376
|
+
"""
|
|
1377
|
+
glob = self.rglob if recursive else self.glob
|
|
1378
|
+
for p in sorted(glob('*.*.*')):
|
|
1379
|
+
if not p.is_dir() and p.is_dataset:
|
|
1380
|
+
yield p
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
class PureWindowsALFPath(pathlib.PureWindowsPath, PureALFPath):
|
|
1384
|
+
"""PureALFPath subclass for Windows systems."""
|
|
1385
|
+
|
|
1386
|
+
pass
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
class PurePosixALFPath(pathlib.PurePosixPath, PureALFPath):
|
|
1390
|
+
"""PureALFPath subclass for non-Windows systems."""
|
|
1391
|
+
|
|
1392
|
+
pass
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
class WindowsALFPath(pathlib.WindowsPath, ALFPath):
|
|
1396
|
+
"""ALFPath subclass for Windows systems."""
|
|
1397
|
+
|
|
1398
|
+
pass
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
class PosixALFPath(pathlib.PosixPath, ALFPath):
|
|
1402
|
+
"""ALFPath subclass for non-Windows systems."""
|
|
1403
|
+
|
|
1404
|
+
pass
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def ensure_alf_path(path) -> Listable(PureALFPath):
|
|
1408
|
+
"""Ensure path is a PureALFPath instance.
|
|
1409
|
+
|
|
1410
|
+
Ensures the path entered is cast to a PureALFPath instance. If input class is PureALFPath or
|
|
1411
|
+
pathlib.PurePath, a PureALFPath instance is returned, otherwise an ALFPath instance is
|
|
1412
|
+
returned.
|
|
1413
|
+
|
|
1414
|
+
Parameters
|
|
1415
|
+
----------
|
|
1416
|
+
path : str, pathlib.PurePath, ALFPath, iterable
|
|
1417
|
+
One or more path-like objects.
|
|
1418
|
+
|
|
1419
|
+
Returns
|
|
1420
|
+
-------
|
|
1421
|
+
ALFPath, PureALFPath, list of ALFPath, list of PureALFPath
|
|
1422
|
+
One or more ALFPath objects.
|
|
1423
|
+
|
|
1424
|
+
Raises
|
|
1425
|
+
------
|
|
1426
|
+
TypeError
|
|
1427
|
+
Unexpected path instance; input must be a str or pathlib.PurePath instance, or an
|
|
1428
|
+
iterable thereof.
|
|
1429
|
+
|
|
1430
|
+
"""
|
|
1431
|
+
if isinstance(path, PureALFPath):
|
|
1432
|
+
# Already an ALFPath instance
|
|
1433
|
+
return path
|
|
1434
|
+
if isinstance(path, pathlib.PurePath):
|
|
1435
|
+
# Cast pathlib instance to equivalent ALFPath
|
|
1436
|
+
if isinstance(path, pathlib.Path):
|
|
1437
|
+
return ALFPath(path)
|
|
1438
|
+
elif isinstance(path, pathlib.PurePosixPath):
|
|
1439
|
+
return PurePosixALFPath(path)
|
|
1440
|
+
elif isinstance(path, pathlib.PureWindowsPath):
|
|
1441
|
+
return PureWindowsALFPath(path)
|
|
1442
|
+
else:
|
|
1443
|
+
return PureALFPath(path)
|
|
1444
|
+
if isinstance(path, str):
|
|
1445
|
+
# Cast str to ALFPath
|
|
1446
|
+
return ALFPath(path)
|
|
1447
|
+
if isinstance(path, Iterable):
|
|
1448
|
+
# Cast list, generator, tuple, etc. to list of ALFPath
|
|
1449
|
+
return list(map(ensure_alf_path, path))
|
|
1450
|
+
raise TypeError(f'expected os.PathLike type, got {type(path)} instead')
|