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,5 +1,3 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
import copy
|
|
3
1
|
import csv
|
|
4
2
|
import ctypes
|
|
5
3
|
import json
|
|
@@ -7,13 +5,13 @@ import logging
|
|
|
7
5
|
import sys
|
|
8
6
|
import time
|
|
9
7
|
import traceback
|
|
10
|
-
import
|
|
11
|
-
from os.path import isfile
|
|
12
|
-
from typing import List, Optional
|
|
13
|
-
from folioclient import FolioClient
|
|
8
|
+
from typing import Annotated, List, Optional
|
|
14
9
|
|
|
15
|
-
|
|
10
|
+
import i18n
|
|
16
11
|
from folio_uuid.folio_namespaces import FOLIONamespaces
|
|
12
|
+
from httpx import HTTPError
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
|
|
17
15
|
from folio_migration_tools.custom_exceptions import (
|
|
18
16
|
TransformationProcessError,
|
|
19
17
|
TransformationRecordFailedError,
|
|
@@ -22,7 +20,6 @@ from folio_migration_tools.helper import Helper
|
|
|
22
20
|
from folio_migration_tools.holdings_helper import HoldingsHelper
|
|
23
21
|
from folio_migration_tools.library_configuration import (
|
|
24
22
|
FileDefinition,
|
|
25
|
-
FolioRelease,
|
|
26
23
|
HridHandling,
|
|
27
24
|
LibraryConfiguration,
|
|
28
25
|
)
|
|
@@ -32,32 +29,147 @@ from folio_migration_tools.mapping_file_transformation.holdings_mapper import (
|
|
|
32
29
|
from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
|
|
33
30
|
MappingFileMapperBase,
|
|
34
31
|
)
|
|
32
|
+
from folio_migration_tools.marc_rules_transformation.hrid_handler import HRIDHandler
|
|
35
33
|
from folio_migration_tools.migration_tasks.migration_task_base import MigrationTaskBase
|
|
36
|
-
from
|
|
37
|
-
|
|
34
|
+
from folio_migration_tools.task_configuration import AbstractTaskConfiguration
|
|
38
35
|
|
|
39
36
|
csv.field_size_limit(int(ctypes.c_ulong(-1).value // 2))
|
|
40
37
|
csv.register_dialect("tsv", delimiter="\t")
|
|
41
38
|
|
|
42
39
|
|
|
43
40
|
class HoldingsCsvTransformer(MigrationTaskBase):
|
|
44
|
-
class TaskConfiguration(
|
|
45
|
-
name:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
class TaskConfiguration(AbstractTaskConfiguration):
|
|
42
|
+
name: Annotated[
|
|
43
|
+
str,
|
|
44
|
+
Field(
|
|
45
|
+
title="Task name",
|
|
46
|
+
description="Name of the task",
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
migration_task_type: Annotated[
|
|
50
|
+
str,
|
|
51
|
+
Field(
|
|
52
|
+
title="Migration task type",
|
|
53
|
+
description="Type of migration task",
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
hrid_handling: Annotated[
|
|
57
|
+
HridHandling,
|
|
58
|
+
Field(
|
|
59
|
+
title="HRID handling",
|
|
60
|
+
description=(
|
|
61
|
+
"Determining how the HRID generation "
|
|
62
|
+
"should be handled."
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
files: Annotated[
|
|
67
|
+
List[FileDefinition],
|
|
68
|
+
Field(
|
|
69
|
+
title="Files",
|
|
70
|
+
description="List of files",
|
|
71
|
+
),
|
|
72
|
+
]
|
|
73
|
+
holdings_map_file_name: Annotated[
|
|
74
|
+
str,
|
|
75
|
+
Field(
|
|
76
|
+
title="Holdings map file name",
|
|
77
|
+
description="File name for holdings map",
|
|
78
|
+
),
|
|
79
|
+
]
|
|
80
|
+
location_map_file_name: Annotated[
|
|
81
|
+
str,
|
|
82
|
+
Field(
|
|
83
|
+
title="Location map file name",
|
|
84
|
+
description="File name for location map",
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
default_call_number_type_name: Annotated[
|
|
88
|
+
str,
|
|
89
|
+
Field(
|
|
90
|
+
title="Default call number type name",
|
|
91
|
+
description="Default name for call number type",
|
|
92
|
+
),
|
|
93
|
+
]
|
|
94
|
+
previously_generated_holdings_files: Annotated[
|
|
95
|
+
Optional[list[str]],
|
|
96
|
+
Field(
|
|
97
|
+
title="Previously generated holdings files",
|
|
98
|
+
description=(
|
|
99
|
+
"List of previously generated holdings files. "
|
|
100
|
+
"By default is empty list."
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
] = []
|
|
104
|
+
fallback_holdings_type_id: Annotated[
|
|
105
|
+
str,
|
|
106
|
+
Field(
|
|
107
|
+
title="Fallback holdings type ID",
|
|
108
|
+
description="ID for fallback holdings type",
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
holdings_type_uuid_for_boundwiths: Annotated[
|
|
112
|
+
str,
|
|
113
|
+
Field(
|
|
114
|
+
title="Holdings Type for Boundwith Holdings",
|
|
115
|
+
description=(
|
|
116
|
+
"UUID for a Holdings type (set in Settings->Inventory) "
|
|
117
|
+
"for Bound-with Holdings. Default is empty string."
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
] = ""
|
|
121
|
+
call_number_type_map_file_name: Annotated[
|
|
122
|
+
Optional[str],
|
|
123
|
+
Field(
|
|
124
|
+
title="Call number type map file name",
|
|
125
|
+
description="File name for call number type map",
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
holdings_merge_criteria: Annotated[
|
|
129
|
+
Optional[list[str]],
|
|
130
|
+
Field(
|
|
131
|
+
title="Holdings merge criteria",
|
|
132
|
+
description=(
|
|
133
|
+
"List of holdings merge criteria. "
|
|
134
|
+
"Default value is "
|
|
135
|
+
"['instanceId', 'permanentLocationId', 'callNumber']."
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
] = [
|
|
57
139
|
"instanceId",
|
|
58
140
|
"permanentLocationId",
|
|
59
141
|
"callNumber",
|
|
60
142
|
]
|
|
143
|
+
reset_hrid_settings: Annotated[
|
|
144
|
+
Optional[bool],
|
|
145
|
+
Field(
|
|
146
|
+
title="Reset HRID settings",
|
|
147
|
+
description=(
|
|
148
|
+
"At the end of the run reset "
|
|
149
|
+
"FOLIO with the HRID settings. Default is FALSE."
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
] = False
|
|
153
|
+
update_hrid_settings: Annotated[
|
|
154
|
+
bool,
|
|
155
|
+
Field(
|
|
156
|
+
title="Update HRID settings",
|
|
157
|
+
description=(
|
|
158
|
+
"At the end of the run update "
|
|
159
|
+
"FOLIO with the HRID settings. Default is TRUE."
|
|
160
|
+
),
|
|
161
|
+
),
|
|
162
|
+
] = True
|
|
163
|
+
statistical_codes_map_file_name: Annotated[
|
|
164
|
+
Optional[str],
|
|
165
|
+
Field(
|
|
166
|
+
title="Statistical code map file name",
|
|
167
|
+
description=(
|
|
168
|
+
"Path to the file containing the mapping of statistical codes. "
|
|
169
|
+
"The file should be in TSV format with legacy_stat_code and folio_code columns."
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
] = ""
|
|
61
173
|
|
|
62
174
|
@staticmethod
|
|
63
175
|
def get_object_type() -> FOLIONamespaces:
|
|
@@ -67,46 +179,57 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
67
179
|
self,
|
|
68
180
|
task_config: TaskConfiguration,
|
|
69
181
|
library_config: LibraryConfiguration,
|
|
182
|
+
folio_client,
|
|
70
183
|
use_logging: bool = True,
|
|
71
184
|
):
|
|
72
|
-
super().__init__(library_config, task_config, use_logging)
|
|
185
|
+
super().__init__(library_config, task_config, folio_client, use_logging)
|
|
73
186
|
self.fallback_holdings_type = None
|
|
187
|
+
self.folio_keys, self.holdings_field_map = self.load_mapped_fields()
|
|
188
|
+
if any(k for k in self.folio_keys if k.startswith("statisticalCodeIds")):
|
|
189
|
+
statcode_mapping = self.load_ref_data_mapping_file(
|
|
190
|
+
"statisticalCodeIds",
|
|
191
|
+
self.folder_structure.mapping_files_folder
|
|
192
|
+
/ self.task_configuration.statistical_codes_map_file_name,
|
|
193
|
+
self.folio_keys,
|
|
194
|
+
False,
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
statcode_mapping = None
|
|
74
198
|
try:
|
|
75
|
-
self.task_config = task_config
|
|
76
199
|
self.bound_with_keys = set()
|
|
77
|
-
self.files = self.list_source_files()
|
|
78
200
|
self.mapper = HoldingsMapper(
|
|
79
201
|
self.folio_client,
|
|
80
|
-
self.
|
|
202
|
+
self.holdings_field_map,
|
|
81
203
|
self.load_location_map(),
|
|
82
204
|
self.load_call_number_type_map(),
|
|
83
|
-
self.
|
|
205
|
+
self.load_instance_id_map(True),
|
|
84
206
|
library_config,
|
|
207
|
+
task_config,
|
|
208
|
+
statcode_mapping,
|
|
85
209
|
)
|
|
86
210
|
self.holdings = {}
|
|
87
211
|
self.total_records = 0
|
|
88
|
-
self.holdings_id_map = self.load_id_map(
|
|
89
|
-
self.folder_structure.holdings_id_map_path
|
|
90
|
-
)
|
|
91
|
-
self.holdings_sources = self.get_holdings_sources()
|
|
212
|
+
self.holdings_id_map = self.load_id_map(self.folder_structure.holdings_id_map_path)
|
|
92
213
|
self.results_path = self.folder_structure.created_objects_path
|
|
93
214
|
self.holdings_types = list(
|
|
94
215
|
self.folio_client.folio_get_all("/holdings-types", "holdingsTypes")
|
|
95
216
|
)
|
|
96
217
|
logging.info("%s\tholdings types in tenant", len(self.holdings_types))
|
|
97
218
|
self.validate_merge_criterias()
|
|
98
|
-
|
|
219
|
+
self.check_source_files(
|
|
220
|
+
self.folder_structure.data_folder / "items", self.task_configuration.files
|
|
221
|
+
)
|
|
99
222
|
self.fallback_holdings_type = next(
|
|
100
223
|
h
|
|
101
224
|
for h in self.holdings_types
|
|
102
|
-
if h["id"] == self.
|
|
225
|
+
if h["id"] == self.task_configuration.fallback_holdings_type_id
|
|
103
226
|
)
|
|
104
227
|
if not self.fallback_holdings_type:
|
|
105
228
|
raise TransformationProcessError(
|
|
106
229
|
"",
|
|
107
230
|
(
|
|
108
231
|
"Holdings type with ID "
|
|
109
|
-
f"{self.
|
|
232
|
+
f"{self.task_configuration.fallback_holdings_type_id} "
|
|
110
233
|
"not found in FOLIO."
|
|
111
234
|
),
|
|
112
235
|
)
|
|
@@ -114,35 +237,51 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
114
237
|
"%s will be used as default holdings type",
|
|
115
238
|
self.fallback_holdings_type["name"],
|
|
116
239
|
)
|
|
117
|
-
if any(self.
|
|
118
|
-
for file_name in self.
|
|
240
|
+
if any(self.task_configuration.previously_generated_holdings_files):
|
|
241
|
+
for file_name in self.task_configuration.previously_generated_holdings_files:
|
|
119
242
|
logging.info("Processing %s", file_name)
|
|
120
243
|
self.holdings.update(
|
|
121
244
|
HoldingsHelper.load_previously_generated_holdings(
|
|
122
245
|
self.folder_structure.results_folder / file_name,
|
|
123
|
-
self.
|
|
246
|
+
self.task_configuration.holdings_merge_criteria,
|
|
124
247
|
self.mapper.migration_report,
|
|
125
|
-
self.
|
|
248
|
+
self.task_configuration.holdings_type_uuid_for_boundwiths,
|
|
126
249
|
)
|
|
127
250
|
)
|
|
128
251
|
|
|
129
252
|
else:
|
|
130
253
|
logging.info("No file of legacy holdings setup.")
|
|
131
|
-
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
self.task_configuration.reset_hrid_settings
|
|
257
|
+
and self.task_configuration.update_hrid_settings
|
|
258
|
+
):
|
|
259
|
+
hrid_handler = HRIDHandler(
|
|
260
|
+
self.folio_client, HridHandling.default, self.mapper.migration_report, True
|
|
261
|
+
)
|
|
262
|
+
hrid_handler.reset_holdings_hrid_counter()
|
|
263
|
+
|
|
264
|
+
except HTTPError as http_error:
|
|
265
|
+
logging.critical(http_error)
|
|
266
|
+
sys.exit(1)
|
|
267
|
+
except (FileNotFoundError, TransformationProcessError) as process_error:
|
|
132
268
|
logging.critical(process_error)
|
|
133
269
|
logging.critical("Halting.")
|
|
134
270
|
sys.exit(1)
|
|
271
|
+
except json.JSONDecodeError as jde:
|
|
272
|
+
raise jde
|
|
135
273
|
except Exception as exception:
|
|
136
274
|
logging.info("\n=======ERROR===========")
|
|
137
275
|
logging.info(exception)
|
|
138
276
|
logging.info("\n=======Stack Trace===========")
|
|
139
277
|
traceback.print_exc()
|
|
278
|
+
sys.exit(1)
|
|
140
279
|
logging.info("Init done")
|
|
141
280
|
|
|
142
281
|
def load_call_number_type_map(self):
|
|
143
282
|
with open(
|
|
144
283
|
self.folder_structure.mapping_files_folder
|
|
145
|
-
/ self.
|
|
284
|
+
/ self.task_configuration.call_number_type_map_file_name,
|
|
146
285
|
"r",
|
|
147
286
|
) as callnumber_type_map_f:
|
|
148
287
|
return self.load_ref_data_map_from_file(
|
|
@@ -151,8 +290,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
151
290
|
|
|
152
291
|
def load_location_map(self):
|
|
153
292
|
with open(
|
|
154
|
-
self.folder_structure.mapping_files_folder
|
|
155
|
-
/ self.task_config.location_map_file_name
|
|
293
|
+
self.folder_structure.mapping_files_folder / self.task_configuration.location_map_file_name
|
|
156
294
|
) as location_map_f:
|
|
157
295
|
return self.load_ref_data_map_from_file(
|
|
158
296
|
location_map_f, "Found %s rows in location map"
|
|
@@ -166,13 +304,10 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
166
304
|
|
|
167
305
|
def load_mapped_fields(self):
|
|
168
306
|
with open(
|
|
169
|
-
self.folder_structure.mapping_files_folder
|
|
170
|
-
/ self.task_config.holdings_map_file_name
|
|
307
|
+
self.folder_structure.mapping_files_folder / self.task_configuration.holdings_map_file_name
|
|
171
308
|
) as holdings_mapper_f:
|
|
172
309
|
holdings_map = json.load(holdings_mapper_f)
|
|
173
|
-
logging.info(
|
|
174
|
-
"%s fields in holdings mapping file map", len(holdings_map["data"])
|
|
175
|
-
)
|
|
310
|
+
logging.info("%s fields in holdings mapping file map", len(holdings_map["data"]))
|
|
176
311
|
mapped_fields = MappingFileMapperBase.get_mapped_folio_properties_from_map(
|
|
177
312
|
holdings_map
|
|
178
313
|
)
|
|
@@ -180,91 +315,56 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
180
315
|
"%s mapped fields in holdings mapping file map",
|
|
181
316
|
len(list(mapped_fields)),
|
|
182
317
|
)
|
|
183
|
-
return holdings_map
|
|
184
|
-
|
|
185
|
-
def list_source_files(self):
|
|
186
|
-
# Source data files
|
|
187
|
-
files = [
|
|
188
|
-
self.folder_structure.data_folder / "items" / f.file_name
|
|
189
|
-
for f in self.task_config.files
|
|
190
|
-
if isfile(self.folder_structure.data_folder / "items" / f.file_name)
|
|
191
|
-
]
|
|
192
|
-
if not any(files):
|
|
193
|
-
ret_str = ",".join(f.file_name for f in self.task_config.files)
|
|
194
|
-
raise TransformationProcessError(
|
|
195
|
-
"",
|
|
196
|
-
f"Files {ret_str} not found in {self.folder_structure.data_folder / 'items'}",
|
|
197
|
-
)
|
|
198
|
-
logging.info("Files to process:")
|
|
199
|
-
for filename in files:
|
|
200
|
-
logging.info("\t%s", filename)
|
|
201
|
-
return files
|
|
202
|
-
|
|
203
|
-
def load_instance_id_map(self):
|
|
204
|
-
res = {}
|
|
205
|
-
with open(
|
|
206
|
-
self.folder_structure.instance_id_map_path, "r"
|
|
207
|
-
) as instance_id_map_file:
|
|
208
|
-
for index, json_string in enumerate(instance_id_map_file):
|
|
209
|
-
# Format:{"legacy_id", "folio_id","instanceLevelCallNumber"}
|
|
210
|
-
if index % 100000 == 0:
|
|
211
|
-
print(f"{index} instance ids loaded to map", end="\r")
|
|
212
|
-
map_object = json.loads(json_string)
|
|
213
|
-
res[map_object["legacy_id"]] = map_object
|
|
214
|
-
logging.info("Loaded %s migrated instance IDs", (index + 1))
|
|
215
|
-
return res
|
|
318
|
+
return mapped_fields, holdings_map
|
|
216
319
|
|
|
217
320
|
def do_work(self):
|
|
218
321
|
logging.info("Starting....")
|
|
219
|
-
for
|
|
220
|
-
logging.info("Processing %s", file_name)
|
|
322
|
+
for file_def in self.task_configuration.files:
|
|
323
|
+
logging.info("Processing %s", file_def.file_name)
|
|
221
324
|
try:
|
|
222
|
-
self.process_single_file(
|
|
325
|
+
self.process_single_file(file_def)
|
|
223
326
|
except Exception as ee:
|
|
224
327
|
error_str = (
|
|
225
|
-
f"Processing of {file_name} failed:\n{ee}."
|
|
226
|
-
"\nCheck source files for empty
|
|
328
|
+
f"Processing of {file_def.file_name} failed:\n{ee}."
|
|
329
|
+
"\nCheck source files for empty rows or missing reference data"
|
|
227
330
|
)
|
|
228
331
|
logging.critical(error_str)
|
|
229
332
|
print(f"\n{error_str}\nHalting")
|
|
230
333
|
sys.exit(1)
|
|
231
334
|
logging.info(
|
|
232
|
-
f"processed {self.total_records:,} records in {len(self.files)} files"
|
|
335
|
+
f"processed {self.total_records:,} records in {len(self.task_configuration.files)} files"
|
|
233
336
|
)
|
|
234
337
|
|
|
235
338
|
def wrap_up(self):
|
|
236
|
-
logging.info("
|
|
339
|
+
logging.info("Done. Transformer wrapping up...")
|
|
340
|
+
self.extradata_writer.flush()
|
|
237
341
|
if any(self.holdings):
|
|
238
342
|
logging.info(
|
|
239
343
|
"Saving holdings created to %s",
|
|
240
344
|
self.folder_structure.created_objects_path,
|
|
241
345
|
)
|
|
242
|
-
with open(
|
|
243
|
-
self.folder_structure.created_objects_path, "w+"
|
|
244
|
-
) as holdings_file:
|
|
346
|
+
with open(self.folder_structure.created_objects_path, "w+") as holdings_file:
|
|
245
347
|
for holding in self.holdings.values():
|
|
246
348
|
for legacy_id in holding["formerIds"]:
|
|
247
349
|
# Prevent the first item in a boundwith to be overwritten
|
|
248
350
|
# TODO: Find out why not
|
|
249
351
|
# if legacy_id not in self.holdings_id_map:
|
|
250
|
-
self.holdings_id_map[legacy_id] = self.mapper.
|
|
251
|
-
legacy_id, holding
|
|
352
|
+
self.holdings_id_map[legacy_id] = self.mapper.get_id_map_tuple(
|
|
353
|
+
legacy_id, holding, self.object_type
|
|
252
354
|
)
|
|
253
355
|
Helper.write_to_file(holdings_file, holding)
|
|
254
356
|
self.mapper.migration_report.add_general_statistics(
|
|
255
|
-
"Holdings Records Written to disk"
|
|
357
|
+
i18n.t("Holdings Records Written to disk")
|
|
256
358
|
)
|
|
257
359
|
self.mapper.save_id_map_file(
|
|
258
360
|
self.folder_structure.holdings_id_map_path, self.holdings_id_map
|
|
259
361
|
)
|
|
260
|
-
with open(
|
|
261
|
-
self.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
self.folder_structure.migration_reports_file,
|
|
362
|
+
with open(self.folder_structure.migration_reports_file, "w") as migration_report_file:
|
|
363
|
+
self.mapper.migration_report.write_migration_report(
|
|
364
|
+
i18n.t("Holdings transformation report"),
|
|
365
|
+
migration_report_file,
|
|
366
|
+
self.mapper.start_datetime,
|
|
266
367
|
)
|
|
267
|
-
self.mapper.migration_report.write_migration_report(migration_report_file)
|
|
268
368
|
Helper.print_mapping_report(
|
|
269
369
|
migration_report_file,
|
|
270
370
|
self.total_records,
|
|
@@ -272,17 +372,14 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
272
372
|
self.mapper.mapped_legacy_fields,
|
|
273
373
|
)
|
|
274
374
|
logging.info("All done!")
|
|
375
|
+
self.clean_out_empty_logs()
|
|
275
376
|
|
|
276
377
|
def validate_merge_criterias(self):
|
|
277
378
|
holdings_schema = self.folio_client.get_holdings_schema()
|
|
278
379
|
properties = holdings_schema["properties"].keys()
|
|
279
380
|
logging.info(properties)
|
|
280
|
-
logging.info(self.
|
|
281
|
-
res = [
|
|
282
|
-
mc
|
|
283
|
-
for mc in self.task_config.holdings_merge_criteria
|
|
284
|
-
if mc not in properties
|
|
285
|
-
]
|
|
381
|
+
logging.info(self.task_configuration.holdings_merge_criteria)
|
|
382
|
+
res = [mc for mc in self.task_configuration.holdings_merge_criteria if mc not in properties]
|
|
286
383
|
if any(res):
|
|
287
384
|
logging.critical(
|
|
288
385
|
(
|
|
@@ -293,23 +390,22 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
293
390
|
)
|
|
294
391
|
sys.exit(1)
|
|
295
392
|
|
|
296
|
-
def process_single_file(self,
|
|
297
|
-
|
|
393
|
+
def process_single_file(self, file_def: FileDefinition):
|
|
394
|
+
full_path = self.folder_structure.data_folder / "items" / file_def.file_name
|
|
395
|
+
with open(full_path, encoding="utf-8-sig") as records_file:
|
|
298
396
|
self.mapper.migration_report.add_general_statistics(
|
|
299
|
-
"Number of files processed"
|
|
397
|
+
i18n.t("Number of files processed")
|
|
300
398
|
)
|
|
301
399
|
start = time.time()
|
|
302
400
|
records_processed = 0
|
|
303
|
-
for idx, legacy_record in enumerate(
|
|
304
|
-
self.mapper.get_objects(records_file, file_name)
|
|
305
|
-
):
|
|
401
|
+
for idx, legacy_record in enumerate(self.mapper.get_objects(records_file, full_path)):
|
|
306
402
|
records_processed = idx + 1
|
|
307
403
|
try:
|
|
308
404
|
self.mapper.verify_legacy_record(legacy_record, idx)
|
|
309
405
|
folio_rec, legacy_id = self.mapper.do_map(
|
|
310
406
|
legacy_record, f"row # {idx}", FOLIONamespaces.holdings
|
|
311
407
|
)
|
|
312
|
-
self.post_process_holding(folio_rec, legacy_id)
|
|
408
|
+
self.post_process_holding(folio_rec, legacy_id, file_def)
|
|
313
409
|
except TransformationProcessError as process_error:
|
|
314
410
|
self.mapper.handle_transformation_process_error(idx, process_error)
|
|
315
411
|
except TransformationRecordFailedError as error:
|
|
@@ -317,27 +413,25 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
317
413
|
except Exception as excepion:
|
|
318
414
|
self.mapper.handle_generic_exception(idx, excepion)
|
|
319
415
|
self.mapper.migration_report.add_general_statistics(
|
|
320
|
-
"Number of Legacy items in file"
|
|
416
|
+
i18n.t("Number of Legacy items in file")
|
|
321
417
|
)
|
|
322
418
|
if idx > 1 and idx % 10000 == 0:
|
|
323
419
|
elapsed = idx / (time.time() - start)
|
|
324
420
|
elapsed_formatted = "{0:.4g}".format(elapsed)
|
|
325
|
-
logging.info(
|
|
326
|
-
f"{idx:,} records processed. Recs/sec: {elapsed_formatted} "
|
|
327
|
-
)
|
|
421
|
+
logging.info(f"{idx:,} records processed. Recs/sec: {elapsed_formatted} ")
|
|
328
422
|
self.total_records = records_processed
|
|
329
423
|
logging.info(
|
|
330
|
-
f"Done processing {file_name} containing {self.total_records:,} records. "
|
|
424
|
+
f"Done processing {file_def.file_name} containing {self.total_records:,} records. "
|
|
331
425
|
f"Total records processed: {self.total_records:,}"
|
|
332
426
|
)
|
|
333
427
|
|
|
334
|
-
def post_process_holding(self, folio_rec: dict, legacy_id: str):
|
|
428
|
+
def post_process_holding(self, folio_rec: dict, legacy_id: str, file_def: FileDefinition):
|
|
335
429
|
HoldingsHelper.handle_notes(folio_rec)
|
|
430
|
+
HoldingsHelper.remove_empty_holdings_statements(folio_rec)
|
|
431
|
+
|
|
336
432
|
if not folio_rec.get("holdingsTypeId", ""):
|
|
337
433
|
folio_rec["holdingsTypeId"] = self.fallback_holdings_type["id"]
|
|
338
434
|
|
|
339
|
-
folio_rec["sourceId"] = self.holdings_sources.get("FOLIO")
|
|
340
|
-
|
|
341
435
|
holdings_from_row = []
|
|
342
436
|
all_instance_ids = folio_rec.get("instanceId", [])
|
|
343
437
|
if len(all_instance_ids) == 1:
|
|
@@ -346,152 +440,77 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
346
440
|
holdings_from_row.append(folio_rec)
|
|
347
441
|
|
|
348
442
|
elif len(folio_rec.get("instanceId", [])) > 1: # Bound-with.
|
|
349
|
-
holdings_from_row.extend(
|
|
350
|
-
self.create_bound_with_holdings(folio_rec, legacy_id)
|
|
351
|
-
)
|
|
443
|
+
holdings_from_row.extend(self.create_bound_with_holdings(folio_rec, legacy_id))
|
|
352
444
|
else:
|
|
353
|
-
raise TransformationRecordFailedError(
|
|
354
|
-
legacy_id, "No instance id in parsed record", ""
|
|
355
|
-
)
|
|
445
|
+
raise TransformationRecordFailedError(legacy_id, "No instance id in parsed record", "")
|
|
356
446
|
|
|
357
447
|
for folio_holding in holdings_from_row:
|
|
448
|
+
self.mapper.perform_additional_mappings(legacy_id, folio_holding, file_def)
|
|
358
449
|
self.merge_holding_in(folio_holding, all_instance_ids, legacy_id)
|
|
359
450
|
self.mapper.report_folio_mapping(folio_holding, self.mapper.schema)
|
|
360
451
|
|
|
361
452
|
def create_bound_with_holdings(self, folio_holding, legacy_id: str):
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
"
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# Add former ids
|
|
370
|
-
temp_ids = []
|
|
371
|
-
for former_id in folio_holding.get("formerIds", []):
|
|
372
|
-
if (
|
|
373
|
-
former_id.startswith("[")
|
|
374
|
-
and former_id.endswith("]")
|
|
375
|
-
and "," in former_id
|
|
376
|
-
):
|
|
377
|
-
ids = list(
|
|
378
|
-
former_id[1:-1]
|
|
379
|
-
.replace('"', "")
|
|
380
|
-
.replace(" ", "")
|
|
381
|
-
.replace("'", "")
|
|
382
|
-
.split(",")
|
|
383
|
-
)
|
|
384
|
-
temp_ids.extend(ids)
|
|
385
|
-
else:
|
|
386
|
-
temp_ids.append(former_id)
|
|
387
|
-
folio_holding["formerIds"] = temp_ids
|
|
388
|
-
for bwidx, instance_id in enumerate(folio_holding["instanceId"]):
|
|
389
|
-
if not instance_id:
|
|
390
|
-
raise ValueError(f"No ID for record {folio_holding}")
|
|
391
|
-
|
|
392
|
-
bound_with_holding = copy.deepcopy(folio_holding)
|
|
393
|
-
bound_with_holding["instanceId"] = instance_id
|
|
394
|
-
if folio_holding.get("callNumber", None):
|
|
395
|
-
call_numbers = ast.literal_eval(folio_holding["callNumber"])
|
|
396
|
-
if isinstance(call_numbers, str):
|
|
397
|
-
call_numbers = [call_numbers]
|
|
398
|
-
bound_with_holding["callNumber"] = call_numbers[bwidx]
|
|
399
|
-
if not self.task_config.holdings_type_uuid_for_boundwiths:
|
|
400
|
-
raise TransformationProcessError(
|
|
401
|
-
"",
|
|
402
|
-
(
|
|
403
|
-
"Boundwith UUID not added to task configuration."
|
|
404
|
-
"Add a property to holdingsTypeUuidForBoundwiths to "
|
|
405
|
-
"the task configuration"
|
|
406
|
-
),
|
|
407
|
-
"",
|
|
408
|
-
)
|
|
409
|
-
bound_with_holding[
|
|
410
|
-
"holdingsTypeId"
|
|
411
|
-
] = self.task_config.holdings_type_uuid_for_boundwiths
|
|
412
|
-
bound_with_holding["id"] = str(
|
|
413
|
-
FolioUUID(
|
|
414
|
-
self.folio_client.okapi_url,
|
|
415
|
-
FOLIONamespaces.holdings,
|
|
416
|
-
f'{folio_holding["id"]}-{instance_id}',
|
|
417
|
-
)
|
|
453
|
+
folio_holding["formerIds"] = explode_former_ids(folio_holding)
|
|
454
|
+
return list(
|
|
455
|
+
self.mapper.create_bound_with_holdings(
|
|
456
|
+
folio_holding,
|
|
457
|
+
folio_holding["instanceId"],
|
|
458
|
+
self.task_configuration.holdings_type_uuid_for_boundwiths,
|
|
418
459
|
)
|
|
419
|
-
|
|
420
|
-
"Bound-with holdings created"
|
|
421
|
-
)
|
|
422
|
-
yield bound_with_holding
|
|
423
|
-
|
|
424
|
-
@staticmethod
|
|
425
|
-
def generate_boundwith_part(
|
|
426
|
-
folio_client: FolioClient, legacy_item_id: str, bound_with_holding: dict
|
|
427
|
-
):
|
|
428
|
-
part = {
|
|
429
|
-
"id": str(uuid.uuid4()),
|
|
430
|
-
"holdingsRecordId": bound_with_holding["id"],
|
|
431
|
-
"itemId": str(
|
|
432
|
-
FolioUUID(
|
|
433
|
-
folio_client.okapi_url,
|
|
434
|
-
FOLIONamespaces.items,
|
|
435
|
-
legacy_item_id,
|
|
436
|
-
)
|
|
437
|
-
),
|
|
438
|
-
}
|
|
439
|
-
logging.log(25, f"boundwithPart\t{json.dumps(part)}")
|
|
460
|
+
)
|
|
440
461
|
|
|
441
462
|
def merge_holding_in(
|
|
442
463
|
self, incoming_holding: dict, instance_ids: list[str], legacy_item_id: str
|
|
443
|
-
):
|
|
464
|
+
) -> None:
|
|
444
465
|
"""Determines what newly generated holdingsrecords are to be merged with
|
|
445
466
|
previously created ones. When that is done, it generates the correct boundwith
|
|
446
467
|
parts needed.
|
|
447
468
|
|
|
448
469
|
Args:
|
|
449
|
-
|
|
470
|
+
incoming_holding (dict): The newly created FOLIO Holding
|
|
450
471
|
instance_ids (list): the instance IDs tied to the current item
|
|
451
472
|
legacy_item_id (str): Id of the Item the holding was generated from
|
|
452
473
|
"""
|
|
453
|
-
|
|
454
474
|
if len(instance_ids) > 1:
|
|
455
475
|
# Is boundwith
|
|
456
476
|
bw_key = (
|
|
457
|
-
f"bw_{incoming_holding['instanceId']}_{'
|
|
477
|
+
f"bw_{incoming_holding['instanceId']}_{incoming_holding['permanentLocationId']}_"
|
|
478
|
+
f"{incoming_holding.get('callNumber', '')}_{'_'.join(sorted(instance_ids))}"
|
|
458
479
|
)
|
|
459
480
|
if bw_key not in self.bound_with_keys:
|
|
460
481
|
self.bound_with_keys.add(bw_key)
|
|
461
482
|
self.holdings[bw_key] = incoming_holding
|
|
462
|
-
self.
|
|
463
|
-
self.folio_client, legacy_item_id, incoming_holding
|
|
464
|
-
)
|
|
483
|
+
self.mapper.create_and_write_boundwith_part(legacy_item_id, incoming_holding["id"])
|
|
465
484
|
self.mapper.migration_report.add_general_statistics(
|
|
466
|
-
"Unique BW Holdings created from Items"
|
|
485
|
+
i18n.t("Unique BW Holdings created from Items")
|
|
467
486
|
)
|
|
468
487
|
else:
|
|
469
488
|
self.merge_holding(bw_key, incoming_holding)
|
|
470
|
-
self.
|
|
471
|
-
|
|
489
|
+
self.mapper.create_and_write_boundwith_part(
|
|
490
|
+
legacy_item_id, self.holdings[bw_key]["id"]
|
|
472
491
|
)
|
|
473
|
-
self.holdings_id_map[legacy_item_id] = self.mapper.
|
|
474
|
-
legacy_item_id, self.holdings[bw_key]
|
|
492
|
+
self.holdings_id_map[legacy_item_id] = self.mapper.get_id_map_tuple(
|
|
493
|
+
legacy_item_id, self.holdings[bw_key], self.object_type
|
|
475
494
|
)
|
|
476
495
|
self.mapper.migration_report.add_general_statistics(
|
|
477
|
-
"BW Items found tied to previously created BW Holdings"
|
|
496
|
+
i18n.t("BW Items found tied to previously created BW Holdings")
|
|
478
497
|
)
|
|
479
498
|
else:
|
|
480
499
|
# Regular holding. Merge according to criteria
|
|
481
500
|
new_holding_key = HoldingsHelper.to_key(
|
|
482
501
|
incoming_holding,
|
|
483
|
-
self.
|
|
502
|
+
self.task_configuration.holdings_merge_criteria,
|
|
484
503
|
self.mapper.migration_report,
|
|
485
|
-
self.
|
|
504
|
+
self.task_configuration.holdings_type_uuid_for_boundwiths,
|
|
486
505
|
)
|
|
487
506
|
if self.holdings.get(new_holding_key, None):
|
|
488
507
|
self.mapper.migration_report.add_general_statistics(
|
|
489
|
-
"Holdings already created from Item"
|
|
508
|
+
i18n.t("Holdings already created from Item")
|
|
490
509
|
)
|
|
491
510
|
self.merge_holding(new_holding_key, incoming_holding)
|
|
492
511
|
else:
|
|
493
512
|
self.mapper.migration_report.add_general_statistics(
|
|
494
|
-
"Unique Holdings created from Items"
|
|
513
|
+
i18n.t("Unique Holdings created from Items")
|
|
495
514
|
)
|
|
496
515
|
self.holdings[new_holding_key] = incoming_holding
|
|
497
516
|
|
|
@@ -500,29 +519,15 @@ class HoldingsCsvTransformer(MigrationTaskBase):
|
|
|
500
519
|
self.holdings[holdings_key], new_holdings_record
|
|
501
520
|
)
|
|
502
521
|
|
|
503
|
-
def get_holdings_sources(self):
|
|
504
|
-
res = {}
|
|
505
|
-
if self.library_configuration.folio_release != FolioRelease.juniper:
|
|
506
|
-
holdings_sources = list(
|
|
507
|
-
self.mapper.folio_client.folio_get_all(
|
|
508
|
-
"/holdings-sources", "holdingsRecordsSources"
|
|
509
|
-
)
|
|
510
|
-
)
|
|
511
|
-
logging.info(
|
|
512
|
-
"Fetched %s holdingsRecordsSources from tenant", len(holdings_sources)
|
|
513
|
-
)
|
|
514
|
-
res = {n["name"].upper(): n["id"] for n in holdings_sources}
|
|
515
|
-
if "FOLIO" not in res:
|
|
516
|
-
raise TransformationProcessError(
|
|
517
|
-
"", "No holdings source with name FOLIO in tenant"
|
|
518
|
-
)
|
|
519
|
-
if "MARC" not in res:
|
|
520
|
-
raise TransformationProcessError(
|
|
521
|
-
"", "No holdings source with name MARC in tenant"
|
|
522
|
-
)
|
|
523
|
-
return res
|
|
524
|
-
|
|
525
522
|
|
|
526
|
-
def
|
|
527
|
-
|
|
528
|
-
|
|
523
|
+
def explode_former_ids(folio_holding: dict):
|
|
524
|
+
temp_ids = []
|
|
525
|
+
for former_id in folio_holding.get("formerIds", []):
|
|
526
|
+
if former_id.startswith("[") and former_id.endswith("]") and "," in former_id:
|
|
527
|
+
ids = list(
|
|
528
|
+
former_id[1:-1].replace('"', "").replace(" ", "").replace("'", "").split(",")
|
|
529
|
+
)
|
|
530
|
+
temp_ids.extend(ids)
|
|
531
|
+
else:
|
|
532
|
+
temp_ids.append(former_id)
|
|
533
|
+
return temp_ids
|