dcor-shared 0.12.2__tar.gz → 0.13.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.12.2 → dcor_shared-0.13.0}/.github/workflows/check.yml +14 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/CHANGELOG +13 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/PKG-INFO +2 -2
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/__init__.py +4 -2
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/_version.py +2 -2
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/ckan.py +6 -23
- dcor_shared-0.13.0/dcor_shared/data.py +58 -0
- dcor_shared-0.13.0/dcor_shared/dcinst.py +10 -0
- dcor_shared-0.13.0/dcor_shared/paths.py +16 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/s3.py +8 -3
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/s3cc.py +25 -11
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/testing.py +8 -4
- dcor_shared-0.13.0/dcor_shared/util.py +10 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared.egg-info/PKG-INFO +2 -2
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared.egg-info/SOURCES.txt +1 -1
- dcor_shared-0.13.0/dcor_shared.egg-info/requires.txt +2 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/pyproject.toml +1 -1
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/tests/requirements.txt +1 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/tests/test_ckan.py +14 -1
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/tests/test_s3cc.py +71 -2
- dcor_shared-0.12.2/dcor_shared/data.py +0 -92
- dcor_shared-0.12.2/dcor_shared/dcinst.py +0 -20
- dcor_shared-0.12.2/dcor_shared/paths.py +0 -30
- dcor_shared-0.12.2/dcor_shared.egg-info/requires.txt +0 -2
- dcor_shared-0.12.2/tests/test_basic.py +0 -5
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/.github/workflows/deploy_pypi.yml +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/.gitignore +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/LICENSE +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/MANIFEST.in +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/README.rst +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/mime.py +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/parse.py +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared/rqjob.py +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared.egg-info/dependency_links.txt +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/dcor_shared.egg-info/top_level.txt +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/setup.cfg +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/tests/data/calibration_beads_47.rtdc +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/tests/test_data.py +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/tests/test_s3.py +0 -0
- {dcor_shared-0.12.2 → dcor_shared-0.13.0}/tests/test_testing.py +0 -0
|
@@ -2,6 +2,20 @@ name: Checks
|
|
|
2
2
|
on: [push, pull_request]
|
|
3
3
|
|
|
4
4
|
jobs:
|
|
5
|
+
lint:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
steps:
|
|
8
|
+
- uses: actions/checkout@v4
|
|
9
|
+
- uses: actions/setup-python@v5
|
|
10
|
+
with:
|
|
11
|
+
python-version: '3.12'
|
|
12
|
+
- name: Install requirements
|
|
13
|
+
run: pip install flake8
|
|
14
|
+
- name: Check syntax
|
|
15
|
+
run: |
|
|
16
|
+
flake8 dcor_shared
|
|
17
|
+
flake8 tests
|
|
18
|
+
|
|
5
19
|
test:
|
|
6
20
|
strategy:
|
|
7
21
|
matrix:
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
0.13.0
|
|
2
|
+
- BREAKING: remove `get_resource_path`, `get_dcor_depot_path`,
|
|
3
|
+
`get_dcor_users_depot_path`
|
|
4
|
+
- BREAKING: `wait_for_resource` and `get_dc_instance` now only check S3 data
|
|
5
|
+
- feat: add `is_resource_private` method
|
|
6
|
+
- fix: disable SHA256 verification in boto3 (not supported everywhere);
|
|
7
|
+
note that we are still doing SHA256 verification after the upload
|
|
8
|
+
- enh: add "artifact" kwarg to `get_s3_dc_handle`
|
|
9
|
+
- enh: partial resource ID in basin names for `get_s3_dc_handle_basin_based`
|
|
10
|
+
- enh: add "perishable" key for basins in `get_s3_dc_handle_basin_based`
|
|
11
|
+
- setup: bump dclab to 0.64.0
|
|
12
|
+
0.12.3
|
|
13
|
+
- enh: improved resource availability check
|
|
1
14
|
0.12.2
|
|
2
15
|
- fix: testing did not append S3 resources
|
|
3
16
|
0.12.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dcor_shared
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Functionalities shared by the DCOR CKAN extensions
|
|
5
5
|
Author: Paul Müller
|
|
6
6
|
Maintainer-email: Paul Müller <dev@craban.de>
|
|
@@ -17,7 +17,7 @@ Requires-Python: <4,>=3.8
|
|
|
17
17
|
Description-Content-Type: text/x-rst
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: boto3
|
|
20
|
-
Requires-Dist: dclab[s3]>=0.
|
|
20
|
+
Requires-Dist: dclab[s3]>=0.64.0
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
23
|
dcor_shared
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# flake8: noqa: F401
|
|
2
2
|
from .ckan import (
|
|
3
|
-
get_ckan_config_option,
|
|
3
|
+
get_ckan_config_option, get_ckan_storage_path, get_resource_dc_config,
|
|
4
|
+
get_resource_info, is_resource_private
|
|
4
5
|
)
|
|
5
|
-
from .data import
|
|
6
|
+
from .data import wait_for_resource
|
|
6
7
|
from .dcinst import get_dc_instance
|
|
7
8
|
from .mime import DC_MIME_TYPES, VALID_FORMATS
|
|
8
9
|
from . import paths
|
|
9
10
|
from .rqjob import RQJob, rqjob_register
|
|
11
|
+
from .util import sha256sum
|
|
10
12
|
from ._version import version as __version__
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pathlib
|
|
3
|
-
import warnings
|
|
4
3
|
|
|
5
4
|
from .parse import get_ini_config_option
|
|
6
5
|
|
|
@@ -73,25 +72,9 @@ def get_resource_info(resource_id):
|
|
|
73
72
|
return ds_dict, res_dict
|
|
74
73
|
|
|
75
74
|
|
|
76
|
-
def
|
|
77
|
-
"""Return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"to storing data solely on S3",
|
|
83
|
-
DeprecationWarning)
|
|
84
|
-
rid = resource_id
|
|
85
|
-
resources_path = get_ckan_storage_path() / "resources"
|
|
86
|
-
pdir = resources_path / rid[:3] / rid[3:6]
|
|
87
|
-
path = pdir / rid[6:]
|
|
88
|
-
if create_dirs:
|
|
89
|
-
try:
|
|
90
|
-
pdir.mkdir(parents=True, exist_ok=True)
|
|
91
|
-
os.makedirs(pdir)
|
|
92
|
-
os.chown(pdir,
|
|
93
|
-
os.stat(resources_path).st_uid,
|
|
94
|
-
os.stat(resources_path).st_gid)
|
|
95
|
-
except OSError:
|
|
96
|
-
pass
|
|
97
|
-
return pathlib.Path(path)
|
|
75
|
+
def is_resource_private(resource_id):
|
|
76
|
+
"""Return true if a resource is private"""
|
|
77
|
+
from ckan import model
|
|
78
|
+
resource = model.Resource.get(resource_id)
|
|
79
|
+
dataset = model.Package.get(resource.package_id)
|
|
80
|
+
return dataset.private
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from . import s3cc
|
|
5
|
+
from .util import sha256sum # noqa: F401
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@functools.lru_cache(maxsize=100)
|
|
9
|
+
def wait_for_resource(resource_id: str,
|
|
10
|
+
timeout: float = 10):
|
|
11
|
+
"""Wait for resource to be available
|
|
12
|
+
|
|
13
|
+
This function can be used by other plugins to ensure that
|
|
14
|
+
a resource is available for processing.
|
|
15
|
+
|
|
16
|
+
The only way (since 2024) of uploading data is via pre-signed URLs
|
|
17
|
+
to an S3 instance. Here, we have to make sure that the
|
|
18
|
+
upload is complete and the file exists. If this is the case,
|
|
19
|
+
then uploads should have already completed when this function
|
|
20
|
+
is called, so we only check for the existence of the resource
|
|
21
|
+
in ckan and whether the `s3_available` attribute is defined.
|
|
22
|
+
"""
|
|
23
|
+
from ckan import logic
|
|
24
|
+
|
|
25
|
+
if len(resource_id) != 36:
|
|
26
|
+
raise ValueError(f"Invalid resource id: {resource_id}")
|
|
27
|
+
|
|
28
|
+
resource_show = logic.get_action("resource_show")
|
|
29
|
+
|
|
30
|
+
t0 = time.time()
|
|
31
|
+
while True:
|
|
32
|
+
try:
|
|
33
|
+
res_dict = resource_show(context={'ignore_auth': True,
|
|
34
|
+
'user': 'default'},
|
|
35
|
+
data_dict={"id": resource_id})
|
|
36
|
+
except logic.NotFound:
|
|
37
|
+
# Other processes are still working on getting the resource
|
|
38
|
+
# online. We have to wait.
|
|
39
|
+
time.sleep(5)
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
# object exists in database?
|
|
43
|
+
s3_ok = res_dict.get("s3_available", None)
|
|
44
|
+
if s3_ok:
|
|
45
|
+
# If the resource is on S3, it is considered to be available.
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
# object exists on S3?
|
|
49
|
+
try:
|
|
50
|
+
s3_exist = s3cc.artifact_exists(res_dict["id"])
|
|
51
|
+
except BaseException:
|
|
52
|
+
s3_exist = False
|
|
53
|
+
if s3_exist:
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if time.time() - t0 > timeout:
|
|
57
|
+
raise OSError(f"Data import seems to take too long "
|
|
58
|
+
f"for '{resource_id}'!")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from . import s3cc
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_dc_instance(rid):
|
|
5
|
+
"""Return an instance of dclab's `RTDCBase` for a resource identifier"""
|
|
6
|
+
# The resource must be on S3
|
|
7
|
+
if s3cc.artifact_exists(rid):
|
|
8
|
+
return s3cc.get_s3_dc_handle(rid)
|
|
9
|
+
else:
|
|
10
|
+
raise ValueError(f"Could not find resource {rid} anywhere")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
from .ckan import get_ckan_storage_path, get_ckan_webassets_path # noqa: F401
|
|
4
|
+
from .ckan import get_ckan_config_path # noqa: F401
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_nginx_config_path():
|
|
8
|
+
return pathlib.Path("/etc/nginx/sites-enabled/ckan")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_supervisord_worker_config_path():
|
|
12
|
+
return pathlib.Path("/etc/supervisor/conf.d/ckan-worker.conf")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_uwsgi_config_path():
|
|
16
|
+
return pathlib.Path("/etc/ckan/default/ckan-uwsgi.ini")
|
|
@@ -11,10 +11,11 @@ from typing import List, Tuple
|
|
|
11
11
|
import warnings
|
|
12
12
|
|
|
13
13
|
import boto3
|
|
14
|
+
from botocore.config import Config
|
|
14
15
|
import botocore.exceptions
|
|
15
16
|
|
|
16
17
|
from .ckan import get_ckan_config_option
|
|
17
|
-
from .
|
|
18
|
+
from .util import sha256sum
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def compute_checksum(bucket_name, object_name, max_size=None):
|
|
@@ -305,6 +306,9 @@ def get_s3():
|
|
|
305
306
|
use_ssl=ssl_verify,
|
|
306
307
|
verify=ssl_verify,
|
|
307
308
|
endpoint_url=get_ckan_config_option("dcor_object_store.endpoint_url"),
|
|
309
|
+
# https://github.com/boto/boto3/issues/4400#issuecomment-2600742103
|
|
310
|
+
config=Config(request_checksum_calculation="when_required",
|
|
311
|
+
response_checksum_validation="when_required")
|
|
308
312
|
)
|
|
309
313
|
s3_resource = s3_session.resource(
|
|
310
314
|
service_name="s3",
|
|
@@ -493,7 +497,7 @@ def upload_file(bucket_name, object_name, path, sha256=None, private=True,
|
|
|
493
497
|
|
|
494
498
|
path_size = pathlib.Path(path).stat().st_size
|
|
495
499
|
s3_client, _, _ = get_s3()
|
|
496
|
-
|
|
500
|
+
require_bucket(bucket_name)
|
|
497
501
|
|
|
498
502
|
perform_upload = True
|
|
499
503
|
if not override:
|
|
@@ -501,7 +505,8 @@ def upload_file(bucket_name, object_name, path, sha256=None, private=True,
|
|
|
501
505
|
object_name=object_name)
|
|
502
506
|
|
|
503
507
|
if perform_upload:
|
|
504
|
-
|
|
508
|
+
s3_client.upload_file(Filename=str(path),
|
|
509
|
+
Bucket=bucket_name,
|
|
505
510
|
Key=object_name,
|
|
506
511
|
# ExtraArgs={
|
|
507
512
|
# # verification of the upload (breaks OpenStack)
|
|
@@ -8,7 +8,6 @@ import io
|
|
|
8
8
|
import functools
|
|
9
9
|
import pathlib
|
|
10
10
|
from typing import Literal
|
|
11
|
-
from urllib.parse import urlparse
|
|
12
11
|
import warnings
|
|
13
12
|
|
|
14
13
|
import dclab
|
|
@@ -16,7 +15,8 @@ from dclab.rtdc_dataset import fmt_hdf5, fmt_s3
|
|
|
16
15
|
import h5py
|
|
17
16
|
|
|
18
17
|
from .ckan import (
|
|
19
|
-
get_ckan_config_option, get_resource_dc_config, get_resource_info
|
|
18
|
+
get_ckan_config_option, get_resource_dc_config, get_resource_info,
|
|
19
|
+
is_resource_private
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
from . import s3
|
|
@@ -169,7 +169,9 @@ def get_s3_bucket_name_for_resource(resource_id):
|
|
|
169
169
|
return bucket_name
|
|
170
170
|
|
|
171
171
|
|
|
172
|
-
def get_s3_dc_handle(
|
|
172
|
+
def get_s3_dc_handle(
|
|
173
|
+
resource_id: str,
|
|
174
|
+
artifact: Literal["condensed", "preview", "resource"] = "resource"):
|
|
173
175
|
"""Return an instance of :class:`RTDC_S3`
|
|
174
176
|
|
|
175
177
|
The data are accessed directly via S3 using DCOR's access credentials.
|
|
@@ -178,7 +180,7 @@ def get_s3_dc_handle(resource_id):
|
|
|
178
180
|
The resource with the identifier `resource_id` must exist in the
|
|
179
181
|
CKAN database.
|
|
180
182
|
"""
|
|
181
|
-
s3_url = get_s3_url_for_artifact(resource_id)
|
|
183
|
+
s3_url = get_s3_url_for_artifact(resource_id, artifact=artifact)
|
|
182
184
|
ds = fmt_s3.RTDC_S3(
|
|
183
185
|
url=s3_url,
|
|
184
186
|
access_key_id=get_ckan_config_option(
|
|
@@ -203,11 +205,22 @@ def get_s3_dc_handle_basin_based(resource_id):
|
|
|
203
205
|
that initialization takes slightly longer and that, if private
|
|
204
206
|
resources are accessed, the presigned URLs in the basins are only
|
|
205
207
|
valid for a fixed time period.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
resource_id : str
|
|
212
|
+
CKAN resource identifier
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
ds: RTDCBase
|
|
217
|
+
basin-based dclab dataset
|
|
206
218
|
"""
|
|
207
|
-
|
|
219
|
+
artifacts = ["resource", "condensed"]
|
|
208
220
|
basin_paths = []
|
|
209
|
-
|
|
210
|
-
|
|
221
|
+
private = is_resource_private(resource_id)
|
|
222
|
+
for artifact in artifacts:
|
|
223
|
+
if private:
|
|
211
224
|
bp = create_presigned_url(resource_id, artifact=artifact)
|
|
212
225
|
else:
|
|
213
226
|
bp = get_s3_url_for_artifact(resource_id, artifact=artifact)
|
|
@@ -220,18 +233,19 @@ def get_s3_dc_handle_basin_based(resource_id):
|
|
|
220
233
|
hv.require_group("events")
|
|
221
234
|
hw = dclab.RTDCWriter(hv)
|
|
222
235
|
hw.store_metadata(get_resource_dc_config(resource_id))
|
|
223
|
-
for bp in basin_paths:
|
|
224
|
-
urlp = urlparse(bp) # e.g. http://localhost/bucket/resource/id
|
|
236
|
+
for bp, artifact in zip(basin_paths, artifacts):
|
|
225
237
|
hw.store_basin(
|
|
226
|
-
basin_name=
|
|
238
|
+
basin_name=f"{artifact}-{resource_id[:5]}", # "resource-92a12"
|
|
227
239
|
basin_format="http",
|
|
228
240
|
basin_type="remote",
|
|
229
241
|
basin_locs=[bp],
|
|
230
242
|
# Don't verify anything. This would only cost time,
|
|
231
243
|
# and we know these objects exist.
|
|
232
244
|
verify=False,
|
|
245
|
+
perishable=private,
|
|
233
246
|
)
|
|
234
|
-
|
|
247
|
+
ds = fmt_hdf5.RTDC_HDF5(fd)
|
|
248
|
+
return ds
|
|
235
249
|
|
|
236
250
|
|
|
237
251
|
def get_s3_url_for_artifact(
|
|
@@ -8,6 +8,7 @@ import uuid
|
|
|
8
8
|
import warnings
|
|
9
9
|
|
|
10
10
|
import ckan.authz
|
|
11
|
+
import ckan.model
|
|
11
12
|
import ckan.tests.factories as factories
|
|
12
13
|
import ckan.tests.helpers as helpers
|
|
13
14
|
from ckan.tests.pytest_ckan.fixtures import FakeFileStorage
|
|
@@ -134,7 +135,9 @@ def make_dataset_via_s3(
|
|
|
134
135
|
"""
|
|
135
136
|
if create_context is None:
|
|
136
137
|
user = factories.User()
|
|
138
|
+
user_obj = ckan.model.User.by_name(user["name"])
|
|
137
139
|
create_context = {'ignore_auth': False,
|
|
140
|
+
'auth_user_obj': user_obj,
|
|
138
141
|
'user': user['name'],
|
|
139
142
|
'api_version': 3}
|
|
140
143
|
user_id = user["id"]
|
|
@@ -144,10 +147,11 @@ def make_dataset_via_s3(
|
|
|
144
147
|
create_context["user"], allow_none=True)
|
|
145
148
|
|
|
146
149
|
if owner_org is None:
|
|
147
|
-
owner_org = factories.Organization(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
owner_org = factories.Organization(
|
|
151
|
+
users=[{
|
|
152
|
+
'name': user_id,
|
|
153
|
+
'capacity': 'admin'
|
|
154
|
+
}])
|
|
151
155
|
|
|
152
156
|
if "title" not in kwargs:
|
|
153
157
|
kwargs["title"] = "test-dataset-S3"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dcor_shared
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Functionalities shared by the DCOR CKAN extensions
|
|
5
5
|
Author: Paul Müller
|
|
6
6
|
Maintainer-email: Paul Müller <dev@craban.de>
|
|
@@ -17,7 +17,7 @@ Requires-Python: <4,>=3.8
|
|
|
17
17
|
Description-Content-Type: text/x-rst
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: boto3
|
|
20
|
-
Requires-Dist: dclab[s3]>=0.
|
|
20
|
+
Requires-Dist: dclab[s3]>=0.64.0
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
23
|
dcor_shared
|
|
@@ -18,13 +18,13 @@ dcor_shared/rqjob.py
|
|
|
18
18
|
dcor_shared/s3.py
|
|
19
19
|
dcor_shared/s3cc.py
|
|
20
20
|
dcor_shared/testing.py
|
|
21
|
+
dcor_shared/util.py
|
|
21
22
|
dcor_shared.egg-info/PKG-INFO
|
|
22
23
|
dcor_shared.egg-info/SOURCES.txt
|
|
23
24
|
dcor_shared.egg-info/dependency_links.txt
|
|
24
25
|
dcor_shared.egg-info/requires.txt
|
|
25
26
|
dcor_shared.egg-info/top_level.txt
|
|
26
27
|
tests/requirements.txt
|
|
27
|
-
tests/test_basic.py
|
|
28
28
|
tests/test_ckan.py
|
|
29
29
|
tests/test_data.py
|
|
30
30
|
tests/test_s3.py
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import pathlib
|
|
2
2
|
from unittest import mock
|
|
3
3
|
|
|
4
|
-
from dcor_shared import
|
|
4
|
+
from dcor_shared import (
|
|
5
|
+
get_resource_dc_config, get_resource_info, is_resource_private
|
|
6
|
+
)
|
|
5
7
|
|
|
6
8
|
import pytest
|
|
7
9
|
from dcor_shared.testing import make_dataset_via_s3, synchronous_enqueue_job
|
|
@@ -36,3 +38,14 @@ def test_get_resource_info(enqueue_job_mock):
|
|
|
36
38
|
ds_dict_2, res_dict_2 = get_resource_info(res_dict["id"])
|
|
37
39
|
assert ds_dict == ds_dict_2
|
|
38
40
|
assert res_dict == res_dict_2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
44
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
45
|
+
@pytest.mark.parametrize("private", [True, False])
|
|
46
|
+
def test_is_resource_private(private):
|
|
47
|
+
_, res_dict = make_dataset_via_s3(
|
|
48
|
+
resource_path=data_path / "calibration_beads_47.rtdc",
|
|
49
|
+
private=private,
|
|
50
|
+
activate=True)
|
|
51
|
+
assert is_resource_private(res_dict["id"]) == private
|
|
@@ -20,7 +20,9 @@ import requests
|
|
|
20
20
|
data_path = pathlib.Path(__file__).parent / "data"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def setup_s3_resource_on_ckan(private=False,
|
|
23
|
+
def setup_s3_resource_on_ckan(private: bool = False,
|
|
24
|
+
resource_path: str | pathlib.Path = None
|
|
25
|
+
):
|
|
24
26
|
"""Create an S3 resource in CKAN"""
|
|
25
27
|
if resource_path is None:
|
|
26
28
|
resource_path = data_path / "calibration_beads_47.rtdc"
|
|
@@ -34,7 +36,9 @@ def setup_s3_resource_on_ckan(private=False, resource_path=None):
|
|
|
34
36
|
}])
|
|
35
37
|
|
|
36
38
|
test_context = {'ignore_auth': False,
|
|
37
|
-
'user': user['name'],
|
|
39
|
+
'user': user['name'],
|
|
40
|
+
'model': model,
|
|
41
|
+
'api_version': 3}
|
|
38
42
|
|
|
39
43
|
# Upload the resource to S3 (note that it is not required that the
|
|
40
44
|
# dataset exists)
|
|
@@ -167,6 +171,23 @@ def test_get_s3_handle(enqueue_job_mock):
|
|
|
167
171
|
assert len(ds) == 47
|
|
168
172
|
|
|
169
173
|
|
|
174
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas dc_serve')
|
|
175
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
176
|
+
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
177
|
+
side_effect=synchronous_enqueue_job)
|
|
178
|
+
def test_get_s3_handle_condensed(enqueue_job_mock, tmp_path):
|
|
179
|
+
resource_path = tmp_path / "data.rtdc"
|
|
180
|
+
shutil.copy2(data_path / "calibration_beads_47.rtdc", resource_path)
|
|
181
|
+
_, res_dict = make_dataset_via_s3(
|
|
182
|
+
resource_path=resource_path,
|
|
183
|
+
activate=True)
|
|
184
|
+
rid = res_dict["id"]
|
|
185
|
+
expected_path = s3cc.get_s3_url_for_artifact(rid, artifact="condensed")
|
|
186
|
+
with s3cc.get_s3_dc_handle(rid, artifact="condensed") as ds:
|
|
187
|
+
assert ds.path == expected_path
|
|
188
|
+
assert len(ds) == 47
|
|
189
|
+
|
|
190
|
+
|
|
170
191
|
@pytest.mark.ckan_config('ckan.plugins', 'dcor_depot dcor_schemas dc_serve')
|
|
171
192
|
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
172
193
|
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
@@ -198,6 +219,54 @@ def test_get_s3_dc_handle_basin_based(enqueue_job_mock, tmp_path):
|
|
|
198
219
|
assert len(ds2) == 47
|
|
199
220
|
|
|
200
221
|
|
|
222
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_depot dcor_schemas dc_serve')
|
|
223
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
224
|
+
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
225
|
+
side_effect=synchronous_enqueue_job)
|
|
226
|
+
def test_get_s3_dc_handle_basin_based_public_urls(enqueue_job_mock, tmp_path):
|
|
227
|
+
resource_path = tmp_path / "data.rtdc"
|
|
228
|
+
shutil.copy2(data_path / "calibration_beads_47.rtdc", resource_path)
|
|
229
|
+
with h5py.File(resource_path, "a") as h5:
|
|
230
|
+
del h5["events/volume"]
|
|
231
|
+
|
|
232
|
+
_, res_dict = make_dataset_via_s3(
|
|
233
|
+
resource_path=resource_path,
|
|
234
|
+
private=False,
|
|
235
|
+
activate=True)
|
|
236
|
+
rid = res_dict["id"]
|
|
237
|
+
|
|
238
|
+
with s3cc.get_s3_dc_handle_basin_based(rid) as ds:
|
|
239
|
+
# get the basins
|
|
240
|
+
for bn_dict in ds.basins_get_dicts():
|
|
241
|
+
assert not bn_dict["perishable"]
|
|
242
|
+
for url in bn_dict["urls"]:
|
|
243
|
+
assert not url.lower().count("expires")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_depot dcor_schemas dc_serve')
|
|
247
|
+
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
248
|
+
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
249
|
+
side_effect=synchronous_enqueue_job)
|
|
250
|
+
def test_get_s3_dc_handle_basin_based_private_urls(enqueue_job_mock, tmp_path):
|
|
251
|
+
resource_path = tmp_path / "data.rtdc"
|
|
252
|
+
shutil.copy2(data_path / "calibration_beads_47.rtdc", resource_path)
|
|
253
|
+
with h5py.File(resource_path, "a") as h5:
|
|
254
|
+
del h5["events/volume"]
|
|
255
|
+
|
|
256
|
+
_, res_dict = make_dataset_via_s3(
|
|
257
|
+
resource_path=resource_path,
|
|
258
|
+
private=True,
|
|
259
|
+
activate=True)
|
|
260
|
+
rid = res_dict["id"]
|
|
261
|
+
|
|
262
|
+
with s3cc.get_s3_dc_handle_basin_based(rid) as ds:
|
|
263
|
+
# get the basins
|
|
264
|
+
for bn_dict in ds.basins_get_dicts():
|
|
265
|
+
assert bn_dict["perishable"]
|
|
266
|
+
for url in bn_dict["urls"]:
|
|
267
|
+
assert url.lower().count("expires")
|
|
268
|
+
|
|
269
|
+
|
|
201
270
|
@pytest.mark.ckan_config('ckan.plugins', 'dcor_schemas')
|
|
202
271
|
@pytest.mark.usefixtures('clean_db', 'with_request_context')
|
|
203
272
|
@mock.patch('ckan.plugins.toolkit.enqueue_job',
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import hashlib
|
|
3
|
-
import pathlib
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
from .ckan import get_resource_path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#: Content of the dummy file created when importing data.
|
|
10
|
-
DUMMY_BYTES = b"[Data import pending]"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def sha256sum(path):
|
|
14
|
-
"""Compute the SHA256 hash of a file in 1MB chunks"""
|
|
15
|
-
file_hash = hashlib.sha256()
|
|
16
|
-
with open(path, "rb") as fd:
|
|
17
|
-
while data := fd.read(2 ** 20):
|
|
18
|
-
file_hash.update(data)
|
|
19
|
-
return file_hash.hexdigest()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@functools.lru_cache(maxsize=100)
|
|
23
|
-
def wait_for_resource(resource_id: str,
|
|
24
|
-
timeout: float = 10):
|
|
25
|
-
"""Wait for resource to be available
|
|
26
|
-
|
|
27
|
-
This function can be used by other plugins to ensure that
|
|
28
|
-
a resource is available for processing.
|
|
29
|
-
|
|
30
|
-
There multiple ways for data to become available:
|
|
31
|
-
|
|
32
|
-
1. The ckanext-dcor_depot plugin imports data by touching
|
|
33
|
-
dummy files and then sym-linking to data on disk. Here
|
|
34
|
-
we just check that the file is not a dummy file anymore.
|
|
35
|
-
2. Legacy uploads via nginx/uwsgi directly into CKAN and onto
|
|
36
|
-
the local block storage worked the same way. We have to wait
|
|
37
|
-
for the dummy file to be replaced.
|
|
38
|
-
3. The new (2024) way of uploading data is via pre-signed URLs
|
|
39
|
-
to an S3 instance. Here, we have to make sure that the
|
|
40
|
-
upload is complete and the file exists. If this is the case,
|
|
41
|
-
then uploads should have already completed when this function
|
|
42
|
-
is called, so we only check for the existence of the resource
|
|
43
|
-
in ckan and whether the `s3_available` attribute is defined.
|
|
44
|
-
"""
|
|
45
|
-
from ckan.common import config
|
|
46
|
-
from ckan import logic
|
|
47
|
-
|
|
48
|
-
if len(resource_id) != 36:
|
|
49
|
-
raise ValueError(f"Invalid resource id: {resource_id}")
|
|
50
|
-
|
|
51
|
-
resource_show = logic.get_action("resource_show")
|
|
52
|
-
path = pathlib.Path(get_resource_path(resource_id))
|
|
53
|
-
|
|
54
|
-
dcor_depot_available = "dcor_depot" in config.get('ckan.plugins', "")
|
|
55
|
-
# Initially this was set to 10s, but if `/data` is mounted on a
|
|
56
|
-
# network share then this part here just takes too long.
|
|
57
|
-
t0 = time.time()
|
|
58
|
-
ld = len(DUMMY_BYTES)
|
|
59
|
-
while True:
|
|
60
|
-
try:
|
|
61
|
-
res_dict = resource_show(context={'ignore_auth': True,
|
|
62
|
-
'user': 'default'},
|
|
63
|
-
data_dict={"id": resource_id})
|
|
64
|
-
except logic.NotFound:
|
|
65
|
-
# Other processes are still working on getting the resource
|
|
66
|
-
# online. We have to wait.
|
|
67
|
-
time.sleep(5)
|
|
68
|
-
continue
|
|
69
|
-
|
|
70
|
-
s3_ok = res_dict.get("s3_available", None)
|
|
71
|
-
if time.time() - t0 > timeout:
|
|
72
|
-
raise OSError("Data import seems to take too long "
|
|
73
|
-
"for '{}'!".format(path))
|
|
74
|
-
elif s3_ok is not None:
|
|
75
|
-
# If the dataset is on S3, it is considered to be available.
|
|
76
|
-
break
|
|
77
|
-
elif not path.exists():
|
|
78
|
-
time.sleep(5)
|
|
79
|
-
continue
|
|
80
|
-
elif dcor_depot_available and not path.is_symlink():
|
|
81
|
-
# Resource is only available when it is symlinked by
|
|
82
|
-
# the ckanext.dcor_depot `symlink_user_dataset` job
|
|
83
|
-
# (or by the ckanext.dcor_depot importers).
|
|
84
|
-
time.sleep(5)
|
|
85
|
-
continue
|
|
86
|
-
elif path.stat().st_size == ld and path.read_bytes() == DUMMY_BYTES:
|
|
87
|
-
# wait a bit
|
|
88
|
-
time.sleep(5)
|
|
89
|
-
continue
|
|
90
|
-
else:
|
|
91
|
-
# not a dummy file
|
|
92
|
-
break
|
|
@@ -1,20 +0,0 @@
|
|
|
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")
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import pathlib
|
|
2
|
-
|
|
3
|
-
from .ckan import get_ckan_config_path, get_ckan_config_option
|
|
4
|
-
from .ckan import get_ckan_storage_path, get_ckan_webassets_path # noqa: F401
|
|
5
|
-
|
|
6
|
-
from .parse import get_ini_config_option
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_dcor_depot_path():
|
|
10
|
-
return pathlib.Path(get_ckan_config_option(
|
|
11
|
-
"ckanext.dcor_depot.depots_path"))
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def get_dcor_users_depot_path():
|
|
15
|
-
depot = get_dcor_depot_path()
|
|
16
|
-
return depot / get_ini_config_option(
|
|
17
|
-
"ckanext.dcor_depot.users_depot_name",
|
|
18
|
-
get_ckan_config_path())
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def get_nginx_config_path():
|
|
22
|
-
return pathlib.Path("/etc/nginx/sites-enabled/ckan")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def get_supervisord_worker_config_path():
|
|
26
|
-
return pathlib.Path("/etc/supervisor/conf.d/ckan-worker.conf")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def get_uwsgi_config_path():
|
|
30
|
-
return pathlib.Path("/etc/ckan/default/ckan-uwsgi.ini")
|
|
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
|