dcor-shared 0.5.4__tar.gz → 0.5.6__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 (24) hide show
  1. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/CHANGELOG +5 -0
  2. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/PKG-INFO +1 -1
  3. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/_version_save.py +1 -1
  4. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/s3.py +48 -28
  5. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/PKG-INFO +1 -1
  6. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/tests/test_s3.py +42 -2
  7. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/LICENSE +0 -0
  8. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/MANIFEST.in +0 -0
  9. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/README.rst +0 -0
  10. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/__init__.py +0 -0
  11. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/_version.py +0 -0
  12. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/ckan.py +0 -0
  13. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/data.py +0 -0
  14. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/mime.py +0 -0
  15. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/parse.py +0 -0
  16. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared/paths.py +0 -0
  17. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/SOURCES.txt +0 -0
  18. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/dependency_links.txt +0 -0
  19. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/requires.txt +0 -0
  20. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/dcor_shared.egg-info/top_level.txt +0 -0
  21. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/setup.cfg +0 -0
  22. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/setup.py +0 -0
  23. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/tests/test_basic.py +0 -0
  24. {dcor_shared-0.5.4 → dcor_shared-0.5.6}/tests/test_data.py +0 -0
@@ -1,3 +1,8 @@
1
+ 0.5.6
2
+ - maintenance release
3
+ 0.5.5
4
+ - ref: factor out s3.object_exists method
5
+ - ref: factor out s3.compute_checksum method
1
6
  0.5.4
2
7
  - enh: cache presigned URLs with 10% tolerance
3
8
  0.5.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcor_shared
3
- Version: 0.5.4
3
+ Version: 0.5.6
4
4
  Summary: Functionalities shared by the DCOR CKAN extensions
5
5
  Home-page: https://github.com/DCOR-dev/dcor_shared
6
6
  Author: Paul Müller
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env python
2
2
  # This file was created automatically
3
- longversion = '0.5.4'
3
+ longversion = '0.5.6'
@@ -11,6 +11,36 @@ import botocore.exceptions
11
11
  from .ckan import get_ckan_config_option
12
12
 
13
13
 
14
+ def compute_checksum(bucket_name, object_name, max_size=None):
15
+ """Compute the SHA256 checksum of an S3 object
16
+
17
+ The hash is computed in memory as the file is downloaded in chunks.
18
+ """
19
+ s3_client, _, s3_resource = get_s3()
20
+ if max_size is None:
21
+ obj = s3_resource.Object(bucket_name=bucket_name, key=object_name)
22
+ max_size = obj.content_length
23
+ hasher = hashlib.sha256()
24
+ # This is an increment of 1MB. If you change this, please also update
25
+ # the tests for large uploads.
26
+ increment = 2 ** 20
27
+ start_byte = 0
28
+ stop_byte = min(increment, max_size)
29
+ while start_byte < max_size:
30
+ resp = s3_client.get_object(
31
+ Bucket=bucket_name,
32
+ Key=object_name,
33
+ Range=f"bytes={start_byte}-{stop_byte}")
34
+ content = resp['Body'].read()
35
+ if not content:
36
+ break
37
+ hasher.update(content)
38
+ start_byte = stop_byte + 1 # range is inclusive
39
+ stop_byte = min(max_size, stop_byte + increment)
40
+ s3_sha256 = hasher.hexdigest()
41
+ return s3_sha256
42
+
43
+
14
44
  def create_presigned_url(bucket_name, object_name, expiration=3600,
15
45
  filename=None):
16
46
  """Generate a presigned URL to share an S3 object
@@ -165,6 +195,19 @@ def make_object_public(bucket_name, object_name, missing_ok=False):
165
195
  )
166
196
 
167
197
 
198
+ def object_exists(bucket_name, object_name):
199
+ """Check whether an object exists"""
200
+ s3_client, _, _ = get_s3()
201
+ try:
202
+ s3_client.head_object(Bucket=bucket_name, Key=object_name)
203
+ except (s3_client.exceptions.NoSuchKey,
204
+ s3_client.exceptions.ClientError):
205
+ obj_exists = False
206
+ else:
207
+ obj_exists = True
208
+ return obj_exists
209
+
210
+
168
211
  @functools.lru_cache()
169
212
  def require_bucket(bucket_name):
170
213
  """Create an S3 bucket if it does not exist yet
