rara-tools 0.0.12__tar.gz → 0.1.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.

Potentially problematic release.


This version of rara-tools might be problematic. Click here for more details.

Files changed (32) hide show
  1. {rara_tools-0.0.12/rara_tools.egg-info → rara_tools-0.1.0}/PKG-INFO +3 -2
  2. rara_tools-0.1.0/VERSION +1 -0
  3. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/constants/digitizer.py +12 -0
  4. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/constants/general.py +1 -0
  5. rara_tools-0.1.0/rara_tools/converters.py +81 -0
  6. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/exceptions.py +4 -0
  7. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/s3.py +53 -4
  8. {rara_tools-0.0.12 → rara_tools-0.1.0/rara_tools.egg-info}/PKG-INFO +3 -2
  9. {rara_tools-0.0.12 → rara_tools-0.1.0}/tests/test_converters.py +33 -11
  10. {rara_tools-0.0.12 → rara_tools-0.1.0}/tests/test_elastic_vector_and_search_operations.py +1 -1
  11. rara_tools-0.0.12/VERSION +0 -1
  12. rara_tools-0.0.12/rara_tools/converters.py +0 -41
  13. {rara_tools-0.0.12 → rara_tools-0.1.0}/LICENSE.md +0 -0
  14. {rara_tools-0.0.12 → rara_tools-0.1.0}/README.md +0 -0
  15. {rara_tools-0.0.12 → rara_tools-0.1.0}/pyproject.toml +0 -0
  16. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/constants/__init__.py +0 -0
  17. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/decorators.py +0 -0
  18. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/digar_schema_converter.py +0 -0
  19. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/elastic.py +0 -0
  20. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/task_reporter.py +0 -0
  21. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools/utils.py +0 -0
  22. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools.egg-info/SOURCES.txt +0 -0
  23. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools.egg-info/dependency_links.txt +0 -0
  24. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools.egg-info/requires.txt +0 -0
  25. {rara_tools-0.0.12 → rara_tools-0.1.0}/rara_tools.egg-info/top_level.txt +0 -0
  26. {rara_tools-0.0.12 → rara_tools-0.1.0}/requirements.txt +0 -0
  27. {rara_tools-0.0.12 → rara_tools-0.1.0}/setup.cfg +0 -0
  28. {rara_tools-0.0.12 → rara_tools-0.1.0}/tests/test_digar_schema_converter.py +0 -0
  29. {rara_tools-0.0.12 → rara_tools-0.1.0}/tests/test_elastic.py +0 -0
  30. {rara_tools-0.0.12 → rara_tools-0.1.0}/tests/test_s3_exceptions.py +0 -0
  31. {rara_tools-0.0.12 → rara_tools-0.1.0}/tests/test_s3_file_operations.py +0 -0
  32. {rara_tools-0.0.12 → rara_tools-0.1.0}/tests/test_task_reporter.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: rara-tools
3
- Version: 0.0.12
3
+ Version: 0.1.0
4
4
  Summary: Tools to support Kata's work.
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.10
@@ -18,6 +18,7 @@ Requires-Dist: iso639-lang
18
18
  Provides-Extra: testing
19
19
  Requires-Dist: pytest>=8.0; extra == "testing"
20
20
  Requires-Dist: pytest-order; extra == "testing"
21
+ Dynamic: license-file
21
22
 
22
23
  # RaRa Tools
23
24
 
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -1,4 +1,12 @@
1
+ COMPONENT_KEY = "digitizer"
2
+
3
+
4
+ class ModelTypes:
5
+ IMAGE_PROCESSOR = "image_processor"
6
+
7
+
1
8
  class StatusKeys:
9
+ DOWNLOAD_MODELS = "digitizer_download_models"
2
10
  CLEAN_UP = "digitizer_clean_up"
3
11
  ELASTICSEARCH_UPLOAD = "digitizer_elasticsearch_upload"
4
12
  UPLOAD = "s3_upload"
@@ -11,3 +19,7 @@ class Queue:
11
19
  DOWNLOAD = "download"
12
20
  FINISH = "finish"
13
21
  OCR = "ocr"
22
+
23
+
24
+ class Tasks:
25
+ MODEL_UPDATE = "component_model_update"
@@ -1,4 +1,5 @@
1
1
  class Status:
2
+ SKIPPED = "SKIPPED"
2
3
  FAILED = "FAILED"
3
4
  PENDING = "PENDING"
