folio-migration-tools 1.9.10__py3-none-any.whl → 1.10.0b1__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 +3 -4
- folio_migration_tools/__main__.py +44 -31
- folio_migration_tools/circulation_helper.py +114 -105
- folio_migration_tools/custom_dict.py +2 -2
- folio_migration_tools/custom_exceptions.py +4 -5
- folio_migration_tools/folder_structure.py +1 -1
- folio_migration_tools/helper.py +1 -1
- folio_migration_tools/library_configuration.py +65 -37
- folio_migration_tools/mapper_base.py +38 -25
- folio_migration_tools/mapping_file_transformation/courses_mapper.py +1 -1
- folio_migration_tools/mapping_file_transformation/holdings_mapper.py +7 -3
- folio_migration_tools/mapping_file_transformation/item_mapper.py +13 -26
- folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +1 -2
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +13 -11
- folio_migration_tools/mapping_file_transformation/order_mapper.py +6 -5
- folio_migration_tools/mapping_file_transformation/organization_mapper.py +3 -3
- folio_migration_tools/mapping_file_transformation/user_mapper.py +43 -28
- folio_migration_tools/marc_rules_transformation/conditions.py +84 -70
- folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +13 -5
- folio_migration_tools/marc_rules_transformation/hrid_handler.py +3 -2
- folio_migration_tools/marc_rules_transformation/marc_file_processor.py +14 -22
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +1 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +46 -36
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +25 -15
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +62 -32
- folio_migration_tools/migration_report.py +1 -1
- folio_migration_tools/migration_tasks/authority_transformer.py +1 -2
- folio_migration_tools/migration_tasks/batch_poster.py +78 -68
- folio_migration_tools/migration_tasks/bibs_transformer.py +12 -7
- folio_migration_tools/migration_tasks/courses_migrator.py +2 -3
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py +14 -15
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py +11 -21
- folio_migration_tools/migration_tasks/items_transformer.py +17 -30
- folio_migration_tools/migration_tasks/loans_migrator.py +53 -131
- folio_migration_tools/migration_tasks/migration_task_base.py +33 -55
- folio_migration_tools/migration_tasks/orders_transformer.py +21 -39
- folio_migration_tools/migration_tasks/organization_transformer.py +9 -18
- folio_migration_tools/migration_tasks/requests_migrator.py +11 -15
- folio_migration_tools/migration_tasks/reserves_migrator.py +1 -1
- folio_migration_tools/migration_tasks/user_transformer.py +10 -15
- folio_migration_tools/task_configuration.py +6 -7
- folio_migration_tools/transaction_migration/legacy_loan.py +15 -27
- folio_migration_tools/transaction_migration/legacy_request.py +1 -1
- {folio_migration_tools-1.9.10.dist-info → folio_migration_tools-1.10.0b1.dist-info}/METADATA +18 -28
- {folio_migration_tools-1.9.10.dist-info → folio_migration_tools-1.10.0b1.dist-info}/RECORD +47 -50
- folio_migration_tools-1.10.0b1.dist-info/WHEEL +4 -0
- folio_migration_tools-1.10.0b1.dist-info/entry_points.txt +3 -0
- folio_migration_tools/test_infrastructure/__init__.py +0 -0
- folio_migration_tools/test_infrastructure/mocked_classes.py +0 -406
- folio_migration_tools-1.9.10.dist-info/WHEEL +0 -4
- folio_migration_tools-1.9.10.dist-info/entry_points.txt +0 -3
- folio_migration_tools-1.9.10.dist-info/licenses/LICENSE +0 -21
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import Annotated
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic import BaseModel, Field, model_validator
|
|
5
5
|
from pydantic.types import DirectoryPath
|
|
6
6
|
|
|
7
7
|
|
|
@@ -25,8 +25,7 @@ class FileDefinition(BaseModel):
|
|
|
25
25
|
Field(
|
|
26
26
|
title="File name",
|
|
27
27
|
description=(
|
|
28
|
-
"Name of the file to be processed. "
|
|
29
|
-
"The location of the file depends on the context"
|
|
28
|
+
"Name of the file to be processed. The location of the file depends on the context"
|
|
30
29
|
),
|
|
31
30
|
),
|
|
32
31
|
] = ""
|
|
@@ -37,10 +36,9 @@ class FileDefinition(BaseModel):
|
|
|
37
36
|
Field(
|
|
38
37
|
title="Service point ID",
|
|
39
38
|
description=(
|
|
40
|
-
"Service point to be used for "
|
|
41
|
-
"transactions created from this file (Loans-only)."
|
|
39
|
+
"Service point to be used for transactions created from this file (Loans-only)."
|
|
42
40
|
),
|
|
43
|
-
)
|
|
41
|
+
),
|
|
44
42
|
] = ""
|
|
45
43
|
statistical_code: Annotated[
|
|
46
44
|
str,
|
|
@@ -51,7 +49,7 @@ class FileDefinition(BaseModel):
|
|
|
51
49
|
"this file (Instances, Holdings, Items). Specify multiple codes using "
|
|
52
50
|
"multi_field_delimiter."
|
|
53
51
|
),
|
|
54
|
-
)
|
|
52
|
+
),
|
|
55
53
|
] = ""
|
|
56
54
|
create_source_records: Annotated[
|
|
57
55
|
bool,
|
|
@@ -85,6 +83,7 @@ class FolioRelease(str, Enum):
|
|
|
85
83
|
ramsons = "ramsons"
|
|
86
84
|
sunflower = "sunflower"
|
|
87
85
|
trillium = "trillium"
|
|
86
|
+
umbrellaleaf = "umbrellaleaf"
|
|
88
87
|
|
|
89
88
|
|
|
90
89
|
class LibraryConfiguration(BaseModel):
|
|
@@ -96,7 +95,6 @@ class LibraryConfiguration(BaseModel):
|
|
|
96
95
|
"The URL of the FOLIO API gateway instance. "
|
|
97
96
|
"You can find this in Settings > Software versions > API gateway services."
|
|
98
97
|
),
|
|
99
|
-
alias="okapi_url"
|
|
100
98
|
),
|
|
101
99
|
]
|
|
102
100
|
tenant_id: Annotated[
|
|
@@ -106,7 +104,8 @@ class LibraryConfiguration(BaseModel):
|
|
|
106
104
|
description=(
|
|
107
105
|
"The ID of the FOLIO tenant instance. "
|
|
108
106
|
"You can find this in Settings > Software versions > API gateway services. "
|
|
109
|
-
"In an ECS environment, this is the ID of the central tenant, for all
|
|
107
|
+
"In an ECS environment, this is the ID of the central tenant, for all "
|
|
108
|
+
"configurations."
|
|
110
109
|
),
|
|
111
110
|
),
|
|
112
111
|
]
|
|
@@ -128,18 +127,14 @@ class LibraryConfiguration(BaseModel):
|
|
|
128
127
|
"The username for the FOLIO user account performing the migration. "
|
|
129
128
|
"User should have a full admin permissions/roles in FOLIO. "
|
|
130
129
|
),
|
|
131
|
-
alias="okapi_username"
|
|
132
130
|
),
|
|
133
131
|
]
|
|
134
132
|
folio_password: Annotated[
|
|
135
133
|
str,
|
|
136
134
|
Field(
|
|
137
135
|
title="FOLIO API Gateway password",
|
|
138
|
-
description=(
|
|
139
|
-
|
|
140
|
-
),
|
|
141
|
-
alias="okapi_password"
|
|
142
|
-
)
|
|
136
|
+
description=("The password for the FOLIO user account performing the migration. "),
|
|
137
|
+
),
|
|
143
138
|
]
|
|
144
139
|
base_folder: DirectoryPath = Field(
|
|
145
140
|
description=(
|
|
@@ -153,7 +148,8 @@ class LibraryConfiguration(BaseModel):
|
|
|
153
148
|
title="Multi field delimiter",
|
|
154
149
|
description=(
|
|
155
150
|
"The delimiter used to separate multiple values in a single field. "
|
|
156
|
-
"This is used for delimited text (CSV/TSV) fields with multiple sub-delimited
|
|
151
|
+
"This is used for delimited text (CSV/TSV) fields with multiple sub-delimited "
|
|
152
|
+
"values."
|
|
157
153
|
),
|
|
158
154
|
),
|
|
159
155
|
] = "<delimiter>"
|
|
@@ -163,36 +159,42 @@ class LibraryConfiguration(BaseModel):
|
|
|
163
159
|
] = 5000
|
|
164
160
|
failed_percentage_threshold: Annotated[
|
|
165
161
|
int,
|
|
166
|
-
Field(
|
|
167
|
-
description=("Percentage of failed records until the process shuts down")
|
|
168
|
-
),
|
|
162
|
+
Field(description=("Percentage of failed records until the process shuts down")),
|
|
169
163
|
] = 20
|
|
170
164
|
generic_exception_threshold: Annotated[
|
|
171
165
|
int,
|
|
166
|
+
Field(description=("Number of generic exceptions until the process shuts down")),
|
|
167
|
+
] = 50
|
|
168
|
+
library_name: Annotated[str, Field(description="Name of the library being migrated")]
|
|
169
|
+
log_level_debug: Annotated[bool, Field(description="Enable debug level logging")] = False
|
|
170
|
+
folio_release: Annotated[
|
|
171
|
+
FolioRelease,
|
|
172
172
|
Field(
|
|
173
|
-
description=(
|
|
173
|
+
description=(
|
|
174
|
+
"The Flavour of the ILS you are migrating from. This choice is "
|
|
175
|
+
"maninly tied to the handling of legacy identifiers and thereby the "
|
|
176
|
+
"deterministic UUIDs generated from them."
|
|
177
|
+
)
|
|
174
178
|
),
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
]
|
|
180
|
+
iteration_identifier: Annotated[
|
|
181
|
+
str,
|
|
182
|
+
Field(
|
|
183
|
+
description="The name of the current directory under base_folder/iterations/ to be "
|
|
184
|
+
"used for this migration."
|
|
185
|
+
),
|
|
186
|
+
]
|
|
187
|
+
add_time_stamp_to_file_names: Annotated[bool, Field(title="Add time stamp to file names")] = (
|
|
188
|
+
False
|
|
184
189
|
)
|
|
185
|
-
iteration_identifier: str
|
|
186
|
-
add_time_stamp_to_file_names: Annotated[
|
|
187
|
-
bool, Field(title="Add time stamp to file names")
|
|
188
|
-
] = False
|
|
189
190
|
use_gateway_url_for_uuids: Annotated[
|
|
190
191
|
bool,
|
|
191
192
|
Field(
|
|
192
193
|
title="Use gateway URL for UUIDs",
|
|
193
194
|
description=(
|
|
194
|
-
"If set to true, folio_uuid will use the gateway URL when generating deterministic
|
|
195
|
-
"If set to false (default), the UUIDs will be generated
|
|
195
|
+
"If set to true, folio_uuid will use the gateway URL when generating deterministic"
|
|
196
|
+
" UUIDs for FOLIO records. If set to false (default), the UUIDs will be generated"
|
|
197
|
+
" using the tenant_id (or ecs_tenant_id)."
|
|
196
198
|
),
|
|
197
199
|
),
|
|
198
200
|
] = False
|
|
@@ -212,8 +214,34 @@ class LibraryConfiguration(BaseModel):
|
|
|
212
214
|
Field(
|
|
213
215
|
title="ECS central iteration identifier",
|
|
214
216
|
description=(
|
|
215
|
-
"The iteration_identifier value from the central tenant configuration that
|
|
216
|
-
"to this configuration's iteration_identifier. Used to access the
|
|
217
|
+
"The iteration_identifier value from the central tenant configuration that "
|
|
218
|
+
"corresponds to this configuration's iteration_identifier. Used to access the "
|
|
219
|
+
"central instances_id_map."
|
|
217
220
|
),
|
|
218
221
|
),
|
|
219
222
|
] = ""
|
|
223
|
+
|
|
224
|
+
@model_validator(mode="before")
|
|
225
|
+
@classmethod
|
|
226
|
+
def handle_legacy_field_names(cls, values):
|
|
227
|
+
"""Handle backward compatibility for legacy okapi field names."""
|
|
228
|
+
# Handle folio_password / okapi_password backward compatibility
|
|
229
|
+
if "folio_password" not in values and "okapi_password" in values:
|
|
230
|
+
values["folio_password"] = values["okapi_password"]
|
|
231
|
+
if "gateway_url" not in values and "okapi_url" in values:
|
|
232
|
+
values["gateway_url"] = values["okapi_url"]
|
|
233
|
+
if "folio_username" not in values and "okapi_username" in values:
|
|
234
|
+
values["folio_username"] = values["okapi_username"]
|
|
235
|
+
return values
|
|
236
|
+
|
|
237
|
+
@model_validator(mode="before")
|
|
238
|
+
@classmethod
|
|
239
|
+
def set_error_thresholds_for_debug(cls, values):
|
|
240
|
+
"""If log_level_debug is true, set error thresholds to very high values to avoid
|
|
241
|
+
process shutdown during debugging.
|
|
242
|
+
"""
|
|
243
|
+
if values.get("log_level_debug", False):
|
|
244
|
+
values["failed_records_threshold"] = 10_000_000
|
|
245
|
+
values["failed_percentage_threshold"] = 100
|
|
246
|
+
values["generic_exception_threshold"] = 10_000_000
|
|
247
|
+
return values
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
import uuid
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Dict, List,
|
|
9
|
+
from typing import Dict, List, Tuple
|
|
10
10
|
|
|
11
11
|
import i18n
|
|
12
12
|
from folio_uuid.folio_namespaces import FOLIONamespaces
|
|
@@ -38,10 +38,10 @@ class MapperBase:
|
|
|
38
38
|
library_configuration: LibraryConfiguration,
|
|
39
39
|
task_configuration: AbstractTaskConfiguration,
|
|
40
40
|
folio_client: FolioClient,
|
|
41
|
-
parent_id_map: Dict[str, Tuple] =
|
|
41
|
+
parent_id_map: Dict[str, Tuple] | None = None,
|
|
42
42
|
):
|
|
43
43
|
logging.info("MapperBase initiating")
|
|
44
|
-
self.parent_id_map: dict[str, tuple] = parent_id_map
|
|
44
|
+
self.parent_id_map: dict[str, tuple] = parent_id_map or {}
|
|
45
45
|
self.extradata_writer: ExtradataWriter = ExtradataWriter(Path(""))
|
|
46
46
|
self.start_datetime = datetime.now(timezone.utc)
|
|
47
47
|
self.folio_client: FolioClient = folio_client
|
|
@@ -119,8 +119,8 @@ class MapperBase:
|
|
|
119
119
|
self.migration_report.add(
|
|
120
120
|
ref_data_mapping.blurb_id,
|
|
121
121
|
(
|
|
122
|
-
f'
|
|
123
|
-
f
|
|
122
|
+
f"{' - '.join(fieldvalues)} "
|
|
123
|
+
f"-> {right_mapping[f'folio_{ref_data_mapping.key_type}']}"
|
|
124
124
|
),
|
|
125
125
|
)
|
|
126
126
|
return next(v for k, v in right_mapping.items() if k.startswith("folio_"))
|
|
@@ -129,14 +129,14 @@ class MapperBase:
|
|
|
129
129
|
if prevent_default:
|
|
130
130
|
self.migration_report.add(
|
|
131
131
|
ref_data_mapping.blurb_id,
|
|
132
|
-
(f
|
|
132
|
+
(f'Not to be mapped. (No default) -- {" - ".join(fieldvalues)} -> ""'),
|
|
133
133
|
)
|
|
134
134
|
return ""
|
|
135
135
|
self.migration_report.add(
|
|
136
136
|
ref_data_mapping.blurb_id,
|
|
137
137
|
(
|
|
138
138
|
f"Unmapped (Default value was set) -- "
|
|
139
|
-
f'
|
|
139
|
+
f"{' - '.join(fieldvalues)} -> {ref_data_mapping.default_name}"
|
|
140
140
|
),
|
|
141
141
|
)
|
|
142
142
|
return ref_data_mapping.default_name
|
|
@@ -192,8 +192,8 @@ class MapperBase:
|
|
|
192
192
|
self.migration_report.add(
|
|
193
193
|
ref_data_mapping.blurb_id,
|
|
194
194
|
(
|
|
195
|
-
f'
|
|
196
|
-
f
|
|
195
|
+
f"{' - '.join(fieldvalues)} "
|
|
196
|
+
f"-> {right_mapping[f'folio_{ref_data_mapping.key_type}']}"
|
|
197
197
|
),
|
|
198
198
|
)
|
|
199
199
|
return right_mapping["folio_id"]
|
|
@@ -201,14 +201,14 @@ class MapperBase:
|
|
|
201
201
|
if prevent_default:
|
|
202
202
|
self.migration_report.add(
|
|
203
203
|
ref_data_mapping.blurb_id,
|
|
204
|
-
(f
|
|
204
|
+
(f'Not to be mapped. (No default) -- {" - ".join(fieldvalues)} -> ""'),
|
|
205
205
|
)
|
|
206
206
|
return ""
|
|
207
207
|
self.migration_report.add(
|
|
208
208
|
ref_data_mapping.blurb_id,
|
|
209
209
|
(
|
|
210
210
|
f"Unmapped (Default value was set) -- "
|
|
211
|
-
f'
|
|
211
|
+
f"{' - '.join(fieldvalues)} -> {ref_data_mapping.default_name}"
|
|
212
212
|
),
|
|
213
213
|
)
|
|
214
214
|
return ref_data_mapping.default_id
|
|
@@ -430,7 +430,7 @@ class MapperBase:
|
|
|
430
430
|
folio_holding["id"], instance_uuid
|
|
431
431
|
)
|
|
432
432
|
if bound_with_holding.get("hrid", ""):
|
|
433
|
-
bound_with_holding["hrid"] = f
|
|
433
|
+
bound_with_holding["hrid"] = f"{bound_with_holding['hrid']}_bw_{bwidx}"
|
|
434
434
|
self.migration_report.add_general_statistics(i18n.t("Bound-with holdings created"))
|
|
435
435
|
yield bound_with_holding
|
|
436
436
|
|
|
@@ -443,7 +443,12 @@ class MapperBase:
|
|
|
443
443
|
)
|
|
444
444
|
)
|
|
445
445
|
|
|
446
|
-
def map_statistical_codes(
|
|
446
|
+
def map_statistical_codes(
|
|
447
|
+
self,
|
|
448
|
+
folio_record: dict,
|
|
449
|
+
file_def: FileDefinition,
|
|
450
|
+
legacy_record: dict | Record | None = None,
|
|
451
|
+
):
|
|
447
452
|
"""Map statistical codes to the folio record.
|
|
448
453
|
|
|
449
454
|
This method checks if the file definition contains statistical codes and
|
|
@@ -454,13 +459,15 @@ class MapperBase:
|
|
|
454
459
|
Args:
|
|
455
460
|
folio_record (dict): The FOLIO record to which the statistical codes will be added.
|
|
456
461
|
file_def (FileDefinition): The file definition containing the statistical codes.
|
|
457
|
-
legacy_record (
|
|
458
|
-
"""
|
|
462
|
+
legacy_record (dict | Record | None): The legacy record from which the statistical codes are derived.
|
|
463
|
+
""" # noqa: E501
|
|
459
464
|
if file_def.statistical_code:
|
|
460
465
|
code_strings = file_def.statistical_code.split(
|
|
461
466
|
self.library_configuration.multi_field_delimiter
|
|
462
467
|
)
|
|
463
|
-
folio_record["statisticalCodeIds"] =
|
|
468
|
+
folio_record["statisticalCodeIds"] = (
|
|
469
|
+
folio_record.get("statisticalCodeIds", []) + code_strings
|
|
470
|
+
)
|
|
464
471
|
|
|
465
472
|
def setup_statistical_codes_map(self, statistical_codes_map):
|
|
466
473
|
if statistical_codes_map:
|
|
@@ -472,7 +479,9 @@ class MapperBase:
|
|
|
472
479
|
"code",
|
|
473
480
|
"StatisticalCodeMapping",
|
|
474
481
|
)
|
|
475
|
-
logging.info(
|
|
482
|
+
logging.info(
|
|
483
|
+
f"Statistical codes mapping set up {self.statistical_codes_mapping.mapped_legacy_keys}" # noqa: E501
|
|
484
|
+
)
|
|
476
485
|
else:
|
|
477
486
|
self.statistical_codes_mapping = None
|
|
478
487
|
logging.info("Statistical codes map is not set up")
|
|
@@ -492,13 +501,13 @@ class MapperBase:
|
|
|
492
501
|
)
|
|
493
502
|
return ""
|
|
494
503
|
|
|
495
|
-
def map_statistical_code_ids(
|
|
496
|
-
|
|
497
|
-
):
|
|
498
|
-
if stat_codes := {x: None for x in folio_record.pop("statisticalCodeIds", [])}:
|
|
504
|
+
def map_statistical_code_ids(self, legacy_ids, folio_record: dict):
|
|
505
|
+
if stat_codes := dict.fromkeys(folio_record.pop("statisticalCodeIds", [])):
|
|
499
506
|
folio_code_ids = set()
|
|
500
507
|
for stat_code in stat_codes:
|
|
501
|
-
if stat_code_id := self.get_statistical_code(
|
|
508
|
+
if stat_code_id := self.get_statistical_code(
|
|
509
|
+
{"legacy_stat_code": stat_code}, "statisticalCodeId", legacy_ids
|
|
510
|
+
):
|
|
502
511
|
folio_code_ids.add(stat_code_id)
|
|
503
512
|
else:
|
|
504
513
|
Helper.log_data_issue(
|
|
@@ -513,7 +522,10 @@ class MapperBase:
|
|
|
513
522
|
|
|
514
523
|
@property
|
|
515
524
|
def base_string_for_folio_uuid(self):
|
|
516
|
-
if
|
|
525
|
+
if (
|
|
526
|
+
self.library_configuration.use_gateway_url_for_uuids
|
|
527
|
+
and not self.library_configuration.is_ecs
|
|
528
|
+
):
|
|
517
529
|
return str(self.folio_client.gateway_url)
|
|
518
530
|
elif self.library_configuration.ecs_tenant_id:
|
|
519
531
|
return str(self.library_configuration.ecs_tenant_id)
|
|
@@ -522,8 +534,8 @@ class MapperBase:
|
|
|
522
534
|
|
|
523
535
|
@staticmethod
|
|
524
536
|
def validate_location_map(location_map: List[Dict], locations: List[Dict]) -> List[Dict]:
|
|
525
|
-
mapped_codes = [x[
|
|
526
|
-
existing_codes = [x[
|
|
537
|
+
mapped_codes = [x["folio_code"] for x in location_map]
|
|
538
|
+
existing_codes = [x["code"] for x in locations]
|
|
527
539
|
missing_codes = set(mapped_codes) - set(existing_codes)
|
|
528
540
|
if missing_codes:
|
|
529
541
|
raise TransformationProcessError(
|
|
@@ -537,6 +549,7 @@ class MapperBase:
|
|
|
537
549
|
def get_object_type() -> FOLIONamespaces:
|
|
538
550
|
raise NotImplementedError("This method should be overridden in subclasses")
|
|
539
551
|
|
|
552
|
+
|
|
540
553
|
def flatten(my_dict: dict, path=""):
|
|
541
554
|
for k, v in iter(my_dict.items()):
|
|
542
555
|
if not path:
|
|
@@ -6,7 +6,10 @@ import i18n
|
|
|
6
6
|
from folio_uuid.folio_uuid import FOLIONamespaces
|
|
7
7
|
from folioclient import FolioClient
|
|
8
8
|
|
|
9
|
-
from folio_migration_tools.custom_exceptions import
|
|
9
|
+
from folio_migration_tools.custom_exceptions import (
|
|
10
|
+
TransformationProcessError,
|
|
11
|
+
TransformationRecordFailedError,
|
|
12
|
+
)
|
|
10
13
|
from folio_migration_tools.library_configuration import (
|
|
11
14
|
FileDefinition,
|
|
12
15
|
LibraryConfiguration,
|
|
@@ -19,6 +22,7 @@ from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
|
|
|
19
22
|
)
|
|
20
23
|
from folio_migration_tools.task_configuration import AbstractTaskConfiguration
|
|
21
24
|
|
|
25
|
+
|
|
22
26
|
class HoldingsMapper(MappingFileMapperBase):
|
|
23
27
|
def __init__(
|
|
24
28
|
self,
|
|
@@ -40,7 +44,7 @@ class HoldingsMapper(MappingFileMapperBase):
|
|
|
40
44
|
statistical_codes_map,
|
|
41
45
|
FOLIONamespaces.holdings,
|
|
42
46
|
library_configuration,
|
|
43
|
-
task_config
|
|
47
|
+
task_config,
|
|
44
48
|
)
|
|
45
49
|
self.holdings_map = holdings_map
|
|
46
50
|
|
|
@@ -86,7 +90,7 @@ class HoldingsMapper(MappingFileMapperBase):
|
|
|
86
90
|
folio_record["discoverySuppress"] = file_def.discovery_suppressed
|
|
87
91
|
self.migration_report.add(
|
|
88
92
|
"Suppression",
|
|
89
|
-
i18n.t("Suppressed from discovery") + f
|
|
93
|
+
i18n.t("Suppressed from discovery") + f" = {folio_record['discoverySuppress']}",
|
|
90
94
|
)
|
|
91
95
|
|
|
92
96
|
def get_prop(self, legacy_item, folio_prop_name, index_or_id, schema_default_value):
|
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import sys
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
|
-
from typing import Dict, List, Set
|
|
5
|
+
from typing import Dict, List, Set
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
8
8
|
import i18n
|
|
@@ -117,7 +117,9 @@ class ItemMapper(MappingFileMapperBase):
|
|
|
117
117
|
"LocationMapping",
|
|
118
118
|
)
|
|
119
119
|
|
|
120
|
-
def perform_additional_mappings(
|
|
120
|
+
def perform_additional_mappings(
|
|
121
|
+
self, legacy_ids: List[str] | str, folio_rec: Dict, file_def: FileDefinition
|
|
122
|
+
):
|
|
121
123
|
self.handle_suppression(folio_rec, file_def)
|
|
122
124
|
self.map_statistical_codes(folio_rec, file_def)
|
|
123
125
|
self.map_statistical_code_ids(legacy_ids, folio_rec)
|
|
@@ -126,24 +128,17 @@ class ItemMapper(MappingFileMapperBase):
|
|
|
126
128
|
folio_record["discoverySuppress"] = file_def.discovery_suppressed
|
|
127
129
|
self.migration_report.add(
|
|
128
130
|
"Suppression",
|
|
129
|
-
i18n.t("Suppressed from discovery")
|
|
130
|
-
+ f" = {folio_record['discoverySuppress']}",
|
|
131
|
+
i18n.t("Suppressed from discovery") + f" = {folio_record['discoverySuppress']}",
|
|
131
132
|
)
|
|
132
133
|
|
|
133
134
|
def setup_status_mapping(self, item_statuses_map):
|
|
134
|
-
statuses = self.item_schema["properties"]["status"]["properties"]["name"][
|
|
135
|
-
"enum"
|
|
136
|
-
]
|
|
135
|
+
statuses = self.item_schema["properties"]["status"]["properties"]["name"]["enum"]
|
|
137
136
|
for mapping in item_statuses_map:
|
|
138
137
|
if "folio_name" not in mapping:
|
|
139
|
-
logging.critical(
|
|
140
|
-
"folio_name is not a column in the status mapping file"
|
|
141
|
-
)
|
|
138
|
+
logging.critical("folio_name is not a column in the status mapping file")
|
|
142
139
|
sys.exit(1)
|
|
143
140
|
elif "legacy_code" not in mapping:
|
|
144
|
-
logging.critical(
|
|
145
|
-
"legacy_code is not a column in the status mapping file"
|
|
146
|
-
)
|
|
141
|
+
logging.critical("legacy_code is not a column in the status mapping file")
|
|
147
142
|
sys.exit(1)
|
|
148
143
|
elif mapping["folio_name"] not in statuses:
|
|
149
144
|
logging.critical(
|
|
@@ -158,9 +153,7 @@ class ItemMapper(MappingFileMapperBase):
|
|
|
158
153
|
)
|
|
159
154
|
sys.exit(1)
|
|
160
155
|
elif not all(mapping.values()):
|
|
161
|
-
logging.critical(
|
|
162
|
-
"empty value in mapping %s. Check mapping file", mapping.values()
|
|
163
|
-
)
|
|
156
|
+
logging.critical("empty value in mapping %s. Check mapping file", mapping.values())
|
|
164
157
|
sys.exit(1)
|
|
165
158
|
else:
|
|
166
159
|
self.status_mapping = {
|
|
@@ -168,7 +161,7 @@ class ItemMapper(MappingFileMapperBase):
|
|
|
168
161
|
}
|
|
169
162
|
logging.info(json.dumps(statuses, indent=True))
|
|
170
163
|
|
|
171
|
-
def get_prop(self, legacy_item, folio_prop_name, index_or_id, schema_default_value):
|
|
164
|
+
def get_prop(self, legacy_item, folio_prop_name, index_or_id, schema_default_value): # noqa: C901
|
|
172
165
|
if folio_prop_name == "permanentLocationId":
|
|
173
166
|
return self.get_mapped_ref_data_value(
|
|
174
167
|
self.location_mapping,
|
|
@@ -213,9 +206,7 @@ class ItemMapper(MappingFileMapperBase):
|
|
|
213
206
|
index_or_id,
|
|
214
207
|
True,
|
|
215
208
|
)
|
|
216
|
-
self.migration_report.add(
|
|
217
|
-
"TemporaryLoanTypeMapping", f"{folio_prop_name} -> {ltid}"
|
|
218
|
-
)
|
|
209
|
+
self.migration_report.add("TemporaryLoanTypeMapping", f"{folio_prop_name} -> {ltid}")
|
|
219
210
|
return ltid
|
|
220
211
|
elif folio_prop_name == "permanentLoanTypeId":
|
|
221
212
|
return self.get_mapped_ref_data_value(
|
|
@@ -232,9 +223,7 @@ class ItemMapper(MappingFileMapperBase):
|
|
|
232
223
|
normalized_barcode = barcode.strip().lower()
|
|
233
224
|
if normalized_barcode and normalized_barcode in self.unique_barcodes:
|
|
234
225
|
Helper.log_data_issue(index_or_id, "Duplicate barcode", mapped_value)
|
|
235
|
-
self.migration_report.add_general_statistics(
|
|
236
|
-
i18n.t("Duplicate barcodes")
|
|
237
|
-
)
|
|
226
|
+
self.migration_report.add_general_statistics(i18n.t("Duplicate barcodes"))
|
|
238
227
|
return f"{barcode}-{uuid4()}"
|
|
239
228
|
else:
|
|
240
229
|
if normalized_barcode:
|
|
@@ -259,9 +248,7 @@ class ItemMapper(MappingFileMapperBase):
|
|
|
259
248
|
self.migration_report.add("UnmappedProperties", f"{folio_prop_name}")
|
|
260
249
|
return ""
|
|
261
250
|
|
|
262
|
-
def get_item_level_call_number_type_id(
|
|
263
|
-
self, legacy_item, folio_prop_name: str, index_or_id
|
|
264
|
-
):
|
|
251
|
+
def get_item_level_call_number_type_id(self, legacy_item, folio_prop_name: str, index_or_id):
|
|
265
252
|
if self.call_number_mapping:
|
|
266
253
|
return self.get_mapped_ref_data_value(
|
|
267
254
|
self.call_number_mapping, legacy_item, index_or_id, folio_prop_name
|
|
@@ -172,8 +172,7 @@ class ManualFeeFinesMapper(MappingFileMapperBase):
|
|
|
172
172
|
except TypeError as te:
|
|
173
173
|
raise TransformationProcessError(
|
|
174
174
|
"",
|
|
175
|
-
"Failed to fetch Tenant Locale Settings. "
|
|
176
|
-
"Is your library configuration correct?",
|
|
175
|
+
"Failed to fetch Tenant Locale Settings. Is your library configuration correct?",
|
|
177
176
|
) from te
|
|
178
177
|
except KeyError as ke:
|
|
179
178
|
raise TransformationProcessError(
|
|
@@ -133,7 +133,7 @@ class MappingFileMapperBase(MapperBase):
|
|
|
133
133
|
raise TransformationProcessError(
|
|
134
134
|
"",
|
|
135
135
|
f"property legacyIdentifier not setup in map: "
|
|
136
|
-
f"{field_map.get('legacyIdentifier', '')
|
|
136
|
+
f"{field_map.get('legacyIdentifier', '')({exception})}",
|
|
137
137
|
) from exception
|
|
138
138
|
del field_map["legacyIdentifier"]
|
|
139
139
|
return field_map
|
|
@@ -213,12 +213,10 @@ class MappingFileMapperBase(MapperBase):
|
|
|
213
213
|
}
|
|
214
214
|
)
|
|
215
215
|
if object_type == FOLIONamespaces.holdings and hasattr(self, "holdings_sources"):
|
|
216
|
-
folio_object[
|
|
216
|
+
folio_object["sourceId"] = self.holdings_sources.get("FOLIO")
|
|
217
217
|
elif object_type == FOLIONamespaces.holdings and not hasattr(self, "holdings_sources"):
|
|
218
218
|
raise TransformationProcessError(
|
|
219
|
-
index_or_id,
|
|
220
|
-
"Holdings source not set in the mapper",
|
|
221
|
-
None
|
|
219
|
+
index_or_id, "Holdings source not set in the mapper", None
|
|
222
220
|
)
|
|
223
221
|
return folio_object, legacy_id
|
|
224
222
|
|
|
@@ -400,7 +398,7 @@ class MappingFileMapperBase(MapperBase):
|
|
|
400
398
|
value = replaced_val
|
|
401
399
|
if value and mapping_file_entry.get("rules", {}).get("regexGetFirstMatchOrEmpty", ""):
|
|
402
400
|
my_pattern = (
|
|
403
|
-
f
|
|
401
|
+
f"{mapping_file_entry.get('rules', {}).get('regexGetFirstMatchOrEmpty')}|$"
|
|
404
402
|
)
|
|
405
403
|
value = re.findall(my_pattern, value)[0]
|
|
406
404
|
if not value and mapping_file_entry.get("fallback_legacy_field", ""):
|
|
@@ -498,7 +496,7 @@ class MappingFileMapperBase(MapperBase):
|
|
|
498
496
|
set_deep(folio_object, schema_property_name, temp_object)
|
|
499
497
|
# folio_object[schema_property_name] = temp_object
|
|
500
498
|
|
|
501
|
-
def map_objects_array_props(
|
|
499
|
+
def map_objects_array_props( # noqa: C901
|
|
502
500
|
self,
|
|
503
501
|
legacy_object,
|
|
504
502
|
prop_name: str,
|
|
@@ -553,7 +551,9 @@ class MappingFileMapperBase(MapperBase):
|
|
|
553
551
|
)
|
|
554
552
|
multi_field_props.append(sub_prop_name)
|
|
555
553
|
else:
|
|
556
|
-
self.validate_enums(
|
|
554
|
+
self.validate_enums(
|
|
555
|
+
res, sub_prop, sub_prop_name, index_or_id, required
|
|
556
|
+
)
|
|
557
557
|
|
|
558
558
|
if res or isinstance(res, bool):
|
|
559
559
|
temp_object[sub_prop_name] = res
|
|
@@ -619,8 +619,8 @@ class MappingFileMapperBase(MapperBase):
|
|
|
619
619
|
@staticmethod
|
|
620
620
|
def split_obj_by_delim(delimiter: str, folio_obj: dict, delimited_props: List[str]):
|
|
621
621
|
non_split_props = [(k, v) for k, v in folio_obj.items() if k not in delimited_props]
|
|
622
|
-
delimited_props =
|
|
623
|
-
zipped = list(zip(*delimited_props))
|
|
622
|
+
delimited_props = ([x, *folio_obj[x].split(delimiter)] for x in delimited_props)
|
|
623
|
+
zipped = list(zip(*delimited_props, strict=False))
|
|
624
624
|
res = []
|
|
625
625
|
for (prop_name_idx, prop_name), (value_idx, ra) in itertools.product(
|
|
626
626
|
enumerate(zipped[0]), enumerate(zipped[1:])
|
|
@@ -973,4 +973,6 @@ def in_deep(dictionary, keys):
|
|
|
973
973
|
|
|
974
974
|
|
|
975
975
|
def is_set_or_bool_or_numeric(any_value):
|
|
976
|
-
return (isinstance(any_value, str) and (any_value.strip() not in empty_vals)) or isinstance(
|
|
976
|
+
return (isinstance(any_value, str) and (any_value.strip() not in empty_vals)) or isinstance(
|
|
977
|
+
any_value, (int, float, complex)
|
|
978
|
+
)
|
|
@@ -25,7 +25,6 @@ from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class CompositeOrderMapper(MappingFileMapperBase):
|
|
28
|
-
|
|
29
28
|
def __init__(
|
|
30
29
|
self,
|
|
31
30
|
folio_client: FolioClient,
|
|
@@ -291,7 +290,9 @@ class CompositeOrderMapper(MappingFileMapperBase):
|
|
|
291
290
|
):
|
|
292
291
|
object_schema["properties"] = CompositeOrderMapper.inject_schema_by_ref(
|
|
293
292
|
submodule_path, github_headers, object_schema
|
|
294
|
-
).get(
|
|
293
|
+
).get(
|
|
294
|
+
"properties", {}
|
|
295
|
+
) # TODO: Investigate new CustomFields schema and figure out how to actually handle it # noqa: E501
|
|
295
296
|
|
|
296
297
|
for property_name_level1, property_level1 in object_schema.get(
|
|
297
298
|
"properties", {}
|
|
@@ -400,9 +401,9 @@ class CompositeOrderMapper(MappingFileMapperBase):
|
|
|
400
401
|
return composite_order
|
|
401
402
|
|
|
402
403
|
def validate_po_number(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
404
|
+
self,
|
|
405
|
+
index_or_id: str,
|
|
406
|
+
po_number: str,
|
|
406
407
|
):
|
|
407
408
|
if not self.is_valid_po_number(po_number):
|
|
408
409
|
self.migration_report.add(
|
|
@@ -330,9 +330,9 @@ class OrganizationMapper(MappingFileMapperBase):
|
|
|
330
330
|
["username", "password", "interfaceId"],
|
|
331
331
|
)
|
|
332
332
|
|
|
333
|
-
interface_schema["properties"][
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
interface_schema["properties"]["interfaceCredential"] = (
|
|
334
|
+
interface_credential_schema
|
|
335
|
+
)
|
|
336
336
|
|
|
337
337
|
property_level1["items"] = interface_schema
|
|
338
338
|
|