large-image-source-dicom 1.27.2.dev8__tar.gz → 1.27.2.dev12__tar.gz
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.
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/PKG-INFO +1 -1
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py +131 -34
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom.egg-info/PKG-INFO +1 -1
- large-image-source-dicom-1.27.2.dev12/large_image_source_dicom.egg-info/requires.txt +5 -0
- large-image-source-dicom-1.27.2.dev8/large_image_source_dicom.egg-info/requires.txt +0 -5
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/LICENSE +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/README.rst +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/__init__.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/assetstore/__init__.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/assetstore/rest.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/dicom_metadata.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/dicom_tags.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/dicomweb_utils.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/girder_plugin.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/girder_source.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/constants.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/main.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/models/AssetstoreModel.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/package.json +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/routes.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/templates/assetstoreImport.pug +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreCreate.pug +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreEditFields.pug +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreImportButton.pug +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreMixins.pug +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/views/AssetstoresView.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/views/AuthOptions.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/views/DICOMwebImportView.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/views/EditAssetstoreWidget.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/web_client/views/NewAssetstoreWidget.js +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom.egg-info/SOURCES.txt +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom.egg-info/dependency_links.txt +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom.egg-info/entry_points.txt +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom.egg-info/top_level.txt +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/pyproject.toml +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/setup.cfg +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/setup.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/test_dicom/__init__.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/test_dicom/test_web_client.py +0 -0
- {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/test_dicom/web_client_specs/dicomWebSpec.js +0 -0
|
@@ -139,41 +139,14 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
139
139
|
def downloadFile(self, file, offset=0, headers=True, endByte=None,
|
|
140
140
|
contentDisposition=None, extraParameters=None, **kwargs):
|
|
141
141
|
|
|
142
|
-
from dicomweb_client.web import _Transaction
|
|
143
|
-
|
|
144
|
-
dicom_uids = file['dicom_uids']
|
|
145
|
-
study_uid = dicom_uids['study_uid']
|
|
146
|
-
series_uid = dicom_uids['series_uid']
|
|
147
|
-
instance_uid = dicom_uids['instance_uid']
|
|
148
|
-
|
|
149
|
-
client = _create_dicomweb_client(self.assetstore_meta)
|
|
150
|
-
|
|
151
142
|
if headers:
|
|
152
143
|
setResponseHeader('Accept-Ranges', 'bytes')
|
|
153
144
|
self.setContentHeaders(file, offset, endByte, contentDisposition)
|
|
154
145
|
|
|
155
|
-
# Create the URL
|
|
156
|
-
url = client._get_instances_url(
|
|
157
|
-
_Transaction.RETRIEVE,
|
|
158
|
-
study_uid,
|
|
159
|
-
series_uid,
|
|
160
|
-
instance_uid,
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
# Build the headers
|
|
164
|
-
transfer_syntax = '*'
|
|
165
|
-
accept_parts = [
|
|
166
|
-
'multipart/related',
|
|
167
|
-
'type="application/dicom"',
|
|
168
|
-
f'transfer-syntax={transfer_syntax}',
|
|
169
|
-
]
|
|
170
|
-
request_headers = {
|
|
171
|
-
'Accept': '; '.join(accept_parts),
|
|
172
|
-
}
|
|
173
|
-
|
|
174
146
|
def stream():
|
|
175
147
|
# Perform the request
|
|
176
|
-
|
|
148
|
+
# Try a single-part download first. If that doesn't work, do multipart.
|
|
149
|
+
response = self._request_retrieve_instance_prefer_singlepart(file)
|
|
177
150
|
|
|
178
151
|
bytes_read = 0
|
|
179
152
|
for chunk in self._stream_retrieve_instance_response(response):
|
|
@@ -203,6 +176,76 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
203
176
|
|
|
204
177
|
return stream
|
|
205
178
|
|
|
179
|
+
def _request_retrieve_instance_prefer_singlepart(self, file, transfer_syntax='*'):
|
|
180
|
+
# Try to perform a singlepart request. If it fails, perform a multipart request
|
|
181
|
+
# instead.
|
|
182
|
+
response = None
|
|
183
|
+
try:
|
|
184
|
+
response = self._request_retrieve_instance(file, multipart=False,
|
|
185
|
+
transfer_syntax=transfer_syntax)
|
|
186
|
+
except requests.HTTPError:
|
|
187
|
+
# If there is an HTTPError, the server might not accept single-part requests...
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
if self._is_singlepart_response(response):
|
|
191
|
+
return response
|
|
192
|
+
|
|
193
|
+
# Perform the multipart request instead
|
|
194
|
+
return self._request_retrieve_instance(file, transfer_syntax=transfer_syntax)
|
|
195
|
+
|
|
196
|
+
def _request_retrieve_instance(self, file, multipart=True, transfer_syntax='*'):
|
|
197
|
+
# Multipart requests are officially supported by the DICOMweb standard.
|
|
198
|
+
# Singlepart requests are not officially supported, but they are easier
|
|
199
|
+
# to work with.
|
|
200
|
+
# Google Healthcare API support it.
|
|
201
|
+
# See here: https://cloud.google.com/healthcare-api/docs/dicom#dicom_instances
|
|
202
|
+
|
|
203
|
+
# Create the URL
|
|
204
|
+
client = _create_dicomweb_client(self.assetstore_meta)
|
|
205
|
+
url = self._create_retrieve_instance_url(client, file)
|
|
206
|
+
|
|
207
|
+
# Build the headers
|
|
208
|
+
headers = {}
|
|
209
|
+
if multipart:
|
|
210
|
+
# This is officially supported by the DICOMweb standard.
|
|
211
|
+
headers['Accept'] = '; '.join((
|
|
212
|
+
'multipart/related',
|
|
213
|
+
'type="application/dicom"',
|
|
214
|
+
f'transfer-syntax={transfer_syntax}',
|
|
215
|
+
))
|
|
216
|
+
else:
|
|
217
|
+
# This is not officially supported by the DICOMweb standard,
|
|
218
|
+
# but it is easier to work with, and some servers such as
|
|
219
|
+
# Google Healthcare API support it.
|
|
220
|
+
# See here: https://cloud.google.com/healthcare-api/docs/dicom#dicom_instances
|
|
221
|
+
headers['Accept'] = f'application/dicom; transfer-syntax={transfer_syntax}'
|
|
222
|
+
|
|
223
|
+
return client._http_get(url, headers=headers, stream=True)
|
|
224
|
+
|
|
225
|
+
def _create_retrieve_instance_url(self, client, file):
|
|
226
|
+
from dicomweb_client.web import _Transaction
|
|
227
|
+
|
|
228
|
+
dicom_uids = file['dicom_uids']
|
|
229
|
+
study_uid = dicom_uids['study_uid']
|
|
230
|
+
series_uid = dicom_uids['series_uid']
|
|
231
|
+
instance_uid = dicom_uids['instance_uid']
|
|
232
|
+
|
|
233
|
+
return client._get_instances_url(
|
|
234
|
+
_Transaction.RETRIEVE,
|
|
235
|
+
study_uid,
|
|
236
|
+
series_uid,
|
|
237
|
+
instance_uid,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _stream_retrieve_instance_response(self, response):
|
|
241
|
+
# Check if the original request asked for multipart data
|
|
242
|
+
if 'multipart/related' in response.request.headers.get('Accept', ''):
|
|
243
|
+
yield from self._stream_dicom_multipart_response(response)
|
|
244
|
+
else:
|
|
245
|
+
# The content should *only* contain the DICOM file
|
|
246
|
+
with response:
|
|
247
|
+
yield from response.iter_content(BUF_SIZE)
|
|
248
|
+
|
|
206
249
|
def _extract_media_type_and_boundary(self, response):
|
|
207
250
|
content_type = response.headers['content-type']
|
|
208
251
|
media_type, *ct_info = (ct.strip() for ct in content_type.split(';'))
|
|
@@ -215,7 +258,7 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
215
258
|
|
|
216
259
|
return media_type, boundary
|
|
217
260
|
|
|
218
|
-
def
|
|
261
|
+
def _stream_dicom_multipart_response(self, response):
|
|
219
262
|
# The first part of this function was largely copied from dicomweb-client's
|
|
220
263
|
# _decode_multipart_message() function. But we can't use that function here
|
|
221
264
|
# because it relies on reading the whole DICOM file into memory. We want to
|
|
@@ -307,6 +350,50 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
307
350
|
msg = 'Failed to find ending boundary in response content'
|
|
308
351
|
raise ValueError(msg)
|
|
309
352
|
|
|
353
|
+
def _infer_file_size(self, file):
|
|
354
|
+
# Try various methods to infer the file size, without streaming the
|
|
355
|
+
# whole file. Returns the file size if successful, or `None` if unsuccessful.
|
|
356
|
+
if file.get('size') is not None:
|
|
357
|
+
# The file size was already determined.
|
|
358
|
+
return file['size']
|
|
359
|
+
|
|
360
|
+
# Only method currently is inferring from single-part content_length
|
|
361
|
+
return self._infer_file_size_singlepart_content_length(file)
|
|
362
|
+
|
|
363
|
+
def _is_singlepart_response(self, response):
|
|
364
|
+
if response is None:
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
content_type = response.headers.get('Content-Type')
|
|
368
|
+
return (
|
|
369
|
+
response.status_code == 200 and
|
|
370
|
+
not any(x in content_type for x in ('multipart/related', 'boundary'))
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def _infer_file_size_singlepart_content_length(self, file):
|
|
374
|
+
# First, try to see if single-part requests work, and if the Content-Length
|
|
375
|
+
# is returned. This works for Google Healthcare API.
|
|
376
|
+
try:
|
|
377
|
+
response = self._request_retrieve_instance(file, multipart=False)
|
|
378
|
+
except requests.HTTPError:
|
|
379
|
+
# If there is an HTTPError, the server might not accept single-part requests...
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
if not self._is_singlepart_response(response):
|
|
383
|
+
# Does not support single-part requests...
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
content_length = response.headers.get('Content-Length')
|
|
387
|
+
if not content_length:
|
|
388
|
+
# The server did not return a Content-Length
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
# The DICOM file size is equal to the Content-Length
|
|
393
|
+
return int(content_length)
|
|
394
|
+
except ValueError:
|
|
395
|
+
return
|
|
396
|
+
|
|
310
397
|
def importData(self, parent, parentType, params, progress, user, **kwargs):
|
|
311
398
|
"""
|
|
312
399
|
Import DICOMweb WSI instances from a DICOMweb server.
|
|
@@ -408,7 +495,10 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
408
495
|
'instance_uid': instance_uid,
|
|
409
496
|
}
|
|
410
497
|
file['imported'] = True
|
|
411
|
-
|
|
498
|
+
|
|
499
|
+
# Try to infer the file size without streaming, if possible.
|
|
500
|
+
file['size'] = self._infer_file_size(file)
|
|
501
|
+
file = File().save(file)
|
|
412
502
|
|
|
413
503
|
items.append(item)
|
|
414
504
|
|
|
@@ -420,16 +510,23 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
420
510
|
|
|
421
511
|
def getFileSize(self, file):
|
|
422
512
|
# This function will compute the size of the DICOM file (a potentially
|
|
423
|
-
# expensive operation, since it may have to stream the whole file)
|
|
424
|
-
#
|
|
513
|
+
# expensive operation, since it may have to stream the whole file).
|
|
514
|
+
# The caller is expected to cache the result in file['size'].
|
|
425
515
|
# This function is called when the size is needed, such as the girder
|
|
426
516
|
# fuse mount code, and range requests.
|
|
427
517
|
if file.get('size') is not None:
|
|
428
518
|
# It has already been computed once. Return the cached size.
|
|
429
519
|
return file['size']
|
|
430
520
|
|
|
521
|
+
# Try to infer the file size without streaming, if possible.
|
|
522
|
+
size = self._infer_file_size(file)
|
|
523
|
+
if size:
|
|
524
|
+
return size
|
|
525
|
+
|
|
526
|
+
# We must stream the whole file to get the file size...
|
|
431
527
|
size = 0
|
|
432
|
-
|
|
528
|
+
response = self._request_retrieve_instance_prefer_singlepart(file)
|
|
529
|
+
for chunk in self._stream_retrieve_instance_response(response):
|
|
433
530
|
size += len(chunk)
|
|
434
531
|
|
|
435
532
|
# This should get cached in file['size'] in File().updateSize().
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|