4
5
  RUNNING = "RUNNING"
@@ -0,0 +1,81 @@
1
+ from .exceptions import SierraResponseConverterException
2
+
3
+
4
+ class SierraResponseConverter:
5
+ """Converts a JSON response from the Sierra API to MARC-in-JSON format."""
6
+
7
+ def __init__(self, response: dict):
8
+ if not isinstance(response, dict):
9
+ raise SierraResponseConverterException("Please provide a valid JSON response.")
10
+ self.response = response
11
+
12
+ def _map_control_fields(self, field: dict) -> dict:
13
+ # for tags < 010, no subfields, instead one str value in "value"
14
+ return {field["tag"]: field["value"]}
15
+
16
+ def _map_data_fields(self, field: dict) -> dict:
17
+ """ Maps marc fields > 010.
18
+
19
+ Args:
20
+ field (dict): Contains the marc tag and list with indicators and subfields.
21
+
22
+ Returns:
23
+ dict: standardised marc-in-json format.
24
+ """
25
+
26
+ data = field["data"]
27
+
28
+ # Order matters ind1, in2, subfields
29
+ field_data = {
30
+ "ind1": data.get("ind1", " "),
31
+ "ind2": data.get("ind2", " "),
32
+ "subfields": data.get("subfields", [])
33
+ }
34
+
35
+ return {field["tag"]: field_data}
36
+
37
+ def _is_marc21structured(self, field: dict) -> bool:
38
+ """Checks if the field is already structured according to MARC21 in JSON"""
39
+ return any(key.isdigit() for key in field.keys())
40
+
41
+
42
+ def _handle_field_type(self, field: dict) -> dict:
43
+
44
+ if self._is_marc21structured(field):
45
+ return field
46
+
47
+ if field.get("data"):
48
+ return self._map_data_fields(field)
49
+
50
+ tag = field.get("tag")
51
+
52
+ if not tag:
53
+ raise SierraResponseConverterException("Field is missing MARC21 tag.")
54
+
55
+ if tag < "010":
56
+ return self._map_control_fields(field)
57
+ else:
58
+ return self._map_data_fields(field)
59
+
60
+ def _convert_response(self) -> list:
61
+ entries = self.response.get("entries")
62
+ if not entries:
63
+ raise SierraResponseConverterException("No entries found in the response.")
64
+
65
+ try:
66
+ return {"fields": [
67
+ {e["id"]: [
68
+ self._handle_field_type(f) for f in e["marc"]["fields"]
69
+ ]}
70
+ for e in entries
71
+ ]}
72
+
73
+ except KeyError as e:
74
+ raise SierraResponseConverterException(f"Malformed response: missing key {e}")
75
+
76
+
77
+ def convert(self) -> list:
78
+ try:
79
+ return self._convert_response()
80
+ except Exception as e:
81
+ raise SierraResponseConverterException(f"An unexpected error occurred: {e}")
@@ -7,6 +7,10 @@ class S3InitException(Exception):
7
7
  class S3ConnectionException(Exception):
8
8
  """Raised S3 Bucket/Connection Error."""
9
9
 
10
+ class S3DownloadException(Exception):
11
+ """Raised S3 Download Error."""
12
+
13
+
10
14
  class ElasticsearchException(Exception):
11
15
  """Raised Elasticsearch Error."""
12
16
 
@@ -1,11 +1,20 @@
1
+ import logging
1
2
  import os
3
+ import pathlib
4
+ import time
2
5
  import uuid
3
6
  from typing import Any, Generator, List, Optional
4
7
 
5
- from minio import Minio
8
+ from minio import Minio, S3Error
6
9
 
7
- from .exceptions import (S3ConnectionException, S3InitException,
8
- S3InputException)
10
+ from .exceptions import (
11
+ S3ConnectionException,
12
+ S3InitException,
13
+ S3InputException,
14
+ S3DownloadException
15
+ )
16
+
17
+ logger = logging.getLogger("tools.s3")
9
18
 
10
19
 
11
20
  class S3Files:
@@ -76,9 +85,49 @@ class S3Files:
76
85
  list_of_objects = list(self.minio_client.list_objects(self.bucket, prefix=path, recursive=True))
77
86
  for minio_object in list_of_objects:
78
87
  full_path = os.path.join(download_dir, minio_object.object_name)
79
- self.minio_client.fget_object(self.bucket, minio_object.object_name, full_path)
88
+ self._download_file(minio_object.object_name, full_path)
80
89
  yield full_path
