large-image-source-dicom 1.27.1.dev65__tar.gz → 1.27.2.dev4__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.1.dev65 → large-image-source-dicom-1.27.2.dev4}/PKG-INFO +1 -1
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.py +78 -17
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom.egg-info/PKG-INFO +1 -1
- large-image-source-dicom-1.27.2.dev4/large_image_source_dicom.egg-info/requires.txt +5 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/test_dicom/web_client_specs/dicomWebSpec.js +30 -1
- large-image-source-dicom-1.27.1.dev65/large_image_source_dicom.egg-info/requires.txt +0 -5
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/LICENSE +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/README.rst +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/__init__.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/assetstore/__init__.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/assetstore/rest.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/dicom_metadata.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/dicom_tags.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/dicomweb_utils.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/girder_plugin.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/girder_source.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/constants.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/main.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/models/AssetstoreModel.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/package.json +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/routes.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/templates/assetstoreImport.pug +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreCreate.pug +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreEditFields.pug +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreImportButton.pug +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/templates/dicomwebAssetstoreMixins.pug +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/views/AssetstoresView.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/views/AuthOptions.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/views/DICOMwebImportView.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/views/EditAssetstoreWidget.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom/web_client/views/NewAssetstoreWidget.js +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom.egg-info/SOURCES.txt +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom.egg-info/dependency_links.txt +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom.egg-info/entry_points.txt +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/large_image_source_dicom.egg-info/top_level.txt +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/pyproject.toml +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/setup.cfg +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/setup.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/test_dicom/__init__.py +0 -0
- {large-image-source-dicom-1.27.1.dev65 → large-image-source-dicom-1.27.2.dev4}/test_dicom/test_web_client.py +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import cherrypy
|
|
1
2
|
import requests
|
|
2
3
|
from large_image_source_dicom.dicom_tags import dicom_key_to_tag
|
|
3
4
|
from large_image_source_dicom.dicomweb_utils import get_dicomweb_metadata
|
|
@@ -105,14 +106,39 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
105
106
|
# We don't actually need to do anything special
|
|
106
107
|
pass
|
|
107
108
|
|
|
109
|
+
def setContentHeaders(self, file, offset, endByte, contentDisposition=None):
|
|
110
|
+
"""
|
|
111
|
+
Sets the Content-Length, Content-Disposition, Content-Type, and also
|
|
112
|
+
the Content-Range header if this is a partial download.
|
|
113
|
+
|
|
114
|
+
:param file: The file being downloaded.
|
|
115
|
+
:param offset: The start byte of the download.
|
|
116
|
+
:type offset: int
|
|
117
|
+
:param endByte: The end byte of the download (non-inclusive).
|
|
118
|
+
:type endByte: int or None
|
|
119
|
+
:param contentDisposition: Content-Disposition response header
|
|
120
|
+
disposition-type value, if None, Content-Disposition will
|
|
121
|
+
be set to 'attachment; filename=$filename'.
|
|
122
|
+
:type contentDisposition: str or None
|
|
123
|
+
"""
|
|
124
|
+
isRangeRequest = cherrypy.request.headers.get('Range')
|
|
125
|
+
setResponseHeader('Content-Type', file['mimeType'])
|
|
126
|
+
setContentDisposition(file['name'], contentDisposition or 'attachment')
|
|
127
|
+
|
|
128
|
+
if file.get('size') is not None:
|
|
129
|
+
# Only set Content-Length and range request headers if we have a file size
|
|
130
|
+
size = file['size']
|
|
131
|
+
if endByte is None or endByte > size:
|
|
132
|
+
endByte = size
|
|
133
|
+
|
|
134
|
+
setResponseHeader('Content-Length', max(endByte - offset, 0))
|
|
135
|
+
|
|
136
|
+
if offset or endByte < size or isRangeRequest:
|
|
137
|
+
setResponseHeader('Content-Range', f'bytes {offset}-{endByte - 1}/{size}')
|
|
138
|
+
|
|
108
139
|
def downloadFile(self, file, offset=0, headers=True, endByte=None,
|
|
109
140
|
contentDisposition=None, extraParameters=None, **kwargs):
|
|
110
141
|
|
|
111
|
-
if offset != 0 or endByte is not None:
|
|
112
|
-
# FIXME: implement range requests
|
|
113
|
-
msg = 'Range requests are not yet implemented'
|
|
114
|
-
raise NotImplementedError(msg)
|
|
115
|
-
|
|
116
142
|
from dicomweb_client.web import _Transaction
|
|
117
143
|
|
|
118
144
|
dicom_uids = file['dicom_uids']
|
|
@@ -123,15 +149,8 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
123
149
|
client = _create_dicomweb_client(self.assetstore_meta)
|
|
124
150
|
|
|
125
151
|
if headers:
|
|
126
|
-
setResponseHeader('
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# The filesystem assetstore calls the following function, which sets
|
|
130
|
-
# the above and also sets the range and content-length headers:
|
|
131
|
-
# `self.setContentHeaders(file, offset, endByte, contentDisposition)`
|
|
132
|
-
# However, we can't call that since we don't have a great way of
|
|
133
|
-
# determining the DICOM file size without downloading the whole thing.
|
|
134
|
-
# FIXME: call that function if we find a way to determine file size.
|
|
152
|
+
setResponseHeader('Accept-Ranges', 'bytes')
|
|
153
|
+
self.setContentHeaders(file, offset, endByte, contentDisposition)
|
|
135
154
|
|
|
136
155
|
# Create the URL
|
|
137
156
|
url = client._get_instances_url(
|
|
@@ -148,14 +167,39 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
148
167
|
'type="application/dicom"',
|
|
149
168
|
f'transfer-syntax={transfer_syntax}',
|
|
150
169
|
]
|
|
151
|
-
|
|
170
|
+
request_headers = {
|
|
152
171
|
'Accept': '; '.join(accept_parts),
|
|
153
172
|
}
|
|
154
173
|
|
|
155
174
|
def stream():
|
|
156
175
|
# Perform the request
|
|
157
|
-
response = client._http_get(url, headers=
|
|
158
|
-
|
|
176
|
+
response = client._http_get(url, headers=request_headers, stream=True)
|
|
177
|
+
|
|
178
|
+
bytes_read = 0
|
|
179
|
+
for chunk in self._stream_retrieve_instance_response(response):
|
|
180
|
+
if bytes_read < offset:
|
|
181
|
+
# We haven't reached the start of the offset yet
|
|
182
|
+
bytes_needed = offset - bytes_read
|
|
183
|
+
if bytes_needed >= len(chunk):
|
|
184
|
+
# Skip over the whole chunk...
|
|
185
|
+
bytes_read += len(chunk)
|
|
186
|
+
continue
|
|
187
|
+
else:
|
|
188
|
+
# Discard all bytes before the offset
|
|
189
|
+
chunk = chunk[bytes_needed:]
|
|
190
|
+
bytes_read += bytes_needed
|
|
191
|
+
|
|
192
|
+
if endByte is not None and bytes_read + len(chunk) >= endByte:
|
|
193
|
+
# We have reached the end... remove all bytes after endByte
|
|
194
|
+
chunk = chunk[:endByte - bytes_read]
|
|
195
|
+
if chunk:
|
|
196
|
+
yield chunk
|
|
197
|
+
|
|
198
|
+
bytes_read += len(chunk)
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
yield chunk
|
|
202
|
+
bytes_read += len(chunk)
|
|
159
203
|
|
|
160
204
|
return stream
|
|
161
205
|
|
|
@@ -374,6 +418,23 @@ class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter):
|
|
|
374
418
|
def auth_session(self):
|
|
375
419
|
return _create_auth_session(self.assetstore_meta)
|
|
376
420
|
|
|
421
|
+
def getFileSize(self, file):
|
|
422
|
+
# 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'].
|
|
425
|
+
# This function is called when the size is needed, such as the girder
|
|
426
|
+
# fuse mount code, and range requests.
|
|
427
|
+
if file.get('size') is not None:
|
|
428
|
+
# It has already been computed once. Return the cached size.
|
|
429
|
+
return file['size']
|
|
430
|
+
|
|
431
|
+
size = 0
|
|
432
|
+
for chunk in self.downloadFile(file, headers=False)():
|
|
433
|
+
size += len(chunk)
|
|
434
|
+
|
|
435
|
+
# This should get cached in file['size'] in File().updateSize().
|
|
436
|
+
return size
|
|
437
|
+
|
|
377
438
|
|
|
378
439
|
def _create_auth_session(meta):
|
|
379
440
|
auth_type = meta.get('auth_type')
|
|
@@ -20,6 +20,7 @@ describe('DICOMWeb assetstore', function () {
|
|
|
20
20
|
var destinationType;
|
|
21
21
|
var itemId;
|
|
22
22
|
var fileId;
|
|
23
|
+
var dicomFileContent;
|
|
23
24
|
|
|
24
25
|
// After importing, we will verify that this item exists
|
|
25
26
|
const verifyItemName = '1.3.6.1.4.1.5962.99.1.3205815762.381594633.1639588388306.2.0';
|
|
@@ -235,7 +236,35 @@ describe('DICOMWeb assetstore', function () {
|
|
|
235
236
|
});
|
|
236
237
|
|
|
237
238
|
// Should be larger than 500k bytes
|
|
238
|
-
|
|
239
|
+
const success = resp.status === 200 && resp.responseText.length > 500000;
|
|
240
|
+
if (!success) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Save the response text so we can compare with range requests...
|
|
245
|
+
dicomFileContent = resp.responseText;
|
|
246
|
+
return true;
|
|
239
247
|
}, 'Wait to download a single DICOM file');
|
|
248
|
+
|
|
249
|
+
// Verify that we can make a range request
|
|
250
|
+
waitsFor(function() {
|
|
251
|
+
const resp = girder.rest.restRequest({
|
|
252
|
+
url: 'file/' + fileId + '/download',
|
|
253
|
+
type: 'GET',
|
|
254
|
+
async: false,
|
|
255
|
+
headers: {
|
|
256
|
+
'Range': 'bytes=1000-1250'
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
// 206 is a partial content response
|
|
262
|
+
resp.status === 206 &&
|
|
263
|
+
// Should be exactly 251 bytes
|
|
264
|
+
resp.responseText.length === 251 &&
|
|
265
|
+
// It should be equivalent to the slice of the file content
|
|
266
|
+
resp.responseText === dicomFileContent.slice(1000, 1251)
|
|
267
|
+
);
|
|
268
|
+
}, 'Wait for DICOM range request');
|
|
240
269
|
});
|
|
241
270
|
});
|
|
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.1.dev65 → large-image-source-dicom-1.27.2.dev4}/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|