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
|
@@ -1,48 +1,57 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
1
4
|
import logging
|
|
2
5
|
import sys
|
|
3
|
-
import
|
|
4
|
-
import
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
import i18n
|
|
12
|
+
from folio_uuid.folio_namespaces import FOLIONamespaces
|
|
13
|
+
from folio_uuid.folio_uuid import FolioUUID
|
|
14
|
+
from folioclient import FolioClient
|
|
15
|
+
from pymarc import Record
|
|
5
16
|
|
|
6
17
|
from folio_migration_tools.custom_exceptions import (
|
|
18
|
+
TransformationFieldMappingError,
|
|
7
19
|
TransformationProcessError,
|
|
8
20
|
TransformationRecordFailedError,
|
|
9
21
|
)
|
|
10
|
-
from
|
|
11
|
-
from
|
|
22
|
+
from folio_migration_tools.extradata_writer import ExtradataWriter
|
|
23
|
+
from folio_migration_tools.helper import Helper
|
|
24
|
+
from folio_migration_tools.library_configuration import FileDefinition, LibraryConfiguration
|
|
12
25
|
from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
|
|
13
26
|
RefDataMapping,
|
|
14
27
|
)
|
|
15
|
-
from folio_migration_tools.library_configuration import LibraryConfiguration
|
|
16
28
|
from folio_migration_tools.migration_report import MigrationReport
|
|
17
|
-
from folio_migration_tools.
|
|
29
|
+
from folio_migration_tools.task_configuration import AbstractTaskConfiguration
|
|
18
30
|
|
|
19
31
|
|
|
20
32
|
class MapperBase:
|
|
33
|
+
legacy_id_template = "Identifier(s) from previous system:"
|
|
34
|
+
bib_id_template = "Bib id: "
|
|
35
|
+
|
|
21
36
|
def __init__(
|
|
22
37
|
self,
|
|
23
38
|
library_configuration: LibraryConfiguration,
|
|
39
|
+
task_configuration: AbstractTaskConfiguration,
|
|
24
40
|
folio_client: FolioClient,
|
|
41
|
+
parent_id_map: Dict[str, Tuple] = {},
|
|
25
42
|
):
|
|
26
43
|
logging.info("MapperBase initiating")
|
|
27
|
-
self.
|
|
28
|
-
self.
|
|
44
|
+
self.parent_id_map: dict[str, tuple] = parent_id_map
|
|
45
|
+
self.extradata_writer: ExtradataWriter = ExtradataWriter(Path(""))
|
|
46
|
+
self.start_datetime = datetime.now(timezone.utc)
|
|
47
|
+
self.folio_client: FolioClient = folio_client
|
|
29
48
|
self.library_configuration: LibraryConfiguration = library_configuration
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
32
|
-
self.
|
|
33
|
-
self.holdings_hrid_prefix = self.hrid_settings["holdings"]["prefix"]
|
|
34
|
-
self.holdings_hrid_counter = self.hrid_settings["holdings"]["startNumber"]
|
|
35
|
-
logging.info("Fetched HRID settings.")
|
|
36
|
-
logging.info("Instance HRID prefix is %s", self.instance_hrid_prefix)
|
|
37
|
-
logging.info("Instance start number is %s", self.instance_hrid_counter)
|
|
38
|
-
logging.info("Holdings HRID prefix is %s", self.instance_hrid_prefix)
|
|
39
|
-
logging.info("Holdings start number is %s", self.holdings_hrid_counter)
|
|
40
|
-
|
|
41
|
-
self.mapped_folio_fields = {}
|
|
42
|
-
self.migration_report = MigrationReport()
|
|
49
|
+
self.task_configuration: AbstractTaskConfiguration = task_configuration
|
|
50
|
+
self.mapped_folio_fields: dict = {}
|
|
51
|
+
self.migration_report: MigrationReport = MigrationReport()
|
|
43
52
|
self.num_criticalerrors = 0
|
|
44
|
-
self.
|
|
45
|
-
self.mapped_legacy_fields = {}
|
|
53
|
+
self.num_exceptions = 0
|
|
54
|
+
self.mapped_legacy_fields: dict = {}
|
|
46
55
|
self.schema_properties = None
|
|
47
56
|
|
|
48
57
|
def report_legacy_mapping(self, field_name, present, mapped):
|
|
@@ -55,7 +64,7 @@ class MapperBase:
|
|
|
55
64
|
|
|
56
65
|
def report_folio_mapping(self, folio_record, schema):
|
|
57
66
|
try:
|
|
58
|
-
for field_name in flatten(folio_record):
|
|
67
|
+
for field_name in set(flatten(folio_record)):
|
|
59
68
|
try:
|
|
60
69
|
self.mapped_folio_fields[field_name][0] += 1
|
|
61
70
|
except KeyError:
|
|
@@ -73,29 +82,42 @@ class MapperBase:
|
|
|
73
82
|
logging.error(ee, stack_info=True)
|
|
74
83
|
raise ee from ee
|
|
75
84
|
|
|
85
|
+
def report_legacy_mapping_no_schema(self, legacy_object):
|
|
86
|
+
for field_name, value in legacy_object.items():
|
|
87
|
+
v = 1 if value else 0
|
|
88
|
+
if field_name not in self.mapped_legacy_fields:
|
|
89
|
+
self.mapped_legacy_fields[field_name] = [1, v]
|
|
90
|
+
else:
|
|
91
|
+
self.mapped_legacy_fields[field_name][0] += 1
|
|
92
|
+
self.mapped_legacy_fields[field_name][1] += v
|
|
93
|
+
|
|
94
|
+
def report_folio_mapping_no_schema(self, folio_object):
|
|
95
|
+
for field_name in set(flatten(folio_object)):
|
|
96
|
+
if field_name not in self.mapped_folio_fields:
|
|
97
|
+
self.mapped_folio_fields[field_name] = [1, 1]
|
|
98
|
+
else:
|
|
99
|
+
self.mapped_folio_fields[field_name][0] += 1
|
|
100
|
+
self.mapped_folio_fields[field_name][1] += 1
|
|
101
|
+
|
|
76
102
|
def get_mapped_name(
|
|
77
103
|
self,
|
|
78
104
|
ref_data_mapping: RefDataMapping,
|
|
79
105
|
legacy_object,
|
|
80
106
|
index_or_id,
|
|
81
|
-
folio_property_name="",
|
|
82
107
|
prevent_default=False,
|
|
83
108
|
):
|
|
84
109
|
try:
|
|
85
110
|
# Get the values in the fields that will be used for mapping
|
|
86
|
-
fieldvalues = [
|
|
87
|
-
legacy_object.get(k) for k in ref_data_mapping.mapped_legacy_keys
|
|
88
|
-
]
|
|
111
|
+
fieldvalues = [legacy_object.get(k) for k in ref_data_mapping.mapped_legacy_keys]
|
|
89
112
|
|
|
90
113
|
# Gets the first line in the map satisfying all legacy mapping values.
|
|
91
114
|
# Case insensitive, strips away whitespace
|
|
92
|
-
# TODO: add option for Wild card matching in individual columns
|
|
93
115
|
right_mapping = ref_data_mapping.get_ref_data_mapping(legacy_object)
|
|
94
116
|
|
|
95
117
|
if not right_mapping:
|
|
96
118
|
raise StopIteration()
|
|
97
119
|
self.migration_report.add(
|
|
98
|
-
ref_data_mapping.
|
|
120
|
+
ref_data_mapping.blurb_id,
|
|
99
121
|
(
|
|
100
122
|
f'{" - ".join(fieldvalues)} '
|
|
101
123
|
f'-> {right_mapping[f"folio_{ref_data_mapping.key_type}"]}'
|
|
@@ -106,15 +128,12 @@ class MapperBase:
|
|
|
106
128
|
except StopIteration:
|
|
107
129
|
if prevent_default:
|
|
108
130
|
self.migration_report.add(
|
|
109
|
-
ref_data_mapping.
|
|
110
|
-
(
|
|
111
|
-
f"Not to be mapped. "
|
|
112
|
-
f'(No default) -- {" - ".join(fieldvalues)} -> ""'
|
|
113
|
-
),
|
|
131
|
+
ref_data_mapping.blurb_id,
|
|
132
|
+
(f"Not to be mapped. " f'(No default) -- {" - ".join(fieldvalues)} -> ""'),
|
|
114
133
|
)
|
|
115
134
|
return ""
|
|
116
135
|
self.migration_report.add(
|
|
117
|
-
ref_data_mapping.
|
|
136
|
+
ref_data_mapping.blurb_id,
|
|
118
137
|
(
|
|
119
138
|
f"Unmapped (Default value was set) -- "
|
|
120
139
|
f'{" - ".join(fieldvalues)} -> {ref_data_mapping.default_name}'
|
|
@@ -130,8 +149,17 @@ class MapperBase:
|
|
|
130
149
|
"a recognized field in the legacy data."
|
|
131
150
|
),
|
|
132
151
|
) from exception
|
|
152
|
+
except KeyError as exception:
|
|
153
|
+
raise TransformationProcessError(
|
|
154
|
+
index_or_id,
|
|
155
|
+
(
|
|
156
|
+
f"{ref_data_mapping.name} mapping - folio_{ref_data_mapping.key_type} "
|
|
157
|
+
f"({ref_data_mapping.mapped_legacy_keys}) is not "
|
|
158
|
+
f"a recognized field in the legacy data. KeyError: {exception}"
|
|
159
|
+
),
|
|
160
|
+
) from exception
|
|
133
161
|
except Exception as exception:
|
|
134
|
-
raise
|
|
162
|
+
raise TransformationProcessError(
|
|
135
163
|
index_or_id,
|
|
136
164
|
(
|
|
137
165
|
f"{ref_data_mapping.name} - folio_{ref_data_mapping.key_type} "
|
|
@@ -139,7 +167,7 @@ class MapperBase:
|
|
|
139
167
|
),
|
|
140
168
|
) from exception
|
|
141
169
|
|
|
142
|
-
def
|
|
170
|
+
def get_mapped_ref_data_value(
|
|
143
171
|
self,
|
|
144
172
|
ref_data_mapping: RefDataMapping,
|
|
145
173
|
legacy_object,
|
|
@@ -147,17 +175,13 @@ class MapperBase:
|
|
|
147
175
|
folio_property_name="",
|
|
148
176
|
prevent_default=False,
|
|
149
177
|
):
|
|
150
|
-
|
|
151
178
|
# Gets mapped value from mapping file, translated to the right FOLIO UUID
|
|
152
179
|
try:
|
|
153
180
|
# Get the values in the fields that will be used for mapping
|
|
154
|
-
fieldvalues = [
|
|
155
|
-
legacy_object.get(k) for k in ref_data_mapping.mapped_legacy_keys
|
|
156
|
-
]
|
|
181
|
+
fieldvalues = [legacy_object.get(k) for k in ref_data_mapping.mapped_legacy_keys]
|
|
157
182
|
|
|
158
183
|
# Gets the first line in the map satisfying all legacy mapping values.
|
|
159
184
|
# Case insensitive, strips away whitespace
|
|
160
|
-
# TODO: add option for Wild card matching in individual columns
|
|
161
185
|
right_mapping = ref_data_mapping.get_ref_data_mapping(legacy_object)
|
|
162
186
|
if not right_mapping:
|
|
163
187
|
# Not all fields matched. Could it be a hybrid wildcard map?
|
|
@@ -166,7 +190,7 @@ class MapperBase:
|
|
|
166
190
|
if not right_mapping:
|
|
167
191
|
raise StopIteration()
|
|
168
192
|
self.migration_report.add(
|
|
169
|
-
ref_data_mapping.
|
|
193
|
+
ref_data_mapping.blurb_id,
|
|
170
194
|
(
|
|
171
195
|
f'{" - ".join(fieldvalues)} '
|
|
172
196
|
f'-> {right_mapping[f"folio_{ref_data_mapping.key_type}"]}'
|
|
@@ -176,15 +200,12 @@ class MapperBase:
|
|
|
176
200
|
except StopIteration:
|
|
177
201
|
if prevent_default:
|
|
178
202
|
self.migration_report.add(
|
|
179
|
-
ref_data_mapping.
|
|
180
|
-
(
|
|
181
|
-
f"Not to be mapped. "
|
|
182
|
-
f'(No default) -- {" - ".join(fieldvalues)} -> ""'
|
|
183
|
-
),
|
|
203
|
+
ref_data_mapping.blurb_id,
|
|
204
|
+
(f"Not to be mapped. " f'(No default) -- {" - ".join(fieldvalues)} -> ""'),
|
|
184
205
|
)
|
|
185
206
|
return ""
|
|
186
207
|
self.migration_report.add(
|
|
187
|
-
ref_data_mapping.
|
|
208
|
+
ref_data_mapping.blurb_id,
|
|
188
209
|
(
|
|
189
210
|
f"Unmapped (Default value was set) -- "
|
|
190
211
|
f'{" - ".join(fieldvalues)} -> {ref_data_mapping.default_name}'
|
|
@@ -210,15 +231,13 @@ class MapperBase:
|
|
|
210
231
|
) from exception
|
|
211
232
|
|
|
212
233
|
def handle_transformation_field_mapping_error(self, index_or_id, error):
|
|
213
|
-
self.migration_report.add(
|
|
234
|
+
self.migration_report.add("FieldMappingErrors", error)
|
|
214
235
|
error.id = error.id or index_or_id
|
|
215
236
|
error.log_it()
|
|
216
|
-
self.migration_report.add_general_statistics("Field Mapping Errors found")
|
|
237
|
+
self.migration_report.add_general_statistics(i18n.t("Field Mapping Errors found"))
|
|
217
238
|
|
|
218
|
-
def handle_transformation_process_error(
|
|
219
|
-
self
|
|
220
|
-
):
|
|
221
|
-
self.migration_report.add_general_statistics("Transformation process error")
|
|
239
|
+
def handle_transformation_process_error(self, idx, error: TransformationProcessError):
|
|
240
|
+
self.migration_report.add_general_statistics(i18n.t("Transformation process error"))
|
|
222
241
|
logging.critical("%s\t%s", idx, error)
|
|
223
242
|
print(f"\n{error.message}: {error.data_value}")
|
|
224
243
|
sys.exit(1)
|
|
@@ -227,16 +246,15 @@ class MapperBase:
|
|
|
227
246
|
self, records_processed: int, error: TransformationRecordFailedError
|
|
228
247
|
):
|
|
229
248
|
self.migration_report.add(
|
|
230
|
-
|
|
249
|
+
"GeneralStatistics", i18n.t("FAILED Records failed due to an error")
|
|
231
250
|
)
|
|
232
|
-
error.
|
|
251
|
+
error.index_or_id = error.index_or_id or records_processed
|
|
233
252
|
error.log_it()
|
|
234
253
|
self.num_criticalerrors += 1
|
|
235
254
|
if (
|
|
236
255
|
self.num_criticalerrors / (records_processed + 1)
|
|
237
256
|
> (self.library_configuration.failed_percentage_threshold / 100)
|
|
238
|
-
and self.num_criticalerrors
|
|
239
|
-
> self.library_configuration.failed_records_threshold
|
|
257
|
+
and self.num_criticalerrors > self.library_configuration.failed_records_threshold
|
|
240
258
|
):
|
|
241
259
|
logging.fatal(
|
|
242
260
|
(
|
|
@@ -252,78 +270,76 @@ class MapperBase:
|
|
|
252
270
|
)
|
|
253
271
|
sys.exit(1)
|
|
254
272
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
273
|
+
def get_id_map_tuple(self, legacy_id: str, folio_record: dict, object_type: FOLIONamespaces):
|
|
274
|
+
if all(
|
|
275
|
+
[
|
|
276
|
+
object_type == FOLIONamespaces.instances,
|
|
277
|
+
(not getattr(self.task_configuration, "data_import_marc", False)),
|
|
278
|
+
getattr(self, "create_source_records", True),
|
|
279
|
+
]
|
|
280
|
+
):
|
|
281
|
+
return (legacy_id, folio_record["id"], folio_record["hrid"])
|
|
282
|
+
return (legacy_id, folio_record["id"])
|
|
258
283
|
|
|
259
|
-
def handle_generic_exception(self, idx,
|
|
260
|
-
self.
|
|
284
|
+
def handle_generic_exception(self, idx, exception: Exception):
|
|
285
|
+
self.num_exceptions += 1
|
|
261
286
|
print("\n=======ERROR===========")
|
|
262
287
|
print(
|
|
263
|
-
f"Row {idx:,} failed with the following unhandled Exception: {
|
|
264
|
-
f"of type {type(
|
|
288
|
+
f"Row {idx:,} failed with the following unhandled Exception: {exception} "
|
|
289
|
+
f"of type {type(exception).__name__}"
|
|
265
290
|
)
|
|
266
|
-
logging.error(
|
|
267
|
-
if self.
|
|
291
|
+
logging.error(exception, exc_info=True)
|
|
292
|
+
if self.num_exceptions > self.library_configuration.generic_exception_threshold:
|
|
268
293
|
logging.fatal(
|
|
269
294
|
"Stopping. More than %s unhandled exceptions. Code needs fixing",
|
|
270
|
-
self.
|
|
295
|
+
self.num_exceptions,
|
|
271
296
|
)
|
|
272
297
|
sys.exit(1)
|
|
273
298
|
|
|
274
|
-
|
|
275
|
-
def save_id_map_file(path, legacy_map: dict):
|
|
299
|
+
def save_id_map_file(self, path, legacy_map: dict):
|
|
276
300
|
with open(path, "w") as legacy_map_file:
|
|
277
301
|
for id_string in legacy_map.values():
|
|
278
302
|
legacy_map_file.write(f"{json.dumps(id_string)}\n")
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
logging.info("
|
|
283
|
-
try:
|
|
284
|
-
self.hrid_settings["instances"]["startNumber"] = (
|
|
285
|
-
self.instance_hrid_counter + 1
|
|
286
|
-
)
|
|
287
|
-
self.hrid_settings["holdings"]["startNumber"] = (
|
|
288
|
-
self.holdings_hrid_counter + 1
|
|
289
|
-
)
|
|
290
|
-
url = self.folio_client.okapi_url + self.hrid_path
|
|
291
|
-
resp = requests.put(
|
|
292
|
-
url,
|
|
293
|
-
data=json.dumps(self.hrid_settings),
|
|
294
|
-
headers=self.folio_client.okapi_headers,
|
|
295
|
-
)
|
|
296
|
-
resp.raise_for_status()
|
|
297
|
-
logging.info("%s Successfully set HRID settings.", resp.status_code)
|
|
298
|
-
a = self.folio_client.folio_get_single_object(self.hrid_path)
|
|
299
|
-
logging.info("Current hrid settings: %s", json.dumps(a, indent=4))
|
|
300
|
-
except Exception:
|
|
301
|
-
logging.exception(
|
|
302
|
-
f"Something went wrong when setting the HRID settings. "
|
|
303
|
-
f"Update them manually. {json.dumps(self.hrid_settings)}"
|
|
304
|
-
)
|
|
303
|
+
self.migration_report.add(
|
|
304
|
+
"GeneralStatistics", i18n.t("Unique ID:s written to legacy map")
|
|
305
|
+
)
|
|
306
|
+
logging.info("Wrote legacy id map to %s", path)
|
|
305
307
|
|
|
306
308
|
@staticmethod
|
|
307
309
|
def validate_required_properties(
|
|
308
310
|
legacy_id, folio_object: dict, schema: dict, object_type: FOLIONamespaces
|
|
309
311
|
):
|
|
310
312
|
cleaned_folio_object = MapperBase.clean_none_props(folio_object)
|
|
311
|
-
required =
|
|
313
|
+
required = []
|
|
312
314
|
missing = []
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
if object_type != FOLIONamespaces.note:
|
|
316
|
+
required = schema.get("required", [])
|
|
317
|
+
missing = list(MapperBase.list_missing(required, cleaned_folio_object))
|
|
318
|
+
else:
|
|
319
|
+
required = (
|
|
320
|
+
schema.get("properties", {}).get("notes", {}).get("items", {}).get("required", [])
|
|
321
|
+
)
|
|
322
|
+
for note in cleaned_folio_object.get("notes", []):
|
|
323
|
+
missing.extend(MapperBase.list_missing(required, note))
|
|
324
|
+
|
|
318
325
|
if any(missing):
|
|
319
326
|
raise TransformationRecordFailedError(
|
|
320
327
|
legacy_id,
|
|
321
328
|
"One or many required properties empty",
|
|
322
329
|
f"{json.dumps(missing)}",
|
|
323
330
|
)
|
|
324
|
-
|
|
331
|
+
if object_type not in [FOLIONamespaces.users]:
|
|
332
|
+
cleaned_folio_object.pop("type", None)
|
|
325
333
|
return cleaned_folio_object
|
|
326
334
|
|
|
335
|
+
@staticmethod
|
|
336
|
+
def list_missing(required: list, cleaned_folio_object: dict):
|
|
337
|
+
for required_prop in required:
|
|
338
|
+
if required_prop not in cleaned_folio_object:
|
|
339
|
+
yield f"Missing: {required_prop}"
|
|
340
|
+
elif not cleaned_folio_object[required_prop]:
|
|
341
|
+
yield f"Empty: {required_prop}"
|
|
342
|
+
|
|
327
343
|
@staticmethod
|
|
328
344
|
def clean_none_props(d: dict):
|
|
329
345
|
clean = {}
|
|
@@ -338,15 +354,210 @@ class MapperBase:
|
|
|
338
354
|
clean[k] = v
|
|
339
355
|
return clean
|
|
340
356
|
|
|
357
|
+
def add_legacy_id_to_admin_note(self, folio_record: dict, legacy_id: str):
|
|
358
|
+
if not legacy_id:
|
|
359
|
+
raise TransformationFieldMappingError(
|
|
360
|
+
legacy_id, i18n.t("Legacy id is empty"), legacy_id
|
|
361
|
+
)
|
|
362
|
+
if "administrativeNotes" not in folio_record:
|
|
363
|
+
folio_record["administrativeNotes"] = []
|
|
364
|
+
if id_string := next(
|
|
365
|
+
(f for f in folio_record["administrativeNotes"] if MapperBase.legacy_id_template in f),
|
|
366
|
+
None,
|
|
367
|
+
):
|
|
368
|
+
if legacy_id not in id_string:
|
|
369
|
+
folio_record["administrativeNotes"] = [
|
|
370
|
+
f
|
|
371
|
+
for f in folio_record["administrativeNotes"]
|
|
372
|
+
if MapperBase.legacy_id_template not in f
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
folio_record["administrativeNotes"].append(f"{id_string}, {legacy_id}")
|
|
376
|
+
else:
|
|
377
|
+
folio_record["administrativeNotes"].append(
|
|
378
|
+
f"{MapperBase.legacy_id_template} {legacy_id}"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def create_and_write_boundwith_part(self, legacy_item_id: str, bound_with_holding_uuid: dict):
|
|
382
|
+
part = {
|
|
383
|
+
"id": str(uuid.uuid4()),
|
|
384
|
+
"holdingsRecordId": bound_with_holding_uuid,
|
|
385
|
+
"itemId": str(
|
|
386
|
+
FolioUUID(
|
|
387
|
+
self.base_string_for_folio_uuid,
|
|
388
|
+
FOLIONamespaces.items,
|
|
389
|
+
legacy_item_id,
|
|
390
|
+
)
|
|
391
|
+
),
|
|
392
|
+
}
|
|
393
|
+
self.extradata_writer.write("boundwithPart", part)
|
|
394
|
+
|
|
395
|
+
def create_bound_with_holdings(
|
|
396
|
+
self,
|
|
397
|
+
folio_holding: dict,
|
|
398
|
+
instance_ids: list,
|
|
399
|
+
bound_with_holdings_type_id: str,
|
|
400
|
+
):
|
|
401
|
+
if not bound_with_holdings_type_id:
|
|
402
|
+
raise TransformationProcessError(
|
|
403
|
+
"Missing task setting holdingsTypeUuidForBoundwiths. Add a "
|
|
404
|
+
"holdingstype specifically for boundwith holdings and reference "
|
|
405
|
+
"the UUID in this parameter."
|
|
406
|
+
)
|
|
407
|
+
for bwidx, instance_uuid in enumerate(instance_ids):
|
|
408
|
+
if not instance_uuid:
|
|
409
|
+
raise ValueError(f"No Instance ID for record {folio_holding}")
|
|
410
|
+
bound_with_holding = copy.deepcopy(folio_holding)
|
|
411
|
+
bound_with_holding["instanceId"] = instance_uuid
|
|
412
|
+
|
|
413
|
+
if call_number := folio_holding.get("callNumber", None):
|
|
414
|
+
if "[" in call_number:
|
|
415
|
+
try:
|
|
416
|
+
call_numbers: List = ast.literal_eval(str(folio_holding["callNumber"]))
|
|
417
|
+
bound_with_holding["callNumber"] = call_numbers[bwidx]
|
|
418
|
+
except IndexError:
|
|
419
|
+
if call_numbers:
|
|
420
|
+
bound_with_holding["callNumber"] = call_numbers[0]
|
|
421
|
+
except (SyntaxError, ValueError):
|
|
422
|
+
bound_with_holding["callNumber"] = call_number
|
|
423
|
+
else:
|
|
424
|
+
bound_with_holding["callNumber"] = call_number
|
|
425
|
+
bound_with_holding["holdingsTypeId"] = bound_with_holdings_type_id
|
|
426
|
+
|
|
427
|
+
# The subsequent copies gets different ids, but the original is maintained.
|
|
428
|
+
if bwidx > 0:
|
|
429
|
+
bound_with_holding["id"] = self.generate_boundwith_holding_uuid(
|
|
430
|
+
folio_holding["id"], instance_uuid
|
|
431
|
+
)
|
|
432
|
+
if bound_with_holding.get("hrid", ""):
|
|
433
|
+
bound_with_holding["hrid"] = f'{bound_with_holding["hrid"]}_bw_{bwidx}'
|
|
434
|
+
self.migration_report.add_general_statistics(i18n.t("Bound-with holdings created"))
|
|
435
|
+
yield bound_with_holding
|
|
436
|
+
|
|
437
|
+
def generate_boundwith_holding_uuid(self, holding_uuid, instance_uuid):
|
|
438
|
+
return str(
|
|
439
|
+
FolioUUID(
|
|
440
|
+
self.base_string_for_folio_uuid,
|
|
441
|
+
FOLIONamespaces.holdings,
|
|
442
|
+
f"{holding_uuid}-{instance_uuid}",
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def map_statistical_codes(self, folio_record: dict, file_def: FileDefinition, legacy_record: Optional[Union[dict, Record]] = None):
|
|
447
|
+
"""Map statistical codes to the folio record.
|
|
448
|
+
|
|
449
|
+
This method checks if the file definition contains statistical codes and
|
|
450
|
+
if so, it splits the codes by the multi-field delimiter and adds them to
|
|
451
|
+
the folio record's 'statisticalCodeIds' field. If the field does not exist,
|
|
452
|
+
it initializes it as an empty list before appending the codes.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
folio_record (dict): The FOLIO record to which the statistical codes will be added.
|
|
456
|
+
file_def (FileDefinition): The file definition containing the statistical codes.
|
|
457
|
+
legacy_record (Optional[Union[dict, Record]]): The legacy record from which the statistical codes are derived.
|
|
458
|
+
"""
|
|
459
|
+
if file_def.statistical_code:
|
|
460
|
+
code_strings = file_def.statistical_code.split(
|
|
461
|
+
self.library_configuration.multi_field_delimiter
|
|
462
|
+
)
|
|
463
|
+
folio_record["statisticalCodeIds"] = folio_record.get("statisticalCodeIds", []) + code_strings
|
|
464
|
+
|
|
465
|
+
def setup_statistical_codes_map(self, statistical_codes_map):
|
|
466
|
+
if statistical_codes_map:
|
|
467
|
+
self.statistical_codes_mapping = RefDataMapping(
|
|
468
|
+
self.folio_client,
|
|
469
|
+
"/statistical-codes",
|
|
470
|
+
"statisticalCodes",
|
|
471
|
+
statistical_codes_map,
|
|
472
|
+
"code",
|
|
473
|
+
"StatisticalCodeMapping",
|
|
474
|
+
)
|
|
475
|
+
logging.info(f"Statistical codes mapping set up {self.statistical_codes_mapping.mapped_legacy_keys}")
|
|
476
|
+
else:
|
|
477
|
+
self.statistical_codes_mapping = None
|
|
478
|
+
logging.info("Statistical codes map is not set up")
|
|
479
|
+
|
|
480
|
+
def get_statistical_code(self, legacy_item: dict, folio_prop_name: str, index_or_id):
|
|
481
|
+
if self.statistical_codes_mapping:
|
|
482
|
+
return self.get_mapped_ref_data_value(
|
|
483
|
+
self.statistical_codes_mapping,
|
|
484
|
+
legacy_item,
|
|
485
|
+
index_or_id,
|
|
486
|
+
folio_prop_name,
|
|
487
|
+
True,
|
|
488
|
+
)
|
|
489
|
+
self.migration_report.add(
|
|
490
|
+
"StatisticalCodeMapping",
|
|
491
|
+
i18n.t("Mapping not set up"),
|
|
492
|
+
)
|
|
493
|
+
return ""
|
|
494
|
+
|
|
495
|
+
def map_statistical_code_ids(
|
|
496
|
+
self, legacy_ids, folio_record: dict
|
|
497
|
+
):
|
|
498
|
+
if stat_codes := {x: None for x in folio_record.pop("statisticalCodeIds", [])}:
|
|
499
|
+
folio_code_ids = set()
|
|
500
|
+
for stat_code in stat_codes:
|
|
501
|
+
if stat_code_id := self.get_statistical_code({"legacy_stat_code": stat_code}, "statisticalCodeId", legacy_ids):
|
|
502
|
+
folio_code_ids.add(stat_code_id)
|
|
503
|
+
else:
|
|
504
|
+
Helper.log_data_issue(
|
|
505
|
+
legacy_ids,
|
|
506
|
+
i18n.t(
|
|
507
|
+
"Statistical code '%{code}' not found in FOLIO",
|
|
508
|
+
code=stat_code,
|
|
509
|
+
),
|
|
510
|
+
stat_code,
|
|
511
|
+
)
|
|
512
|
+
folio_record["statisticalCodeIds"] = list(folio_code_ids)
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def base_string_for_folio_uuid(self):
|
|
516
|
+
if self.library_configuration.use_gateway_url_for_uuids and not self.library_configuration.is_ecs:
|
|
517
|
+
return str(self.folio_client.gateway_url)
|
|
518
|
+
elif self.library_configuration.ecs_tenant_id:
|
|
519
|
+
return str(self.library_configuration.ecs_tenant_id)
|
|
520
|
+
else:
|
|
521
|
+
return str(self.library_configuration.tenant_id)
|
|
522
|
+
|
|
523
|
+
@staticmethod
|
|
524
|
+
def validate_location_map(location_map: List[Dict], locations: List[Dict]) -> List[Dict]:
|
|
525
|
+
mapped_codes = [x['folio_code'] for x in location_map]
|
|
526
|
+
existing_codes = [x['code'] for x in locations]
|
|
527
|
+
missing_codes = set(mapped_codes) - set(existing_codes)
|
|
528
|
+
if missing_codes:
|
|
529
|
+
raise TransformationProcessError(
|
|
530
|
+
"",
|
|
531
|
+
f"Location map contains codes not found in locations: {', '.join(missing_codes)}",
|
|
532
|
+
"",
|
|
533
|
+
)
|
|
534
|
+
return location_map
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def get_object_type() -> FOLIONamespaces:
|
|
538
|
+
raise NotImplementedError("This method should be overridden in subclasses")
|
|
341
539
|
|
|
342
540
|
def flatten(my_dict: dict, path=""):
|
|
343
541
|
for k, v in iter(my_dict.items()):
|
|
542
|
+
if not path:
|
|
543
|
+
yield k
|
|
344
544
|
if v:
|
|
345
545
|
if isinstance(v, list):
|
|
546
|
+
if path and check_if_list_with_dict_keys(v):
|
|
547
|
+
yield f"{path}.{k}".strip(".")
|
|
346
548
|
for e in v:
|
|
347
549
|
if isinstance(e, dict):
|
|
348
550
|
yield from flatten(dict(e), f"{path}.{k}")
|
|
551
|
+
elif isinstance(e, str) and path:
|
|
552
|
+
yield f"{path}.{k}".strip(".")
|
|
553
|
+
|
|
349
554
|
elif isinstance(v, dict):
|
|
555
|
+
if path:
|
|
556
|
+
yield f"{path}.{k}".strip(".")
|
|
350
557
|
yield from flatten(dict(v), f"{path}.{k}")
|
|
351
|
-
|
|
558
|
+
elif path:
|
|
352
559
|
yield f"{path}.{k}".strip(".")
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def check_if_list_with_dict_keys(data):
|
|
563
|
+
return isinstance(data, list) and all(isinstance(x, dict) for x in data)
|