81
90
 
91
+ def _download_file(self, path, download_dir=".", max_retries=3) -> str:
92
+ """Download a single file with retry and resume support."""
93
+ attempts = 0
94
+
95
+ while attempts < max_retries:
96
+ try:
97
+ stat = self.minio_client.stat_object(self.bucket, path)
98
+ file_size = stat.size
99
+ temp_path = download_dir + ".part"
100
+ pathlib.Path(temp_path).parent.mkdir(parents=True, exist_ok=True)
101
+
102
+ # Check if a partial file exists
103
+ downloaded_size = os.path.getsize(temp_path) if os.path.exists(temp_path) else 0
104
+
105
+ if downloaded_size >= file_size:
106
+ os.rename(temp_path, download_dir) # Rename to final filename
107
+ logger.info(f"Completed: {path}")
108
+ return str(pathlib.Path(download_dir) / path)
109
+
110
+ logger.info(f"Downloading {path} ({downloaded_size}/{file_size} bytes)...")
111
+
112
+ # Open file in append mode to resume download
113
+ with open(temp_path, "ab") as f:
114
+ response = self.minio_client.get_object(self.bucket, path, offset=downloaded_size)
115
+ for data in response.stream(32 * 1024): # 32KB chunks
116
+ f.write(data)
117
+ response.close()
118
+ response.release_conn()
119
+
120
+ os.rename(temp_path, download_dir) # Rename temp to final
121
+ logger.info(f"Downloaded: {path}")
122
+ return str(pathlib.Path(download_dir) / path)
123
+
124
+ except S3Error as e:
125
+ logger.info(f"Error downloading {path}, attempt {attempts + 1}: {e}")
126
+ attempts += 1
127
+ time.sleep(2 ** attempts) # Exponential backoff
128
+
129
+ raise S3DownloadException(f"Failed to download {path} after {max_retries} attempts.")
130
+
82
131
  def upload(self, path: str, prefix: Optional[str] = "") -> str:
