dcor-shared 0.5.6__tar.gz → 0.6.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.5.6 → dcor_shared-0.6.0}/CHANGELOG +5 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/PKG-INFO +1 -1
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/_version_save.py +1 -1
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/ckan.py +2 -5
- dcor_shared-0.6.0/dcor_shared/data.py +93 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/paths.py +1 -4
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/PKG-INFO +1 -1
- dcor_shared-0.6.0/tests/test_data.py +68 -0
- dcor_shared-0.5.6/dcor_shared/data.py +0 -59
- dcor_shared-0.5.6/tests/test_data.py +0 -9
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/LICENSE +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/MANIFEST.in +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/README.rst +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/__init__.py +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/_version.py +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/mime.py +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/parse.py +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/s3.py +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/SOURCES.txt +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/dependency_links.txt +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/requires.txt +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/top_level.txt +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/setup.cfg +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/setup.py +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/tests/test_basic.py +0 -0
- {dcor_shared-0.5.6 → dcor_shared-0.6.0}/tests/test_s3.py +0 -0
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pathlib
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
from ckan import common as ckan_common
|
|
6
|
-
except ImportError:
|
|
7
|
-
ckan_common = None
|
|
4
|
+
from ckan import common as ckan_common
|
|
8
5
|
|
|
9
6
|
from .parse import get_ini_config_option
|
|
10
7
|
|
|
@@ -16,7 +13,7 @@ def get_ckan_config_option(option):
|
|
|
16
13
|
taken from there. Else, the configuration is taken from the
|
|
17
14
|
configuration file returned by :func:`get_ckan_config_path`.
|
|
18
15
|
"""
|
|
19
|
-
if ckan_common
|
|
16
|
+
if ckan_common.config:
|
|
20
17
|
# Get from current configuration (The `get` method in CKAN 2.10
|
|
21
18
|
# returns default values if not placeholder is specified)
|
|
22
19
|
opt = ckan_common.config.get(option)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import hashlib
|
|
3
|
+
import pathlib
|
|
4
|
+
import time
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
from ckan.common import config
|
|
8
|
+
from ckan import logic
|
|
9
|
+
|
|
10
|
+
from .ckan import get_resource_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
#: Content of the dummy file created when importing data.
|
|
14
|
+
DUMMY_BYTES = b"[Data import pending]"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def sha256sum(path):
|
|
18
|
+
"""Compute the SHA256 hash of a file in 1MB chunks"""
|
|
19
|
+
file_hash = hashlib.sha256()
|
|
20
|
+
with open(path, "rb") as fd:
|
|
21
|
+
while data := fd.read(2 ** 20):
|
|
22
|
+
file_hash.update(data)
|
|
23
|
+
return file_hash.hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@functools.lru_cache(maxsize=100)
|
|
27
|
+
def wait_for_resource(resource_id: str,
|
|
28
|
+
timeout: float = 10):
|
|
29
|
+
"""Wait for resource to be available
|
|
30
|
+
|
|
31
|
+
This function can be used by other plugins to ensure that
|
|
32
|
+
a resource is available for processing.
|
|
33
|
+
|
|
34
|
+
There multiple ways for data to become available:
|
|
35
|
+
|
|
36
|
+
1. The ckanext-dcor_depot plugin imports data by touching
|
|
37
|
+
dummy files and then sym-linking to data on disk. Here
|
|
38
|
+
we just check that the file is not a dummy file anymore.
|
|
39
|
+
2. Legacy uploads via nginx/uwsgi directly into CKAN and onto
|
|
40
|
+
the local block storage worked the same way. We have to wait
|
|
41
|
+
for the dummy file to be replaced.
|
|
42
|
+
3. The new (2024) way of uploading data is via pre-signed URLs
|
|
43
|
+
to an S3 instance. Here, we have to make sure that the
|
|
44
|
+
upload is complete and the file exists. If this is the case,
|
|
45
|
+
then uploads should have already completed when this function
|
|
46
|
+
is called, so we only check for the existence of the resource
|
|
47
|
+
in ckan and whether the `s3_available` attribute is defined.
|
|
48
|
+
"""
|
|
49
|
+
if len(resource_id) != 36:
|
|
50
|
+
warnings.warn("Please pass the CKAN resource id as `resource_id` "
|
|
51
|
+
"instead of a string or a path. This will raise an "
|
|
52
|
+
"exception in the future.",
|
|
53
|
+
DeprecationWarning)
|
|
54
|
+
# The user passed a local path on the block storage. These look like
|
|
55
|
+
# this: /data/dcor-instance/resources/RE/SO/URCE-ID
|
|
56
|
+
resource_id = "".join(resource_id.split("/")[-3])
|
|
57
|
+
|
|
58
|
+
resource_show = logic.get_action("resource_show")
|
|
59
|
+
path = pathlib.Path(get_resource_path(resource_id))
|
|
60
|
+
|
|
61
|
+
dcor_depot_available = "dcor_depot" in config.get('ckan.plugins', "")
|
|
62
|
+
# Initially this was set to 10s, but if `/data` is mounted on a
|
|
63
|
+
# network share then this part here just takes too long.
|
|
64
|
+
t0 = time.time()
|
|
65
|
+
ld = len(DUMMY_BYTES)
|
|
66
|
+
while True:
|
|
67
|
+
res_dict = resource_show(context={'ignore_auth': True,
|
|
68
|
+
'user': 'default'},
|
|
69
|
+
data_dict={"id": resource_id})
|
|
70
|
+
|
|
71
|
+
s3_url = res_dict.get("s3_url", None)
|
|
72
|
+
if time.time() - t0 > timeout:
|
|
73
|
+
raise OSError("Data import seems to take too long "
|
|
74
|
+
"for '{}'!".format(path))
|
|
75
|
+
elif s3_url is not None:
|
|
76
|
+
# If the dataset is on S3, it is considered to be available.
|
|
77
|
+
break
|
|
78
|
+
elif not path.exists():
|
|
79
|
+
time.sleep(0.05)
|
|
80
|
+
continue
|
|
81
|
+
elif dcor_depot_available and not path.is_symlink():
|
|
82
|
+
# Resource is only available when it is symlinked by
|
|
83
|
+
# the ckanext.dcor_depot `symlink_user_dataset` job
|
|
84
|
+
# (or by the ckanext.dcor_depot importers).
|
|
85
|
+
time.sleep(0.05)
|
|
86
|
+
continue
|
|
87
|
+
elif path.stat().st_size == ld and path.read_bytes() == DUMMY_BYTES:
|
|
88
|
+
# wait a bit
|
|
89
|
+
time.sleep(0.01)
|
|
90
|
+
continue
|
|
91
|
+
else:
|
|
92
|
+
# not a dummy file
|
|
93
|
+
break
|
|
@@ -6,10 +6,7 @@ from .ckan import get_ckan_storage_path, get_ckan_webassets_path # noqa: F401
|
|
|
6
6
|
from .parse import get_ini_config_option
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
from ckan.common import config as ckan_config
|
|
11
|
-
except ImportError:
|
|
12
|
-
ckan_config = None
|
|
9
|
+
from ckan.common import config as ckan_config # noqa: F401
|
|
13
10
|
|
|
14
11
|
|
|
15
12
|
def get_dcor_depot_path():
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from ckan import logic
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from dcor_shared import get_resource_path, sha256sum, wait_for_resource
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_sha256sum(tmp_path):
|
|
12
|
+
p = tmp_path / "test.txt"
|
|
13
|
+
p.write_text("Sum this up!")
|
|
14
|
+
ist = sha256sum(p)
|
|
15
|
+
soll = "d00df55b97a60c78bbb137540e1b60647a5e6b216262a95ab96cafd4519bcf6a"
|
|
16
|
+
assert ist == soll
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_wait_for_resource_s3(monkeypatch):
|
|
20
|
+
res_id = str(uuid.uuid4())
|
|
21
|
+
monkeypatch.setattr(logic, "get_action",
|
|
22
|
+
lambda x: lambda context, data_dict: {
|
|
23
|
+
"id": res_id,
|
|
24
|
+
"s3_url": "https://bucket.name/object"})
|
|
25
|
+
# Should not raise an exception
|
|
26
|
+
wait_for_resource(res_id)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.ckan_config('ckan.plugins', '') # disable plugins
|
|
30
|
+
def test_wait_for_resource_no_s3_url_no_plugins(monkeypatch):
|
|
31
|
+
res_id = str(uuid.uuid4())
|
|
32
|
+
monkeypatch.setattr(logic, "get_action",
|
|
33
|
+
lambda x: lambda context, data_dict: {
|
|
34
|
+
"id": res_id})
|
|
35
|
+
# Create the actual path
|
|
36
|
+
rp = pathlib.Path(get_resource_path(res_id))
|
|
37
|
+
rp.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
rp.write_bytes(b"This is not a dummy file, but a fake DC file!")
|
|
39
|
+
# Should not raise an exception
|
|
40
|
+
wait_for_resource(res_id)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.ckan_config('ckan.plugins', 'dcor_depot')
|
|
44
|
+
def test_wait_for_resource_no_s3_url_with_dcor_depot_plugin(monkeypatch):
|
|
45
|
+
res_id = str(uuid.uuid4())
|
|
46
|
+
monkeypatch.setattr(logic, "get_action",
|
|
47
|
+
lambda x: lambda context, data_dict: {
|
|
48
|
+
"id": res_id})
|
|
49
|
+
# Create the actual path
|
|
50
|
+
rp = pathlib.Path(get_resource_path(res_id))
|
|
51
|
+
rp.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
rp.write_bytes(b"This is not a dummy file, but a fake DC file!")
|
|
53
|
+
rp2 = rp.with_name("test.rtdc")
|
|
54
|
+
rp.rename(rp2)
|
|
55
|
+
rp.symlink_to(rp2)
|
|
56
|
+
|
|
57
|
+
# Should not raise an exception
|
|
58
|
+
wait_for_resource(res_id)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_wait_resource_not_available(monkeypatch):
|
|
62
|
+
res_id = str(uuid.uuid4())
|
|
63
|
+
monkeypatch.setattr(logic, "get_action",
|
|
64
|
+
lambda x: lambda context, data_dict: {
|
|
65
|
+
"id": res_id})
|
|
66
|
+
# Should raise an exception
|
|
67
|
+
with pytest.raises(OSError, match="Data import seems to take too long"):
|
|
68
|
+
wait_for_resource(res_id, timeout=1)
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
import pathlib
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
from ckan.common import config
|
|
7
|
-
except ImportError:
|
|
8
|
-
config = {}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#: Content of the dummy file created when importing data.
|
|
12
|
-
DUMMY_BYTES = b"[Data import pending]"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def sha256sum(path):
|
|
16
|
-
"""Compute the SHA256 hash of a file in 1MB chunks"""
|
|
17
|
-
file_hash = hashlib.sha256()
|
|
18
|
-
with open(path, "rb") as fd:
|
|
19
|
-
while data := fd.read(2 ** 20):
|
|
20
|
-
file_hash.update(data)
|
|
21
|
-
return file_hash.hexdigest()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def wait_for_resource(path, timeout=10):
|
|
25
|
-
"""Wait for resource if it is uploaded manually
|
|
26
|
-
|
|
27
|
-
This function can be used by other plugins to ensure that
|
|
28
|
-
a resource is available for processing.
|
|
29
|
-
|
|
30
|
-
The ckanext-dcor_depot plugin imports data by uploading
|
|
31
|
-
dummy files and then sym-linking to data on disk. Here
|
|
32
|
-
we just check that the file is not a dummy file anymore.
|
|
33
|
-
"""
|
|
34
|
-
path = pathlib.Path(path)
|
|
35
|
-
dcor_depot_available = "dcor_depot" in config.get('ckan.plugins', "")
|
|
36
|
-
# Initially this was set to 10s, but if `/data` is mounted on a
|
|
37
|
-
# network share then this part here just takes too long.
|
|
38
|
-
t0 = time.time()
|
|
39
|
-
ld = len(DUMMY_BYTES)
|
|
40
|
-
while True:
|
|
41
|
-
if time.time() - t0 > timeout:
|
|
42
|
-
raise OSError("Data import seems to take too long "
|
|
43
|
-
"for '{}'!".format(path))
|
|
44
|
-
elif not path.exists():
|
|
45
|
-
time.sleep(0.05)
|
|
46
|
-
continue
|
|
47
|
-
elif dcor_depot_available and not path.is_symlink():
|
|
48
|
-
# Resource is only available when it is symlinked by
|
|
49
|
-
# the ckanext.dcor_depot `symlink_user_dataset` job
|
|
50
|
-
# (or by the ckanext.dcor_depot importers).
|
|
51
|
-
time.sleep(0.05)
|
|
52
|
-
continue
|
|
53
|
-
elif path.stat().st_size == ld and path.read_bytes() == DUMMY_BYTES:
|
|
54
|
-
# wait a bit
|
|
55
|
-
time.sleep(0.01)
|
|
56
|
-
continue
|
|
57
|
-
else:
|
|
58
|
-
# not a dummy file
|
|
59
|
-
break
|
|
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
|