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.
Files changed (26) hide show
  1. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/CHANGELOG +5 -0
  2. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/PKG-INFO +1 -1
  3. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/_version_save.py +1 -1
  4. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/ckan.py +2 -5
  5. dcor_shared-0.6.0/dcor_shared/data.py +93 -0
  6. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/paths.py +1 -4
  7. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/PKG-INFO +1 -1
  8. dcor_shared-0.6.0/tests/test_data.py +68 -0
  9. dcor_shared-0.5.6/dcor_shared/data.py +0 -59
  10. dcor_shared-0.5.6/tests/test_data.py +0 -9
  11. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/LICENSE +0 -0
  12. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/MANIFEST.in +0 -0
  13. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/README.rst +0 -0
  14. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/__init__.py +0 -0
  15. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/_version.py +0 -0
  16. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/mime.py +0 -0
  17. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/parse.py +0 -0
  18. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared/s3.py +0 -0
  19. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/SOURCES.txt +0 -0
  20. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/dependency_links.txt +0 -0
  21. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/requires.txt +0 -0
  22. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/dcor_shared.egg-info/top_level.txt +0 -0
  23. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/setup.cfg +0 -0
  24. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/setup.py +0 -0
  25. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/tests/test_basic.py +0 -0
  26. {dcor_shared-0.5.6 → dcor_shared-0.6.0}/tests/test_s3.py +0 -0
@@ -1,3 +1,8 @@
1
+ 0.6.0
2
+ - BREAKING CHANGE: wait_for_resource now requires the resource_id
3
+ instead of the resource path
4
+ - BREAKING CHANGE: ckan installation is now mandatory
5
+ - enh: cache calls to data.wait_for_resource
1
6
  0.5.6
2
7
  - maintenance release
3
8
  0.5.5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcor_shared
3
- Version: 0.5.6
3
+ Version: 0.6.0
4
4
  Summary: Functionalities shared by the DCOR CKAN extensions
5
5
  Home-page: https://github.com/DCOR-dev/dcor_shared
6
6
  Author: Paul Müller
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env python
2
2
  # This file was created automatically
3
- longversion = '0.5.6'
3
+ longversion = '0.6.0'
@@ -1,10 +1,7 @@
1
1
  import os
2
2
  import pathlib
3
3
 
4
- try:
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 and ckan_common.config:
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
- try:
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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcor-shared
3
- Version: 0.5.6
3
+ Version: 0.6.0
4
4
  Summary: Functionalities shared by the DCOR CKAN extensions
5
5
  Home-page: https://github.com/DCOR-dev/dcor_shared
6
6
  Author: Paul Müller
@@ -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
@@ -1,9 +0,0 @@
1
- from dcor_shared import sha256sum
2
-
3
-
4
- def test_sha256sum(tmp_path):
5
- p = tmp_path / "test.txt"
6
- p.write_text("Sum this up!")
7
- ist = sha256sum(p)
8
- soll = "d00df55b97a60c78bbb137540e1b60647a5e6b216262a95ab96cafd4519bcf6a"
9
- assert ist == soll
File without changes
File without changes
File without changes
File without changes
File without changes