83
132
  """Uploads file or folder to S3 bucket.
84
133
  :param: path str: Path to the file to upload in local file system.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: rara-tools
3
- Version: 0.0.12
3
+ Version: 0.1.0
4
4
  Summary: Tools to support Kata's work.
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.10
@@ -18,6 +18,7 @@ Requires-Dist: iso639-lang
18
18
  Provides-Extra: testing
19
19
  Requires-Dist: pytest>=8.0; extra == "testing"
20
20
  Requires-Dist: pytest-order; extra == "testing"
21
+ Dynamic: license-file
21
22
 
22
23
  # RaRa Tools
23
24
 
@@ -5,12 +5,22 @@ import pytest
5
5
  from rara_tools.converters import SierraResponseConverter
6
6
  from rara_tools.exceptions import SierraResponseConverterException
7
7
 
8
+ import json
9
+
8
10
  root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
11
 
10
12
  SIERRA_TEST_DATA_DIR = os.path.join(root, "tests", "test_data", "sierra")
11
13
  INPUT_DIR = os.path.join(SIERRA_TEST_DATA_DIR, "input")
12
14
  OUTPUT_DIR = os.path.join(SIERRA_TEST_DATA_DIR, "output")
13
15
 
16
+ def compare_results(expected, converted):
17
+ return json.dumps(expected) == json.dumps(converted)
18
+
19
+ def read_json_file(file_path):
20
+ with open(file_path, "r") as f:
21
+ data = f.read()
22
+ return json.loads(data)
23
+
14
24
  example_res = {
15
25
  "total": 100,
16
26
  "start": 50000,
@@ -27,6 +37,8 @@ example_res = {
27
37
  {
28
38
  # "tag": "100",
29
39
  "data": {
40
+ "ind1": "1",
41
+ "ind2": " ",
30
42
  "subfields": [
31
43
  {
32
44
  "code": "a",
@@ -36,18 +48,13 @@ example_res = {
36
48
  "code": "d",
37
49
  "data": "1975-"
38
50
  }
39
- ],
40
- "ind1": "1",
41
- "ind2": " "
51
+ ]
42
52
  }
43
53
  },
44
54
  ]}}]}
45
55
 
46
56
 
47
- def read_json_file(file_path):
48
- with open(file_path, "r") as f:
49
- data = f.read()
50
- return json.loads(data)
57
+
51
58
 
52
59
  def test_convert_bibs_response():
53
60
  response = read_json_file(os.path.join(INPUT_DIR, "bibs.json"))
@@ -55,8 +62,9 @@ def test_convert_bibs_response():
55
62
  converter = SierraResponseConverter(response)
56
63
  data = converter.convert()
57
64
 
58
- expected = read_json_file(os.path.join(OUTPUT_DIR, "bibs.json"))
59
- assert data == expected
65
+ expected = read_json_file(os.path.join(OUTPUT_DIR, "bibs.json"))
66
+
67
+ assert compare_results(expected, data)
60
68
 
61
69
 
62
70
  def test_convert_keywords_response():
@@ -67,9 +75,10 @@ def test_convert_keywords_response():
67
75
  converter = SierraResponseConverter(response)
68
76
  data = converter.convert()
69
77
 
78
+
70
79
  expected = read_json_file(os.path.join(OUTPUT_DIR, "keywords.json"))
71
80
 
72
- assert data == expected
81
+ assert compare_results(expected, data)
73
82
 
74
83
 
75
84
  def test_convert_authorities_response():
@@ -82,7 +91,20 @@ def test_convert_authorities_response():
82
91
 
83
92
  expected = read_json_file(os.path.join(OUTPUT_DIR, "authorities.json"))
84
93
 
85
- assert data == expected
94
+ assert compare_results(expected, data)
95
+
96
+ def test_converter_handles_marc_in_json_response():
97
+ """ Gracefully handle entries already in MARC-in-JSON format """
98
+ with open(os.path.join(INPUT_DIR, "bibsmarc.json"), "r") as f:
99
+ response = f.read()
100
+ response = json.loads(response)
101
+
102
+ converter = SierraResponseConverter(response)
103
+ data = converter.convert()
104
+
105
+ expected = read_json_file(os.path.join(OUTPUT_DIR, "bibsmarc.json"))
106
+
107
+ assert compare_results(expected, data)
86
108
 
87
109
  def test_convert_with_wrong_format():
88
110
  with pytest.raises(SierraResponseConverterException):
@@ -15,7 +15,7 @@ TEST_DOCUMENTS = load_json("./tests/test_data/elastic_vectorized_docs.json")
15
15
  TEST_VECTOR_DATA = load_json("./tests/test_data/test_vector_data.json")
16
16
  TEST_VECTOR = TEST_VECTOR_DATA.get("vector")
17
17
 
18
- es_url = os.getenv("ELASTIC_TEST_URL", "http://rara-elastic.texta.ee:9200")#http://localhost:9200")
18
+ es_url = os.getenv("ELASTIC_TEST_URL", "http://localhost:9200")
19
19
  ELASTIC = KataElastic(es_url)
20
20
 
21
21
  TEST_KNN_INDEX_NAME = "tools_knn_testing_index"
rara_tools-0.0.12/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.0.12
@@ -1,41 +0,0 @@
1
- from .exceptions import SierraResponseConverterException
2
-
3
-
4
- class SierraResponseConverter:
5
- """ Takes a JSON response from the Sierra API (https://tester.ester.ee/iii/sierra-api/swagger/index.html)
6
- and converts it to MARC-in-JSON format.
7
-
8
- """
9
-
10
- def __init__(self, response: dict):
11
- if not isinstance(response, dict):
12
- raise SierraResponseConverterException("Please provide a valid JSON response.")
13
- self.response = response
14
-
15
- def _map_field_data(self, field):
16
- tag = field.get("tag")
17
- if not tag:
18
- raise SierraResponseConverterException("Field is missing a valid 'tag'.")
19
- data = field.get("data", {})
20
- return {tag: data}
21
-
22
- def _convert_response(self):
23
- response = self.response
24
-
25
- entries = response.get("entries")
26
- if not entries:
27
- raise SierraResponseConverterException("No entries found in the response.")
28
-
29
- try:
30
- fields = [self._map_field_data(f) for e in entries for f in e["marc"]["fields"]]
31
- except KeyError as e:
32
- raise SierraResponseConverterException(f"Missing expected MARC fields in the response: {e}")
33
-
34
- return {"fields": fields}
35
-
36
- def convert(self):
37
- """Runner method, converts the response to MARC-in-JSON format with error handling."""
38
- try:
39
- return self._convert_response()
40
- except Exception as e:
41
- raise SierraResponseConverterException(f"An unexpected error occurred during conversion: {e}")
File without changes
File without changes
File without changes
File without changes
File without changes