pymodaq_data 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1031 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created the 15/11/2022
4
+
5
+ @author: Sebastien Weber
6
+ """
7
+ import numpy as np
8
+ import importlib
9
+ from importlib import metadata
10
+ import pickle
11
+ from typing import Dict
12
+
13
+ from pymodaq_utils.logger import set_logger, get_module_name
14
+ from pymodaq_utils.config import Config
15
+ from pymodaq_utils.utils import capitalize, JsonConverter
16
+ from pymodaq_utils import utils
17
+ from pymodaq_utils.enums import BaseEnum, enum_checker
18
+
19
+
20
+ config = Config()
21
+ logger = set_logger(get_module_name(__file__))
22
+
23
+ backends_available = []
24
+
25
+ # default backend
26
+ is_tables = True
27
+ try:
28
+ import tables
29
+ backends_available.append('tables')
30
+ except Exception as e: # pragma: no cover
31
+ logger.warning(str(e))
32
+ is_tables = False
33
+
34
+ is_h5py = True
35
+ # other possibility
36
+ try:
37
+ import h5py
38
+ backends_available.append('h5py')
39
+ except Exception as e: # pragma: no cover
40
+ logger.warning(str(e))
41
+ is_h5py = False
42
+
43
+ is_h5pyd = True
44
+ # this one is to be used for remote reading/writing towards a HSDS server (or h5serv), see HDFGroup
45
+ try:
46
+ import h5pyd
47
+ backends_available.append('h5pyd')
48
+ except Exception as e: # pragma: no cover
49
+ logger.warning(str(e))
50
+ is_h5pyd = False
51
+
52
+ if not (is_tables or is_h5py or is_h5pyd):
53
+ logger.exception('No valid hdf5 backend has been installed, please install either pytables or h5py')
54
+
55
+
56
+ class NodeError(Exception):
57
+ pass
58
+
59
+
60
+ class SaveType(BaseEnum):
61
+ scan = 0
62
+ detector = 1
63
+ logger = 2
64
+ custom = 3
65
+ actuator = 4
66
+
67
+
68
+ class GroupType(BaseEnum):
69
+ detector = 0
70
+ actuator = 1
71
+ data = 2
72
+ ch = 3
73
+ scan = 4
74
+ external_h5 = 5
75
+ data_dim = 6
76
+ data_logger = 7
77
+
78
+
79
+ class InvalidExport(Exception):
80
+ pass
81
+
82
+
83
+ def check_mandatory_attrs(attr_name, attr):
84
+ """for cross compatibility between different backends. If these attributes have binary value, then decode them
85
+
86
+ Parameters
87
+ ----------
88
+ attr_name
89
+ attr
90
+
91
+ Returns
92
+ -------
93
+
94
+ """
95
+ if attr_name == 'TITLE' or attr_name == 'CLASS' or attr_name == 'EXTDIM':
96
+ if isinstance(attr, bytes):
97
+ return attr.decode()
98
+ else:
99
+ return attr
100
+ else:
101
+ return attr
102
+
103
+
104
+ def get_attr(node, attr_name, backend='tables'):
105
+ if backend == 'tables':
106
+ if attr_name is not None:
107
+ attr = node._v_attrs[attr_name]
108
+ attr = check_mandatory_attrs(attr_name, attr)
109
+ return JsonConverter.json2object(attr)
110
+ else:
111
+ attrs = dict([])
112
+ for attr_name in node._v_attrs._v_attrnames:
113
+ attrval = node._v_attrs[attr_name]
114
+ attrval = check_mandatory_attrs(attr_name, attrval)
115
+ attrs[attr_name] = JsonConverter.json2object(attrval)
116
+ return attrs
117
+ else:
118
+ if attr_name is not None:
119
+ attr = node.attrs[attr_name]
120
+ attr = check_mandatory_attrs(attr_name, attr)
121
+ return JsonConverter.json2object(attr)
122
+ else:
123
+ attrs = dict([])
124
+ for attr_name in node.attrs.keys():
125
+ attrval = node.attrs[attr_name]
126
+ attrval = check_mandatory_attrs(attr_name, attrval)
127
+ attrs[attr_name] = JsonConverter.json2object(attrval)
128
+ return attrs
129
+
130
+
131
+ def set_attr(node, attr_name, attr_value, backend='tables'):
132
+ if backend == 'tables':
133
+ node._v_attrs[attr_name] = JsonConverter.object2json(attr_value)
134
+ else:
135
+ node.attrs[attr_name] = JsonConverter.object2json(attr_value)
136
+
137
+
138
+ class InvalidGroupType(Exception):
139
+ pass
140
+
141
+
142
+ class InvalidSave(Exception):
143
+ pass
144
+
145
+
146
+ class InvalidGroupDataType(Exception):
147
+ pass
148
+
149
+
150
+ class InvalidDataType(Exception):
151
+ pass
152
+
153
+
154
+ class InvalidDataDimension(Exception):
155
+ pass
156
+
157
+
158
+ class InvalidScanType(Exception):
159
+ pass
160
+
161
+
162
+ class Node(object):
163
+ def __init__(self, node, backend):
164
+ if isinstance(node, Node): # to ovoid recursion if one call Node(Node()) or even more
165
+ self._node = node.node
166
+ else:
167
+ self._node = node
168
+ self.backend = backend
169
+ self._attrs = Attributes(self, backend)
170
+
171
+ def __str__(self):
172
+ # Get this class name
173
+ classname = self.__class__.__name__
174
+ # The title
175
+ title = self.attrs['TITLE']
176
+ return "%s (%s) %r" % \
177
+ (self.path, classname, title)
178
+
179
+ @property
180
+ def node(self):
181
+ return self._node
182
+
183
+ def __eq__(self, other):
184
+ return self.node == other.node
185
+
186
+ @property
187
+ def parent_node(self) -> 'GROUP':
188
+ if self.path == '/':
189
+ return None
190
+ mod = importlib.import_module('.backends', 'pymodaq_data.h5modules')
191
+
192
+ if self.backend == 'tables':
193
+ p = self.node._v_parent
194
+ else:
195
+ p = self.node.parent
196
+ klass = get_attr(p, 'CLASS', self.backend)
197
+ _cls = getattr(mod, klass)
198
+ return _cls(p, self.backend)
199
+
200
+ @property
201
+ def h5file(self):
202
+ if self.backend == 'tables':
203
+ return self.node._v_file
204
+ else:
205
+ return self.node.file
206
+
207
+ def to_h5_backend(self) -> 'H5Backend':
208
+ h5_backend = H5Backend(self.backend)
209
+ h5_backend.h5file = self.h5file
210
+ return h5_backend
211
+
212
+ def set_attr(self, key, value):
213
+ self.attrs[key] = value
214
+
215
+ def get_attr(self, item):
216
+ return self.attrs[item]
217
+
218
+ @property
219
+ def attrs(self):
220
+ return self._attrs
221
+
222
+ @property
223
+ def name(self):
224
+ """return node name
225
+ """
226
+ if self.backend == 'tables':
227
+ return self._node._v_name
228
+ else:
229
+ path = self._node.name
230
+ if path == '/':
231
+ return path
232
+ else:
233
+ return path.split('/')[-1]
234
+
235
+ @property
236
+ def title(self):
237
+ return self.attrs['TITLE']
238
+
239
+ @property
240
+ def path(self):
241
+ """return node path
242
+ Parameters
243
+ ----------
244
+ node (str or node instance), see h5py and pytables documentation on nodes
245
+
246
+ Returns
247
+ -------
248
+ str : full path of the node
249
+ """
250
+ if self.backend == 'tables':
251
+ return self._node._v_pathname
252
+ else:
253
+ return self._node.name
254
+
255
+
256
+ class GROUP(Node):
257
+ def __init__(self, node, backend):
258
+ super().__init__(node, backend)
259
+
260
+ def __str__(self):
261
+ """Return a short string representation of the group.
262
+ """
263
+
264
+ pathname = self.path
265
+ classname = self.__class__.__name__
266
+ title = self.attrs['TITLE']
267
+ return "%s (%s) %r" % (pathname, classname, title)
268
+
269
+ def __repr__(self):
270
+ """Return a detailed string representation of the group.
271
+ """
272
+
273
+ rep = [
274
+ '%r (%s)' % (childname, child.__class__.__name__)
275
+ for (childname, child) in self.children().items()
276
+ ]
277
+ childlist = '[%s]' % (', '.join(rep))
278
+
279
+ return "%s\n children := %s" % (str(self), childlist)
280
+
281
+ def children(self) -> Dict[str, Node]:
282
+ """Get a dict containing all children node hanging from self whith their name as keys
283
+
284
+ Returns
285
+ -------
286
+ dict: keys are children node names, values are the children nodes
287
+
288
+ See Also
289
+ --------
290
+ children_name
291
+ """
292
+ mod = importlib.import_module('.backends', 'pymodaq_data.h5modules')
293
+ children = dict([])
294
+ if self.backend == 'tables':
295
+ for child_name, child in self.node._v_children.items():
296
+ klass = get_attr(child, 'CLASS', self.backend)
297
+ if 'ARRAY' in klass:
298
+ _cls = getattr(mod, klass)
299
+ else:
300
+ _cls = GROUP
301
+ children[child_name] = _cls(child, self.backend)
302
+ else:
303
+ for child_name, child in self.node.items():
304
+
305
+ klass = get_attr(child, 'CLASS', self.backend)
306
+ if 'ARRAY' in klass:
307
+ _cls = getattr(mod, klass)
308
+ else:
309
+ _cls = GROUP
310
+ children[child_name] = _cls(child, self.backend)
311
+ return children
312
+
313
+ def get_child(self, name: str) -> Node:
314
+ return self.children()[name]
315
+
316
+ def children_name(self):
317
+ """Gets the sorted list of children name hanging from self
318
+
319
+ Returns
320
+ -------
321
+ list: list of name of the children
322
+ """
323
+ if self.backend == 'tables':
324
+ return sorted(list(self.node._v_children.keys()))
325
+ else:
326
+ return sorted(list(self.node.keys()))
327
+ pass
328
+
329
+ def remove_children(self):
330
+ children_dict = self.children()
331
+ for child_name in children_dict:
332
+ if self.backend == 'tables':
333
+ children_dict[child_name].node._f_remove(recursive=True)
334
+ else:
335
+ self.node.__delitem__(child_name)
336
+
337
+
338
+ class CARRAY(Node):
339
+ def __init__(self, node, backend):
340
+ super().__init__(node, backend)
341
+ self._array = node
342
+
343
+ @property
344
+ def array(self):
345
+ return self._array
346
+
347
+ def __repr__(self):
348
+ """This provides more metainfo in addition to standard __str__"""
349
+
350
+ return """%s
351
+ shape := %s
352
+ dtype := %s""" % (self, str(self.attrs['shape']), self.attrs['dtype'])
353
+
354
+ def __getitem__(self, item):
355
+ return self._array.__getitem__(item)
356
+
357
+ def __setitem__(self, key, value):
358
+ self._array.__setitem__(key, value)
359
+
360
+ def read(self):
361
+ if self.backend == 'tables':
362
+ return self._array.read()
363
+ else:
364
+ return self._array[:]
365
+
366
+ def __len__(self):
367
+ if self.backend == 'tables':
368
+ return self.array.nrows
369
+ else:
370
+ return len(self.array)
371
+
372
+
373
+ class EARRAY(CARRAY):
374
+ def __init__(self, array, backend):
375
+ super().__init__(array, backend)
376
+
377
+ def append(self, data: np.ndarray, expand=True):
378
+ """ appends a ndarray after the current data in the enlargeable array
379
+
380
+ Considering the shape length of the enlargeable array is n+1
381
+
382
+ The data to append could be:
383
+
384
+ * a single element (without the enlargeable shape index that is always the first
385
+ index, that is of shape length n). In that case the first index of the enlargeable array
386
+ is increased by one.
387
+ * an ensemble of elements (a ndarray) of shape length of (n+1).
388
+
389
+ Parameters
390
+ ----------
391
+ data: np.ndarray
392
+ the data array to append to the enlargeable node
393
+ expand: bool
394
+ If True the data array will have its shape expanded by one dim
395
+
396
+ """
397
+ if not isinstance(data, np.ndarray):
398
+ raise TypeError('The appended object should be a ndarray')
399
+ if len(self.attrs['shape']) > 1 and data.shape == self.attrs['shape'][1:]:
400
+ shape = [1]
401
+ shape.extend(data.shape)
402
+ data = data.reshape(shape)
403
+ extended_first_index = 1
404
+ else:
405
+ extended_first_index = data.shape[0]
406
+ if expand and (len(data.shape) == 1 and not data.shape == (1, )):
407
+ data = np.expand_dims(data, 1)
408
+ self.append_backend(data)
409
+
410
+ sh = list(self.attrs['shape'])
411
+ sh[0] += extended_first_index
412
+ self.attrs['shape'] = tuple(sh)
413
+
414
+ def append_backend(self, data):
415
+ if self.backend == 'tables':
416
+ self.array.append(data)
417
+ else:
418
+ self.array.resize(self.array.len() + 1, axis=0)
419
+ self.array[-1] = data
420
+
421
+
422
+ class VLARRAY(EARRAY):
423
+ def __init__(self, array, backend):
424
+ super().__init__(array, backend)
425
+
426
+ def append(self, data):
427
+ self.append_backend(data)
428
+
429
+ sh = list(self.attrs['shape'])
430
+ sh[0] += 1
431
+ self.attrs['shape'] = tuple(sh)
432
+
433
+
434
+ class StringARRAY(VLARRAY):
435
+ def __init__(self, array, backend):
436
+ super().__init__(array, backend)
437
+
438
+ def __getitem__(self, item):
439
+ return self.array_to_string(super().__getitem__(item))
440
+
441
+ def read(self):
442
+ data_list = super().read()
443
+ return [self.array_to_string(data) for data in data_list]
444
+
445
+ def append(self, string):
446
+ data = self.string_to_array(string)
447
+ super().append(data)
448
+
449
+ def array_to_string(self, array):
450
+ return pickle.loads(array)
451
+
452
+ def string_to_array(self, string):
453
+ return np.frombuffer(pickle.dumps(string), np.uint8)
454
+
455
+
456
+ class Attributes(object):
457
+ def __init__(self, node, backend='tables'):
458
+ self._node = node
459
+ self.backend = backend
460
+
461
+ def __getitem__(self, item):
462
+ if item == 'title':
463
+ item = item.upper()
464
+ attr = get_attr(self._node.node, item, backend=self.backend)
465
+ # if isinstance(attr, bytes):
466
+ # attr = attr.decode()
467
+ return attr
468
+
469
+ def __setitem__(self, key, value):
470
+ if key == 'title':
471
+ key = key.upper()
472
+ set_attr(self._node.node, key, value, backend=self.backend)
473
+
474
+ def __iter__(self):
475
+ self._iter_index = 0
476
+ return self
477
+
478
+ def __next__(self):
479
+ if self._iter_index < len(self):
480
+ self._iter_index += 1
481
+ return self.attrs_name[self._iter_index-1]
482
+ else:
483
+ raise StopIteration
484
+
485
+ def __len__(self):
486
+ return len(self.attrs_name)
487
+
488
+ def to_dict(self) -> dict:
489
+ """Returns attributes name/value as a dict"""
490
+ attrs_dict = dict()
491
+ for name in self.attrs_name:
492
+ attrs_dict[name] = self[name]
493
+ return attrs_dict
494
+
495
+ @property
496
+ def node(self):
497
+ return self._node
498
+
499
+ @property
500
+ def attrs_name(self):
501
+ if self.backend == 'tables':
502
+ return [k for k in self.node.node._v_attrs._v_attrnames]
503
+ else:
504
+ return [k for k in self.node.node.attrs.keys()]
505
+
506
+ def __str__(self):
507
+ """The string representation for this object."""
508
+
509
+ # The pathname
510
+ if self.backend == 'tables':
511
+ pathname = self._node.node._v_pathname
512
+ else:
513
+ pathname = self._node.node.name
514
+ # Get this class name
515
+ classname = self.__class__.__name__
516
+ # The attribute names
517
+ attrnumber = len([n for n in self.attrs_name])
518
+ return "%s.attrs (%s), %s attributes" % \
519
+ (pathname, classname, attrnumber)
520
+
521
+ def __repr__(self):
522
+ attrnames = self.attrs_name
523
+ if len(attrnames):
524
+ rep = ['%s := %s' % (attr, str(self[attr]))
525
+ for attr in attrnames]
526
+ attrlist = '[%s]' % (',\n '.join(rep))
527
+
528
+ return "%s:\n %s" % (str(self), attrlist)
529
+ else:
530
+ return str(self)
531
+
532
+
533
+ class H5Backend:
534
+ def __init__(self, backend='tables'):
535
+
536
+ self._h5file = None
537
+ self.backend = backend
538
+ self.file_path = None
539
+ self.compression = None
540
+ if backend == 'tables':
541
+ if is_tables:
542
+ self.h5_library = tables
543
+ else:
544
+ raise ImportError('the pytables module is not present')
545
+ elif backend == 'h5py':
546
+ if is_h5py:
547
+ self.h5_library = h5py
548
+ else:
549
+ raise ImportError('the h5py module is not present')
550
+ elif backend == 'h5pyd':
551
+ if is_h5pyd:
552
+ self.h5_library = h5pyd
553
+ else:
554
+ raise ImportError('the h5pyd module is not present')
555
+
556
+ @property
557
+ def h5file(self):
558
+ return self._h5file
559
+
560
+ @h5file.setter
561
+ def h5file(self, file):
562
+ self.file_path = file.filename
563
+ self._h5file = file
564
+
565
+ @property
566
+ def filename(self):
567
+ return self._h5file.filename
568
+
569
+ def isopen(self):
570
+ if self._h5file is None:
571
+ return False
572
+ if self.backend == 'tables':
573
+ return bool(self._h5file.isopen)
574
+ elif self.backend == 'h5py':
575
+ return bool(self._h5file.id.valid)
576
+ else:
577
+ return self._h5file.id.http_conn is not None
578
+
579
+ def close_file(self):
580
+ """Flush data and close the h5file
581
+ """
582
+ try:
583
+ if self._h5file is not None:
584
+ self.flush()
585
+ if self.isopen():
586
+ self._h5file.close()
587
+ except Exception as e:
588
+ print(e) # no big deal
589
+
590
+ def open_file(self, fullpathname, mode='r', title='PyMoDAQ file', **kwargs):
591
+ self.file_path = fullpathname
592
+ if self.backend == 'tables':
593
+ self._h5file = self.h5_library.open_file(str(fullpathname), mode=mode, title=title, **kwargs)
594
+ if mode == 'w':
595
+ try:
596
+ self.root().attrs['pymodaq_version'] = utils.get_version('pymodaq')
597
+ except importlib.metadata.PackageNotFoundError:
598
+ self.root().attrs['pymodaq_version'] = '0.0.0'
599
+ self.root().attrs['pymodaq_data_version'] = utils.get_version('pymodaq_data')
600
+ return self._h5file
601
+ else:
602
+ self._h5file = self.h5_library.File(str(fullpathname), mode=mode, **kwargs)
603
+
604
+ if mode == 'w':
605
+ self.root().attrs['TITLE'] = title
606
+ try:
607
+ self.root().attrs['pymodaq_version'] = utils.get_version('pymodaq')
608
+ except importlib.metadata.PackageNotFoundError:
609
+ self.root().attrs['pymodaq_version'] = '0.0.0'
610
+ self.root().attrs['pymodaq_data_version'] = utils.get_version('pymodaq_data')
611
+ return self._h5file
612
+
613
+ def save_file_as(self, filenamepath='h5copy.txt'):
614
+ if self.backend == 'tables':
615
+ self.h5file.copy_file(str(filenamepath))
616
+ else:
617
+ raise Warning(f'Not possible to copy the file with the "{self.backend}" backend')
618
+
619
+ def root(self):
620
+ if self.backend == 'tables':
621
+ return GROUP(self._h5file.get_node('/'), self.backend)
622
+ else:
623
+ return GROUP(self._h5file, self.backend)
624
+
625
+ def get_attr(self, node, attr_name=None):
626
+ if isinstance(node, Node):
627
+ node = node.node
628
+ return get_attr(node, attr_name, self.backend)
629
+
630
+ def set_attr(self, node, attr_name, attr_value):
631
+ if isinstance(node, Node):
632
+ node = node.node
633
+ return set_attr(node, attr_name, attr_value, self.backend)
634
+
635
+ def has_attr(self, node, attr_name):
636
+ return attr_name in self.get_node(node).attrs.attrs_name
637
+
638
+ def flush(self):
639
+ if self._h5file is not None:
640
+ self._h5file.flush()
641
+
642
+ def define_compression(self, compression, compression_opts):
643
+ """Define cmpression library and level of compression
644
+ Parameters
645
+ ----------
646
+ compression: (str) either gzip and zlib are supported here as they are compatible
647
+ but zlib is used by pytables while gzip is used by h5py
648
+ compression_opts (int) : 0 to 9 0: None, 9: maximum compression
649
+ """
650
+ #
651
+ if self.backend == 'tables':
652
+ if compression == 'gzip':
653
+ compression = 'zlib'
654
+ self.compression = self.h5_library.Filters(complevel=compression_opts, complib=compression)
655
+ else:
656
+ if compression == 'zlib':
657
+ compression = 'gzip'
658
+ self.compression = dict(compression=compression, compression_opts=compression_opts)
659
+
660
+ def get_set_group(self, where, name, title=''):
661
+ """Retrieve or create (if absent) a node group
662
+ Get attributed to the class attribute ``current_group``
663
+
664
+ Parameters
665
+ ----------
666
+ where: str or node
667
+ path or parent node instance
668
+ name: str
669
+ group node name
670
+ title: str
671
+ node title
672
+
673
+ Returns
674
+ -------
675
+ group: group node
676
+ """
677
+ if isinstance(where, Node):
678
+ where = where.node
679
+
680
+ if name not in list(self.get_children(where)):
681
+ if self.backend == 'tables':
682
+ group = self._h5file.create_group(where, name, title)
683
+ else:
684
+ group = self.get_node(where).node.create_group(name)
685
+ group.attrs['TITLE'] = title
686
+ group.attrs['CLASS'] = 'GROUP'
687
+
688
+ else:
689
+ group = self.get_node(where, name)
690
+ return GROUP(group, self.backend)
691
+
692
+ def get_group_by_title(self, where, title):
693
+ if isinstance(where, Node):
694
+ where = where.node
695
+
696
+ node = self.get_node(where).node
697
+ for child_name in self.get_children(node):
698
+ child = node[child_name]
699
+ if 'TITLE' in self.get_attr(child):
700
+ if self.get_attr(child, 'TITLE') == title and self.get_attr(child, 'CLASS') == 'GROUP':
701
+ return GROUP(child, self.backend)
702
+ return None
703
+
704
+ def is_node_in_group(self, where, name):
705
+ """
706
+ Check if a given node with name is in the group defined by where (comparison on lower case strings)
707
+ Parameters
708
+ ----------
709
+ where: (str or node)
710
+ path or parent node instance
711
+ name: (str)
712
+ group node name
713
+
714
+ Returns
715
+ -------
716
+ bool
717
+ True if node exists, False otherwise
718
+ """
719
+ if isinstance(where, Node):
720
+ where = where.node
721
+
722
+ return name.lower() in [name.lower() for name in self.get_children(where)]
723
+
724
+ def get_node(self, where, name=None) -> Node:
725
+ if isinstance(where, Node):
726
+ where = where.node
727
+ try:
728
+ if self.backend == 'tables':
729
+ node = self._h5file.get_node(where, name)
730
+ else:
731
+ if name is not None:
732
+ if isinstance(where, str):
733
+ where += f'/{name}'
734
+ node = self._h5file.get(where)
735
+ else:
736
+ where = where.get(name)
737
+ node = where
738
+ else:
739
+ if isinstance(where, str):
740
+ node = self._h5file.get(where)
741
+ else:
742
+ node = where
743
+ except Exception as e:
744
+ raise NodeError(str(e))
745
+
746
+ if 'CLASS' not in self.get_attr(node):
747
+ self.set_attr(node, 'CLASS', 'GROUP')
748
+ return GROUP(node, self.backend)
749
+ else:
750
+ attr = self.get_attr(node, 'CLASS')
751
+ if 'ARRAY' not in attr:
752
+ return GROUP(node, self.backend)
753
+ elif attr == 'CARRAY':
754
+ return CARRAY(node, self.backend)
755
+ elif attr == 'EARRAY':
756
+ return EARRAY(node, self.backend)
757
+ elif attr == 'VLARRAY':
758
+ if self.get_attr(node, 'subdtype') == 'string':
759
+ return StringARRAY(node, self.backend)
760
+ else:
761
+ return VLARRAY(node, self.backend)
762
+
763
+ def get_node_name(self, node):
764
+ """return node name
765
+ Parameters
766
+ ----------
767
+ node (str or node instance), see h5py and pytables documentation on nodes
768
+
769
+ Returns
770
+ -------
771
+ str: name of the node
772
+ """
773
+ if isinstance(node, Node):
774
+ node = node.node
775
+ return self.get_node(node).name
776
+
777
+ def get_node_path(self, node):
778
+ """return node path
779
+ Parameters
780
+ ----------
781
+ node (str or node instance), see h5py and pytables documentation on nodes
782
+
783
+ Returns
784
+ -------
785
+ str : full path of the node
786
+ """
787
+ if isinstance(node, Node):
788
+ node = node.node
789
+ return self.get_node(node).path
790
+
791
+ def get_parent_node(self, node):
792
+ if node == self.root():
793
+ return None
794
+ if isinstance(node, Node):
795
+ node = node.node
796
+
797
+ if self.backend == 'tables':
798
+ return self.get_node(node._v_parent)
799
+ else:
800
+ return self.get_node(node.parent)
801
+
802
+ def get_children(self, where):
803
+ """Get a dict containing all children node hanging from where with their name as keys and types among Node,
804
+ CARRAY, EARRAY, VLARRAY or StringARRAY
805
+
806
+ Parameters
807
+ ----------
808
+ where (str or node instance)
809
+ see h5py and pytables documentation on nodes, and Node objects of this module
810
+
811
+ Returns
812
+ -------
813
+ dict: keys are children node names, values are the children nodes
814
+
815
+ See Also
816
+ --------
817
+ :meth:`.GROUP.children_name`
818
+
819
+ """
820
+ where = self.get_node(where) # return a node object in case where is a string
821
+ if isinstance(where, Node):
822
+ where = where.node
823
+
824
+ mod = importlib.import_module('.backends', 'pymodaq_data.h5modules')
825
+ children = dict([])
826
+ if self.backend == 'tables':
827
+ for child_name, child in where._v_children.items():
828
+ klass = get_attr(child, 'CLASS', self.backend)
829
+ if 'ARRAY' in klass:
830
+ _cls = getattr(mod, klass)
831
+ else:
832
+ _cls = GROUP
833
+ children[child_name] = _cls(child, self.backend)
834
+ else:
835
+ for child_name, child in where.items():
836
+ klass = get_attr(child, 'CLASS', self.backend)
837
+ if 'ARRAY' in klass:
838
+ _cls = getattr(mod, klass)
839
+ else:
840
+ _cls = GROUP
841
+ children[child_name] = _cls(child, self.backend)
842
+ return children
843
+
844
+ def walk_nodes(self, where):
845
+ where = self.get_node(where) # return a node object in case where is a string
846
+ yield where
847
+ for gr in self.walk_groups(where):
848
+ for child in self.get_children(gr).values():
849
+ yield child
850
+
851
+ def walk_groups(self, where):
852
+ where = self.get_node(where) # return a node object in case where is a string
853
+ if where.attrs['CLASS'] != 'GROUP':
854
+ return None
855
+ if self.backend == 'tables':
856
+ for ch in self.h5file.walk_groups(where.node):
857
+ yield self.get_node(ch)
858
+ else:
859
+ stack = [where]
860
+ yield where
861
+ while stack:
862
+ obj = stack.pop()
863
+ children = [child for child in self.get_children(obj).values() if child.attrs['CLASS'] == 'GROUP']
864
+ for child in children:
865
+ stack.append(child)
866
+ yield child
867
+
868
+ def read(self, array, *args, **kwargs):
869
+ if isinstance(array, CARRAY):
870
+ array = array.array
871
+ if self.backend == 'tables':
872
+ return array.read()
873
+ else:
874
+ return array[:]
875
+
876
+ def create_carray(self, where, name, obj=None, title=''):
877
+ if isinstance(where, Node):
878
+ where = where.node
879
+ if obj is None:
880
+ raise ValueError('Data to be saved as carray cannot be None')
881
+ dtype = obj.dtype
882
+ if self.backend == 'tables':
883
+ array = CARRAY(self._h5file.create_carray(where, name, obj=obj,
884
+ title=title,
885
+ filters=self.compression), self.backend)
886
+ else:
887
+ if self.compression is not None:
888
+ array = CARRAY(self.get_node(where).node.create_dataset(name, data=obj, **self.compression),
889
+ self.backend)
890
+ else:
891
+ array = CARRAY(self.get_node(where).node.create_dataset(name, data=obj), self.backend)
892
+ array.array.attrs['TITLE'] = title
893
+ array.array.attrs[
894
+ 'CLASS'] = 'CARRAY' # direct writing using h5py to be compatible with pytable automatic class writing as binary
895
+ array.attrs['shape'] = obj.shape
896
+ array.attrs['dtype'] = dtype.name
897
+ array.attrs['subdtype'] = ''
898
+ array.attrs['backend'] = self.backend
899
+ return array
900
+
901
+ def create_earray(self, where, name, dtype, data_shape=None, title=''):
902
+ """create enlargeable arrays from data with a given shape and of a given type. The array is enlargeable along
903
+ the first dimension
904
+ """
905
+ if isinstance(where, Node):
906
+ where = where.node
907
+ dtype = np.dtype(dtype)
908
+ shape = [0]
909
+ if data_shape is not None:
910
+ shape.extend(list(data_shape))
911
+ shape = tuple(shape)
912
+
913
+ if self.backend == 'tables':
914
+ atom = self.h5_library.Atom.from_dtype(dtype)
915
+ array = EARRAY(self._h5file.create_earray(where, name, atom, shape=shape, title=title,
916
+ filters=self.compression), self.backend)
917
+ else:
918
+ maxshape = [None]
919
+ if data_shape is not None:
920
+ maxshape.extend(list(data_shape))
921
+ maxshape = tuple(maxshape)
922
+ if self.compression is not None:
923
+ array = EARRAY(
924
+ self.get_node(where).node.create_dataset(name, shape=shape, dtype=dtype, maxshape=maxshape,
925
+ **self.compression), self.backend)
926
+ else:
927
+ array = EARRAY(
928
+ self.get_node(where).node.create_dataset(name, shape=shape, dtype=dtype, maxshape=maxshape),
929
+ self.backend)
930
+ array.array.attrs['TITLE'] = title
931
+ array.array.attrs[
932
+ 'CLASS'] = 'EARRAY' # direct writing using h5py to be compatible with pytable automatic class writing as binary
933
+ array.array.attrs['EXTDIM'] = 0
934
+ array.attrs['shape'] = shape
935
+ array.attrs['dtype'] = dtype.name
936
+ array.attrs['subdtype'] = ''
937
+ array.attrs['backend'] = self.backend
938
+ return array
939
+
940
+ def create_vlarray(self, where, name, dtype, title=''):
941
+ """create variable data length and type and enlargeable 1D arrays
942
+
943
+ Parameters
944
+ ----------
945
+ where: (str) group location in the file where to create the array node
946
+ name: (str) name of the array
947
+ dtype: (dtype) numpy dtype style, for particular case of strings, use dtype='string'
948
+ title: (str) node title attribute (written in capitals)
949
+
950
+ Returns
951
+ -------
952
+ array
953
+
954
+ """
955
+ if isinstance(where, Node):
956
+ where = where.node
957
+ if dtype == 'string':
958
+ dtype = np.dtype(np.uint8)
959
+ subdtype = 'string'
960
+ else:
961
+ dtype = np.dtype(dtype)
962
+ subdtype = ''
963
+ if self.backend == 'tables':
964
+ atom = self.h5_library.Atom.from_dtype(dtype)
965
+ if subdtype == 'string':
966
+ array = StringARRAY(self._h5file.create_vlarray(where, name, atom, title=title,
967
+ filters=self.compression), self.backend)
968
+ else:
969
+ array = VLARRAY(self._h5file.create_vlarray(where, name, atom, title=title,
970
+ filters=self.compression), self.backend)
971
+ else:
972
+ maxshape = (None,)
973
+ if self.backend == 'h5py':
974
+ dt = self.h5_library.vlen_dtype(dtype)
975
+ else:
976
+ dt = h5pyd.special_dtype(dtype)
977
+ if self.compression is not None:
978
+ if subdtype == 'string':
979
+ array = StringARRAY(self.get_node(where).node.create_dataset(name, (0,), dtype=dt,
980
+ **self.compression, maxshape=maxshape),
981
+ self.backend)
982
+ else:
983
+ array = VLARRAY(self.get_node(where).node.create_dataset(name, (0,), dtype=dt, **self.compression,
984
+ maxshape=maxshape), self.backend)
985
+ else:
986
+ if subdtype == 'string':
987
+ array = StringARRAY(self.get_node(where).node.create_dataset(name, (0,), dtype=dt,
988
+ maxshape=maxshape), self.backend)
989
+ else:
990
+ array = VLARRAY(self.get_node(where).node.create_dataset(name, (0,), dtype=dt,
991
+ maxshape=maxshape), self.backend)
992
+ array.array.attrs['TITLE'] = title
993
+ array.array.attrs[
994
+ 'CLASS'] = 'VLARRAY' # direct writing using h5py to be compatible with pytable automatic class writing as binary
995
+ array.array.attrs['EXTDIM'] = 0
996
+ array.attrs['shape'] = (0,)
997
+ array.attrs['dtype'] = dtype.name
998
+ array.attrs['subdtype'] = subdtype
999
+ array.attrs['backend'] = self.backend
1000
+ return array
1001
+
1002
+ def add_group(self, group_name, group_type: GroupType, where, title='', metadata=dict([])) -> GROUP:
1003
+ """
1004
+ Add a node in the h5 file tree of the group type
1005
+ Parameters
1006
+ ----------
1007
+ group_name: (str) a custom name for this group
1008
+ group_type: str or GroupType enum
1009
+ one of the possible values of GroupType
1010
+ where: (str or node) parent node where to create the new group
1011
+ metadata: (dict) extra metadata to be saved with this new group node
1012
+
1013
+ Returns
1014
+ -------
1015
+ (node): newly created group node
1016
+ """
1017
+ if isinstance(where, Node):
1018
+ where = where.node
1019
+
1020
+ group_type = enum_checker(GroupType, group_type)
1021
+
1022
+ if group_name in self.get_children(self.get_node(where)):
1023
+ node = self.get_node(where, group_name)
1024
+
1025
+ else:
1026
+ node = self.get_set_group(where, utils.capitalize(group_name), title)
1027
+ node.attrs['type'] = group_type.name.lower()
1028
+ for metadat in metadata:
1029
+ node.attrs[metadat] = metadata[metadat]
1030
+ node.attrs['backend'] = self.backend
1031
+ return node