dcor-shared 0.7.3__tar.gz → 0.7.4__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.3 → dcor_shared-0.7.4}/CHANGELOG +3 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/PKG-INFO +1 -1
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/__init__.py +2 -1
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/_version_save.py +1 -1
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/data.py +0 -17
- dcor_shared-0.7.4/dcor_shared/dcinst.py +20 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/s3cc.py +75 -3
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared.egg-info/PKG-INFO +1 -1
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared.egg-info/SOURCES.txt +1 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/tests/test_s3cc.py +51 -10
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/LICENSE +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/MANIFEST.in +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/README.rst +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/_version.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/ckan.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/mime.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/parse.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/paths.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/s3.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared/testing.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared.egg-info/dependency_links.txt +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared.egg-info/requires.txt +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/dcor_shared.egg-info/top_level.txt +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/setup.cfg +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/setup.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/tests/test_basic.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/tests/test_data.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/tests/test_s3.py +0 -0
- {dcor_shared-0.7.3 → dcor_shared-0.7.4}/tests/test_testing.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# flake8: noqa: F401
|
|
2
2
|
from .ckan import get_ckan_config_option, get_resource_path
|
|
3
|
-
from .data import DUMMY_BYTES,
|
|
3
|
+
from .data import DUMMY_BYTES, sha256sum, wait_for_resource
|
|
4
|
+
from .dcinst import get_dc_instance
|
|
4
5
|
from .mime import DC_MIME_TYPES, VALID_FORMATS
|
|
5
6
|
from . import paths
|
|
6
7
|
from ._version import version as __version__
|
|
@@ -4,30 +4,13 @@ import pathlib
|
|
|
4
4
|
import time
|
|
5
5
|
import warnings
|
|
6
6
|
|
|
7
|
-
from dclab.rtdc_dataset import RTDC_HDF5
|
|
8
|
-
|
|
9
7
|
from .ckan import get_resource_path
|
|
10
|
-
from . import s3cc
|
|
11
8
|
|
|
12
9
|
|
|
13
10
|
#: Content of the dummy file created when importing data.
|
|
14
11
|
DUMMY_BYTES = b"[Data import pending]"
|
|
15
12
|
|
|
16
13
|
|
|
17
|
-
def get_dc_instance(rid):
|
|
18
|
-
"""Return an instance of dclab's `RTDCBase` for a resource identifier"""
|
|
19
|
-
# Try local file first
|
|
20
|
-
path = get_resource_path(rid)
|
|
21
|
-
if path.is_file():
|
|
22
|
-
return RTDC_HDF5(path)
|
|
23
|
-
else:
|
|
24
|
-
# The resource must be on S3
|
|
25
|
-
if s3cc.object_exists(rid):
|
|
26
|
-
return s3cc.get_s3_dc_handle(rid)
|
|
27
|
-
else:
|
|
28
|
-
raise ValueError(f"Could not find resource {rid} anywhere")
|
|
29
|
-
|
|
30
|
-
|
|
31
14
|
def sha256sum(path):
|
|
32
15
|
"""Compute the SHA256 hash of a file in 1MB chunks"""
|
|
33
16
|
file_hash = hashlib.sha256()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dclab.rtdc_dataset import RTDC_HDF5
|
|
2
|
+
|
|
3
|
+
from .ckan import get_resource_path
|
|
4
|
+
from . import s3cc
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_dc_instance(rid):
|
|
8
|
+
"""Return an instance of dclab's `RTDCBase` for a resource identifier"""
|
|
9
|
+
# Try local file first
|
|
10
|
+
path = get_resource_path(rid)
|
|
11
|
+
if path.is_file():
|
|
12
|
+
# Disable basins, because they could point to files on the local
|
|
13
|
+
# file system (security).
|
|
14
|
+
return RTDC_HDF5(path, enable_basins=False)
|
|
15
|
+
else:
|
|
16
|
+
# The resource must be on S3
|
|
17
|
+
if s3cc.artifact_exists(rid):
|
|
18
|
+
return s3cc.get_s3_dc_handle(rid)
|
|
19
|
+
else:
|
|
20
|
+
raise ValueError(f"Could not find resource {rid} anywhere")
|
|
@@ -3,16 +3,32 @@
|
|
|
3
3
|
Contains methods to directly interact with CKAN resources that are on S3
|
|
4
4
|
via just the resource ID.
|
|
5
5
|
"""
|
|
6
|
+
from __future__ import annotations
|
|
6
7
|
import functools
|
|
7
|
-
|
|
8
|
+
import pathlib
|
|
8
9
|
from typing import Literal
|
|
10
|
+
import warnings
|
|
9
11
|
|
|
10
12
|
from dclab.rtdc_dataset import fmt_s3
|
|
11
13
|
|
|
12
14
|
from .ckan import get_ckan_config_option
|
|
15
|
+
from .data import sha256sum
|
|
13
16
|
from . import s3
|
|
14
17
|
|
|
15
18
|
|
|
19
|
+
def artifact_exists(
|
|
20
|
+
resource_id: str,
|
|
21
|
+
artifact: Literal["condensed", "preview", "resource"] = "resource"):
|
|
22
|
+
"""Check whether an artifact is available on S3
|
|
23
|
+
|
|
24
|
+
The resource with the identifier `resource_id` must exist in the
|
|
25
|
+
CKAN database.
|
|
26
|
+
"""
|
|
27
|
+
bucket_name, object_name = get_s3_bucket_object_for_artifact(
|
|
28
|
+
resource_id=resource_id, artifact=artifact)
|
|
29
|
+
return s3.object_exists(bucket_name=bucket_name, object_name=object_name)
|
|
30
|
+
|
|
31
|
+
|
|
16
32
|
def compute_checksum(resource_id):
|
|
17
33
|
"""Compute the SHA256 checksum of the corresponding CKAN resource"""
|
|
18
34
|
bucket_name, object_name = get_s3_bucket_object_for_artifact(
|
|
@@ -91,6 +107,9 @@ def get_s3_dc_handle(resource_id):
|
|
|
91
107
|
"dcor_object_store.access_key_id"),
|
|
92
108
|
secret_key=get_ckan_config_option(
|
|
93
109
|
"dcor_object_store.secret_access_key"),
|
|
110
|
+
# Disable basins, because they could point to files on the
|
|
111
|
+
# local file system (security).
|
|
112
|
+
enable_basins=False,
|
|
94
113
|
)
|
|
95
114
|
return ds
|
|
96
115
|
|
|
@@ -130,11 +149,64 @@ def make_resource_public(resource_id: str,
|
|
|
130
149
|
def object_exists(
|
|
131
150
|
resource_id: str,
|
|
132
151
|
artifact: Literal["condensed", "preview", "resource"] = "resource"):
|
|
133
|
-
"""Check whether an
|
|
152
|
+
"""Check whether an artifact is available on S3
|
|
134
153
|
|
|
135
154
|
The resource with the identifier `resource_id` must exist in the
|
|
136
155
|
CKAN database.
|
|
137
156
|
"""
|
|
157
|
+
warnings.warn("`s3cc.object_exists` is deprecated, please use"
|
|
158
|
+
"`s3cc.artifact_exists` instead",
|
|
159
|
+
DeprecationWarning)
|
|
160
|
+
return artifact_exists(resource_id, artifact)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def upload_artifact(
|
|
164
|
+
resource_id: str,
|
|
165
|
+
path_artifact: str | pathlib.Path,
|
|
166
|
+
artifact: Literal["condensed", "preview", "resource"] = "resource",
|
|
167
|
+
sha256: str = None,
|
|
168
|
+
private: bool = None,
|
|
169
|
+
override: bool = False
|
|
170
|
+
):
|
|
171
|
+
"""Upload an artifact to S3
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
resource_id: str
|
|
176
|
+
The resource identifier for the artifact
|
|
177
|
+
path_artifact: pathlib.Path
|
|
178
|
+
The path to the artifact file
|
|
179
|
+
artifact: str
|
|
180
|
+
The artifact type that the file represents
|
|
181
|
+
sha256: str
|
|
182
|
+
The SHA256 sum of `path_artifcat`, will be computed if not provided
|
|
183
|
+
private: bool
|
|
184
|
+
Whether the dataset that the resource belongs to is private.
|
|
185
|
+
Leave this blank if you don't know and we will do a database
|
|
186
|
+
look-up to determine the correct value.
|
|
187
|
+
override: bool
|
|
188
|
+
Whether to override a possibly existing object on S3.
|
|
189
|
+
"""
|
|
138
190
|
bucket_name, object_name = get_s3_bucket_object_for_artifact(
|
|
139
191
|
resource_id=resource_id, artifact=artifact)
|
|
140
|
-
|
|
192
|
+
|
|
193
|
+
if private is None:
|
|
194
|
+
# User did not say whether the resource is private. We have to
|
|
195
|
+
# find out ourselves.
|
|
196
|
+
import ckan.logic
|
|
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"]})
|
|
203
|
+
private = ds_dict["private"]
|
|
204
|
+
|
|
205
|
+
rid = resource_id
|
|
206
|
+
s3.upload_file(
|
|
207
|
+
bucket_name=bucket_name,
|
|
208
|
+
object_name=f"{artifact}/{rid[:3]}/{rid[3:6]}/{rid[6:]}",
|
|
209
|
+
path=path_artifact,
|
|
210
|
+
sha256=sha256 or sha256sum(path_artifact),
|
|
211
|
+
private=private,
|
|
212
|
+
override=override)
|
|
@@ -16,6 +16,23 @@ import requests
|
|
|
16
16
|
data_path = pathlib.Path(__file__).parent / "data"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
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
|
+
|
|
19
36
|
def setup_s3_resource_on_ckan(private=False):
|
|
20
37
|
"""Create an S3 resource in CKAN"""
|
|
21
38
|
user = factories.User()
|
|
@@ -151,14 +168,38 @@ def test_make_resource_public(enqueue_job_mock):
|
|
|
151
168
|
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
152
169
|
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
153
170
|
side_effect=synchronous_enqueue_job)
|
|
154
|
-
def
|
|
171
|
+
def test_upload_artifact(enqueue_job_mock, tmp_path):
|
|
155
172
|
rid, s3_url, _, org_dict = setup_s3_resource_on_ckan(private=True)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
164
|
-
|
|
173
|
+
path_fake_preview = tmp_path / "test_preview.jpg"
|
|
174
|
+
path_fake_preview.write_text("This is not a real image!")
|
|
175
|
+
# upload the preview
|
|
176
|
+
s3cc.upload_artifact(rid,
|
|
177
|
+
path_artifact=path_fake_preview,
|
|
178
|
+
artifact="preview")
|
|
179
|
+
# make sure that worked
|
|
180
|
+
assert s3cc.artifact_exists(rid, "preview")
|
|
181
|
+
# attempt to download the private artifact
|
|
182
|
+
resp1 = requests.get(s3_url.replace("resource", "preview"))
|
|
183
|
+
assert not resp1.ok, "sanity check"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
187
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
188
|
+
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
189
|
+
side_effect=synchronous_enqueue_job)
|
|
190
|
+
def test_upload_artifact_public(enqueue_job_mock, tmp_path):
|
|
191
|
+
rid, s3_url, _, org_dict = setup_s3_resource_on_ckan(private=True)
|
|
192
|
+
path_fake_preview = tmp_path / "test_preview.jpg"
|
|
193
|
+
path_fake_preview.write_text("This is not a real image!")
|
|
194
|
+
# upload the preview
|
|
195
|
+
s3cc.upload_artifact(rid,
|
|
196
|
+
path_artifact=path_fake_preview,
|
|
197
|
+
artifact="preview",
|
|
198
|
+
# force public resource even though dataset is not
|
|
199
|
+
# (this has no real-life use case)
|
|
200
|
+
private=False)
|
|
201
|
+
# make sure that worked
|
|
202
|
+
assert s3cc.artifact_exists(rid, "preview")
|
|
203
|
+
# attempt to download the private artifact
|
|
204
|
+
resp1 = requests.get(s3_url.replace("resource", "preview"))
|
|
205
|
+
assert resp1.ok, "preview should be public"
|
|
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
|