dcor-shared 0.8.0__tar.gz → 0.9.0__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 (31) hide show
  1. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/CHANGELOG +4 -0
  2. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/PKG-INFO +1 -1
  3. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/_version_save.py +1 -1
  4. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/s3.py +22 -9
  5. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/s3cc.py +2 -2
  6. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/testing.py +127 -4
  7. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared.egg-info/PKG-INFO +1 -1
  8. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared.egg-info/requires.txt +1 -1
  9. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/setup.py +1 -1
  10. dcor_shared-0.9.0/tests/test_testing.py +85 -0
  11. dcor_shared-0.8.0/tests/test_testing.py +0 -44
  12. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/LICENSE +0 -0
  13. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/MANIFEST.in +0 -0
  14. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/README.rst +0 -0
  15. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/__init__.py +0 -0
  16. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/_version.py +0 -0
  17. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/ckan.py +0 -0
  18. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/data.py +0 -0
  19. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/dcinst.py +0 -0
  20. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/mime.py +0 -0
  21. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/parse.py +0 -0
  22. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared/paths.py +0 -0
  23. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared.egg-info/SOURCES.txt +0 -0
  24. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared.egg-info/dependency_links.txt +0 -0
  25. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/dcor_shared.egg-info/top_level.txt +0 -0
  26. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/setup.cfg +0 -0
  27. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/tests/test_basic.py +0 -0
  28. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/tests/test_ckan.py +0 -0
  29. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/tests/test_data.py +0 -0
  30. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/tests/test_s3.py +0 -0
  31. {dcor_shared-0.8.0 → dcor_shared-0.9.0}/tests/test_s3cc.py +0 -0
@@ -1,3 +1,7 @@
1
+ 0.9.0
2
+ - feat: introduce `testing.make_dataset_via_s3` and
3
+ `testing.make_resource_via_s3` which uploads resources via S3
4
+ - enh: make sha256 optional in `s3.upload_file`
1
5
  0.8.0
2
6
  - feat: introduce `get_resource_dc_config`, `get_resource_info`, and
3
7
  `s3cc.get_s3_dc_handle_basin_based`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcor_shared
3
- Version: 0.8.0
3
+ Version: 0.9.0
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.8.0'
3
+ longversion = '0.9.0'
@@ -1,4 +1,6 @@
1
1
  """Basic S3 utility functions"""
2
+ from __future__ import annotations
3
+
2
4
  import functools
3
5
  import hashlib
4
6
  import json
@@ -10,6 +12,7 @@ import boto3
10
12
  import botocore.exceptions
11
13
 
12
14
  from .ckan import get_ckan_config_option
15
+ from .data import sha256sum
13
16
 
14
17
 
15
18
  def compute_checksum(bucket_name, object_name, max_size=None):
