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 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
+ }