mrd-python 2.0.0rc1__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.
- mrd/__init__.py +96 -0
- mrd/_binary.py +1339 -0
- mrd/_dtypes.py +89 -0
- mrd/_ndjson.py +1194 -0
- mrd/binary.py +680 -0
- mrd/ndjson.py +2716 -0
- mrd/protocols.py +180 -0
- mrd/tools/export_png_images.py +39 -0
- mrd/tools/phantom.py +161 -0
- mrd/tools/simulation.py +173 -0
- mrd/tools/stream_recon.py +184 -0
- mrd/tools/transform.py +37 -0
- mrd/types.py +1714 -0
- mrd/yardl_types.py +303 -0
- mrd_python-2.0.0rc1.dist-info/LICENSE +8 -0
- mrd_python-2.0.0rc1.dist-info/METADATA +28 -0
- mrd_python-2.0.0rc1.dist-info/RECORD +19 -0
- mrd_python-2.0.0rc1.dist-info/WHEEL +5 -0
- mrd_python-2.0.0rc1.dist-info/top_level.txt +1 -0
mrd/yardl_types.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
# pyright: reportUnknownArgumentType=false
|
|
5
|
+
# pyright: reportUnknownVariableType=false
|
|
6
|
+
|
|
7
|
+
from abc import ABC
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Annotated, Generic, TypeVar, Union
|
|
10
|
+
import numpy as np
|
|
11
|
+
import datetime
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProtocolError(Exception):
|
|
16
|
+
"""Raised when the contract of a protocol is not respected."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OutOfRangeEnum(Enum):
|
|
22
|
+
"""Enum that allows values outside of the its defined values."""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _missing_(cls, value: object):
|
|
26
|
+
if not isinstance(value, int):
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
obj = object.__new__(cls)
|
|
30
|
+
obj._value_ = value
|
|
31
|
+
obj._name_ = ""
|
|
32
|
+
return obj
|
|
33
|
+
|
|
34
|
+
def __eq__(self, other: object):
|
|
35
|
+
return isinstance(other, self.__class__) and self.value == other.value
|
|
36
|
+
|
|
37
|
+
def __hash__(self) -> int:
|
|
38
|
+
return hash(self.value)
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
if self._name_ != "":
|
|
42
|
+
return super().__str__()
|
|
43
|
+
|
|
44
|
+
return f"{self.__class__.__name__}({self.value})"
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
if self._name_ != "":
|
|
48
|
+
return super().__repr__()
|
|
49
|
+
|
|
50
|
+
return f"<{self.__class__.__name__}: {self.value}>"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DateTime:
|
|
54
|
+
"""A basic datetime with nanosecond precision, always in UTC."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, nanoseconds_from_epoch: Union[int, np.datetime64] = 0):
|
|
57
|
+
if isinstance(nanoseconds_from_epoch, np.datetime64):
|
|
58
|
+
if nanoseconds_from_epoch.dtype != "datetime64[ns]":
|
|
59
|
+
self._value = np.datetime64(nanoseconds_from_epoch, "ns")
|
|
60
|
+
else:
|
|
61
|
+
self._value = nanoseconds_from_epoch
|
|
62
|
+
else:
|
|
63
|
+
self._value = np.datetime64(nanoseconds_from_epoch, "ns")
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def numpy_value(self) -> np.datetime64:
|
|
67
|
+
return self._value
|
|
68
|
+
|
|
69
|
+
def to_datetime(self) -> datetime.datetime:
|
|
70
|
+
return datetime.datetime.utcfromtimestamp(self._value.astype(int) / 1e9)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def from_components(
|
|
74
|
+
year: int,
|
|
75
|
+
month: int,
|
|
76
|
+
day: int,
|
|
77
|
+
hour: int = 0,
|
|
78
|
+
minute: int = 0,
|
|
79
|
+
second: int = 0,
|
|
80
|
+
nanosecond: int = 0,
|
|
81
|
+
) -> "DateTime":
|
|
82
|
+
if not 0 <= nanosecond <= 999_999_999:
|
|
83
|
+
raise ValueError("nanosecond must be in 0..999_999_999", nanosecond)
|
|
84
|
+
|
|
85
|
+
return DateTime(
|
|
86
|
+
int(datetime.datetime(year, month, day, hour, minute, second).timestamp())
|
|
87
|
+
* 1_000_000_000
|
|
88
|
+
+ nanosecond
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def from_datetime(dt: datetime.datetime) -> "DateTime":
|
|
93
|
+
return DateTime(round(dt.timestamp() * 1e6) * 1000)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def parse(s: str) -> "DateTime":
|
|
97
|
+
return DateTime(np.datetime64(s, "ns"))
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def now() -> "DateTime":
|
|
101
|
+
return DateTime(time.time_ns())
|
|
102
|
+
|
|
103
|
+
def __str__(self) -> str:
|
|
104
|
+
return str(self._value)
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
return f"DateTime({self})"
|
|
108
|
+
|
|
109
|
+
def __eq__(self, other: object) -> bool:
|
|
110
|
+
return (
|
|
111
|
+
self._value == other._value
|
|
112
|
+
if isinstance(other, DateTime)
|
|
113
|
+
else (isinstance(other, np.datetime64) and self._value == other)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def __hash__(self) -> int:
|
|
117
|
+
return hash(self._value)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Time:
|
|
121
|
+
"""A basic time of day with nanosecond precision. It is not timezone-aware and is meant
|
|
122
|
+
to represent a wall clock time.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
_NANOSECONDS_PER_DAY = 24 * 60 * 60 * 1_000_000_000
|
|
126
|
+
|
|
127
|
+
def __init__(self, nanoseconds_since_midnight: Union[int, np.timedelta64] = 0):
|
|
128
|
+
if isinstance(nanoseconds_since_midnight, np.timedelta64):
|
|
129
|
+
if nanoseconds_since_midnight.dtype != "timedelta64[ns]":
|
|
130
|
+
self._value = np.timedelta64(nanoseconds_since_midnight, "ns")
|
|
131
|
+
nanoseconds_since_midnight = nanoseconds_since_midnight.astype(int)
|
|
132
|
+
else:
|
|
133
|
+
self._value = nanoseconds_since_midnight
|
|
134
|
+
else:
|
|
135
|
+
self._value = np.timedelta64(nanoseconds_since_midnight, "ns")
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
nanoseconds_since_midnight < 0
|
|
139
|
+
or nanoseconds_since_midnight >= Time._NANOSECONDS_PER_DAY
|
|
140
|
+
):
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"TimeOfDay must be between 00:00:00 and 23:59:59.999999999"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def numpy_value(self) -> np.timedelta64:
|
|
147
|
+
return self._value
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def from_components(
|
|
151
|
+
hour: int, minute: int, second: int = 0, nanosecond: int = 0
|
|
152
|
+
) -> "Time":
|
|
153
|
+
if not 0 <= hour <= 23:
|
|
154
|
+
raise ValueError("hour must be in 0..23", hour)
|
|
155
|
+
if not 0 <= minute <= 59:
|
|
156
|
+
raise ValueError("minute must be in 0..59", minute)
|
|
157
|
+
if not 0 <= second <= 59:
|
|
158
|
+
raise ValueError("second must be in 0..59", second)
|
|
159
|
+
if not 0 <= nanosecond <= 999_999_999:
|
|
160
|
+
raise ValueError("nanosecond must be in 0..999_999_999", nanosecond)
|
|
161
|
+
|
|
162
|
+
return Time(
|
|
163
|
+
hour * 3_600_000_000_000
|
|
164
|
+
+ minute * 60_000_000_000
|
|
165
|
+
+ second * 1_000_000_000
|
|
166
|
+
+ nanosecond
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def from_time(t: datetime.time) -> "Time":
|
|
171
|
+
return Time(
|
|
172
|
+
t.hour * 3_600_000_000_000
|
|
173
|
+
+ t.minute * 60_000_000_000
|
|
174
|
+
+ t.second * 1_000_000_000
|
|
175
|
+
+ t.microsecond * 1_000
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def parse(s: str) -> "Time":
|
|
180
|
+
components = s.split(":")
|
|
181
|
+
if len(components) == 2:
|
|
182
|
+
hour = int(components[0])
|
|
183
|
+
minute = int(components[1])
|
|
184
|
+
return Time(hour * 3_600_000_000_000 + minute * 60_000_000_000)
|
|
185
|
+
if len(components) == 3:
|
|
186
|
+
hour = int(components[0])
|
|
187
|
+
minute = int(components[1])
|
|
188
|
+
second_components = components[2].split(".")
|
|
189
|
+
if len(second_components) <= 2:
|
|
190
|
+
second = int(second_components[0])
|
|
191
|
+
if len(second_components) == 2:
|
|
192
|
+
fraction = int(second_components[1].ljust(9, "0")[:9])
|
|
193
|
+
else:
|
|
194
|
+
fraction = 0
|
|
195
|
+
return Time(
|
|
196
|
+
hour * 3_600_000_000_000
|
|
197
|
+
+ minute * 60_000_000_000
|
|
198
|
+
+ second * 1_000_000_000
|
|
199
|
+
+ fraction
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
raise ValueError("TimeOfDay must be in the format HH:MM:SS[.fffffffff]")
|
|
203
|
+
|
|
204
|
+
def __str__(self) -> str:
|
|
205
|
+
nanoseconds_since_midnight = self._value.astype(int)
|
|
206
|
+
hours, r = divmod(nanoseconds_since_midnight, 3_600_000_000_000)
|
|
207
|
+
minutes, r = divmod(r, 60_000_000_000)
|
|
208
|
+
seconds, nanoseconds = divmod(r, 1_000_000_000)
|
|
209
|
+
if nanoseconds == 0:
|
|
210
|
+
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
|
211
|
+
|
|
212
|
+
return f"{hours:02}:{minutes:02}:{seconds:02}.{str(nanoseconds).rjust(9, '0').rstrip('0')}"
|
|
213
|
+
|
|
214
|
+
def __repr__(self) -> str:
|
|
215
|
+
return f"Time({self})"
|
|
216
|
+
|
|
217
|
+
def __eq__(self, other: object) -> bool:
|
|
218
|
+
return (
|
|
219
|
+
self._value == other._value
|
|
220
|
+
if isinstance(other, Time)
|
|
221
|
+
else (isinstance(other, np.timedelta64) and self._value == other)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
Int8 = Annotated[int, "Int8"]
|
|
226
|
+
UInt8 = Annotated[int, "UInt8"]
|
|
227
|
+
Int16 = Annotated[int, "Int16"]
|
|
228
|
+
UInt16 = Annotated[int, "UInt16"]
|
|
229
|
+
Int32 = Annotated[int, "Int32"]
|
|
230
|
+
UInt32 = Annotated[int, "UInt32"]
|
|
231
|
+
Int64 = Annotated[int, "Int64"]
|
|
232
|
+
UInt64 = Annotated[int, "UInt64"]
|
|
233
|
+
Size = Annotated[int, "Size"]
|
|
234
|
+
Float32 = Annotated[float, "Float32"]
|
|
235
|
+
Float64 = Annotated[float, "Float64"]
|
|
236
|
+
ComplexFloat = Annotated[complex, "ComplexFloat"]
|
|
237
|
+
ComplexDouble = Annotated[complex, "ComplexDouble"]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def structural_equal(a: object, b: object) -> bool:
|
|
241
|
+
if a is None:
|
|
242
|
+
return b is None
|
|
243
|
+
|
|
244
|
+
if isinstance(a, list):
|
|
245
|
+
if not isinstance(b, list):
|
|
246
|
+
if isinstance(b, np.ndarray):
|
|
247
|
+
return b.shape == (len(a),) and all(
|
|
248
|
+
structural_equal(x, y) for x, y in zip(a, b)
|
|
249
|
+
)
|
|
250
|
+
return False
|
|
251
|
+
return len(a) == len(b) and all(structural_equal(x, y) for x, y in zip(a, b))
|
|
252
|
+
|
|
253
|
+
if isinstance(a, np.ndarray):
|
|
254
|
+
if not isinstance(b, np.ndarray):
|
|
255
|
+
if isinstance(b, list):
|
|
256
|
+
return a.shape == (len(b),) and all(
|
|
257
|
+
structural_equal(x, y) for x, y in zip(a, b)
|
|
258
|
+
)
|
|
259
|
+
return False
|
|
260
|
+
if a.dtype.hasobject: # pyright: ignore [reportUnknownMemberType]
|
|
261
|
+
return (
|
|
262
|
+
a.dtype == b.dtype # pyright: ignore [reportUnknownMemberType]
|
|
263
|
+
and a.shape == b.shape
|
|
264
|
+
and all(structural_equal(x, y) for x, y in zip(a, b))
|
|
265
|
+
)
|
|
266
|
+
return np.array_equal(a, b)
|
|
267
|
+
|
|
268
|
+
if isinstance(a, np.void):
|
|
269
|
+
if not isinstance(b, np.void):
|
|
270
|
+
return b == a
|
|
271
|
+
return a.dtype == b.dtype and all(
|
|
272
|
+
structural_equal(x, y)
|
|
273
|
+
for x, y in zip(a, b) # pyright: ignore [reportGeneralTypeIssues]
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if isinstance(b, np.void):
|
|
277
|
+
return a == b
|
|
278
|
+
|
|
279
|
+
return a == b
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
_T = TypeVar("_T")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class UnionCase(ABC, Generic[_T]):
|
|
286
|
+
index: int
|
|
287
|
+
tag: str
|
|
288
|
+
|
|
289
|
+
def __init__(self, value: _T) -> None:
|
|
290
|
+
self.value = value
|
|
291
|
+
|
|
292
|
+
def __str__(self) -> str:
|
|
293
|
+
return str(self.value)
|
|
294
|
+
|
|
295
|
+
def __repr__(self) -> str:
|
|
296
|
+
return f"{self.__class__.__name__}({self.value})"
|
|
297
|
+
|
|
298
|
+
def __eq__(self, other: object) -> bool:
|
|
299
|
+
# Note we could codegen a more efficient version of this that does not
|
|
300
|
+
# (always) call structural_equal
|
|
301
|
+
return type(self) == type(other) and structural_equal(
|
|
302
|
+
self.value, other.value # pyright: ignore
|
|
303
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright © 2024 ISMRMRD Steering Committee
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mrd-python
|
|
3
|
+
Version: 2.0.0rc1
|
|
4
|
+
Summary: Libraries and tools for working with data in the ISMRM Raw Data (ISMRMRD or MRD).
|
|
5
|
+
License: The MIT License (MIT)
|
|
6
|
+
Copyright © 2024 ISMRMRD Steering Committee
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
13
|
+
|
|
14
|
+
Project-URL: Homepage, https://github.com/ismrmrd/mrd
|
|
15
|
+
Project-URL: Documentation, https://github.com/ismrmrd/mrd
|
|
16
|
+
Project-URL: Repository, https://github.com/ismrmrd/mrd
|
|
17
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Intended Audience :: Science/Research
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: numpy >=1.22.0
|
|
27
|
+
Requires-Dist: pillow >=9.2.0
|
|
28
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
mrd/__init__.py,sha256=drUoXRSnt-Z2nh-vDjlcpzg9cUFS2yexVEliTpwsvjU,2275
|
|
2
|
+
mrd/_binary.py,sha256=zir3uLbk5r1cpER8YL4Hk2qZdKZHlOj4gk9PN8sgcdg,46614
|
|
3
|
+
mrd/_dtypes.py,sha256=R8jl2IPI2lUXotCGFXfDCMjHZcsmEWni-yO_0SpMSZc,3390
|
|
4
|
+
mrd/_ndjson.py,sha256=VnDjo-X0yI2u-5qYWpDXRVasnC7BhAX1EdkLD864WRY,39791
|
|
5
|
+
mrd/binary.py,sha256=am3aHLTVhl6h9lH1WGFyUn9Kvo_8BrwxL3t56069rIw,53801
|
|
6
|
+
mrd/ndjson.py,sha256=vYhPBW4RuciHrv5UFceXKiV2IqK648ijFh-XmEFWs8Y,169451
|
|
7
|
+
mrd/protocols.py,sha256=eETX65nTfoExyvVrgrABajPuW6nvdchhf8vX4dsUwPc,23566
|
|
8
|
+
mrd/types.py,sha256=vMVa9MBBsXkwOehCNDbT3iVwaHJ9d-vluhi1sqJtV2Q,93782
|
|
9
|
+
mrd/yardl_types.py,sha256=i1L_uFUPWdcCt8-DzA8yaRtJC_XQtwsiE26_CAc3ff0,9563
|
|
10
|
+
mrd/tools/export_png_images.py,sha256=77YLfoG45beKYwpb2cB1Me9SNKcEVQn1kaQsTdOTdhY,1372
|
|
11
|
+
mrd/tools/phantom.py,sha256=MUEykvImaEpMBd9DHnmZc583I528xipHHcoJ1UvwroA,5866
|
|
12
|
+
mrd/tools/simulation.py,sha256=JIIgy2__GVJF3b_lF9mj6gVJFlApXDCSyizQp6QuY8A,6220
|
|
13
|
+
mrd/tools/stream_recon.py,sha256=adDgxejRBxsV6QPb9jurW-oYbmET2aWypNUqg3KcX5k,8396
|
|
14
|
+
mrd/tools/transform.py,sha256=s4A9K2OFbO6ZAABzzSeXhlNfLO6qfbBOdze87tWbEuI,1337
|
|
15
|
+
mrd_python-2.0.0rc1.dist-info/LICENSE,sha256=dROqWaV3PdJDrrpWY_T-L-FSnW98mhaTAL3N-8JgjIY,1099
|
|
16
|
+
mrd_python-2.0.0rc1.dist-info/METADATA,sha256=vjqZoOdYvmZYzEPbQQROI1hbuSGAKbKhNyYNsgO5Znk,1957
|
|
17
|
+
mrd_python-2.0.0rc1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
18
|
+
mrd_python-2.0.0rc1.dist-info/top_level.txt,sha256=joUr2Pp4ds1cn6mxUoE_jPSUeIFkN2DvB2Qymkbfq-I,4
|
|
19
|
+
mrd_python-2.0.0rc1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mrd
|