@@ -124,8 +127,11 @@ def create_presigned_upload_url(bucket_name, object_name,
124
127
  return response["url"], response["fields"]
125
128
 
126
129
 
127
- def create_presigned_url(bucket_name, object_name, expiration=3600,
128
- filename=None):
130
+ def create_presigned_url(
131
+ bucket_name: str,
132
+ object_name: str,
133
+ expiration: int = 3600,
134
+ filename: str | None = None):
129
135
  """Generate a presigned URL to share an S3 object
130
136
 
131
137
  Parameters
@@ -164,8 +170,11 @@ def create_presigned_url(bucket_name, object_name, expiration=3600,
164
170
 
165
171
 
166
172
  @functools.lru_cache()
167
- def create_presigned_url_until(bucket_name, object_name, expires_at,
168
- filename=None):
173
+ def create_presigned_url_until(
174
+ bucket_name: str,
175
+ object_name: str,
176
+ expires_at: int,
177
+ filename: str | None = None) -> str:
169
178
  """Cached `create_presigned_url` with expiry time point
170
179
 
171
180
  Parameters
@@ -182,7 +191,7 @@ def create_presigned_url_until(bucket_name, object_name, expires_at,
182
191
 
183
192
  Returns
184
193
  -------
185
- psurl: str
194
+ ps_url: str
186
195
  Presigned URL as string.
187
196
  """
188
197
  # Generate a presigned URL for the S3 object
@@ -192,12 +201,12 @@ def create_presigned_url_until(bucket_name, object_name, expires_at,
192
201
  if filename is not None:
193
202
  params["ResponseContentDisposition"] = \
194
203
  f"attachment; filename = {filename}"
195
- psurl = s3_client.generate_presigned_url(
204
+ ps_url = s3_client.generate_presigned_url(
196
205
  'get_object',
197
206
  Params=params,
198
207
  ExpiresIn=expires_at - create_time())
199
208
  # The response contains the presigned URL
200
- return psurl
209
+ return ps_url
201
210
 
202
211
 
203
212
  def create_time():
@@ -367,7 +376,7 @@ def require_bucket(bucket_name):
367
376
  return s3_bucket
368
377
 
369
378
 
370
- def upload_file(bucket_name, object_name, path, sha256, private=True,
379
+ def upload_file(bucket_name, object_name, path, sha256=None, private=True,
371
380
  override=False):
372
381
  """Upload a file to a bucket
373
382
 
@@ -380,7 +389,8 @@ def upload_file(bucket_name, object_name, path, sha256, private=True,
380
389
  path: str or pathlib.Path
381
390
  Local path of the file to be uploaded
382
391
  sha256: str
383
- SHA256 checksum of the file to be uploaded
392
+ SHA256 checksum of the file to be uploaded, will be computed
393
+ if not provided
384
394
  private: bool
385
395
  Whether the object should remain private. If set to False,
386
396
  a tag "public:true" is added to the object which is picket up
@@ -393,6 +403,9 @@ def upload_file(bucket_name, object_name, path, sha256, private=True,
393
403
  s3_url: str
394
404
  URL to the S3 object
395
405
  """
406
+ if not sha256:
407
+ sha256 = sha256sum(path)
408
+
396
409
  path_size = pathlib.Path(path).stat().st_size
397
410
  s3_client, _, _ = get_s3()
398
411
  s3_bucket = require_bucket(bucket_name)
@@ -18,7 +18,7 @@ import h5py
18
18
  from .ckan import (
19
19
  get_ckan_config_option, get_resource_dc_config, get_resource_info
20
20
  )
21
- from .data import sha256sum
21
+
22
22
  from . import s3
23
23
 
24
24
 
@@ -246,7 +246,7 @@ def upload_artifact(
246
246
  bucket_name=bucket_name,
247
247
  object_name=f"{artifact}/{rid[:3]}/{rid[3:6]}/{rid[6:]}",
248
248
  path=path_artifact,
249
- sha256=sha256 or sha256sum(path_artifact),
249
+ sha256=sha256,
250
250
  private=private,
251
251
  override=override)
252
252
  return s3_url
@@ -1,4 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  from io import BytesIO
4
+ import numbers
5
+ import pathlib
6
+ from typing import Dict
7
+ import uuid
2
8
 
3
9
  import ckan.authz
4
10
  import ckan.tests.factories as factories
@@ -9,6 +15,9 @@ from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
9
15
 
10
16
  import pytest
11
17
 
18
+ from .ckan import get_ckan_config_option
19
+ from . import s3
20
+
12
21
 
13
22
  @pytest.fixture
14
23
  def create_with_upload_no_temp(clean_db, ckan_config, monkeypatch):
@@ -47,7 +56,8 @@ def make_dataset(create_context=None, owner_org=None, create_with_upload=None,
47
56
  user_id = user["id"]
48
57
  else:
49
58
  # get user ID from create_context
50
- user_id = ckan.authz.get_user_id_for_username(create_context["user"])
59
+ user_id = ckan.authz.get_user_id_for_username(
60
+ create_context["user"], allow_none=True)
51
61
 
52
62
  if owner_org is None:
53
63
  owner_org = factories.Organization(users=[{
@@ -77,9 +87,11 @@ def make_dataset(create_context=None, owner_org=None, create_with_upload=None,
77
87
  dataset_id=ds["id"])
78
88
 
79
89
  if activate:
80
- helpers.call_action("package_patch", create_context,
81
- id=ds["id"],
82
- state="active")
90
+ revise_dict = {
91
+ "match": {"id": ds["id"]},
92
+ "update": {"state": "active"}
93
+ }
94
+ helpers.call_action("package_revise", create_context, **revise_dict)
83
95
 
84
96
  ds_dict = helpers.call_action("package_show", id=ds["id"])
85
97
 
@@ -91,6 +103,90 @@ def make_dataset(create_context=None, owner_org=None, create_with_upload=None,
91
103
  return ds_dict
92
104
 
93
105
 
106
+ def make_dataset_via_s3(
107
+ create_context: Dict | None = None,
108
+ owner_org: Dict | None = None,
109
+ resource_path: str | pathlib.Path | None = None,
110
+ activate: bool = False,
111
+ **kwargs: str | numbers.Number):
112
+ """Create a dataset and upload the given resource directly via S3
113
+
114
+ For this action to work, the "ckanext-dcor_schemas" extension
115
+ must be loaded.
116
+
117
+ Parameters
118
+ ----------
119
+ create_context: dict
120
+ CKAN context for creating the dataset
121
+ owner_org: dict
122
+ CKAN dictionary of the owner organization
123
+ resource_path: str or pathlib.Path
124
+ path to the resource file to upload. If not specified, then
125
+ not resource is created for the dataset
126
+ activate: bool
127
+ whether to activate the dataset
128
+ kwargs: dict
129
+ keyword arguments passed to `package_create`
130
+ """
131
+ if create_context is None:
132
+ user = factories.User()
133
+ create_context = {'ignore_auth': False,
134
+ 'user': user['name'],
135
+ 'api_version': 3}
136
+ user_id = user["id"]
137
+ else:
138
+ # get user ID from create_context
139
+ user_id = ckan.authz.get_user_id_for_username(
140
+ create_context["user"], allow_none=True)
141
+
142
+ if owner_org is None:
143
+ owner_org = factories.Organization(users=[{
144
+ 'name': user_id,
145
+ 'capacity': 'admin'
146
+ }])
147
+
148
+ if "title" not in kwargs:
149
+ kwargs["title"] = "test-dataset-S3"
150
+ if "authors" not in kwargs:
151
+ kwargs["authors"] = "Peter Fly Pan"
152
+ if "license_id" not in kwargs:
153
+ kwargs["license_id"] = "CC-BY-4.0"
154
+ assert "state" not in kwargs, "must not be set"
155
+ assert "owner_org" not in kwargs, "must not be set"
156
+ # create a dataset
157
+ ds_dict = helpers.call_action("package_create", create_context,
158
+ owner_org=owner_org["name"],
159
+ state="draft",
160
+ **kwargs
161
+ )
162
+
163
+ if resource_path is not None:
164
+ rid = make_resource_via_s3(resource_path=resource_path,
165
+ organization_id=owner_org["id"],
166
+ dataset_id=ds_dict["id"],
167
+ create_context=create_context,
168
+ private=ds_dict.get("private", False)
169
+ )
170
+ else:
171
+ rid = None
172
+
173
+ if activate:
174
+ revise_dict = {
175
+ "match": {"id": ds_dict["id"]},
176
+ "update": {"state": "active"}
177
+ }
178
+ helpers.call_action("package_revise", create_context, **revise_dict)
179
+
180
+ ds_dict = helpers.call_action("package_show", id=ds_dict["id"])
181
+
182
+ if rid is not None:
183
+ # fetch resource dictionary
184
+ rs_dict = helpers.call_action("resource_show", id=rid)
185
+ return ds_dict, rs_dict
186
+ else:
187
+ return ds_dict
188
+
189
+
94
190
  def make_resource(resource_path, create_with_upload, create_context,
95
191
  dataset_id):
96
192
  content = resource_path.read_bytes()
@@ -105,6 +201,33 @@ def make_resource(resource_path, create_with_upload, create_context,
105
201
  return resource
106
202
 
107
203
 
204
+ def make_resource_via_s3(resource_path, organization_id, dataset_id,
205
+ create_context, private=False):
206
+ """Upload a resource to S3 and register it with CKAN"""
207
+ bucket_name = get_ckan_config_option(
208
+ "dcor_object_store.bucket_name").format(
209
+ organization_id=organization_id)
210
+ rid = str(uuid.uuid4())
211
+ object_name = f"resource/{rid[:3]}/{rid[3:6]}/{rid[6:]}"
212
+
213
+ s3.upload_file(bucket_name=bucket_name,
214
+ object_name=object_name,
215
+ path=resource_path,
216
+ private=private)
217
+
218
+ revise_dict = {
219
+ "match": {"id": dataset_id},
220
+ "update": {"resources": [{"id": rid,
221
+ "name": resource_path.name,
222
+ "s3_available": True,
223
+ }
224
+ ]
225
+ }
226
+ }
227
+ helpers.call_action("package_revise", create_context, **revise_dict)
228
+ return rid
229
+
230
+
108
231
  def synchronous_enqueue_job(job_func, args=None, kwargs=None, title=None,
109
232
  queue=None, rq_kwargs=None):
110
233
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcor-shared
3
- Version: 0.8.0
3
+ Version: 0.9.0
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,5 +1,5 @@
1
1
  boto3
2
- dclab[s3]>=0.57.3
2
+ dclab[s3]>=0.57.5
3
3
 
4
4
  [tests]
5
5
  requests
@@ -44,7 +44,7 @@ setup(
44
44
  long_description=open('README.rst').read() if exists('README.rst') else '',
45
45
  install_requires=[
46
46
  "boto3",
47
- "dclab[s3]>=0.57.3",
47
+ "dclab[s3]>=0.57.5",
48
48
  ],
49
49
  extras_require={
50
50
  # Required for the `dcor_shared.testing` submodule
@@ -0,0 +1,85 @@
1
+ import pathlib
2
+ import pytest
3
+
4
+ import ckan.tests.factories as factories
5
+ import requests
6
+
7
+ from dcor_shared.testing import make_dataset, make_dataset_via_s3
8
+ from dcor_shared import s3cc, sha256sum
9
+
10
+ data_path = pathlib.Path(__file__).parent / "data"
11
+
12
+
13
+ @pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
14
+ @pytest.mark.usefixtures('clean_db', 'with_request_context')
15
+ def test_make_dataset():
16
+ user = factories.User()
17
+ owner_org = factories.Organization(users=[{
18
+ 'name': user['id'],
19
+ 'capacity': 'admin'
20
+ }])
21
+ # Note: `call_action` bypasses authorization!
22
+ create_context = {'ignore_auth': False,
23
+ 'user': user['name'], 'api_version': 3}
24
+ ds_dict = make_dataset(create_context, owner_org,
25
+ activate=False)
26
+ assert ds_dict["state"] == "draft"
27
+
28
+
29
+ @pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
30
+ @pytest.mark.usefixtures('clean_db', 'with_request_context')
31
+ def test_make_dataset_no_user_specified():
32
+ ds_dict = make_dataset(activate=False)
33
+ assert ds_dict["state"] == "draft"
34
+
35
+
36
+ @pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
37
+ @pytest.mark.usefixtures('clean_db', 'with_request_context')
38
+ def test_make_dataset_with_resource(create_with_upload):
39
+ # upload a *valid* [sic] .rtdc File (this is the control)
40
+ ds_dict, res_dict = make_dataset(
41
+ create_with_upload=create_with_upload,
42
+ resource_path=data_path / "calibration_beads_47.rtdc",
43
+ activate=True)
44
+ assert len(ds_dict["resources"]) == 1
45
+ assert "id" in res_dict
46
+ assert res_dict["package_id"] == ds_dict["id"]
47
+
48
+
49
+ @pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
50
+ @pytest.mark.usefixtures('clean_db', 'with_request_context')
51
+ def test_make_dataset_via_s3():
52
+ user = factories.User()
53
+ owner_org = factories.Organization(users=[{
54
+ 'name': user['id'],
55
+ 'capacity': 'admin'
56
+ }])
57
+ # Note: `call_action` bypasses authorization!
58
+ create_context = {'ignore_auth': False,
59
+ 'user': user['name'], 'api_version': 3}
60
+ ds_dict = make_dataset_via_s3(create_context, owner_org, activate=False)
61
+ assert ds_dict["state"] == "draft"
62
+
63
+
64
+ @pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
65
+ @pytest.mark.usefixtures('clean_db', 'with_request_context')
66
+ def test_make_dataset_with_resource_via_s3(tmp_path):
67
+ # upload a *valid* [sic] .rtdc File (this is the control)
68
+ ds_dict, res_dict = make_dataset_via_s3(
69
+ resource_path=data_path / "calibration_beads_47.rtdc",
70
+ activate=True,
71
+ private=False,
72
+ )
73
+ assert len(ds_dict["resources"]) == 1
74
+ assert "id" in res_dict
75
+ assert res_dict["package_id"] == ds_dict["id"]
76
+
77
+ # verify SHA256 sum of the resource
78
+ ps_url = s3cc.get_s3_url_for_artifact(res_dict["id"], artifact="resource")
79
+ response = requests.get(ps_url)
80
+ dl_path = tmp_path / "calbeads.rtdc"
81
+ with dl_path.open("wb") as fd:
82
+ fd.write(response.content)
83
+
84
+ assert sha256sum(dl_path) == \
85
+ "490efdf5d9bb4cd4b2a6bcf2fe54d4dc201c38530140bcb168980bf8bf846c73"
@@ -1,44 +0,0 @@
1
- import pathlib
2
- import pytest
3
-
4
- import ckan.tests.factories as factories
5
-
6
- from dcor_shared.testing import make_dataset
7
-
8
- data_path = pathlib.Path(__file__).parent / "data"
9
-
10
-
11
- @pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
12
- @pytest.mark.usefixtures('clean_db', 'with_request_context')
13
- def test_make_dataset():
14
- user = factories.User()
15
- owner_org = factories.Organization(users=[{
16
- 'name': user['id'],
17
- 'capacity': 'admin'
18
- }])
19
- # Note: `call_action` bypasses authorization!
20
- create_context = {'ignore_auth': False,
21
- 'user': user['name'], 'api_version': 3}
22
- ds_dict = make_dataset(create_context, owner_org,
23
- activate=False)
24
- assert ds_dict["state"] == "draft"
25
-
26
-
27
- @pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
28
- @pytest.mark.usefixtures('clean_db', 'with_request_context')
29
- def test_make_dataset_no_user_specified():
30
- ds_dict = make_dataset(activate=False)
31
- assert ds_dict["state"] == "draft"
32
-
33
-
34
- @pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
35
- @pytest.mark.usefixtures('clean_db', 'with_request_context')
36
- def test_make_dataset_with_resource(create_with_upload):
37
- # upload a *valid* [sic] .rtdc File (this is the control)
38
- ds_dict, res_dict = make_dataset(
39
- create_with_upload=create_with_upload,
40
- resource_path=data_path / "calibration_beads_47.rtdc",
41
- activate=True)
42
- assert len(ds_dict["resources"]) == 1
43
- assert "id" in res_dict
44
- assert res_dict["package_id"] == ds_dict["id"]
File without changes
File without changes
File without changes
File without changes