@@ -272,14 +315,8 @@ def upload_file(bucket_name, object_name, path, sha256, private=True,
272
315
 
273
316
  perform_upload = True
274
317
  if not override:
275
- try:
276
- s3_client.head_object(Bucket=bucket_name, Key=object_name)
277
- except (s3_client.exceptions.NoSuchKey,
278
- s3_client.exceptions.ClientError):
279
- object_exists = False
280
- else:
281
- object_exists = True
282
- perform_upload = not object_exists
318
+ perform_upload = not object_exists(bucket_name=bucket_name,
319
+ object_name=object_name)
283
320
 
284
321
  if perform_upload:
285
322
  s3_bucket.upload_file(Filename=str(path),
@@ -292,26 +329,9 @@ def upload_file(bucket_name, object_name, path, sha256, private=True,
292
329
  # }
293
330
  )
294
331
  # Make sure the upload worked properly by computing the SHA256 sum.
295
- # Download the file directly into the hasher.
296
- hasher = hashlib.sha256()
297
- # This is an increment of 1MB. If you change this, please also update
298
- # the tests for large uploads.
299
- increment = 2**20
300
- start_byte = 0
301
- max_size = path_size
302
- stop_byte = min(increment, max_size)
303
- while start_byte < max_size:
304
- resp = s3_client.get_object(
305
- Bucket=bucket_name,
306
- Key=object_name,
307
- Range=f"bytes={start_byte}-{stop_byte}")
308
- content = resp['Body'].read()
309
- if not content:
310
- break
311
- hasher.update(content)
312
- start_byte = stop_byte + 1 # range is inclusive
313
- stop_byte = min(max_size, stop_byte + increment)
314
- s3_sha256 = hasher.hexdigest()
332
+ s3_sha256 = compute_checksum(bucket_name=bucket_name,
333
+ object_name=object_name,
334
+ max_size=path_size)
315
335
  if sha256 != s3_sha256:
316
336
  raise ValueError(
317
337
  f"Checksum mismatch for {bucket_name}:{object_name}!")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcor-shared
3
- Version: 0.5.4
3
+ Version: 0.5.6
4
4
  Summary: Functionalities shared by the DCOR CKAN extensions
5
5
  Home-page: https://github.com/DCOR-dev/dcor_shared
6
6
  Author: Paul Müller
@@ -1,4 +1,5 @@
1
- import mock
1
+ import hashlib
2
+ from unittest import mock
2
3
  import pathlib
3
4
  import uuid
4
5
 
@@ -11,12 +12,30 @@ from dcor_shared import s3, sha256sum
11
12
  data_path = pathlib.Path(__file__).parent / "data"
12
13
 
13
14
 
15
+ def test_compute_checksum():
16
+ path = data_path / "calibration_beads_47.rtdc"
17
+ bucket_name = f"test-circle-{uuid.uuid4()}"
18
+ rid = str(uuid.uuid4())
19
+ object_name = f"resource/{rid[:3]}/{rid[3:6]}/{rid[6:]}"
20
+ s3.upload_file(
21
+ bucket_name=bucket_name,
22
+ object_name=object_name,
23
+ path=path,
24
+ sha256=sha256sum(path),
25
+ private=True)
26
+
27
+ hash_exp = hashlib.sha256(path.read_bytes()).hexdigest()
28
+ hash_act = s3.compute_checksum(bucket_name=bucket_name,
29
+ object_name=object_name)
30
+ assert hash_exp == hash_act
31
+
32
+
14
33
  def test_create_bucket_again():
15
34
  bucket_name = f"test-circle-{uuid.uuid4()}"
16
35
  bucket = s3.require_bucket(bucket_name)
17
36
  # this is cached
18
37
  bucket2 = s3.require_bucket(bucket_name)
19
- assert bucket2 is bucket, "chached"
38
+ assert bucket2 is bucket, "cached"
20
39
  s3.require_bucket.cache_clear()
21
40
  bucket3 = s3.require_bucket(bucket_name)
22
41
  assert bucket3 is not bucket, "new object"
@@ -77,6 +96,27 @@ def test_make_object_public_no_such_key(tmp_path):
77
96
  missing_ok=False)
78
97
 
79
98
 
99
+ def test_object_exists():
100
+ path = data_path / "calibration_beads_47.rtdc"
101
+ bucket_name = f"test-circle-{uuid.uuid4()}"
102
+ rid = str(uuid.uuid4())
103
+ object_name = f"resource/{rid[:3]}/{rid[3:6]}/{rid[6:]}"
104
+ s3.upload_file(
105
+ bucket_name=bucket_name,
106
+ object_name=object_name,
107
+ path=path,
108
+ sha256=sha256sum(path),
109
+ private=True)
110
+
111
+ assert s3.object_exists(bucket_name=bucket_name,
112
+ object_name=object_name)
113
+ # sanity checks
114
+ assert not s3.object_exists(bucket_name=bucket_name,
115
+ object_name=f"peter/pan-{uuid.uuid4()}")
116
+ assert not s3.object_exists(bucket_name=f"hansgunter-{uuid.uuid4()}",
117
+ object_name=object_name)
118
+
119
+
80
120
  def test_presigned_url(tmp_path):
81
121
  path = data_path / "calibration_beads_47.rtdc"
82
122
  bucket_name = f"test-circle-{uuid.uuid4()}"
File without changes
File without changes
File without changes
File without changes
File without changes