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.
Files changed (73) hide show
  1. folio_migration_tools/__init__.py +11 -0
  2. folio_migration_tools/__main__.py +169 -85
  3. folio_migration_tools/circulation_helper.py +96 -59
  4. folio_migration_tools/config_file_load.py +66 -0
  5. folio_migration_tools/custom_dict.py +6 -4
  6. folio_migration_tools/custom_exceptions.py +21 -19
  7. folio_migration_tools/extradata_writer.py +46 -0
  8. folio_migration_tools/folder_structure.py +63 -66
  9. folio_migration_tools/helper.py +29 -21
  10. folio_migration_tools/holdings_helper.py +57 -34
  11. folio_migration_tools/i18n_config.py +9 -0
  12. folio_migration_tools/library_configuration.py +173 -13
  13. folio_migration_tools/mapper_base.py +317 -106
  14. folio_migration_tools/mapping_file_transformation/courses_mapper.py +203 -0
  15. folio_migration_tools/mapping_file_transformation/holdings_mapper.py +83 -69
  16. folio_migration_tools/mapping_file_transformation/item_mapper.py +98 -94
  17. folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +352 -0
  18. folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +702 -223
  19. folio_migration_tools/mapping_file_transformation/notes_mapper.py +90 -0
  20. folio_migration_tools/mapping_file_transformation/order_mapper.py +492 -0
  21. folio_migration_tools/mapping_file_transformation/organization_mapper.py +389 -0
  22. folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +38 -27
  23. folio_migration_tools/mapping_file_transformation/user_mapper.py +149 -361
  24. folio_migration_tools/marc_rules_transformation/conditions.py +650 -246
  25. folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +292 -130
  26. folio_migration_tools/marc_rules_transformation/hrid_handler.py +244 -0
  27. folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +20846 -0
  28. folio_migration_tools/marc_rules_transformation/marc_file_processor.py +300 -0
  29. folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +136 -0
  30. folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +241 -0
  31. folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +681 -201
  32. folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +395 -429
  33. folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +531 -100
  34. folio_migration_tools/migration_report.py +85 -38
  35. folio_migration_tools/migration_tasks/__init__.py +1 -3
  36. folio_migration_tools/migration_tasks/authority_transformer.py +119 -0
  37. folio_migration_tools/migration_tasks/batch_poster.py +911 -198
  38. folio_migration_tools/migration_tasks/bibs_transformer.py +121 -116
  39. folio_migration_tools/migration_tasks/courses_migrator.py +192 -0
  40. folio_migration_tools/migration_tasks/holdings_csv_transformer.py +252 -247
  41. folio_migration_tools/migration_tasks/holdings_marc_transformer.py +321 -115
  42. folio_migration_tools/migration_tasks/items_transformer.py +264 -84
  43. folio_migration_tools/migration_tasks/loans_migrator.py +506 -195
  44. folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +187 -0
  45. folio_migration_tools/migration_tasks/migration_task_base.py +364 -74
  46. folio_migration_tools/migration_tasks/orders_transformer.py +373 -0
  47. folio_migration_tools/migration_tasks/organization_transformer.py +451 -0
  48. folio_migration_tools/migration_tasks/requests_migrator.py +130 -62
  49. folio_migration_tools/migration_tasks/reserves_migrator.py +253 -0
  50. folio_migration_tools/migration_tasks/user_transformer.py +180 -139
  51. folio_migration_tools/task_configuration.py +46 -0
  52. folio_migration_tools/test_infrastructure/__init__.py +0 -0
  53. folio_migration_tools/test_infrastructure/mocked_classes.py +406 -0
  54. folio_migration_tools/transaction_migration/legacy_loan.py +148 -34
  55. folio_migration_tools/transaction_migration/legacy_request.py +65 -25
  56. folio_migration_tools/transaction_migration/legacy_reserve.py +47 -0
  57. folio_migration_tools/transaction_migration/transaction_result.py +12 -1
  58. folio_migration_tools/translations/en.json +476 -0
  59. folio_migration_tools-1.9.10.dist-info/METADATA +169 -0
  60. folio_migration_tools-1.9.10.dist-info/RECORD +67 -0
  61. {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info}/WHEEL +1 -2
  62. folio_migration_tools-1.9.10.dist-info/entry_points.txt +3 -0
  63. folio_migration_tools/generate_schemas.py +0 -46
  64. folio_migration_tools/mapping_file_transformation/mapping_file_mapping_base_impl.py +0 -44
  65. folio_migration_tools/mapping_file_transformation/user_mapper_base.py +0 -212
  66. folio_migration_tools/marc_rules_transformation/bibs_processor.py +0 -163
  67. folio_migration_tools/marc_rules_transformation/holdings_processor.py +0 -284
  68. folio_migration_tools/report_blurbs.py +0 -219
  69. folio_migration_tools/transaction_migration/legacy_fee_fine.py +0 -36
  70. folio_migration_tools-1.2.1.dist-info/METADATA +0 -134
  71. folio_migration_tools-1.2.1.dist-info/RECORD +0 -50
  72. folio_migration_tools-1.2.1.dist-info/top_level.txt +0 -1
  73. {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 fieldnames without leading and trailing
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() and lower() the input key
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
- class TransfomationError(Exception):
7
+
8
+ class TransformationError(Exception):
5
9
  pass
6
10
 
7
11
 
8
- class TransformationFieldMappingError(TransfomationError):
9
- """Raised when the a field mapping fails, but the error is not critical.
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
- f"Data issue. Consider fixing the record. "
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(TransfomationError):
35
- """Raised when the a field mapping fails, Error is critical and means tranformation fails"""
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(TransfomationError):
64
- """Raised when the mapping fails due to disconnects in ref data.
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
- data_value="",
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("Setting up folder structure")
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
- self.data_folder = self.base_folder / "data"
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}_{file_template}.log"
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"failed_records_{file_template}_{time_stamp}.txt"
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"extradata_{file_template}.extradata"
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}_{file_template}.json"
93
+ self.results_folder / f"folio_{object_type_string}{self.file_template}.json"
101
94
  )
102
-
103
- self.failed_bibs_file = (
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}_{file_template}.json"
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"instance_id_map_{self.iteration_identifier}.json"
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"holdings_id_map_{self.iteration_identifier}.json"
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 "data/" not in contents:
146
- f.write("data/\n")
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)
@@ -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> \n\n"
12
+ "<details><summary>" + i18n.t("Click to expand field report") + "</summary>\n\n"
12
13
  )
13
- details_end = "</details> \n"
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("FOLIO Field | Mapped | Unmapped \n")
21
- report_file.write("--- | --- | ---: \n")
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:,} \n"
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
- report_file.write("Legacy Field | Present | Mapped | Unmapped \n")
38
- report_file.write("--- | --- | --- | ---: \n")
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):,} ({mapped_per}) | {unmapped:,} \n"
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, pg_dump=False):
59
- """Writes record to file. pg_dump=true for importing directly via the
60
- psql copy command"""
61
- if pg_dump:
62
- file.write("{}\t{}\n".format(folio_record["id"], json.dumps(folio_record)))
63
- else:
64
- file.write("{}\n".format(json.dumps(folio_record)))
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")