fctpd 0.2.5__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.
- fctpd/__init__.py +14 -0
- fctpd/enums.py +20 -0
- fctpd/fetch.py +142 -0
- fctpd/fields.py +212 -0
- fctpd/headers.py +510 -0
- fctpd/log.py +18 -0
- fctpd/options.py +22 -0
- fctpd/projection.py +117 -0
- fctpd/readers.py +160 -0
- fctpd/sensitive.py +69 -0
- fctpd/writers.py +221 -0
- fctpd-0.2.5.dist-info/METADATA +111 -0
- fctpd-0.2.5.dist-info/RECORD +16 -0
- fctpd-0.2.5.dist-info/WHEEL +5 -0
- fctpd-0.2.5.dist-info/licenses/LICENSE +24 -0
- fctpd-0.2.5.dist-info/top_level.txt +1 -0
fctpd/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from . import fetch, fields
|
|
2
|
+
from .enums import AnonymizationLevel, PixelEncoding
|
|
3
|
+
from .headers import (
|
|
4
|
+
marshal,
|
|
5
|
+
set_projection_uris,
|
|
6
|
+
unmarshal,
|
|
7
|
+
FileHeader,
|
|
8
|
+
ProjectionHeader,
|
|
9
|
+
)
|
|
10
|
+
from .options import WriteOptions
|
|
11
|
+
from .projection import Projection
|
|
12
|
+
from .readers import FPBReader, FPHReader
|
|
13
|
+
from .sensitive import set_sensitive_fields
|
|
14
|
+
from .writers import FPBWriter, FPHWriter
|
fctpd/enums.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AnonymizationLevel(Enum):
|
|
5
|
+
"""
|
|
6
|
+
AnonymizationLevel describes how a Reader or Writer should treat
|
|
7
|
+
the sensitive entry collection.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
High = 0
|
|
11
|
+
Low = 2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PixelEncoding(Enum):
|
|
15
|
+
"""
|
|
16
|
+
PixelEncoding describes how a Writer should serialize pixel data.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
F32 = 0
|
|
20
|
+
U16 = 1
|
fctpd/fetch.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from io import BufferedIOBase
|
|
2
|
+
from msgspec import Struct
|
|
3
|
+
from os import path
|
|
4
|
+
from requests import get, Response
|
|
5
|
+
from urllib.parse import urlparse, ParseResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class URIValue(Struct, omit_defaults=True):
|
|
9
|
+
"""
|
|
10
|
+
URIValue is a container representing a URI-addressed value
|
|
11
|
+
from the FPB/FPH specification.
|
|
12
|
+
|
|
13
|
+
Fields
|
|
14
|
+
------
|
|
15
|
+
- URI (str): URI referencing the underlying data.
|
|
16
|
+
- ByteOffset (int | None): File offset to the underlying data.
|
|
17
|
+
- ByteRange (int | None): Length of the underlying data in bytes.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
URI: str
|
|
21
|
+
ByteOffset: int | None = None
|
|
22
|
+
ByteRange: int | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SelfValue(Struct, omit_defaults=True):
|
|
26
|
+
"""
|
|
27
|
+
SelfValue is a container representing the file-schemed value
|
|
28
|
+
from the FPB specification.
|
|
29
|
+
|
|
30
|
+
Fields
|
|
31
|
+
------
|
|
32
|
+
- Offset (int): File offset to the underlying data.
|
|
33
|
+
- Range (int | None): Length of the underlying data in bytes.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
Offset: int
|
|
37
|
+
Range: int | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fetch(val: URIValue) -> bytes | SelfValue:
|
|
41
|
+
"""
|
|
42
|
+
Fetch URI-linked data from an input value.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
- val (URIValue): Value to parse.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
- data (bytes | SelfValue): Data linked by the value.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Retrieve fields from the value
|
|
54
|
+
uri: str = val.URI
|
|
55
|
+
offset: int = val.ByteOffset if val.ByteOffset is not None else 0
|
|
56
|
+
rng: int | None = val.ByteRange
|
|
57
|
+
|
|
58
|
+
# Validate whether the URI is valid
|
|
59
|
+
uri_parse: ParseResult = urlparse(uri)
|
|
60
|
+
if not uri_parse.scheme:
|
|
61
|
+
raise ValueError("invalid URI-addressed value: URI is invalid")
|
|
62
|
+
|
|
63
|
+
# Match on the scheme
|
|
64
|
+
match uri_parse.scheme:
|
|
65
|
+
case "file":
|
|
66
|
+
return _fetch_file(uri_parse.path, offset, rng)
|
|
67
|
+
|
|
68
|
+
case "http" | "https":
|
|
69
|
+
return _fetch_http(uri, offset, rng)
|
|
70
|
+
|
|
71
|
+
case "self":
|
|
72
|
+
return SelfValue(offset, rng)
|
|
73
|
+
|
|
74
|
+
case _:
|
|
75
|
+
raise NotImplementedError(f"URI scheme {uri_parse.scheme} not supported")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _fetch_file(infile: str | BufferedIOBase, offset: int, rng: int | None) -> bytes:
|
|
79
|
+
"""
|
|
80
|
+
Internal fetch function for local files. Returns bytes
|
|
81
|
+
corresponding to the specified range and offset.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
- infile (str | BufferedIOBase): Local file to data.
|
|
86
|
+
- offset (int): File byte offset to data.
|
|
87
|
+
- rng: (int | None): Size of byte data to read.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
match infile:
|
|
91
|
+
case str():
|
|
92
|
+
# Check the path exists and is a file
|
|
93
|
+
if not path.exists(infile) or path.isdir(infile):
|
|
94
|
+
raise FileNotFoundError
|
|
95
|
+
|
|
96
|
+
# Adjust the range
|
|
97
|
+
rng = rng if rng is not None else path.getsize(infile)
|
|
98
|
+
|
|
99
|
+
# Load the file pointer into memory
|
|
100
|
+
with open(infile, "rb", buffering=0) as f:
|
|
101
|
+
f.seek(offset, 0)
|
|
102
|
+
data: bytes = f.read(rng)
|
|
103
|
+
f.close()
|
|
104
|
+
|
|
105
|
+
case BufferedIOBase():
|
|
106
|
+
# Adjust the range
|
|
107
|
+
if rng is None:
|
|
108
|
+
infile.seek(0, 0)
|
|
109
|
+
rng = infile.seek(0, 2)
|
|
110
|
+
|
|
111
|
+
# Load the file pointer into memory
|
|
112
|
+
infile.seek(offset, 0)
|
|
113
|
+
data: bytes = infile.read(rng)
|
|
114
|
+
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _fetch_http(uri: str, offset: int, rng: int | None) -> bytes:
|
|
119
|
+
"""
|
|
120
|
+
Internal fetch function for HTTP/HTTPS. Returns bytes contained
|
|
121
|
+
in the corresponding the HTTP response body.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
- uri (str): HTTP-schemed URI to data.
|
|
126
|
+
- offset (int): File byte offset to data.
|
|
127
|
+
- rng: (int | None): Size of byte data to read.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
# Get the range header
|
|
131
|
+
range_hdr: dict[str, str] = (
|
|
132
|
+
{"Range": f"bytes={offset}-"}
|
|
133
|
+
if rng is None
|
|
134
|
+
else {"Range": f"bytes={offset}-{offset+rng}"}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Perform the request
|
|
138
|
+
resp: Response = get(uri, headers=range_hdr)
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
|
|
141
|
+
# Return the response body
|
|
142
|
+
return resp.content
|
fctpd/fields.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# File IO fields
|
|
2
|
+
ByteOffset: str = "ByteOffset"
|
|
3
|
+
ByteRange: str = "ByteRange"
|
|
4
|
+
URI: str = "URI"
|
|
5
|
+
|
|
6
|
+
# Scanner fields
|
|
7
|
+
Rows: str = "NumberOfRows"
|
|
8
|
+
Channels: str = "NumberOfChannels"
|
|
9
|
+
CollimatedSliceWidth: str = "CollimatedSliceWidth"
|
|
10
|
+
CentralRow: str = "CentralRow"
|
|
11
|
+
CentralChannel: str = "CentralChannel"
|
|
12
|
+
AcquisitionFOV: str = "AcquisitionFOV"
|
|
13
|
+
RowWidth: str = "RowWidth"
|
|
14
|
+
ChannelWidth: str = "ChannelWidth"
|
|
15
|
+
AnodeAngle: str = "AnodeAngle"
|
|
16
|
+
Manufacturer: str = "Manufacturer"
|
|
17
|
+
ScannerModel: str = "ScannerModel"
|
|
18
|
+
DistanceSourceToIsocenter: str = "DistanceSourceToIsocenter"
|
|
19
|
+
DistanceSourceToDetector: str = "DistanceSourceToDetector"
|
|
20
|
+
Modality: str = "Modality"
|
|
21
|
+
DetectorShape: str = "DetectorShape"
|
|
22
|
+
CentralChannelOffset: str = "CentralChannelOffset"
|
|
23
|
+
WedgeProfile: str = "WedgeProfile"
|
|
24
|
+
|
|
25
|
+
# Acquisition fields
|
|
26
|
+
TableSpeed: str = "TableSpeed"
|
|
27
|
+
RotationTime: str = "RotationTime"
|
|
28
|
+
ScanTime: str = "ScanTime"
|
|
29
|
+
TableHeight: str = "TableHeight"
|
|
30
|
+
TableFeedDirection: str = "TableFeedDirection"
|
|
31
|
+
TubeAngleDirection: str = "TubeAngleDirection"
|
|
32
|
+
FlyingFocalSpotMode: str = "FlyingFocalSpotMode"
|
|
33
|
+
Exposure: str = "Exposure"
|
|
34
|
+
ExposureNominal: str = "ExposureNominal"
|
|
35
|
+
TubeVoltage: str = "TubeVoltage"
|
|
36
|
+
CTDIvol: str = "CTDIvol"
|
|
37
|
+
CTDIw: str = "CTDIw"
|
|
38
|
+
PatientPosition: str = "PatientPosition"
|
|
39
|
+
BodyPartExamined: str = "BodyPartExamined"
|
|
40
|
+
StartTablePosition: str = "StartTablePosition"
|
|
41
|
+
EndTablePosition: str = "EndTablePosition"
|
|
42
|
+
StartTubeAngle: str = "StartTubeAngle"
|
|
43
|
+
EndTubeAngle: str = "EndTubeAngle"
|
|
44
|
+
ProtocolName: str = "ProtocolName"
|
|
45
|
+
SeriesDescription: str = "SeriesDescription"
|
|
46
|
+
StudyDescription: str = "StudyDescription"
|
|
47
|
+
NumberOfSpectra: str = "NumberOfSpectra"
|
|
48
|
+
SpectrumIndex: str = "SpectrumIndex"
|
|
49
|
+
NoiseEquivalentQuanta: str = "NoiseEquivalentQuanta"
|
|
50
|
+
SourcePath: str = "SourcePath"
|
|
51
|
+
RayGeometry: str = "RayGeometry"
|
|
52
|
+
NumberOfProjections: str = "NumberOfProjections"
|
|
53
|
+
LogarithmicEncoding: str = "LogarithmicEncoding"
|
|
54
|
+
|
|
55
|
+
# Projection-specific fields
|
|
56
|
+
TablePosition: str = "TablePosition"
|
|
57
|
+
TubeAngle: str = "TubeAngle"
|
|
58
|
+
SourceAngularPositionShift: str = "SourceAngularPositionShift"
|
|
59
|
+
SourceAxialPositionShift: str = "SourceAxialPositionShift"
|
|
60
|
+
SourceRadialDistanceShift: str = "SourceRadialDistanceShift"
|
|
61
|
+
ReadingIndex: str = "ReadingIndex"
|
|
62
|
+
Timestamp: str = "Timestamp"
|
|
63
|
+
MinimumPixelValue: str = "MinimumPixelValue"
|
|
64
|
+
MaximumPixelValue: str = "MaximumPixelValue"
|
|
65
|
+
TubeCurrent: str = "TubeCurrent"
|
|
66
|
+
Data: str = "Data"
|
|
67
|
+
View: str = "View"
|
|
68
|
+
|
|
69
|
+
# Reconstruction fields
|
|
70
|
+
ConeAngle: str = "ConeAngle"
|
|
71
|
+
ProjectionsPerRotation: str = "ProjectionsPerRotation"
|
|
72
|
+
TableFeedPerRotation: str = "TableFeedPerRotation"
|
|
73
|
+
FanAngleIncrement: str = "FanAngleIncrement"
|
|
74
|
+
Pitch: str = "Pitch"
|
|
75
|
+
|
|
76
|
+
# Image fields
|
|
77
|
+
RescaleSlope: str = "RescaleSlope"
|
|
78
|
+
RescaleIntercept: str = "RescaleIntercept"
|
|
79
|
+
HounsfieldUnitCalibration: str = "HounsfieldUnitCalibration"
|
|
80
|
+
PixelRepresentation: str = "PixelRepresentation"
|
|
81
|
+
PhotometricInterpretation: str = "PhotometricInterpretation"
|
|
82
|
+
HighBit: str = "HighBit"
|
|
83
|
+
BitsAllocated: str = "BitsAllocated"
|
|
84
|
+
BitsStored: str = "BitsStored"
|
|
85
|
+
SamplesPerPixel: str = "SamplesPerPixel"
|
|
86
|
+
MatrixRows: str = "MatrixRows"
|
|
87
|
+
MatrixColumns: str = "MatrixColumns"
|
|
88
|
+
|
|
89
|
+
# Sensitive Fields
|
|
90
|
+
PatientAge: str = "PatientAge"
|
|
91
|
+
PatientBirthDate: str = "PatientBirthDate"
|
|
92
|
+
PatientID: str = "PatientID"
|
|
93
|
+
PatientName: str = "PatientName"
|
|
94
|
+
PatientSex: str = "PatientSex"
|
|
95
|
+
PatientSize: str = "PatientSize"
|
|
96
|
+
PatientWeight: str = "PatientWeight"
|
|
97
|
+
PregnancyStatus: str = "PregnancyStatus"
|
|
98
|
+
RequestedProcedure: str = "RequestedProcedure"
|
|
99
|
+
SmokingStatus: str = "SmokingStatus"
|
|
100
|
+
PhysicianName: str = "PhysicianName"
|
|
101
|
+
AccessionNumber: str = "AccessionNumber"
|
|
102
|
+
InstitutionName: str = "InstitutionName"
|
|
103
|
+
InstitutionAddress: str = "InstitutionAddress"
|
|
104
|
+
SOPClassUID: str = "SOPClassUID"
|
|
105
|
+
SOPInstanceUID: str = "SOPInstanceUID"
|
|
106
|
+
StudyInstanceUID: str = "StudyInstanceUID"
|
|
107
|
+
SeriesInstanceUID: str = "SeriesInstanceUID"
|
|
108
|
+
IrradiationEventUID: str = "IrradiationEventUID"
|
|
109
|
+
AcquisitionDate: str = "AcquisitionDate"
|
|
110
|
+
SeriesDate: str = "SeriesDate"
|
|
111
|
+
Diagnosis: str = "Diagnosis"
|
|
112
|
+
EthnicGroup: str = "EthnicGroup"
|
|
113
|
+
AcquisitionTime: str = "AcquisitionTime"
|
|
114
|
+
RangeName: str = "RangeName"
|
|
115
|
+
|
|
116
|
+
# Sensitive fields
|
|
117
|
+
SensitiveFields: set[str] = {
|
|
118
|
+
PregnancyStatus,
|
|
119
|
+
PatientID,
|
|
120
|
+
PatientWeight,
|
|
121
|
+
PatientAge,
|
|
122
|
+
PatientBirthDate,
|
|
123
|
+
PatientName,
|
|
124
|
+
PatientSex,
|
|
125
|
+
PatientSize,
|
|
126
|
+
RangeName,
|
|
127
|
+
SmokingStatus,
|
|
128
|
+
PhysicianName,
|
|
129
|
+
AccessionNumber,
|
|
130
|
+
InstitutionName,
|
|
131
|
+
InstitutionAddress,
|
|
132
|
+
StudyInstanceUID,
|
|
133
|
+
SeriesInstanceUID,
|
|
134
|
+
IrradiationEventUID,
|
|
135
|
+
AcquisitionDate,
|
|
136
|
+
AcquisitionTime,
|
|
137
|
+
SeriesDate,
|
|
138
|
+
Diagnosis,
|
|
139
|
+
EthnicGroup,
|
|
140
|
+
SOPInstanceUID,
|
|
141
|
+
Timestamp,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Required reconstruction fields
|
|
146
|
+
ReconFieldsRequired: set[str] = {
|
|
147
|
+
Rows,
|
|
148
|
+
Channels,
|
|
149
|
+
NumberOfProjections,
|
|
150
|
+
ProjectionsPerRotation,
|
|
151
|
+
CentralRow,
|
|
152
|
+
CentralChannel,
|
|
153
|
+
AcquisitionFOV,
|
|
154
|
+
RowWidth,
|
|
155
|
+
ChannelWidth,
|
|
156
|
+
DistanceSourceToIsocenter,
|
|
157
|
+
DistanceSourceToDetector,
|
|
158
|
+
TableSpeed,
|
|
159
|
+
RotationTime,
|
|
160
|
+
FlyingFocalSpotMode,
|
|
161
|
+
RayGeometry,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Recommended reconstruction fields
|
|
165
|
+
ReconFieldsRecommended: set[str] = {
|
|
166
|
+
TableFeedDirection,
|
|
167
|
+
TubeAngleDirection,
|
|
168
|
+
AnodeAngle,
|
|
169
|
+
HounsfieldUnitCalibration,
|
|
170
|
+
FanAngleIncrement,
|
|
171
|
+
Pitch,
|
|
172
|
+
ScanTime,
|
|
173
|
+
TableHeight,
|
|
174
|
+
Manufacturer,
|
|
175
|
+
PatientPosition,
|
|
176
|
+
ScannerModel,
|
|
177
|
+
SpectrumIndex,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Optional reconstruction fields
|
|
182
|
+
ReconFieldsOptional: set[str] = {
|
|
183
|
+
SourcePath,
|
|
184
|
+
LogarithmicEncoding,
|
|
185
|
+
NumberOfSpectra,
|
|
186
|
+
TubeVoltage,
|
|
187
|
+
PixelRepresentation,
|
|
188
|
+
HighBit,
|
|
189
|
+
BitsAllocated,
|
|
190
|
+
BitsStored,
|
|
191
|
+
SamplesPerPixel,
|
|
192
|
+
CentralChannelOffset,
|
|
193
|
+
StartTablePosition,
|
|
194
|
+
EndTablePosition,
|
|
195
|
+
StartTubeAngle,
|
|
196
|
+
EndTubeAngle,
|
|
197
|
+
Exposure,
|
|
198
|
+
ExposureNominal,
|
|
199
|
+
CTDIw,
|
|
200
|
+
CTDIvol,
|
|
201
|
+
CollimatedSliceWidth,
|
|
202
|
+
ConeAngle,
|
|
203
|
+
TableFeedPerRotation,
|
|
204
|
+
ProtocolName,
|
|
205
|
+
SOPClassUID,
|
|
206
|
+
SeriesDescription,
|
|
207
|
+
StudyDescription,
|
|
208
|
+
BodyPartExamined,
|
|
209
|
+
Modality,
|
|
210
|
+
DetectorShape,
|
|
211
|
+
PhotometricInterpretation,
|
|
212
|
+
}
|