folio-migration-tools 1.2.1__py3-none-any.whl → 1.9.10__py3-none-any.whl
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.
- folio_migration_tools/__init__.py +11 -0
- folio_migration_tools/__main__.py +169 -85
- folio_migration_tools/circulation_helper.py +96 -59
- folio_migration_tools/config_file_load.py +66 -0
- folio_migration_tools/custom_dict.py +6 -4
- folio_migration_tools/custom_exceptions.py +21 -19
- folio_migration_tools/extradata_writer.py +46 -0
- folio_migration_tools/folder_structure.py +63 -66
- folio_migration_tools/helper.py +29 -21
- folio_migration_tools/holdings_helper.py +57 -34
- folio_migration_tools/i18n_config.py +9 -0
- folio_migration_tools/library_configuration.py +173 -13
- folio_migration_tools/mapper_base.py +317 -106
- folio_migration_tools/mapping_file_transformation/courses_mapper.py +203 -0
- folio_migration_tools/mapping_file_transformation/holdings_mapper.py +83 -69
- folio_migration_tools/mapping_file_transformation/item_mapper.py +98 -94
- folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +352 -0
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +702 -223
- folio_migration_tools/mapping_file_transformation/notes_mapper.py +90 -0
- folio_migration_tools/mapping_file_transformation/order_mapper.py +492 -0
- folio_migration_tools/mapping_file_transformation/organization_mapper.py +389 -0
- folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +38 -27
- folio_migration_tools/mapping_file_transformation/user_mapper.py +149 -361
- folio_migration_tools/marc_rules_transformation/conditions.py +650 -246
- folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +292 -130
- folio_migration_tools/marc_rules_transformation/hrid_handler.py +244 -0
- folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +20846 -0
- folio_migration_tools/marc_rules_transformation/marc_file_processor.py +300 -0
- folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +136 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +241 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +681 -201
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +395 -429
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +531 -100
- folio_migration_tools/migration_report.py +85 -38
- folio_migration_tools/migration_tasks/__init__.py +1 -3
- folio_migration_tools/migration_tasks/authority_transformer.py +119 -0
- folio_migration_tools/migration_tasks/batch_poster.py +911 -198
- folio_migration_tools/migration_tasks/bibs_transformer.py +121 -116
- folio_migration_tools/migration_tasks/courses_migrator.py +192 -0
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py +252 -247
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py +321 -115
- folio_migration_tools/migration_tasks/items_transformer.py +264 -84
- folio_migration_tools/migration_tasks/loans_migrator.py +506 -195
- folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +187 -0
- folio_migration_tools/migration_tasks/migration_task_base.py +364 -74
- folio_migration_tools/migration_tasks/orders_transformer.py +373 -0
- folio_migration_tools/migration_tasks/organization_transformer.py +451 -0
- folio_migration_tools/migration_tasks/requests_migrator.py +130 -62
- folio_migration_tools/migration_tasks/reserves_migrator.py +253 -0
- folio_migration_tools/migration_tasks/user_transformer.py +180 -139
- folio_migration_tools/task_configuration.py +46 -0
- folio_migration_tools/test_infrastructure/__init__.py +0 -0
- folio_migration_tools/test_infrastructure/mocked_classes.py +406 -0
- folio_migration_tools/transaction_migration/legacy_loan.py +148 -34
- folio_migration_tools/transaction_migration/legacy_request.py +65 -25
- folio_migration_tools/transaction_migration/legacy_reserve.py +47 -0
- folio_migration_tools/transaction_migration/transaction_result.py +12 -1
- folio_migration_tools/translations/en.json +476 -0
- folio_migration_tools-1.9.10.dist-info/METADATA +169 -0
- folio_migration_tools-1.9.10.dist-info/RECORD +67 -0
- {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info}/WHEEL +1 -2
- folio_migration_tools-1.9.10.dist-info/entry_points.txt +3 -0
- folio_migration_tools/generate_schemas.py +0 -46
- folio_migration_tools/mapping_file_transformation/mapping_file_mapping_base_impl.py +0 -44
- folio_migration_tools/mapping_file_transformation/user_mapper_base.py +0 -212
- folio_migration_tools/marc_rules_transformation/bibs_processor.py +0 -163
- folio_migration_tools/marc_rules_transformation/holdings_processor.py +0 -284
- folio_migration_tools/report_blurbs.py +0 -219
- folio_migration_tools/transaction_migration/legacy_fee_fine.py +0 -36
- folio_migration_tools-1.2.1.dist-info/METADATA +0 -134
- folio_migration_tools-1.2.1.dist-info/RECORD +0 -50
- folio_migration_tools-1.2.1.dist-info/top_level.txt +0 -1
- {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def deep_merge(target_dict, source_dict, merge_keys=("name", "fileName", "file_name")):
|
|
6
|
+
# deep_merge
|
|
7
|
+
#
|
|
8
|
+
# Deeply merges nested dictionaries and lists of dictionaries.
|
|
9
|
+
# **Muatates the target_dict with the changes**, and returns it.
|
|
10
|
+
#
|
|
11
|
+
# For lists, attempts to match items based on the first found key in merge_keys in each item.
|
|
12
|
+
# If no key is found, or no match, the item is appended to the target list.
|
|
13
|
+
#
|
|
14
|
+
# Deletes any keys with value Null in the source.
|
|
15
|
+
|
|
16
|
+
for k in source_dict:
|
|
17
|
+
if isinstance(target_dict.get(k, None), dict) and isinstance(source_dict[k], dict):
|
|
18
|
+
# Recursive, depth-first merge on dictionaries
|
|
19
|
+
target_dict[k] = deep_merge(target_dict[k], source_dict[k], merge_keys)
|
|
20
|
+
elif isinstance(target_dict.get(k, None), list) and isinstance(source_dict[k], list):
|
|
21
|
+
# Merge lists on keys in merge_keys
|
|
22
|
+
for merging_list_item in source_dict[k]:
|
|
23
|
+
if not isinstance(merging_list_item, dict):
|
|
24
|
+
target_dict[k].append(merging_list_item)
|
|
25
|
+
continue
|
|
26
|
+
merge_key = next((i for i in merge_keys if i in merging_list_item), None)
|
|
27
|
+
if merge_key is None:
|
|
28
|
+
target_dict[k].append(merging_list_item)
|
|
29
|
+
continue
|
|
30
|
+
for index, target in enumerate(target_dict[k]):
|
|
31
|
+
if target[merge_key] == merging_list_item[merge_key]:
|
|
32
|
+
target_dict[k][index] = deep_merge(
|
|
33
|
+
target_dict[k][index],
|
|
34
|
+
merging_list_item,
|
|
35
|
+
merge_keys,
|
|
36
|
+
)
|
|
37
|
+
break
|
|
38
|
+
else:
|
|
39
|
+
target_dict[k].append(merging_list_item)
|
|
40
|
+
else:
|
|
41
|
+
if source_dict[k] is None:
|
|
42
|
+
target_dict.pop(k, None)
|
|
43
|
+
else:
|
|
44
|
+
target_dict[k] = source_dict[k]
|
|
45
|
+
return target_dict
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def merge_load(config_path_str, parsed_config=None):
|
|
49
|
+
# Recursively load JSON files from a configuration file
|
|
50
|
+
#
|
|
51
|
+
# If a configuration file has a "source" key, either a string or list,
|
|
52
|
+
# the file(s) will be loaded in the order presented, and the config file
|
|
53
|
+
# will be merged on top.
|
|
54
|
+
#
|
|
55
|
+
# To delete a key, set it to `null` in the json source file.
|
|
56
|
+
|
|
57
|
+
parsed_config = parsed_config or {}
|
|
58
|
+
config_path = Path(config_path_str)
|
|
59
|
+
with open(config_path) as config_file:
|
|
60
|
+
single_config = json.load(config_file)
|
|
61
|
+
sources = single_config.get("source", [])
|
|
62
|
+
if isinstance(sources, str):
|
|
63
|
+
sources = [sources]
|
|
64
|
+
for source in sources:
|
|
65
|
+
parsed_config = merge_load(config_path.parent / source, parsed_config)
|
|
66
|
+
return deep_merge(parsed_config, single_config)
|
|
@@ -2,17 +2,19 @@ import csv
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class InsensitiveDictReader(csv.DictReader):
|
|
5
|
-
# This class overrides the csv.fieldnames property, which converts all
|
|
5
|
+
# This class overrides the csv.fieldnames property, which converts all
|
|
6
|
+
# fieldnames without leading and trailing
|
|
6
7
|
# spaces and to lower case.
|
|
7
8
|
@property
|
|
8
9
|
def fieldnames(self):
|
|
9
|
-
return [field.strip().lower() for field in csv.DictReader.fieldnames.fget(self)]
|
|
10
|
+
return [field.strip().lower() for field in csv.DictReader.fieldnames.fget(self)] # type: ignore
|
|
10
11
|
|
|
11
12
|
def next(self):
|
|
12
|
-
return InsensitiveDict(csv.DictReader.next(self))
|
|
13
|
+
return InsensitiveDict(csv.DictReader.next(self)) # type: ignore
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class InsensitiveDict(dict):
|
|
16
|
-
# This class overrides the __getitem__ method to automatically strip()
|
|
17
|
+
# This class overrides the __getitem__ method to automatically strip()
|
|
18
|
+
# and lower() the input key
|
|
17
19
|
def __getitem__(self, key):
|
|
18
20
|
return dict.__getitem__(self, key.strip().lower())
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from typing import Union
|
|
3
|
+
import i18n
|
|
2
4
|
|
|
5
|
+
from folio_migration_tools import StrCoercible
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
|
|
8
|
+
class TransformationError(Exception):
|
|
5
9
|
pass
|
|
6
10
|
|
|
7
11
|
|
|
8
|
-
class TransformationFieldMappingError(
|
|
9
|
-
"""Raised when the
|
|
12
|
+
class TransformationFieldMappingError(TransformationError):
|
|
13
|
+
"""Raised when the field mapping fails, but the error is not critical.
|
|
10
14
|
The issue should be logged for the library to act upon it"""
|
|
11
15
|
|
|
12
|
-
def __init__(self, index_or_id="", message="", data_value=""):
|
|
16
|
+
def __init__(self, index_or_id="", message="", data_value: Union[str, StrCoercible]=""):
|
|
13
17
|
self.index_or_id = index_or_id or ""
|
|
14
18
|
self.message = message
|
|
15
|
-
self.data_value = data_value
|
|
19
|
+
self.data_value: Union[str, StrCoercible] = data_value
|
|
16
20
|
super().__init__(self.message)
|
|
17
21
|
|
|
18
22
|
def __str__(self):
|
|
19
23
|
return (
|
|
20
|
-
|
|
21
|
-
f"\t{self.index_or_id}\t{self.message}\t{self.data_value}"
|
|
24
|
+
i18n.t("Data issue. Consider fixing the record. ")
|
|
25
|
+
+ f"\t{self.index_or_id}\t{self.message}\t{self.data_value}"
|
|
22
26
|
)
|
|
23
27
|
|
|
24
28
|
def log_it(self):
|
|
@@ -31,13 +35,13 @@ class TransformationFieldMappingError(TransfomationError):
|
|
|
31
35
|
)
|
|
32
36
|
|
|
33
37
|
|
|
34
|
-
class TransformationRecordFailedError(
|
|
35
|
-
"""Raised when the
|
|
38
|
+
class TransformationRecordFailedError(TransformationError):
|
|
39
|
+
"""Raised when the field mapping fails, Error is critical and means transformation fails"""
|
|
36
40
|
|
|
37
41
|
def __init__(self, index_or_id, message="", data_value=""):
|
|
38
42
|
self.index_or_id = index_or_id
|
|
39
43
|
self.message = message
|
|
40
|
-
self.data_value = data_value
|
|
44
|
+
self.data_value: Union[str, StrCoercible] = data_value
|
|
41
45
|
# logging.log(26, f"RECORD FAILED\t{self.id}\t{self.message}\t{self.data_value}")
|
|
42
46
|
super().__init__(self.message)
|
|
43
47
|
|
|
@@ -48,9 +52,6 @@ class TransformationRecordFailedError(TransfomationError):
|
|
|
48
52
|
)
|
|
49
53
|
|
|
50
54
|
def log_it(self):
|
|
51
|
-
logging.error(
|
|
52
|
-
"Record failed: %s. See the data issues log for details", self.message
|
|
53
|
-
)
|
|
54
55
|
logging.log(
|
|
55
56
|
26,
|
|
56
57
|
"RECORD FAILED\t%s\t%s\t%s",
|
|
@@ -60,15 +61,16 @@ class TransformationRecordFailedError(TransfomationError):
|
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
class TransformationProcessError(
|
|
64
|
-
"""Raised when the
|
|
65
|
-
This error should take the process to a halt"""
|
|
64
|
+
class TransformationProcessError(TransformationError):
|
|
65
|
+
"""Raised when the transformation fails due to incorrect configuration,
|
|
66
|
+
mapping or reference data. This error should take the process to a halt."""
|
|
66
67
|
|
|
67
68
|
def __init__(
|
|
68
69
|
self,
|
|
69
70
|
index_or_id,
|
|
70
|
-
message="Critical Process issue. Transformation failed."
|
|
71
|
-
|
|
71
|
+
message="Critical Process issue. Transformation failed."
|
|
72
|
+
" Check configuration, mapping files and reference data",
|
|
73
|
+
data_value: Union[str, StrCoercible]="",
|
|
72
74
|
):
|
|
73
75
|
self.index_or_id = index_or_id
|
|
74
76
|
self.message = message
|
|
@@ -77,6 +79,6 @@ class TransformationProcessError(TransfomationError):
|
|
|
77
79
|
|
|
78
80
|
def __str__(self):
|
|
79
81
|
return (
|
|
80
|
-
f"Critical Process issue. Check mapping files and reference data
|
|
82
|
+
f"Critical Process issue. Check configuration, mapping files and reference data"
|
|
81
83
|
f"\t{self.index_or_id}\t{self.message}\t{self.data_value}"
|
|
82
84
|
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from folio_migration_tools.custom_exceptions import TransformationProcessError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExtradataWriter:
|
|
11
|
+
__instance = None
|
|
12
|
+
__inited = False
|
|
13
|
+
|
|
14
|
+
def __new__(cls, path_to_file: Path) -> "ExtradataWriter":
|
|
15
|
+
if cls.__instance is None:
|
|
16
|
+
cls.__instance = super().__new__(cls)
|
|
17
|
+
return cls.__instance
|
|
18
|
+
|
|
19
|
+
def __init__(self, path_to_file: Path) -> None:
|
|
20
|
+
if type(self).__inited:
|
|
21
|
+
return
|
|
22
|
+
self.cache: List[str] = []
|
|
23
|
+
self.path_to_file: Path = path_to_file
|
|
24
|
+
if self.path_to_file.is_file():
|
|
25
|
+
os.remove(self.path_to_file)
|
|
26
|
+
type(self).__inited = True
|
|
27
|
+
|
|
28
|
+
def write(self, record_type: str, data_to_write: dict, flush=False):
|
|
29
|
+
try:
|
|
30
|
+
if data_to_write:
|
|
31
|
+
self.cache.append(f"{record_type}\t{json.dumps(data_to_write)}\n")
|
|
32
|
+
if len(self.cache) > 1000 or flush:
|
|
33
|
+
with open(self.path_to_file, "a") as extradata_file:
|
|
34
|
+
extradata_file.writelines(self.cache)
|
|
35
|
+
self.cache = []
|
|
36
|
+
logging.debug("Extradata writer flushing the cache")
|
|
37
|
+
except Exception as ee:
|
|
38
|
+
error_message = "Something went wrong in extradata Writer"
|
|
39
|
+
logging.error(error_message)
|
|
40
|
+
raise TransformationProcessError("", error_message, record_type) from ee
|
|
41
|
+
|
|
42
|
+
def flush(self):
|
|
43
|
+
self.write("", {}, True)
|
|
44
|
+
if self.path_to_file.is_file() and os.stat(self.path_to_file).st_size == 0:
|
|
45
|
+
logging.info("Removing extradata file since it is empty")
|
|
46
|
+
os.remove(self.path_to_file)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
import time
|
|
4
|
+
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from folio_uuid.folio_namespaces import FOLIONamespaces
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ class FolderStructure:
|
|
|
15
15
|
iteration_identifier: str,
|
|
16
16
|
add_time_stamp_to_file_names: bool,
|
|
17
17
|
):
|
|
18
|
-
logging.info("
|
|
18
|
+
logging.info("Validating folder structure")
|
|
19
19
|
|
|
20
20
|
self.object_type: FOLIONamespaces = object_type
|
|
21
21
|
self.migration_task_name = migration_task_name
|
|
@@ -26,25 +26,22 @@ class FolderStructure:
|
|
|
26
26
|
logging.critical("Base Folder Path is not a folder. Exiting.")
|
|
27
27
|
sys.exit(1)
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
verify_folder(self.data_folder)
|
|
31
|
-
|
|
32
|
-
verify_folder(self.data_folder / str(FOLIONamespaces.instances.name).lower())
|
|
33
|
-
verify_folder(self.data_folder / str(FOLIONamespaces.holdings.name).lower())
|
|
34
|
-
verify_folder(self.data_folder / str(FOLIONamespaces.items.name).lower())
|
|
35
|
-
verify_folder(self.data_folder / str(FOLIONamespaces.users.name).lower())
|
|
36
|
-
self.archive_folder = self.base_folder / "archive"
|
|
37
|
-
verify_folder(self.data_folder)
|
|
38
|
-
|
|
39
|
-
self.results_folder = self.base_folder / "results"
|
|
40
|
-
verify_folder(self.results_folder)
|
|
41
|
-
self.reports_folder = self.base_folder / "reports"
|
|
42
|
-
verify_folder(self.reports_folder)
|
|
43
|
-
|
|
29
|
+
# Basic folders
|
|
44
30
|
self.mapping_files_folder = self.base_folder / "mapping_files"
|
|
45
|
-
verify_folder(self.mapping_files_folder)
|
|
31
|
+
self.verify_folder(self.mapping_files_folder)
|
|
46
32
|
gitignore = self.base_folder / ".gitignore"
|
|
47
33
|
verify_git_ignore(gitignore)
|
|
34
|
+
self.verify_folder(self.base_folder / "iterations")
|
|
35
|
+
|
|
36
|
+
# Iteration-specific folders
|
|
37
|
+
self.iteration_folder = self.base_folder / "iterations" / self.iteration_identifier
|
|
38
|
+
self.verify_folder(self.iteration_folder)
|
|
39
|
+
self.data_folder = self.iteration_folder / "source_data"
|
|
40
|
+
self.verify_folder(self.data_folder)
|
|
41
|
+
self.results_folder = self.iteration_folder / "results"
|
|
42
|
+
self.verify_folder(self.results_folder)
|
|
43
|
+
self.reports_folder = self.iteration_folder / "reports"
|
|
44
|
+
self.verify_folder(self.reports_folder)
|
|
48
45
|
|
|
49
46
|
def log_folder_structure(self):
|
|
50
47
|
logging.info("Mapping files folder is %s", self.mapping_files_folder)
|
|
@@ -55,104 +52,104 @@ class FolderStructure:
|
|
|
55
52
|
logging.info("Data folder is %s", self.data_folder)
|
|
56
53
|
logging.info("Source records files folder is %s", self.legacy_records_folder)
|
|
57
54
|
logging.info("Log file will be located at %s", self.transformation_log_path)
|
|
58
|
-
logging.info(
|
|
59
|
-
"Extra data will be stored at%s", self.transformation_extra_data_path
|
|
60
|
-
)
|
|
55
|
+
logging.info("Extra data will be stored at%s", self.transformation_extra_data_path)
|
|
61
56
|
logging.info("Data issue reports %s", self.data_issue_file_path)
|
|
62
57
|
logging.info("Created objects will be stored at %s", self.created_objects_path)
|
|
63
|
-
logging.info(
|
|
64
|
-
"Migration report file will be saved at %s", self.migration_reports_file
|
|
65
|
-
)
|
|
58
|
+
logging.info("Migration report file will be saved at %s", self.migration_reports_file)
|
|
66
59
|
|
|
67
60
|
def setup_migration_file_structure(self, source_file_type: str = ""):
|
|
68
|
-
time_stamp = f'_{time.strftime("%Y%m%d-%H%M%S")}'
|
|
69
|
-
time_str = time_stamp if self.add_time_stamp_to_file_names else ""
|
|
70
|
-
file_template =
|
|
71
|
-
f"{self.iteration_identifier}{time_str}_{self.migration_task_name}"
|
|
72
|
-
)
|
|
61
|
+
self.time_stamp = f'_{time.strftime("%Y%m%d-%H%M%S")}'
|
|
62
|
+
self.time_str = self.time_stamp if self.add_time_stamp_to_file_names else ""
|
|
63
|
+
self.file_template = f"{self.time_str}_{self.migration_task_name}"
|
|
73
64
|
object_type_string = str(self.object_type.name).lower()
|
|
74
65
|
if source_file_type:
|
|
75
66
|
self.legacy_records_folder = self.data_folder / source_file_type
|
|
76
67
|
elif self.object_type == FOLIONamespaces.other:
|
|
77
68
|
self.legacy_records_folder = self.data_folder
|
|
78
|
-
|
|
79
69
|
else:
|
|
80
70
|
self.legacy_records_folder = self.data_folder / object_type_string
|
|
81
|
-
verify_folder(self.legacy_records_folder)
|
|
71
|
+
self.verify_folder(self.legacy_records_folder)
|
|
72
|
+
|
|
73
|
+
# Make sure the items are there if the Holdings processor is run
|
|
74
|
+
if self.object_type == FOLIONamespaces.holdings:
|
|
75
|
+
self.verify_folder(self.data_folder / str(FOLIONamespaces.items.name).lower())
|
|
82
76
|
|
|
83
77
|
self.transformation_log_path = self.reports_folder / (
|
|
84
|
-
f"log_{object_type_string}
|
|
78
|
+
f"log_{object_type_string}{self.file_template}.log"
|
|
85
79
|
)
|
|
86
80
|
|
|
87
81
|
self.failed_recs_path = (
|
|
88
|
-
self.results_folder / f"
|
|
82
|
+
self.results_folder / f"failed_records{self.file_template}{self.time_stamp}.txt"
|
|
89
83
|
)
|
|
90
84
|
|
|
91
85
|
self.transformation_extra_data_path = (
|
|
92
|
-
self.results_folder / f"
|
|
86
|
+
self.results_folder / f"extradata{self.file_template}.extradata"
|
|
93
87
|
)
|
|
94
88
|
|
|
95
89
|
self.data_issue_file_path = (
|
|
96
|
-
self.reports_folder
|
|
97
|
-
/ f"data_issues_log_{object_type_string}_{file_template}.tsv"
|
|
90
|
+
self.reports_folder / f"data_issues_log{self.file_template}.tsv"
|
|
98
91
|
)
|
|
99
92
|
self.created_objects_path = (
|
|
100
|
-
self.results_folder / f"folio_{object_type_string}
|
|
93
|
+
self.results_folder / f"folio_{object_type_string}{self.file_template}.json"
|
|
101
94
|
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
self.results_folder
|
|
105
|
-
/ f"failed_bib_records_{self.iteration_identifier}{time_str}.mrc"
|
|
106
|
-
)
|
|
107
|
-
self.failed_mfhds_file = (
|
|
108
|
-
self.results_folder
|
|
109
|
-
/ f"failed_mfhd_records_{self.iteration_identifier}{time_str}.mrc"
|
|
95
|
+
self.failed_marc_recs_file = (
|
|
96
|
+
self.results_folder / f"failed_records{self.file_template}.mrc"
|
|
110
97
|
)
|
|
111
98
|
|
|
112
|
-
self.migration_reports_file =
|
|
113
|
-
self.reports_folder
|
|
114
|
-
/ f"transformation_report_{object_type_string}_{file_template}.md"
|
|
115
|
-
)
|
|
99
|
+
self.migration_reports_file = self.reports_folder / f"report{self.file_template}.md"
|
|
116
100
|
|
|
117
101
|
self.srs_records_path = (
|
|
118
|
-
self.results_folder / f"folio_srs_{object_type_string}
|
|
102
|
+
self.results_folder / f"folio_srs_{object_type_string}{self.file_template}.json"
|
|
103
|
+
)
|
|
104
|
+
self.data_import_marc_path = (
|
|
105
|
+
self.results_folder / f"folio_marc_{object_type_string}{self.file_template}.mrc"
|
|
106
|
+
)
|
|
107
|
+
self.organizations_id_map_path = (
|
|
108
|
+
self.results_folder / f"{str(FOLIONamespaces.organizations.name).lower()}_id_map.json"
|
|
119
109
|
)
|
|
120
|
-
|
|
121
110
|
self.instance_id_map_path = (
|
|
122
|
-
self.results_folder / f"
|
|
111
|
+
self.results_folder / f"{str(FOLIONamespaces.instances.name).lower()}_id_map.json"
|
|
112
|
+
)
|
|
113
|
+
self.auth_id_map_path = (
|
|
114
|
+
self.results_folder / f"{str(FOLIONamespaces.authorities.name).lower()}_id_map.json"
|
|
123
115
|
)
|
|
124
116
|
|
|
125
117
|
self.holdings_id_map_path = (
|
|
126
|
-
self.results_folder / f"
|
|
118
|
+
self.results_folder / f"{str(FOLIONamespaces.holdings.name).lower()}_id_map.json"
|
|
119
|
+
)
|
|
120
|
+
self.id_map_path = (
|
|
121
|
+
self.results_folder / f"{str(self.object_type.name).lower()}_id_map.json"
|
|
122
|
+
)
|
|
123
|
+
self.boundwith_relationships_map_path = (
|
|
124
|
+
self.results_folder / "boundwith_relationships_map.json"
|
|
127
125
|
)
|
|
128
|
-
|
|
129
126
|
# Mapping files
|
|
130
|
-
self.temp_locations_map_path = self.mapping_files_folder / "temp_locations.tsv"
|
|
131
127
|
self.material_type_map_path = self.mapping_files_folder / "material_types.tsv"
|
|
132
128
|
self.loan_type_map_path = self.mapping_files_folder / "loan_types.tsv"
|
|
133
129
|
self.temp_loan_type_map_path = self.mapping_files_folder / "temp_loan_types.tsv"
|
|
134
130
|
self.statistical_codes_map_path = self.mapping_files_folder / "statcodes.tsv"
|
|
135
131
|
self.item_statuses_map_path = self.mapping_files_folder / "item_statuses.tsv"
|
|
136
132
|
|
|
133
|
+
def verify_folder(self, folder_path: Path):
|
|
134
|
+
if not folder_path.is_dir():
|
|
135
|
+
logging.critical("There is no folder located at %s. Exiting.", folder_path)
|
|
136
|
+
logging.critical("Create a folder by calling\n\tmkdir %s", folder_path)
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
else:
|
|
139
|
+
logging.info("Located %s", folder_path)
|
|
140
|
+
|
|
137
141
|
|
|
138
142
|
def verify_git_ignore(gitignore: Path):
|
|
139
143
|
with open(gitignore, "r+") as f:
|
|
140
144
|
contents = f.read()
|
|
145
|
+
if "reports/" not in contents:
|
|
146
|
+
f.write("reports/\n")
|
|
141
147
|
if "results/" not in contents:
|
|
142
148
|
f.write("results/\n")
|
|
143
149
|
if "archive/" not in contents:
|
|
144
150
|
f.write("archive/\n")
|
|
145
|
-
if "
|
|
146
|
-
f.write("
|
|
151
|
+
if "source_data/" not in contents:
|
|
152
|
+
f.write("source_data/\n")
|
|
147
153
|
if "*.data" not in contents:
|
|
148
154
|
f.write("*.data\n")
|
|
149
155
|
logging.info("Made sure there was a valid .gitignore file at %s", gitignore)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def verify_folder(folder_path: Path):
|
|
153
|
-
if not folder_path.is_dir():
|
|
154
|
-
logging.critical("There is no folder located at %s. Exiting.", folder_path)
|
|
155
|
-
logging.critical("Create a folder by calling\n\tmkdir %s", folder_path)
|
|
156
|
-
sys.exit(1)
|
|
157
|
-
else:
|
|
158
|
-
logging.info("Located %s", folder_path)
|
folio_migration_tools/helper.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import i18n
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class Helper:
|
|
@@ -8,57 +9,64 @@ class Helper:
|
|
|
8
9
|
report_file, total_records: int, mapped_folio_fields, mapped_legacy_fields
|
|
9
10
|
):
|
|
10
11
|
details_start = (
|
|
11
|
-
"<details><summary>Click to expand field report</summary
|
|
12
|
+
"<details><summary>" + i18n.t("Click to expand field report") + "</summary>\n\n"
|
|
12
13
|
)
|
|
13
|
-
details_end = "</details
|
|
14
|
-
report_file.write("\n## Mapped FOLIO fields\n")
|
|
14
|
+
details_end = "</details>\n"
|
|
15
|
+
report_file.write("\n## " + i18n.t("Mapped FOLIO fields") + "\n")
|
|
15
16
|
# report_file.write(f"{blurbs[header]}\n")
|
|
16
17
|
|
|
17
18
|
d_sorted = {k: mapped_folio_fields[k] for k in sorted(mapped_folio_fields)}
|
|
18
19
|
report_file.write(details_start)
|
|
19
|
-
|
|
20
|
-
report_file.write("
|
|
21
|
-
report_file.write("
|
|
20
|
+
columns = [i18n.t("FOLIO Field"), i18n.t("Mapped"), i18n.t("Unmapped")]
|
|
21
|
+
report_file.write(" | ".join(columns) + "\n")
|
|
22
|
+
report_file.write("|".join(len(columns) * ["---"]) + "\n")
|
|
22
23
|
for k, v in d_sorted.items():
|
|
23
24
|
unmapped = max(total_records - v[0], 0)
|
|
24
25
|
mapped = v[0]
|
|
25
26
|
mp = mapped / total_records if total_records else 0
|
|
26
27
|
mapped_per = "{:.0%}".format(max(mp, 0))
|
|
28
|
+
up = unmapped / total_records if total_records else 0
|
|
29
|
+
unmapped_per = "{:.0%}".format(max(up, 0))
|
|
27
30
|
report_file.write(
|
|
28
|
-
f"{k} | {max(mapped, 0):,} ({mapped_per}) | {unmapped:,}
|
|
31
|
+
f"{k} | {max(mapped, 0):,} ({mapped_per}) | {unmapped:,} ({unmapped_per}) \n"
|
|
29
32
|
)
|
|
30
33
|
report_file.write(details_end)
|
|
31
34
|
|
|
32
|
-
report_file.write("\n## Mapped Legacy fields\n")
|
|
35
|
+
report_file.write("\n## " + i18n.t("Mapped Legacy fields") + "\n")
|
|
33
36
|
# report_file.write(f"{blurbs[header]}\n")
|
|
34
37
|
|
|
35
38
|
d_sorted = {k: mapped_legacy_fields[k] for k in sorted(mapped_legacy_fields)}
|
|
36
39
|
report_file.write(details_start)
|
|
37
|
-
|
|
38
|
-
report_file.write("
|
|
40
|
+
columns = [i18n.t("Legacy Field"), i18n.t("Present"), i18n.t("Mapped"), i18n.t("Unmapped")]
|
|
41
|
+
report_file.write("|".join(columns) + "\n")
|
|
42
|
+
report_file.write("|".join(len(columns) * ["---"]) + "\n")
|
|
39
43
|
for k, v in d_sorted.items():
|
|
40
44
|
present = v[0]
|
|
41
|
-
present_per = "{:.1%}".format(
|
|
42
|
-
present / total_records if total_records else 0
|
|
43
|
-
)
|
|
45
|
+
present_per = "{:.1%}".format(present / total_records if total_records else 0)
|
|
44
46
|
unmapped = present - v[1]
|
|
45
47
|
mapped = v[1]
|
|
46
48
|
mp = mapped / total_records if total_records else 0
|
|
47
49
|
mapped_per = "{:.0%}".format(max(mp, 0))
|
|
48
50
|
report_file.write(
|
|
49
|
-
f"{k} | {max(present, 0):,} ({present_per}) | {max(mapped, 0):,}
|
|
51
|
+
f"{k} | {max(present, 0):,} ({present_per}) | {max(mapped, 0):,} "
|
|
52
|
+
f"({mapped_per}) | {unmapped:,} \n"
|
|
50
53
|
)
|
|
51
54
|
report_file.write(details_end)
|
|
52
55
|
|
|
53
56
|
@staticmethod
|
|
54
57
|
def log_data_issue(index_or_id, message, legacy_value):
|
|
55
58
|
logging.log(26, "DATA ISSUE\t%s\t%s\t%s", index_or_id, message, legacy_value)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def log_data_issue_failed(index_or_id, message, legacy_value):
|
|
62
|
+
logging.log(26, "RECORD FAILED\t%s\t%s\t%s", index_or_id, message, legacy_value)
|
|
56
63
|
|
|
57
64
|
@staticmethod
|
|
58
|
-
def write_to_file(file, folio_record
|
|
59
|
-
"""Writes record to file.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
file
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
def write_to_file(file, folio_record):
|
|
66
|
+
"""Writes record to file.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
file (_type_): _description_
|
|
70
|
+
folio_record (_type_): _description_
|
|
71
|
+
"""
|
|
72
|
+
file.write(f"{json.dumps(folio_record)}\n")
|