npTDMS 1.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nptdms/__init__.py +11 -0
- nptdms/base_segment.py +179 -0
- nptdms/channel_data.py +205 -0
- nptdms/common.py +99 -0
- nptdms/daqmx.py +340 -0
- nptdms/export/__init__.py +0 -0
- nptdms/export/hdf_export.py +96 -0
- nptdms/export/pandas_export.py +122 -0
- nptdms/log.py +38 -0
- nptdms/reader.py +527 -0
- nptdms/scaling.py +667 -0
- nptdms/tdms.py +1013 -0
- nptdms/tdms_segment.py +661 -0
- nptdms/tdmsinfo.py +61 -0
- nptdms/test/__init__.py +0 -0
- nptdms/test/data/Digital_Input.tdms +0 -0
- nptdms/test/data/big_endian.tdms +0 -0
- nptdms/test/data/raw1.tdms +0 -0
- nptdms/test/data/raw_timestamps.tdms +0 -0
- nptdms/test/scenarios.py +996 -0
- nptdms/test/test_benchmarks.py +377 -0
- nptdms/test/test_daqmx.py +1085 -0
- nptdms/test/test_example_files.py +89 -0
- nptdms/test/test_hdf.py +328 -0
- nptdms/test/test_pandas.py +466 -0
- nptdms/test/test_scaling.py +830 -0
- nptdms/test/test_tdms_file.py +1243 -0
- nptdms/test/test_tdmsinfo.py +49 -0
- nptdms/test/test_thermocouples.py +213 -0
- nptdms/test/test_timestamps.py +246 -0
- nptdms/test/test_types.py +87 -0
- nptdms/test/util.py +341 -0
- nptdms/test/writer/__init__.py +0 -0
- nptdms/test/writer/test_tdms_segment.py +235 -0
- nptdms/test/writer/test_writer.py +582 -0
- nptdms/thermocouples.py +722 -0
- nptdms/timestamp.py +138 -0
- nptdms/types.py +317 -0
- nptdms/utils.py +46 -0
- nptdms/version.py +5 -0
- nptdms/writer.py +495 -0
- nptdms-1.11.0.dist-info/METADATA +151 -0
- nptdms-1.11.0.dist-info/RECORD +47 -0
- nptdms-1.11.0.dist-info/WHEEL +5 -0
- nptdms-1.11.0.dist-info/entry_points.txt +2 -0
- nptdms-1.11.0.dist-info/licenses/COPYING.LESSER +165 -0
- nptdms-1.11.0.dist-info/top_level.txt +1 -0
nptdms/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Module for reading binary TDMS files produced by LabView"""
|
|
2
|
+
|
|
3
|
+
from __future__ import absolute_import
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Make version number available
|
|
7
|
+
from .version import __version_info__, __version__
|
|
8
|
+
|
|
9
|
+
# Export public objects
|
|
10
|
+
from .tdms import TdmsFile, TdmsGroup, TdmsChannel, DataChunk, GroupDataChunk, ChannelDataChunk
|
|
11
|
+
from .writer import TdmsWriter, RootObject, GroupObject, ChannelObject
|
nptdms/base_segment.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from io import UnsupportedOperation
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from nptdms.log import log_manager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
log = log_manager.get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseSegmentObject(object):
|
|
11
|
+
""" Abstract base class for an object in a TDMS segment
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__slots__ = [
|
|
15
|
+
'path', 'number_values', 'data_size',
|
|
16
|
+
'has_data', 'data_type']
|
|
17
|
+
|
|
18
|
+
def __init__(self, path):
|
|
19
|
+
self.path = path
|
|
20
|
+
self.number_values = 0
|
|
21
|
+
self.data_size = 0
|
|
22
|
+
self.has_data = False
|
|
23
|
+
self.data_type = None
|
|
24
|
+
|
|
25
|
+
def read_raw_data_index(self, file, raw_data_index_header, endianness):
|
|
26
|
+
""" Read the raw data index for a single object in a segment
|
|
27
|
+
"""
|
|
28
|
+
raise NotImplementedError("Segment metadata reading must be implemented in base classes")
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def scaler_data_types(self):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return "%s(%s)" % (self.__class__.__name__, self.path)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseDataReader(object):
|
|
39
|
+
""" Abstract base class for reading data in a segment
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, num_chunks, final_chunk_lengths_override, endianness):
|
|
43
|
+
self.num_chunks = num_chunks
|
|
44
|
+
self.final_chunk_lengths_override = final_chunk_lengths_override
|
|
45
|
+
self.endianness = endianness
|
|
46
|
+
|
|
47
|
+
def read_data_chunks(self, file, data_objects, num_chunks):
|
|
48
|
+
""" Read multiple data chunks at once
|
|
49
|
+
In the base case we read each chunk individually but subclasses can override this
|
|
50
|
+
"""
|
|
51
|
+
for chunk in range(num_chunks):
|
|
52
|
+
yield self._read_data_chunk(file, data_objects, chunk)
|
|
53
|
+
|
|
54
|
+
def _read_data_chunk(self, file, data_objects, chunk_index):
|
|
55
|
+
""" Read data from a chunk for all channels
|
|
56
|
+
"""
|
|
57
|
+
raise NotImplementedError("Data chunk reading must be implemented in base classes")
|
|
58
|
+
|
|
59
|
+
def read_channel_data_chunks(self, file, data_objects, channel_path, chunk_offset, stop_chunk):
|
|
60
|
+
""" Read multiple data chunks for a single channel at once
|
|
61
|
+
In the base case we read each chunk individually but subclasses can override this
|
|
62
|
+
"""
|
|
63
|
+
for chunk_index in range(chunk_offset, stop_chunk):
|
|
64
|
+
yield self._read_channel_data_chunk(file, data_objects, chunk_index, channel_path)
|
|
65
|
+
|
|
66
|
+
def _read_channel_data_chunk(self, file, data_objects, chunk_index, channel_path):
|
|
67
|
+
""" Read data from a chunk for a single channel
|
|
68
|
+
"""
|
|
69
|
+
# In the base case we can read data for all channels
|
|
70
|
+
# and then select only the requested channel.
|
|
71
|
+
# Derived classes can implement more optimised reading.
|
|
72
|
+
data_chunk = self._read_data_chunk(file, data_objects, chunk_index)
|
|
73
|
+
return data_chunk_to_channel_chunk(data_chunk, channel_path)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RawDataChunk(object):
|
|
77
|
+
"""Data read from a single chunk in a TDMS segment
|
|
78
|
+
|
|
79
|
+
:ivar channel_data: A dictionary of channel data chunks.
|
|
80
|
+
Keys are object paths and values are RawChannelDataChunk instances.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, channel_data):
|
|
84
|
+
self.channel_data = channel_data
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def empty():
|
|
88
|
+
return RawDataChunk({})
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def channel_data(data):
|
|
92
|
+
channel_chunks = {
|
|
93
|
+
path: RawChannelDataChunk.channel_data(d)
|
|
94
|
+
for (path, d) in data.items()
|
|
95
|
+
}
|
|
96
|
+
return RawDataChunk(channel_chunks)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def scaler_data(data):
|
|
100
|
+
channel_chunks = {
|
|
101
|
+
path: RawChannelDataChunk.scaler_data(d)
|
|
102
|
+
for (path, d) in data.items()
|
|
103
|
+
}
|
|
104
|
+
return RawDataChunk(channel_chunks)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RawChannelDataChunk(object):
|
|
108
|
+
"""Data read for a single channel from a single chunk in a TDMS segment
|
|
109
|
+
|
|
110
|
+
:ivar data: Raw data in this chunk for a standard TDMS channel.
|
|
111
|
+
:ivar scaler_data: A dictionary of scaler data in this segment for
|
|
112
|
+
DAQmx raw data. Keys are the scaler id and values are data arrays.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, data, scaler_data):
|
|
116
|
+
self.data = data
|
|
117
|
+
self.scaler_data = scaler_data
|
|
118
|
+
|
|
119
|
+
def __len__(self):
|
|
120
|
+
if self.data is not None:
|
|
121
|
+
return len(self.data)
|
|
122
|
+
elif self.scaler_data is not None:
|
|
123
|
+
return next(len(d) for d in self.scaler_data.values())
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def empty():
|
|
128
|
+
return RawChannelDataChunk(None, None)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def channel_data(data):
|
|
132
|
+
return RawChannelDataChunk(data, None)
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def scaler_data(data):
|
|
136
|
+
return RawChannelDataChunk(None, data)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def fromfile(file, dtype, count, *args, **kwargs):
|
|
140
|
+
"""Read from any file-like object into a numpy array"""
|
|
141
|
+
|
|
142
|
+
itemsize = np.dtype(dtype).itemsize
|
|
143
|
+
buffer = np.zeros(count * itemsize, np.uint8)
|
|
144
|
+
bytes_read = -1
|
|
145
|
+
offset = 0
|
|
146
|
+
while bytes_read != 0:
|
|
147
|
+
bytes_read = file.readinto(buffer[offset:])
|
|
148
|
+
offset += bytes_read
|
|
149
|
+
rounded_bytes = (offset // itemsize) * itemsize
|
|
150
|
+
buffer = buffer[:rounded_bytes]
|
|
151
|
+
buffer.dtype = dtype
|
|
152
|
+
return buffer
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def read_interleaved_segment_bytes(f, bytes_per_row, num_values):
|
|
156
|
+
""" Read a segment of interleaved data as rows of bytes
|
|
157
|
+
"""
|
|
158
|
+
number_bytes = bytes_per_row * num_values
|
|
159
|
+
combined_data = fromfile(f, dtype=np.uint8, count=number_bytes)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Reshape, so that one row is all bytes for all objects
|
|
163
|
+
combined_data = combined_data.reshape(-1, bytes_per_row)
|
|
164
|
+
except ValueError:
|
|
165
|
+
# Probably incomplete segment at the end => try to clip data
|
|
166
|
+
crop_len = (combined_data.shape[0] // bytes_per_row)
|
|
167
|
+
crop_len *= bytes_per_row
|
|
168
|
+
log.warning("Cropping data from %d to %d bytes to match segment "
|
|
169
|
+
"size derived from channels",
|
|
170
|
+
combined_data.shape[0], crop_len)
|
|
171
|
+
combined_data = combined_data[:crop_len].reshape(-1, bytes_per_row)
|
|
172
|
+
return combined_data
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def data_chunk_to_channel_chunk(data_chunk, channel_path):
|
|
176
|
+
try:
|
|
177
|
+
return data_chunk.channel_data[channel_path]
|
|
178
|
+
except KeyError:
|
|
179
|
+
return RawChannelDataChunk.empty()
|
nptdms/channel_data.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
""" Responsible for storing data read from TDMS files
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from nptdms import types
|
|
8
|
+
from nptdms.timestamp import TimestampArray
|
|
9
|
+
from nptdms.log import log_manager
|
|
10
|
+
|
|
11
|
+
log = log_manager.get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_data_receiver(obj, num_values, raw_timestamps, memmap_dir=None):
|
|
15
|
+
"""Return a new channel data receiver to use for the given TDMS object
|
|
16
|
+
|
|
17
|
+
:param obj: TDMS channel object to receive data for
|
|
18
|
+
:param num_values: Number of values to be stored
|
|
19
|
+
:param raw_timestamps: Whether to store timestamp data as raw TDMS timestamps or a numpy datetime64 array
|
|
20
|
+
:param memmap_dir: Optional directory to store memory map files,
|
|
21
|
+
or None to not use memory map files
|
|
22
|
+
"""
|
|
23
|
+
if obj.data_type == types.DaqMxRawData:
|
|
24
|
+
return DaqmxDataReceiver(obj, num_values, memmap_dir)
|
|
25
|
+
|
|
26
|
+
if obj.data_type == types.TimeStamp:
|
|
27
|
+
return TimestampDataReceiver(obj, num_values, raw_timestamps, memmap_dir)
|
|
28
|
+
|
|
29
|
+
if obj.data_type is None or obj.data_type.nptype is None:
|
|
30
|
+
return ListDataReceiver(obj)
|
|
31
|
+
|
|
32
|
+
return NumpyDataReceiver(obj, num_values, memmap_dir)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ListDataReceiver(object):
|
|
36
|
+
"""Simple list based data receiver for objects that don't have a
|
|
37
|
+
corresponding numpy data type
|
|
38
|
+
|
|
39
|
+
:ivar data: List of data points
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, channel):
|
|
43
|
+
"""Initialise new data receiver for a TDMS object
|
|
44
|
+
"""
|
|
45
|
+
if channel.data_type == types.String:
|
|
46
|
+
self._dtype = np.dtype('O')
|
|
47
|
+
else:
|
|
48
|
+
self._dtype = None
|
|
49
|
+
self._data = []
|
|
50
|
+
self.scaler_data = {}
|
|
51
|
+
|
|
52
|
+
def append_data(self, data):
|
|
53
|
+
"""Append data from a segment
|
|
54
|
+
"""
|
|
55
|
+
self._data.extend(data)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def data(self):
|
|
59
|
+
dtype = self._dtype if self._dtype is not None else np.dtype('V8')
|
|
60
|
+
return np.array(self._data, dtype=dtype)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class NumpyDataReceiver(object):
|
|
64
|
+
"""Receives data for a TDMS object and stores it in a numpy array
|
|
65
|
+
|
|
66
|
+
:ivar data: Data that has been read for the object
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, obj, num_values, memmap_dir=None):
|
|
70
|
+
"""Initialise data receiver backed by a numpy array
|
|
71
|
+
|
|
72
|
+
:param obj: Object to store data for
|
|
73
|
+
:param num_values: Number of values to be stored
|
|
74
|
+
:param memmap_dir: Optional directory to store memory map files in.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
self.path = obj.path
|
|
78
|
+
self.data = _new_numpy_array(
|
|
79
|
+
obj.data_type.nptype, num_values, memmap_dir)
|
|
80
|
+
self.scaler_data = {}
|
|
81
|
+
self._data_insert_position = 0
|
|
82
|
+
log.debug("Allocated %d sample slots for %s", len(self.data), obj.path)
|
|
83
|
+
|
|
84
|
+
def append_data(self, new_data):
|
|
85
|
+
"""Update the object data with a new array of data"""
|
|
86
|
+
|
|
87
|
+
log.debug("Adding %d data points to data for %s", len(new_data), self.path)
|
|
88
|
+
start_pos = self._data_insert_position
|
|
89
|
+
end_pos = self._data_insert_position + len(new_data)
|
|
90
|
+
self.data[start_pos:end_pos] = new_data
|
|
91
|
+
self._data_insert_position += len(new_data)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DaqmxDataReceiver(object):
|
|
95
|
+
"""Receives raw scaler data for a DAQmx object and stores it in numpy
|
|
96
|
+
arrays
|
|
97
|
+
|
|
98
|
+
:ivar scaler_data: Dictionary mapping from scaler id to data for a scaler
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, obj, num_values, memmap_dir=None):
|
|
102
|
+
"""Initialise data receiver for DAQmx backed by a numpy array
|
|
103
|
+
|
|
104
|
+
:param obj: Object to store data for
|
|
105
|
+
:param memmap_dir: Optional directory to store memory mmap files in.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
self.path = obj.path
|
|
109
|
+
self.data = None
|
|
110
|
+
self.scaler_data = {}
|
|
111
|
+
self._scaler_insert_positions = {}
|
|
112
|
+
for scaler_id, scaler_type in obj.scaler_data_types.items():
|
|
113
|
+
self.scaler_data[scaler_id] = _new_numpy_array(
|
|
114
|
+
scaler_type.nptype, num_values, memmap_dir)
|
|
115
|
+
self._scaler_insert_positions[scaler_id] = 0
|
|
116
|
+
|
|
117
|
+
def append_scaler_data(self, scale_id, new_data):
|
|
118
|
+
"""Append new DAQmx scaler data read from a segment
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
log.debug("Adding %d data points for object %s, scaler %d",
|
|
122
|
+
len(new_data), self.path, scale_id)
|
|
123
|
+
data_array = self.scaler_data[scale_id]
|
|
124
|
+
start_pos = self._scaler_insert_positions[scale_id]
|
|
125
|
+
end_pos = start_pos + len(new_data)
|
|
126
|
+
data_array[start_pos:end_pos] = new_data
|
|
127
|
+
self._scaler_insert_positions[scale_id] += len(new_data)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TimestampDataReceiver(object):
|
|
131
|
+
"""Receives timestamp data for a TDMS object and stores it in a numpy array
|
|
132
|
+
|
|
133
|
+
:ivar data: Data that has been read for the object as either a TimestampArray
|
|
134
|
+
or datetime64 array depending on whether raw_timestamps is True or False
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, obj, num_values, raw_timestamps=False, memmap_dir=None):
|
|
138
|
+
"""Initialise timestamp data receiver backed by a numpy array
|
|
139
|
+
|
|
140
|
+
:param obj: Object to store data for
|
|
141
|
+
:param num_values: Number of values to be stored
|
|
142
|
+
:param raw_timestamps: Whether to store data as raw TDMS timestamps or a numpy datetime64 array
|
|
143
|
+
:param memmap_dir: Optional directory to store memory map files in.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
self.path = obj.path
|
|
147
|
+
self._raw_timestamps = raw_timestamps
|
|
148
|
+
if raw_timestamps:
|
|
149
|
+
byte_array = _new_numpy_array(np.dtype('uint8'), num_values * 16, memmap_dir)
|
|
150
|
+
dtype = np.dtype([('second_fractions', 'uint64'), ('seconds', 'int64')])
|
|
151
|
+
self.data = TimestampArray(byte_array.view(dtype))
|
|
152
|
+
else:
|
|
153
|
+
self.data = _new_numpy_array(np.dtype('datetime64[us]'), num_values, memmap_dir)
|
|
154
|
+
self.scaler_data = {}
|
|
155
|
+
self._data_insert_position = 0
|
|
156
|
+
log.debug("Allocated %d sample slots for %s", len(self.data), obj.path)
|
|
157
|
+
|
|
158
|
+
def append_data(self, new_data):
|
|
159
|
+
"""Update the object data with a new array of data"""
|
|
160
|
+
|
|
161
|
+
log.debug("Adding %d data points to data for %s", len(new_data), self.path)
|
|
162
|
+
start_pos = self._data_insert_position
|
|
163
|
+
end_pos = self._data_insert_position + len(new_data)
|
|
164
|
+
if self._raw_timestamps:
|
|
165
|
+
# Need to be careful about potential endianness mismatch, so order of fields can differ
|
|
166
|
+
self.data['seconds'][start_pos:end_pos] = new_data['seconds']
|
|
167
|
+
self.data['second_fractions'][start_pos:end_pos] = new_data['second_fractions']
|
|
168
|
+
else:
|
|
169
|
+
self.data[start_pos:end_pos] = new_data.as_datetime64()
|
|
170
|
+
self._data_insert_position += len(new_data)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class RawDataSlice(object):
|
|
174
|
+
def __init__(self, data, scaler_data):
|
|
175
|
+
self.data = data
|
|
176
|
+
self.scaler_data = scaler_data
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def slice_raw_data(raw_data, offset, length=None):
|
|
180
|
+
if offset == 0 and length is None:
|
|
181
|
+
return raw_data
|
|
182
|
+
end = None if length is None else offset + length
|
|
183
|
+
data = None if raw_data.data is None else raw_data.data[offset:end]
|
|
184
|
+
scaler_data = dict(
|
|
185
|
+
(scale_id, scaler_data[offset:end]) for (scale_id, scaler_data) in raw_data.scaler_data.items())
|
|
186
|
+
return RawDataSlice(data, scaler_data)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _new_numpy_array(dtype, num_values, memmap_dir=None):
|
|
190
|
+
"""Initialise a new numpy array for data
|
|
191
|
+
|
|
192
|
+
:param dtype: Numpy data type for array
|
|
193
|
+
:param num_values: Capacity required
|
|
194
|
+
:param memmap_dir: Optional directory to store memory mmap files
|
|
195
|
+
"""
|
|
196
|
+
if memmap_dir:
|
|
197
|
+
memmap_file = tempfile.NamedTemporaryFile(
|
|
198
|
+
mode='w+b', prefix="nptdms_", dir=memmap_dir)
|
|
199
|
+
return np.memmap(
|
|
200
|
+
memmap_file.file,
|
|
201
|
+
mode='w+',
|
|
202
|
+
shape=(num_values,),
|
|
203
|
+
dtype=dtype)
|
|
204
|
+
|
|
205
|
+
return np.zeros(num_values, dtype=dtype)
|
nptdms/common.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from itertools import zip_longest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
toc_properties = {
|
|
5
|
+
'kTocMetaData': (1 << 1),
|
|
6
|
+
'kTocRawData': (1 << 3),
|
|
7
|
+
'kTocDAQmxRawData': (1 << 7),
|
|
8
|
+
'kTocInterleavedData': (1 << 5),
|
|
9
|
+
'kTocBigEndian': (1 << 6),
|
|
10
|
+
'kTocNewObjList': (1 << 2)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ObjectPath(object):
|
|
15
|
+
""" Represents the path of an object in a TDMS file
|
|
16
|
+
|
|
17
|
+
:ivar group: Group name or None for the root object
|
|
18
|
+
:ivar channel: Channel name or None for the root object or a group objecct
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, *path_components):
|
|
21
|
+
self.group = None
|
|
22
|
+
self.channel = None
|
|
23
|
+
if len(path_components) > 0:
|
|
24
|
+
self.group = path_components[0]
|
|
25
|
+
if len(path_components) > 1:
|
|
26
|
+
self.channel = path_components[1]
|
|
27
|
+
if len(path_components) > 2:
|
|
28
|
+
raise ValueError("Object path may only have up to two components")
|
|
29
|
+
self._path = _components_to_path(self.group, self.channel)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_root(self):
|
|
33
|
+
return self.group is None
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_group(self):
|
|
37
|
+
return self.group is not None and self.channel is None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_channel(self):
|
|
41
|
+
return self.channel is not None
|
|
42
|
+
|
|
43
|
+
def group_path(self):
|
|
44
|
+
""" For channel paths, returns the path of the channel's group as a string
|
|
45
|
+
"""
|
|
46
|
+
return _components_to_path(self.group, None)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def from_string(path_string):
|
|
50
|
+
components = list(_path_components(path_string))
|
|
51
|
+
return ObjectPath(*components)
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
""" String representation of the object path
|
|
55
|
+
"""
|
|
56
|
+
return self._path
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _path_components(path):
|
|
60
|
+
""" Generator that yields components within an object path
|
|
61
|
+
"""
|
|
62
|
+
# Iterate over each character and the next character
|
|
63
|
+
chars = zip_longest(path, path[1:])
|
|
64
|
+
try:
|
|
65
|
+
# Iterate over components
|
|
66
|
+
while True:
|
|
67
|
+
char, next_char = next(chars)
|
|
68
|
+
if char != '/':
|
|
69
|
+
raise ValueError("Invalid path, expected \"/\"")
|
|
70
|
+
elif next_char is not None and next_char != "'":
|
|
71
|
+
raise ValueError("Invalid path, expected \"'\"")
|
|
72
|
+
else:
|
|
73
|
+
# Consume "'" or raise StopIteration if at the end
|
|
74
|
+
next(chars)
|
|
75
|
+
component = []
|
|
76
|
+
# Iterate over characters in component name
|
|
77
|
+
while True:
|
|
78
|
+
char, next_char = next(chars)
|
|
79
|
+
if char == "'" and next_char == "'":
|
|
80
|
+
component += "'"
|
|
81
|
+
# Consume second "'"
|
|
82
|
+
next(chars)
|
|
83
|
+
elif char == "'":
|
|
84
|
+
yield "".join(component)
|
|
85
|
+
break
|
|
86
|
+
else:
|
|
87
|
+
component += char
|
|
88
|
+
except StopIteration:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _components_to_path(group, channel):
|
|
93
|
+
components = []
|
|
94
|
+
if group is not None:
|
|
95
|
+
components.append(group)
|
|
96
|
+
if channel is not None:
|
|
97
|
+
components.append(channel)
|
|
98
|
+
return ('/' + '/'.join(
|
|
99
|
+
["'" + c.replace("'", "''") + "'" for c in components]))
|