dcor-shared 0.7.4__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.
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/CHANGELOG +10 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/PKG-INFO +1 -1
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/__init__.py +3 -1
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/_version_save.py +1 -1
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/ckan.py +37 -1
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/s3.py +22 -9
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/s3cc.py +59 -19
- dcor_shared-0.9.0/dcor_shared/testing.py +286 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared.egg-info/PKG-INFO +1 -1
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared.egg-info/SOURCES.txt +1 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared.egg-info/requires.txt +1 -1
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/setup.py +1 -1
- dcor_shared-0.9.0/tests/test_ckan.py +53 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/tests/test_data.py +2 -18
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/tests/test_s3cc.py +70 -23
- dcor_shared-0.9.0/tests/test_testing.py +85 -0
- dcor_shared-0.7.4/dcor_shared/testing.py +0 -145
- dcor_shared-0.7.4/tests/test_testing.py +0 -46
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/LICENSE +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/MANIFEST.in +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/README.rst +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/_version.py +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/data.py +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/dcinst.py +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/mime.py +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/parse.py +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared/paths.py +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared.egg-info/dependency_links.txt +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/dcor_shared.egg-info/top_level.txt +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/setup.cfg +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/tests/test_basic.py +0 -0
- {dcor_shared-0.7.4 → dcor_shared-0.9.0}/tests/test_s3.py +0 -0
|
@@ -1,3 +1,13 @@
|
|
|
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`
|
|
5
|
+
0.8.0
|
|
6
|
+
- feat: introduce `get_resource_dc_config`, `get_resource_info`, and
|
|
7
|
+
`s3cc.get_s3_dc_handle_basin_based`
|
|
8
|
+
- enh: return S3 URL in s3cc.upload_artifact
|
|
9
|
+
- enh: make specifying owner organization and create context in
|
|
10
|
+
`testing.make_dataset` optional
|
|
1
11
|
0.7.4
|
|
2
12
|
- fix: disable basins when loading a dataset instance
|
|
3
13
|
- enh: implement s3cc.upload_artifact
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# flake8: noqa: F401
|
|
2
|
-
from .ckan import
|
|
2
|
+
from .ckan import (
|
|
3
|
+
get_ckan_config_option, get_resource_dc_config, get_resource_path, get_resource_info
|
|
4
|
+
)
|
|
3
5
|
from .data import DUMMY_BYTES, sha256sum, wait_for_resource
|
|
4
6
|
from .dcinst import get_dc_instance
|
|
5
7
|
from .mime import DC_MIME_TYPES, VALID_FORMATS
|
|
@@ -41,7 +41,43 @@ def get_ckan_webassets_path():
|
|
|
41
41
|
return pathlib.Path(get_ckan_config_option("ckan.webassets.path"))
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
44
|
+
def get_resource_dc_config(resource_id):
|
|
45
|
+
"""Return the DC metadata for a resource identifier
|
|
46
|
+
|
|
47
|
+
For this to work, the dcor_schemas CKAN extension must be active.
|
|
48
|
+
"""
|
|
49
|
+
_, res_dict = get_resource_info(resource_id)
|
|
50
|
+
# build metadata dictionary from resource metadata
|
|
51
|
+
meta = {}
|
|
52
|
+
for item in res_dict:
|
|
53
|
+
if item.startswith("dc:"):
|
|
54
|
+
_, sec, key = item.split(":", 2)
|
|
55
|
+
meta.setdefault(sec, {})
|
|
56
|
+
meta[sec][key] = res_dict[item]
|
|
57
|
+
return meta
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_resource_info(resource_id):
|
|
61
|
+
"""Return resource and dataset dictionaries for a resource identifier
|
|
62
|
+
|
|
63
|
+
Return the dataset dictionary and the resource dictionary.
|
|
64
|
+
"""
|
|
65
|
+
from ckan import logic
|
|
66
|
+
res_dict = logic.get_action("resource_show")(
|
|
67
|
+
context={'ignore_auth': True, 'user': 'default'},
|
|
68
|
+
data_dict={"id": resource_id})
|
|
69
|
+
ds_dict = logic.get_action("package_show")(
|
|
70
|
+
context={'ignore_auth': True, 'user': 'default'},
|
|
71
|
+
data_dict={"id": res_dict["package_id"]})
|
|
72
|
+
return ds_dict, res_dict
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_resource_path(resource_id, create_dirs=False):
|
|
76
|
+
"""Return the expected local path for a resource identifier
|
|
77
|
+
|
|
78
|
+
If `create_dirs` is True, create the parent directory tree.
|
|
79
|
+
"""
|
|
80
|
+
rid = resource_id
|
|
45
81
|
resources_path = get_ckan_storage_path() / "resources"
|
|
46
82
|
pdir = resources_path / rid[:3] / rid[3:6]
|
|
47
83
|
path = pdir / rid[6:]
|
|
@@ -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(
|
|
128
|
-
|
|
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(
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
@@ -4,15 +4,21 @@ Contains methods to directly interact with CKAN resources that are on S3
|
|
|
4
4
|
via just the resource ID.
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
|
+
import io
|
|
7
8
|
import functools
|
|
8
9
|
import pathlib
|
|
9
10
|
from typing import Literal
|
|
11
|
+
from urllib.parse import urlparse
|
|
10
12
|
import warnings
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
import dclab
|
|
15
|
+
from dclab.rtdc_dataset import fmt_hdf5, fmt_s3
|
|
16
|
+
import h5py
|
|
17
|
+
|
|
18
|
+
from .ckan import (
|
|
19
|
+
get_ckan_config_option, get_resource_dc_config, get_resource_info
|
|
20
|
+
)
|
|
13
21
|
|
|
14
|
-
from .ckan import get_ckan_config_option
|
|
15
|
-
from .data import sha256sum
|
|
16
22
|
from . import s3
|
|
17
23
|
|
|
18
24
|
|
|
@@ -81,13 +87,7 @@ def get_s3_bucket_name_for_resource(resource_id):
|
|
|
81
87
|
The resource with the identifier `resource_id` must exist in the
|
|
82
88
|
CKAN database.
|
|
83
89
|
"""
|
|
84
|
-
|
|
85
|
-
res_dict = ckan.logic.get_action('resource_show')(
|
|
86
|
-
context={'ignore_auth': True, 'user': 'default'},
|
|
87
|
-
data_dict={"id": resource_id})
|
|
88
|
-
ds_dict = ckan.logic.get_action('package_show')(
|
|
89
|
-
context={'ignore_auth': True, 'user': 'default'},
|
|
90
|
-
data_dict={'id': res_dict["package_id"]})
|
|
90
|
+
ds_dict, _ = get_resource_info(resource_id)
|
|
91
91
|
bucket_name = get_ckan_config_option(
|
|
92
92
|
"dcor_object_store.bucket_name").format(
|
|
93
93
|
organization_id=ds_dict["organization"]["id"])
|
|
@@ -97,6 +97,9 @@ def get_s3_bucket_name_for_resource(resource_id):
|
|
|
97
97
|
def get_s3_dc_handle(resource_id):
|
|
98
98
|
"""Return an instance of :class:`RTDC_S3`
|
|
99
99
|
|
|
100
|
+
The data are accessed directly via S3 using DCOR's access credentials.
|
|
101
|
+
Use this if you need to access the original raw file.
|
|
102
|
+
|
|
100
103
|
The resource with the identifier `resource_id` must exist in the
|
|
101
104
|
CKAN database.
|
|
102
105
|
"""
|
|
@@ -114,6 +117,48 @@ def get_s3_dc_handle(resource_id):
|
|
|
114
117
|
return ds
|
|
115
118
|
|
|
116
119
|
|
|
120
|
+
def get_s3_dc_handle_basin_based(resource_id):
|
|
121
|
+
"""Return a :class:`RTDC_HTTP`-basin-backed instance of :class:`RTDC_HDF5`
|
|
122
|
+
|
|
123
|
+
The returned instance does not contain any feature data, but has
|
|
124
|
+
basins defined that link to the original data on S3. The upside
|
|
125
|
+
over :func:`get_s3_dc_handle` is that the returned dataset
|
|
126
|
+
includes the basin with the condensed data and that the returned
|
|
127
|
+
instance does not contain the DCOR S3 credentials. The downside is
|
|
128
|
+
that initialization takes slightly longer and that, if private
|
|
129
|
+
resources are accessed, the presigned URLs in the basins are only
|
|
130
|
+
valid for a fixed time period.
|
|
131
|
+
"""
|
|
132
|
+
ds_dict, res_dict = get_resource_info(resource_id)
|
|
133
|
+
basin_paths = []
|
|
134
|
+
for artifact in ["resource", "condensed"]:
|
|
135
|
+
if ds_dict["private"]:
|
|
136
|
+
bp = create_presigned_url(resource_id, artifact=artifact)
|
|
137
|
+
else:
|
|
138
|
+
bp = get_s3_url_for_artifact(resource_id, artifact=artifact)
|
|
139
|
+
basin_paths.append(bp)
|
|
140
|
+
|
|
141
|
+
fd = io.BytesIO()
|
|
142
|
+
with h5py.File(fd, "w", libver="latest") as hv:
|
|
143
|
+
# We don't use RTDCWriter as a context manager to avoid overhead
|
|
144
|
+
# during __exit__, but then we have to make sure "events" is there.
|
|
145
|
+
hv.require_group("events")
|
|
146
|
+
hw = dclab.RTDCWriter(hv)
|
|
147
|
+
hw.store_metadata(get_resource_dc_config(resource_id))
|
|
148
|
+
for bp in basin_paths:
|
|
149
|
+
urlp = urlparse(bp) # e.g. http://localhost/bucket/resource/id
|
|
150
|
+
hw.store_basin(
|
|
151
|
+
basin_name=urlp.path.split("/")[2], # e.g. "resource"
|
|
152
|
+
basin_format="http",
|
|
153
|
+
basin_type="remote",
|
|
154
|
+
basin_locs=[bp],
|
|
155
|
+
# Don't verify anything. This would only cost time,
|
|
156
|
+
# and we know these objects exist.
|
|
157
|
+
verify=False,
|
|
158
|
+
)
|
|
159
|
+
return fmt_hdf5.RTDC_HDF5(fd)
|
|
160
|
+
|
|
161
|
+
|
|
117
162
|
def get_s3_url_for_artifact(
|
|
118
163
|
resource_id: str,
|
|
119
164
|
artifact: Literal["condensed", "preview", "resource"] = "resource"):
|
|
@@ -193,20 +238,15 @@ def upload_artifact(
|
|
|
193
238
|
if private is None:
|
|
194
239
|
# User did not say whether the resource is private. We have to
|
|
195
240
|
# find out ourselves.
|
|
196
|
-
|
|
197
|
-
res_dict = ckan.logic.get_action('resource_show')(
|
|
198
|
-
context={'ignore_auth': True, 'user': 'default'},
|
|
199
|
-
data_dict={"id": resource_id})
|
|
200
|
-
ds_dict = ckan.logic.get_action('package_show')(
|
|
201
|
-
context={'ignore_auth': True, 'user': 'default'},
|
|
202
|
-
data_dict={'id': res_dict["package_id"]})
|
|
241
|
+
ds_dict, _ = get_resource_info(resource_id)
|
|
203
242
|
private = ds_dict["private"]
|
|
204
243
|
|
|
205
244
|
rid = resource_id
|
|
206
|
-
s3.upload_file(
|
|
245
|
+
s3_url = s3.upload_file(
|
|
207
246
|
bucket_name=bucket_name,
|
|
208
247
|
object_name=f"{artifact}/{rid[:3]}/{rid[3:6]}/{rid[6:]}",
|
|
209
248
|
path=path_artifact,
|
|
210
|
-
sha256=sha256
|
|
249
|
+
sha256=sha256,
|
|
211
250
|
private=private,
|
|
212
251
|
override=override)
|
|
252
|
+
return s3_url
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
import numbers
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import Dict
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
import ckan.authz
|
|
10
|
+
import ckan.tests.factories as factories
|
|
11
|
+
import ckan.tests.helpers as helpers
|
|
12
|
+
from ckan.tests.pytest_ckan.fixtures import FakeFileStorage
|
|
13
|
+
import requests
|
|
14
|
+
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from .ckan import get_ckan_config_option
|
|
19
|
+
from . import s3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def create_with_upload_no_temp(clean_db, ckan_config, monkeypatch):
|
|
24
|
+
"""
|
|
25
|
+
Create upload without tempdir
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def factory(data, filename, context=None, **kwargs):
|
|
29
|
+
if context is None:
|
|
30
|
+
context = {}
|
|
31
|
+
action = kwargs.pop("action", "resource_create")
|
|
32
|
+
field = kwargs.pop("upload_field_name", "upload")
|
|
33
|
+
test_file = BytesIO()
|
|
34
|
+
if type(data) is not bytes:
|
|
35
|
+
data = bytes(data, encoding="utf-8")
|
|
36
|
+
test_file.write(data)
|
|
37
|
+
test_file.seek(0)
|
|
38
|
+
test_resource = FakeFileStorage(test_file, filename)
|
|
39
|
+
|
|
40
|
+
params = {
|
|
41
|
+
field: test_resource,
|
|
42
|
+
}
|
|
43
|
+
params.update(kwargs)
|
|
44
|
+
return helpers.call_action(action, context, **params)
|
|
45
|
+
return factory
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_dataset(create_context=None, owner_org=None, create_with_upload=None,
|
|
49
|
+
resource_path=None, activate=False, **kwargs):
|
|
50
|
+
"""Create a dataset with a resource for testing"""
|
|
51
|
+
if create_context is None:
|
|
52
|
+
user = factories.User()
|
|
53
|
+
create_context = {'ignore_auth': False,
|
|
54
|
+
'user': user['name'],
|
|
55
|
+
'api_version': 3}
|
|
56
|
+
user_id = user["id"]
|
|
57
|
+
else:
|
|
58
|
+
# get user ID from create_context
|
|
59
|
+
user_id = ckan.authz.get_user_id_for_username(
|
|
60
|
+
create_context["user"], allow_none=True)
|
|
61
|
+
|
|
62
|
+
if owner_org is None:
|
|
63
|
+
owner_org = factories.Organization(users=[{
|
|
64
|
+
'name': user_id,
|
|
65
|
+
'capacity': 'admin'
|
|
66
|
+
}])
|
|
67
|
+
|
|
68
|
+
if "title" not in kwargs:
|
|
69
|
+
kwargs["title"] = "test-dataset"
|
|
70
|
+
if "authors" not in kwargs:
|
|
71
|
+
kwargs["authors"] = "Peter Pan"
|
|
72
|
+
if "license_id" not in kwargs:
|
|
73
|
+
kwargs["license_id"] = "CC-BY-4.0"
|
|
74
|
+
assert "state" not in kwargs, "must not be set"
|
|
75
|
+
assert "owner_org" not in kwargs, "must not be set"
|
|
76
|
+
# create a dataset
|
|
77
|
+
ds = helpers.call_action("package_create", create_context,
|
|
78
|
+
owner_org=owner_org["name"],
|
|
79
|
+
state="draft",
|
|
80
|
+
**kwargs
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if create_with_upload is not None:
|
|
84
|
+
rs = make_resource(resource_path=resource_path,
|
|
85
|
+
create_with_upload=create_with_upload,
|
|
86
|
+
create_context=create_context,
|
|
87
|
+
dataset_id=ds["id"])
|
|
88
|
+
|
|
89
|
+
if activate:
|
|
90
|
+
revise_dict = {
|
|
91
|
+
"match": {"id": ds["id"]},
|
|
92
|
+
"update": {"state": "active"}
|
|
93
|
+
}
|
|
94
|
+
helpers.call_action("package_revise", create_context, **revise_dict)
|
|
95
|
+
|
|
96
|
+
ds_dict = helpers.call_action("package_show", id=ds["id"])
|
|
97
|
+
|
|
98
|
+
if create_with_upload is not None:
|
|
99
|
+
# updated resource dictionary
|
|
100
|
+
rs_dict = helpers.call_action("resource_show", id=rs["id"])
|
|
101
|
+
return ds_dict, rs_dict
|
|
102
|
+
else:
|
|
103
|
+
return ds_dict
|
|
104
|
+
|
|
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
|
+
|
|
190
|
+
def make_resource(resource_path, create_with_upload, create_context,
|
|
191
|
+
dataset_id):
|
|
192
|
+
content = resource_path.read_bytes()
|
|
193
|
+
rs = create_with_upload(
|
|
194
|
+
data=content,
|
|
195
|
+
filename='test.rtdc',
|
|
196
|
+
context=create_context,
|
|
197
|
+
package_id=dataset_id,
|
|
198
|
+
url="upload",
|
|
199
|
+
)
|
|
200
|
+
resource = helpers.call_action("resource_show", id=rs["id"])
|
|
201
|
+
return resource
|
|
202
|
+
|
|
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
|
+
|
|
231
|
+
def synchronous_enqueue_job(job_func, args=None, kwargs=None, title=None,
|
|
232
|
+
queue=None, rq_kwargs=None):
|
|
233
|
+
"""
|
|
234
|
+
Synchronous mock for ``ckan.plugins.toolkit.enqueue_job``.
|
|
235
|
+
|
|
236
|
+
Due to the asynchronous nature of background jobs, code that uses them
|
|
237
|
+
needs to be handled specially when writing tests.
|
|
238
|
+
|
|
239
|
+
A common approach is to use the mock package to replace the
|
|
240
|
+
ckan.plugins.toolkit.enqueue_job function with a mock that executes jobs
|
|
241
|
+
synchronously instead of asynchronously
|
|
242
|
+
|
|
243
|
+
Also, since we are running the tests as root on a ckan instance that
|
|
244
|
+
is run by www-data, modifying files on disk in background jobs
|
|
245
|
+
(which were started by supervisor as www-data) does not work.
|
|
246
|
+
"""
|
|
247
|
+
if rq_kwargs is None:
|
|
248
|
+
rq_kwargs = {}
|
|
249
|
+
args = args or []
|
|
250
|
+
kwargs = kwargs or {}
|
|
251
|
+
job_func(*args, **kwargs)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def upload_presigned_to_s3(psurl, fields, path_to_upload):
|
|
255
|
+
"""Helper function for uploading data to S3
|
|
256
|
+
|
|
257
|
+
This is exactly how DCOR-Aid would be uploading things (with the
|
|
258
|
+
requests_toolbelt package). This could have been a little simpler,
|
|
259
|
+
but for the sake of reproducibility, we do it the DCOR-Aid way.
|
|
260
|
+
"""
|
|
261
|
+
# callback function for monitoring the upload progress
|
|
262
|
+
# open the input file for streaming
|
|
263
|
+
with path_to_upload.open("rb") as fd:
|
|
264
|
+
fields["file"] = (fields["key"], fd)
|
|
265
|
+
e = MultipartEncoder(fields=fields)
|
|
266
|
+
m = MultipartEncoderMonitor(
|
|
267
|
+
e, lambda monitor: print(f"Bytes: {monitor.bytes_read}"))
|
|
268
|
+
# Increase the read size to speed-up upload (the default chunk
|
|
269
|
+
# size for uploads in urllib is 8k which results in a lot of
|
|
270
|
+
# Python code being involved in uploading a 20GB file; Setting
|
|
271
|
+
# the chunk size to 4MB should increase the upload speed):
|
|
272
|
+
# https://github.com/requests/toolbelt/issues/75
|
|
273
|
+
# #issuecomment-237189952
|
|
274
|
+
m._read = m.read
|
|
275
|
+
m.read = lambda size: m._read(4 * 1024 * 1024)
|
|
276
|
+
# perform the actual upload
|
|
277
|
+
hrep = requests.post(
|
|
278
|
+
psurl,
|
|
279
|
+
data=m,
|
|
280
|
+
headers={'Content-Type': m.content_type},
|
|
281
|
+
verify=True, # verify SSL connection
|
|
282
|
+
timeout=27.3, # timeout to avoid freezing
|
|
283
|
+
)
|
|
284
|
+
if hrep.status_code != 204:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"Upload failed with {hrep.status_code}: {hrep.reason}")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
import ckanext.dcor_schemas.plugin
|
|
5
|
+
|
|
6
|
+
from dcor_shared import get_resource_dc_config, get_resource_info
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from dcor_shared.testing import make_dataset, synchronous_enqueue_job
|
|
10
|
+
|
|
11
|
+
data_path = pathlib.Path(__file__).parent / "data"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
15
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
16
|
+
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
17
|
+
side_effect=synchronous_enqueue_job)
|
|
18
|
+
def test_get_resource_dc_config(enqueue_job_mock, create_with_upload,
|
|
19
|
+
monkeypatch):
|
|
20
|
+
monkeypatch.setattr(
|
|
21
|
+
ckanext.dcor_schemas.plugin,
|
|
22
|
+
'DISABLE_AFTER_DATASET_CREATE_FOR_CONCURRENT_JOB_TESTS',
|
|
23
|
+
True)
|
|
24
|
+
|
|
25
|
+
_, res_dict = make_dataset(
|
|
26
|
+
create_with_upload=create_with_upload,
|
|
27
|
+
resource_path=data_path / "calibration_beads_47.rtdc",
|
|
28
|
+
activate=True)
|
|
29
|
+
|
|
30
|
+
dc_config = get_resource_dc_config(res_dict["id"])
|
|
31
|
+
assert dc_config["experiment"]["event count"] == 47
|
|
32
|
+
assert dc_config["experiment"]["date"] == "2018-12-11"
|
|
33
|
+
assert dc_config["fluorescence"]["laser count"] == 2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
37
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
38
|
+
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
39
|
+
side_effect=synchronous_enqueue_job)
|
|
40
|
+
def test_get_resource_info(enqueue_job_mock, create_with_upload, monkeypatch):
|
|
41
|
+
monkeypatch.setattr(
|
|
42
|
+
ckanext.dcor_schemas.plugin,
|
|
43
|
+
'DISABLE_AFTER_DATASET_CREATE_FOR_CONCURRENT_JOB_TESTS',
|
|
44
|
+
True)
|
|
45
|
+
|
|
46
|
+
ds_dict, res_dict = make_dataset(
|
|
47
|
+
create_with_upload=create_with_upload,
|
|
48
|
+
resource_path=data_path / "calibration_beads_47.rtdc",
|
|
49
|
+
activate=True)
|
|
50
|
+
|
|
51
|
+
ds_dict_2, res_dict_2 = get_resource_info(res_dict["id"])
|
|
52
|
+
assert ds_dict == ds_dict_2
|
|
53
|
+
assert res_dict == res_dict_2
|
|
@@ -29,19 +29,11 @@ def test_get_dc_instance_file(enqueue_job_mock, create_with_upload,
|
|
|
29
29
|
'DISABLE_AFTER_DATASET_CREATE_FOR_CONCURRENT_JOB_TESTS',
|
|
30
30
|
True)
|
|
31
31
|
|
|
32
|
-
user = factories.User()
|
|
33
|
-
owner_org = factories.Organization(users=[{
|
|
34
|
-
'name': user['id'],
|
|
35
|
-
'capacity': 'admin'
|
|
36
|
-
}])
|
|
37
|
-
create_context = {'ignore_auth': False,
|
|
38
|
-
'user': user['name'],
|
|
39
|
-
'api_version': 3}
|
|
40
32
|
ds_dict, _ = make_dataset(
|
|
41
|
-
create_context, owner_org,
|
|
42
33
|
create_with_upload=create_with_upload,
|
|
43
34
|
resource_path=data_path / "calibration_beads_47.rtdc",
|
|
44
35
|
activate=True)
|
|
36
|
+
|
|
45
37
|
rid = ds_dict["resources"][0]["id"]
|
|
46
38
|
resource_path = pathlib.Path(get_resource_path(rid))
|
|
47
39
|
assert resource_path.exists(), "sanity check"
|
|
@@ -75,19 +67,11 @@ def test_get_dc_instance_s3(enqueue_job_mock, create_with_upload,
|
|
|
75
67
|
'DISABLE_AFTER_DATASET_CREATE_FOR_CONCURRENT_JOB_TESTS',
|
|
76
68
|
True)
|
|
77
69
|
|
|
78
|
-
user = factories.User()
|
|
79
|
-
owner_org = factories.Organization(users=[{
|
|
80
|
-
'name': user['id'],
|
|
81
|
-
'capacity': 'admin'
|
|
82
|
-
}])
|
|
83
|
-
create_context = {'ignore_auth': False,
|
|
84
|
-
'user': user['name'],
|
|
85
|
-
'api_version': 3}
|
|
86
70
|
ds_dict, _ = make_dataset(
|
|
87
|
-
create_context, owner_org,
|
|
88
71
|
create_with_upload=create_with_upload,
|
|
89
72
|
resource_path=data_path / "calibration_beads_47.rtdc",
|
|
90
73
|
activate=True)
|
|
74
|
+
|
|
91
75
|
res_dict = ds_dict["resources"][0]
|
|
92
76
|
rid = res_dict["id"]
|
|
93
77
|
resource_path = pathlib.Path(get_resource_path(rid))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pathlib
|
|
2
|
+
import shutil
|
|
2
3
|
from unittest import mock
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -6,34 +7,21 @@ import pytest
|
|
|
6
7
|
from ckan import model
|
|
7
8
|
import ckan.tests.helpers as helpers
|
|
8
9
|
import ckan.tests.factories as factories
|
|
10
|
+
import ckanext.dcor_schemas.plugin
|
|
9
11
|
|
|
10
|
-
from dcor_shared.testing import
|
|
12
|
+
from dcor_shared.testing import (
|
|
13
|
+
make_dataset, synchronous_enqueue_job, upload_presigned_to_s3
|
|
14
|
+
)
|
|
11
15
|
from dcor_shared import s3, s3cc, sha256sum
|
|
12
16
|
|
|
17
|
+
import h5py
|
|
13
18
|
import requests
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
data_path = pathlib.Path(__file__).parent / "data"
|
|
17
22
|
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
21
|
-
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
22
|
-
side_effect=synchronous_enqueue_job)
|
|
23
|
-
def test_artifact_exists(enqueue_job_mock):
|
|
24
|
-
rid, s3_url, _, org_dict = setup_s3_resource_on_ckan(private=True)
|
|
25
|
-
assert s3cc.artifact_exists(rid)
|
|
26
|
-
# Delete the object
|
|
27
|
-
s3_client, _, _ = s3.get_s3()
|
|
28
|
-
bucket_name, object_name = s3cc.get_s3_bucket_object_for_artifact(rid)
|
|
29
|
-
s3_client.delete_object(
|
|
30
|
-
Bucket=bucket_name,
|
|
31
|
-
Key=object_name
|
|
32
|
-
)
|
|
33
|
-
assert not s3cc.artifact_exists(rid)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def setup_s3_resource_on_ckan(private=False):
|
|
24
|
+
def setup_s3_resource_on_ckan(private=False, resource_path=None):
|
|
37
25
|
"""Create an S3 resource in CKAN"""
|
|
38
26
|
user = factories.User()
|
|
39
27
|
owner_org = factories.Organization(users=[{
|
|
@@ -51,10 +39,13 @@ def setup_s3_resource_on_ckan(private=False):
|
|
|
51
39
|
organization_id=owner_org["id"],
|
|
52
40
|
)
|
|
53
41
|
rid = response["resource_id"]
|
|
42
|
+
if resource_path is None:
|
|
43
|
+
resource_path = data_path / "calibration_beads_47.rtdc"
|
|
44
|
+
|
|
54
45
|
upload_presigned_to_s3(
|
|
55
46
|
psurl=response["url"],
|
|
56
47
|
fields=response["fields"],
|
|
57
|
-
path_to_upload=
|
|
48
|
+
path_to_upload=resource_path)
|
|
58
49
|
|
|
59
50
|
# Create the dataset
|
|
60
51
|
pkg_dict = helpers.call_action("package_create",
|
|
@@ -82,6 +73,23 @@ def setup_s3_resource_on_ckan(private=False):
|
|
|
82
73
|
return rid, s3_url, new_pkg_dict, owner_org
|
|
83
74
|
|
|
84
75
|
|
|
76
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
77
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
78
|
+
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
79
|
+
side_effect=synchronous_enqueue_job)
|
|
80
|
+
def test_artifact_exists(enqueue_job_mock):
|
|
81
|
+
rid, s3_url, _, org_dict = setup_s3_resource_on_ckan(private=True)
|
|
82
|
+
assert s3cc.artifact_exists(rid)
|
|
83
|
+
# Delete the object
|
|
84
|
+
s3_client, _, _ = s3.get_s3()
|
|
85
|
+
bucket_name, object_name = s3cc.get_s3_bucket_object_for_artifact(rid)
|
|
86
|
+
s3_client.delete_object(
|
|
87
|
+
Bucket=bucket_name,
|
|
88
|
+
Key=object_name
|
|
89
|
+
)
|
|
90
|
+
assert not s3cc.artifact_exists(rid)
|
|
91
|
+
|
|
92
|
+
|
|
85
93
|
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
86
94
|
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
87
95
|
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
@@ -134,6 +142,44 @@ def test_get_s3_handle(enqueue_job_mock):
|
|
|
134
142
|
assert len(ds) == 47
|
|
135
143
|
|
|
136
144
|
|
|
145
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_depot dcor_schemas dc_serve')
|
|
146
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
147
|
+
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
148
|
+
side_effect=synchronous_enqueue_job)
|
|
149
|
+
def test_get_s3_dc_handle_basin_based(enqueue_job_mock, create_with_upload,
|
|
150
|
+
monkeypatch, tmp_path):
|
|
151
|
+
monkeypatch.setattr(
|
|
152
|
+
ckanext.dcor_schemas.plugin,
|
|
153
|
+
'DISABLE_AFTER_DATASET_CREATE_FOR_CONCURRENT_JOB_TESTS',
|
|
154
|
+
True)
|
|
155
|
+
|
|
156
|
+
resource_path = tmp_path / "data.rtdc"
|
|
157
|
+
shutil.copy2(data_path / "calibration_beads_47.rtdc", resource_path)
|
|
158
|
+
with h5py.File(resource_path, "a") as h5:
|
|
159
|
+
del h5["events/volume"]
|
|
160
|
+
|
|
161
|
+
_, res_dict = make_dataset(
|
|
162
|
+
create_with_upload=create_with_upload,
|
|
163
|
+
resource_path=resource_path,
|
|
164
|
+
activate=True)
|
|
165
|
+
rid = res_dict["id"]
|
|
166
|
+
|
|
167
|
+
# sanity check with reference
|
|
168
|
+
with s3cc.get_s3_dc_handle(rid) as ds:
|
|
169
|
+
assert "volume" not in ds.features_innate
|
|
170
|
+
assert "image" in ds
|
|
171
|
+
|
|
172
|
+
# actual test, the condensed dataset containing "volume" should be
|
|
173
|
+
# in one of the basins.
|
|
174
|
+
with s3cc.get_s3_dc_handle_basin_based(rid) as ds2:
|
|
175
|
+
assert len(ds2.basins) == 2
|
|
176
|
+
assert "volume" in ds2
|
|
177
|
+
assert "volume" in ds2.features_basin
|
|
178
|
+
assert "volume" not in ds2.features_innate
|
|
179
|
+
assert "image" in ds2
|
|
180
|
+
assert len(ds2) == 47
|
|
181
|
+
|
|
182
|
+
|
|
137
183
|
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
138
184
|
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
139
185
|
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
@@ -173,9 +219,10 @@ def test_upload_artifact(enqueue_job_mock, tmp_path):
|
|
|
173
219
|
path_fake_preview = tmp_path / "test_preview.jpg"
|
|
174
220
|
path_fake_preview.write_text("This is not a real image!")
|
|
175
221
|
# upload the preview
|
|
176
|
-
s3cc.upload_artifact(rid,
|
|
177
|
-
|
|
178
|
-
|
|
222
|
+
s3_url2 = s3cc.upload_artifact(rid,
|
|
223
|
+
path_artifact=path_fake_preview,
|
|
224
|
+
artifact="preview")
|
|
225
|
+
assert s3_url == s3_url2.replace("preview", "resource")
|
|
179
226
|
# make sure that worked
|
|
180
227
|
assert s3cc.artifact_exists(rid, "preview")
|
|
181
228
|
# attempt to download the private artifact
|
|
@@ -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,145 +0,0 @@
|
|
|
1
|
-
from io import BytesIO
|
|
2
|
-
|
|
3
|
-
import ckan.tests.helpers as helpers
|
|
4
|
-
from ckan.tests.pytest_ckan.fixtures import FakeFileStorage
|
|
5
|
-
import requests
|
|
6
|
-
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.fixture
|
|
12
|
-
def create_with_upload_no_temp(clean_db, ckan_config, monkeypatch):
|
|
13
|
-
"""
|
|
14
|
-
Create upload without tempdir
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
def factory(data, filename, context=None, **kwargs):
|
|
18
|
-
if context is None:
|
|
19
|
-
context = {}
|
|
20
|
-
action = kwargs.pop("action", "resource_create")
|
|
21
|
-
field = kwargs.pop("upload_field_name", "upload")
|
|
22
|
-
test_file = BytesIO()
|
|
23
|
-
if type(data) is not bytes:
|
|
24
|
-
data = bytes(data, encoding="utf-8")
|
|
25
|
-
test_file.write(data)
|
|
26
|
-
test_file.seek(0)
|
|
27
|
-
test_resource = FakeFileStorage(test_file, filename)
|
|
28
|
-
|
|
29
|
-
params = {
|
|
30
|
-
field: test_resource,
|
|
31
|
-
}
|
|
32
|
-
params.update(kwargs)
|
|
33
|
-
return helpers.call_action(action, context, **params)
|
|
34
|
-
return factory
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def make_dataset(create_context, owner_org, create_with_upload=None,
|
|
38
|
-
resource_path=None, activate=False, **kwargs):
|
|
39
|
-
if "title" not in kwargs:
|
|
40
|
-
kwargs["title"] = "test-dataset"
|
|
41
|
-
if "authors" not in kwargs:
|
|
42
|
-
kwargs["authors"] = "Peter Pan"
|
|
43
|
-
if "license_id" not in kwargs:
|
|
44
|
-
kwargs["license_id"] = "CC-BY-4.0"
|
|
45
|
-
assert "state" not in kwargs, "must not be set"
|
|
46
|
-
assert "owner_org" not in kwargs, "must not be set"
|
|
47
|
-
# create a dataset
|
|
48
|
-
ds = helpers.call_action("package_create", create_context,
|
|
49
|
-
owner_org=owner_org["name"],
|
|
50
|
-
state="draft",
|
|
51
|
-
**kwargs
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
if create_with_upload is not None:
|
|
55
|
-
rs = make_resource(resource_path=resource_path,
|
|
56
|
-
create_with_upload=create_with_upload,
|
|
57
|
-
create_context=create_context,
|
|
58
|
-
dataset_id=ds["id"])
|
|
59
|
-
|
|
60
|
-
if activate:
|
|
61
|
-
helpers.call_action("package_patch", create_context,
|
|
62
|
-
id=ds["id"],
|
|
63
|
-
state="active")
|
|
64
|
-
|
|
65
|
-
ds_dict = helpers.call_action("package_show", id=ds["id"])
|
|
66
|
-
|
|
67
|
-
if create_with_upload is not None:
|
|
68
|
-
# updated resource dictionary
|
|
69
|
-
rs_dict = helpers.call_action("resource_show", id=rs["id"])
|
|
70
|
-
return ds_dict, rs_dict
|
|
71
|
-
else:
|
|
72
|
-
return ds_dict
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def make_resource(resource_path, create_with_upload, create_context,
|
|
76
|
-
dataset_id):
|
|
77
|
-
content = resource_path.read_bytes()
|
|
78
|
-
rs = create_with_upload(
|
|
79
|
-
data=content,
|
|
80
|
-
filename='test.rtdc',
|
|
81
|
-
context=create_context,
|
|
82
|
-
package_id=dataset_id,
|
|
83
|
-
url="upload",
|
|
84
|
-
)
|
|
85
|
-
resource = helpers.call_action("resource_show", id=rs["id"])
|
|
86
|
-
return resource
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def synchronous_enqueue_job(job_func, args=None, kwargs=None, title=None,
|
|
90
|
-
queue=None, rq_kwargs=None):
|
|
91
|
-
"""
|
|
92
|
-
Synchronous mock for ``ckan.plugins.toolkit.enqueue_job``.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Due to the asynchronous nature of background jobs, code that uses them
|
|
96
|
-
needs to be handled specially when writing tests.
|
|
97
|
-
|
|
98
|
-
A common approach is to use the mock package to replace the
|
|
99
|
-
ckan.plugins.toolkit.enqueue_job function with a mock that executes jobs
|
|
100
|
-
synchronously instead of asynchronously
|
|
101
|
-
|
|
102
|
-
Also, since we are running the tests as root on a ckan instance that
|
|
103
|
-
is run by www-data, modifying files on disk in background jobs
|
|
104
|
-
(which were started by supervisor as www-data) does not work.
|
|
105
|
-
"""
|
|
106
|
-
if rq_kwargs is None:
|
|
107
|
-
rq_kwargs = {}
|
|
108
|
-
args = args or []
|
|
109
|
-
kwargs = kwargs or {}
|
|
110
|
-
job_func(*args, **kwargs)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def upload_presigned_to_s3(psurl, fields, path_to_upload):
|
|
114
|
-
"""Helper function for uploading data to S3
|
|
115
|
-
|
|
116
|
-
This is exactly how DCOR-Aid would be uploading things (with the
|
|
117
|
-
requests_toolbelt package). This could have been a little simpler,
|
|
118
|
-
but for the sake of reproducibility, we do it the DCOR-Aid way.
|
|
119
|
-
"""
|
|
120
|
-
# callback function for monitoring the upload progress
|
|
121
|
-
# open the input file for streaming
|
|
122
|
-
with path_to_upload.open("rb") as fd:
|
|
123
|
-
fields["file"] = (fields["key"], fd)
|
|
124
|
-
e = MultipartEncoder(fields=fields)
|
|
125
|
-
m = MultipartEncoderMonitor(
|
|
126
|
-
e, lambda monitor: print(f"Bytes: {monitor.bytes_read}"))
|
|
127
|
-
# Increase the read size to speed-up upload (the default chunk
|
|
128
|
-
# size for uploads in urllib is 8k which results in a lot of
|
|
129
|
-
# Python code being involved in uploading a 20GB file; Setting
|
|
130
|
-
# the chunk size to 4MB should increase the upload speed):
|
|
131
|
-
# https://github.com/requests/toolbelt/issues/75
|
|
132
|
-
# #issuecomment-237189952
|
|
133
|
-
m._read = m.read
|
|
134
|
-
m.read = lambda size: m._read(4 * 1024 * 1024)
|
|
135
|
-
# perform the actual upload
|
|
136
|
-
hrep = requests.post(
|
|
137
|
-
psurl,
|
|
138
|
-
data=m,
|
|
139
|
-
headers={'Content-Type': m.content_type},
|
|
140
|
-
verify=True, # verify SSL connection
|
|
141
|
-
timeout=27.3, # timeout to avoid freezing
|
|
142
|
-
)
|
|
143
|
-
if hrep.status_code != 204:
|
|
144
|
-
raise ValueError(
|
|
145
|
-
f"Upload failed with {hrep.status_code}: {hrep.reason}")
|
|
@@ -1,46 +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_with_resource(create_with_upload):
|
|
30
|
-
user = factories.User()
|
|
31
|
-
owner_org = factories.Organization(users=[{
|
|
32
|
-
'name': user['id'],
|
|
33
|
-
'capacity': 'admin'
|
|
34
|
-
}])
|
|
35
|
-
# Note: `call_action` bypasses authorization!
|
|
36
|
-
create_context = {'ignore_auth': False,
|
|
37
|
-
'user': user['name'], 'api_version': 3}
|
|
38
|
-
# upload a *valid* [sic] .rtdc File (this is the control)
|
|
39
|
-
ds_dict, res_dict = make_dataset(
|
|
40
|
-
create_context, owner_org,
|
|
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"]
|
|
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
|