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.
Files changed (40) hide show
  1. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/PKG-INFO +1 -1
  2. {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
  3. {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
  4. large-image-source-dicom-1.27.2.dev12/large_image_source_dicom.egg-info/requires.txt +5 -0
  5. large-image-source-dicom-1.27.2.dev8/large_image_source_dicom.egg-info/requires.txt +0 -5
  6. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/LICENSE +0 -0
  7. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/README.rst +0 -0
  8. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/large_image_source_dicom/__init__.py +0 -0
  9. {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
  10. {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
  11. {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
  12. {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
  13. {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
  14. {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
  15. {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
  16. {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
  17. {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
  18. {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
  19. {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
  20. {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
  21. {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
  22. {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
  23. {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
  24. {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
  25. {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
  26. {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
  27. {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
  28. {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
  29. {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
  30. {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
  31. {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
  32. {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
  33. {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
  34. {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
  35. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/pyproject.toml +0 -0
  36. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/setup.cfg +0 -0
  37. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/setup.py +0 -0
  38. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/test_dicom/__init__.py +0 -0
  39. {large-image-source-dicom-1.27.2.dev8 → large-image-source-dicom-1.27.2.dev12}/test_dicom/test_web_client.py +0 -0
  40. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-dicom
3
- Version: 1.27.2.dev8
3
+ Version: 1.27.2.dev12
4
4
  Summary: A DICOM tilesource for large_image.
5
5
  Home-page: https://github.com/girder/large_image
6
6
  Author: Kitware, Inc.
@@ -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
- response = client._http_get(url, headers=request_headers, stream=True)
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 _stream_retrieve_instance_response(self, response):
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
- File().save(file)
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
- # and cache the result in file['size'].
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
- for chunk in self.downloadFile(file, headers=False)():
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().
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-dicom
3
- Version: 1.27.2.dev8
3
+ Version: 1.27.2.dev12
4
4
  Summary: A DICOM tilesource for large_image.
5
5
  Home-page: https://github.com/girder/large_image
6
6
  Author: Kitware, Inc.
@@ -0,0 +1,5 @@
1
+ large-image>=1.27.2.dev12
2
+ wsidicom>=0.9.0
3
+
4
+ [girder]
5
+ girder-large-image>=1.27.2.dev12
@@ -1,5 +0,0 @@
1
- large-image>=1.27.2.dev8
2
- wsidicom>=0.9.0
3
-
4
- [girder]
5
- girder-large-image>=1.27.2.dev8