folio-migration-tools 1.2.1__py3-none-any.whl → 1.9.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- folio_migration_tools/__init__.py +11 -0
- folio_migration_tools/__main__.py +169 -85
- folio_migration_tools/circulation_helper.py +96 -59
- folio_migration_tools/config_file_load.py +66 -0
- folio_migration_tools/custom_dict.py +6 -4
- folio_migration_tools/custom_exceptions.py +21 -19
- folio_migration_tools/extradata_writer.py +46 -0
- folio_migration_tools/folder_structure.py +63 -66
- folio_migration_tools/helper.py +29 -21
- folio_migration_tools/holdings_helper.py +57 -34
- folio_migration_tools/i18n_config.py +9 -0
- folio_migration_tools/library_configuration.py +173 -13
- folio_migration_tools/mapper_base.py +317 -106
- folio_migration_tools/mapping_file_transformation/courses_mapper.py +203 -0
- folio_migration_tools/mapping_file_transformation/holdings_mapper.py +83 -69
- folio_migration_tools/mapping_file_transformation/item_mapper.py +98 -94
- folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +352 -0
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +702 -223
- folio_migration_tools/mapping_file_transformation/notes_mapper.py +90 -0
- folio_migration_tools/mapping_file_transformation/order_mapper.py +492 -0
- folio_migration_tools/mapping_file_transformation/organization_mapper.py +389 -0
- folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +38 -27
- folio_migration_tools/mapping_file_transformation/user_mapper.py +149 -361
- folio_migration_tools/marc_rules_transformation/conditions.py +650 -246
- folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +292 -130
- folio_migration_tools/marc_rules_transformation/hrid_handler.py +244 -0
- folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +20846 -0
- folio_migration_tools/marc_rules_transformation/marc_file_processor.py +300 -0
- folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +136 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +241 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +681 -201
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +395 -429
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +531 -100
- folio_migration_tools/migration_report.py +85 -38
- folio_migration_tools/migration_tasks/__init__.py +1 -3
- folio_migration_tools/migration_tasks/authority_transformer.py +119 -0
- folio_migration_tools/migration_tasks/batch_poster.py +911 -198
- folio_migration_tools/migration_tasks/bibs_transformer.py +121 -116
- folio_migration_tools/migration_tasks/courses_migrator.py +192 -0
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py +252 -247
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py +321 -115
- folio_migration_tools/migration_tasks/items_transformer.py +264 -84
- folio_migration_tools/migration_tasks/loans_migrator.py +506 -195
- folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +187 -0
- folio_migration_tools/migration_tasks/migration_task_base.py +364 -74
- folio_migration_tools/migration_tasks/orders_transformer.py +373 -0
- folio_migration_tools/migration_tasks/organization_transformer.py +451 -0
- folio_migration_tools/migration_tasks/requests_migrator.py +130 -62
- folio_migration_tools/migration_tasks/reserves_migrator.py +253 -0
- folio_migration_tools/migration_tasks/user_transformer.py +180 -139
- folio_migration_tools/task_configuration.py +46 -0
- folio_migration_tools/test_infrastructure/__init__.py +0 -0
- folio_migration_tools/test_infrastructure/mocked_classes.py +406 -0
- folio_migration_tools/transaction_migration/legacy_loan.py +148 -34
- folio_migration_tools/transaction_migration/legacy_request.py +65 -25
- folio_migration_tools/transaction_migration/legacy_reserve.py +47 -0
- folio_migration_tools/transaction_migration/transaction_result.py +12 -1
- folio_migration_tools/translations/en.json +476 -0
- folio_migration_tools-1.9.10.dist-info/METADATA +169 -0
- folio_migration_tools-1.9.10.dist-info/RECORD +67 -0
- {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info}/WHEEL +1 -2
- folio_migration_tools-1.9.10.dist-info/entry_points.txt +3 -0
- folio_migration_tools/generate_schemas.py +0 -46
- folio_migration_tools/mapping_file_transformation/mapping_file_mapping_base_impl.py +0 -44
- folio_migration_tools/mapping_file_transformation/user_mapper_base.py +0 -212
- folio_migration_tools/marc_rules_transformation/bibs_processor.py +0 -163
- folio_migration_tools/marc_rules_transformation/holdings_processor.py +0 -284
- folio_migration_tools/report_blurbs.py +0 -219
- folio_migration_tools/transaction_migration/legacy_fee_fine.py +0 -36
- folio_migration_tools-1.2.1.dist-info/METADATA +0 -134
- folio_migration_tools-1.2.1.dist-info/RECORD +0 -50
- folio_migration_tools-1.2.1.dist-info/top_level.txt +0 -1
- {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from folio_uuid.folio_uuid import FOLIONamespaces
|
|
9
|
+
from folioclient import FolioClient
|
|
10
|
+
|
|
11
|
+
from folio_migration_tools.library_configuration import LibraryConfiguration
|
|
12
|
+
from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
|
|
13
|
+
MappingFileMapperBase,
|
|
14
|
+
)
|
|
15
|
+
from folio_migration_tools.mapping_file_transformation.notes_mapper import NotesMapper
|
|
16
|
+
from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
|
|
17
|
+
RefDataMapping,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OrganizationMapper(MappingFileMapperBase):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
folio_client: FolioClient,
|
|
25
|
+
library_configuration: LibraryConfiguration,
|
|
26
|
+
task_config,
|
|
27
|
+
organization_map: dict,
|
|
28
|
+
organization_types_map,
|
|
29
|
+
address_categories_map,
|
|
30
|
+
email_categories_map,
|
|
31
|
+
phone_categories_map,
|
|
32
|
+
):
|
|
33
|
+
# Build composite organization schema
|
|
34
|
+
if os.environ.get("GITHUB_TOKEN"):
|
|
35
|
+
logging.info("Using GITHUB_TOKEN environment variable for GitHub API Access")
|
|
36
|
+
organization_schema = OrganizationMapper.get_latest_acq_schemas_from_github(
|
|
37
|
+
"folio-org", "mod-organizations-storage", "mod-orgs", "organization"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
super().__init__(
|
|
41
|
+
folio_client,
|
|
42
|
+
organization_schema,
|
|
43
|
+
organization_map,
|
|
44
|
+
None,
|
|
45
|
+
FOLIONamespaces.organizations,
|
|
46
|
+
library_configuration,
|
|
47
|
+
task_config,
|
|
48
|
+
)
|
|
49
|
+
self.organization_schema = organization_schema
|
|
50
|
+
# Set up reference data maps
|
|
51
|
+
self.set_up_reference_data_mapping(
|
|
52
|
+
organization_types_map,
|
|
53
|
+
address_categories_map,
|
|
54
|
+
email_categories_map,
|
|
55
|
+
phone_categories_map,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.folio_client: FolioClient = folio_client
|
|
59
|
+
self.notes_mapper: NotesMapper = NotesMapper(
|
|
60
|
+
library_configuration,
|
|
61
|
+
None,
|
|
62
|
+
self.folio_client,
|
|
63
|
+
organization_map,
|
|
64
|
+
FOLIONamespaces.note,
|
|
65
|
+
True,
|
|
66
|
+
)
|
|
67
|
+
self.notes_mapper.migration_report = self.migration_report
|
|
68
|
+
|
|
69
|
+
# Commence the mapping work
|
|
70
|
+
def get_prop(self, legacy_organization, folio_prop_name, index_or_id, schema_default_value):
|
|
71
|
+
value_tuple = (
|
|
72
|
+
legacy_organization,
|
|
73
|
+
index_or_id,
|
|
74
|
+
folio_prop_name,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Perfrom reference data mappings
|
|
78
|
+
if folio_prop_name == "organizationTypes":
|
|
79
|
+
return self.get_mapped_ref_data_value(
|
|
80
|
+
self.organization_types_map,
|
|
81
|
+
*value_tuple,
|
|
82
|
+
False,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
elif re.compile(r"addresses\[(\d+)\]\.categories\[(\d+)\]").fullmatch(folio_prop_name):
|
|
86
|
+
return self.get_mapped_ref_data_value(
|
|
87
|
+
self.address_categories_map,
|
|
88
|
+
*value_tuple,
|
|
89
|
+
False,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
elif re.compile(r"emails\[(\d+)\]\.categories\[(\d+)\]").fullmatch(folio_prop_name):
|
|
93
|
+
return self.get_mapped_ref_data_value(
|
|
94
|
+
self.email_categories_map,
|
|
95
|
+
*value_tuple,
|
|
96
|
+
False,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
elif re.compile(r"phoneNumbers\[(\d+)\]\.categories\[(\d+)\]").fullmatch(folio_prop_name):
|
|
100
|
+
return self.get_mapped_ref_data_value(
|
|
101
|
+
self.phone_categories_map,
|
|
102
|
+
*value_tuple,
|
|
103
|
+
False,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
elif re.compile(r"interfaces\[(\d+)\]\.interfaceCredential.interfaceId").fullmatch(
|
|
107
|
+
folio_prop_name
|
|
108
|
+
):
|
|
109
|
+
return "replace_with_interface_id"
|
|
110
|
+
|
|
111
|
+
return super().get_prop(
|
|
112
|
+
legacy_organization, folio_prop_name, index_or_id, schema_default_value
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def set_up_reference_data_mapping(
|
|
116
|
+
self,
|
|
117
|
+
organization_types_map,
|
|
118
|
+
address_categories_map,
|
|
119
|
+
email_categories_map,
|
|
120
|
+
phone_categories_map,
|
|
121
|
+
):
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
organization_types_map (_type_): _description_
|
|
126
|
+
address_categories_map (_type_): _description_
|
|
127
|
+
email_categories_map (_type_): _description_
|
|
128
|
+
phone_categories_map (_type_): _description_
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
categories_shared_args = (
|
|
132
|
+
self.folio_client,
|
|
133
|
+
"/organizations-storage/categories",
|
|
134
|
+
"categories",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if address_categories_map:
|
|
138
|
+
self.address_categories_map = RefDataMapping(
|
|
139
|
+
*categories_shared_args, address_categories_map, "value", "CategoriesMapping"
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
self.address_categories_map = None
|
|
143
|
+
|
|
144
|
+
if email_categories_map:
|
|
145
|
+
self.email_categories_map = RefDataMapping(
|
|
146
|
+
*categories_shared_args, email_categories_map, "value", "CategoriesMapping"
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
self.email_categories_map = None
|
|
150
|
+
|
|
151
|
+
if phone_categories_map:
|
|
152
|
+
self.phone_categories_map = RefDataMapping(
|
|
153
|
+
*categories_shared_args, phone_categories_map, "value", "CategoriesMapping"
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
self.phone_categories_map = None
|
|
157
|
+
|
|
158
|
+
if organization_types_map:
|
|
159
|
+
self.organization_types_map = RefDataMapping(
|
|
160
|
+
self.folio_client,
|
|
161
|
+
"/organizations-storage/organization-types",
|
|
162
|
+
"organizationTypes",
|
|
163
|
+
organization_types_map,
|
|
164
|
+
"name",
|
|
165
|
+
"OrganizationTypeMapping",
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
self.organization_types_map = None
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def get_latest_acq_schemas_from_github(owner, repo, module, object):
|
|
172
|
+
"""
|
|
173
|
+
Given a repository owner, a repository, a module name and the name
|
|
174
|
+
of a FOLIO acquisition object, returns a schema for that object that
|
|
175
|
+
also includes the schemas of any other referenced acq objects.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
owner (_type_): _description_
|
|
179
|
+
repo (_type_): _description_
|
|
180
|
+
module (_type_): _description_
|
|
181
|
+
object (_type_): _description_
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
_type_: _description_
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
# Authenticate when calling GitHub, using an API key stored in .env
|
|
188
|
+
github_headers = {
|
|
189
|
+
"content-type": "application/json",
|
|
190
|
+
"User-Agent": "FOLIO Migration Tools (https://github.com/FOLIO-FSE/folio_migration_tools/)", # noqa:E501,B950
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if os.environ.get("GITHUB_TOKEN"):
|
|
194
|
+
github_headers["authorization"] = f"token {os.environ.get('GITHUB_TOKEN')}"
|
|
195
|
+
|
|
196
|
+
# Start talkign to GitHub...
|
|
197
|
+
github_path = "https://raw.githubusercontent.com"
|
|
198
|
+
submodules = OrganizationMapper.get_submodules_of_latest_release(
|
|
199
|
+
owner, repo, github_headers
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Get the sha's of sunmodules acq-models and raml_utils
|
|
203
|
+
acq_models_sha = next(
|
|
204
|
+
(item["sha"] for item in submodules if item["path"] == "acq-models")
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# # TODO Maybe - fetch raml_utils schemas if deemed necessary
|
|
208
|
+
# raml_utils_sha = next((item["sha"] for item in submodules
|
|
209
|
+
# if item["path"] == "raml-utils"))
|
|
210
|
+
|
|
211
|
+
acq_models_path = f"{github_path}/{owner}/acq-models/{acq_models_sha}/{module}/schemas"
|
|
212
|
+
|
|
213
|
+
req = httpx.get(f"{acq_models_path}/{object}.json", headers=github_headers)
|
|
214
|
+
req.raise_for_status()
|
|
215
|
+
|
|
216
|
+
object_schema = json.loads(req.text)
|
|
217
|
+
|
|
218
|
+
# Fetch referenced schemas
|
|
219
|
+
extended_object_schema = OrganizationMapper.build_extended_object(
|
|
220
|
+
object_schema, acq_models_path, github_headers
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return extended_object_schema
|
|
224
|
+
|
|
225
|
+
except httpx.HTTPError as http_error:
|
|
226
|
+
logging.critical(f"Halting! \t{http_error}")
|
|
227
|
+
sys.exit(2)
|
|
228
|
+
|
|
229
|
+
except json.decoder.JSONDecodeError as json_error:
|
|
230
|
+
logging.critical(json_error)
|
|
231
|
+
sys.exit(2)
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def get_submodules_of_latest_release(owner, repo, github_headers):
|
|
235
|
+
"""
|
|
236
|
+
Given a repository owner and a repository, identifies the latest
|
|
237
|
+
release of the repository and returns the submodules associated with
|
|
238
|
+
this release.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
owner (_type_): _description_
|
|
242
|
+
repo (_type_): _description_
|
|
243
|
+
github_headers (_type_): _description_
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
_type_: _description_
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
github_path = "https://api.github.com/repos"
|
|
250
|
+
|
|
251
|
+
# Get metadata for the latest release
|
|
252
|
+
latest_release_path = f"{github_path}/{owner}/{repo}/releases/latest"
|
|
253
|
+
req = httpx.get(f"{latest_release_path}", headers=github_headers, timeout=None)
|
|
254
|
+
req.raise_for_status()
|
|
255
|
+
latest_release = json.loads(req.text)
|
|
256
|
+
|
|
257
|
+
# Get the tag assigned to the latest release
|
|
258
|
+
release_tag = latest_release["tag_name"]
|
|
259
|
+
logging.info(f"Using schemas from latest {repo} release: {release_tag}")
|
|
260
|
+
|
|
261
|
+
# Get the tree for the latest release
|
|
262
|
+
tree_path = f"{github_path}/{owner}/{repo}/git/trees/{release_tag}"
|
|
263
|
+
req = httpx.get(tree_path, headers=github_headers, timeout=None)
|
|
264
|
+
req.raise_for_status()
|
|
265
|
+
release_tree = json.loads(req.text)
|
|
266
|
+
|
|
267
|
+
# Loop through the tree to get the sha of the folder with path "ramls"
|
|
268
|
+
ramls_sha = next((item["sha"] for item in release_tree["tree"] if item["path"] == "ramls"))
|
|
269
|
+
|
|
270
|
+
# Get the tree for the ramls folder
|
|
271
|
+
ramls_path = f"{github_path}/{owner}/{repo}/git/trees/{ramls_sha}"
|
|
272
|
+
req = httpx.get(ramls_path, headers=github_headers, timeout=None)
|
|
273
|
+
req.raise_for_status()
|
|
274
|
+
ramls_tree = json.loads(req.text)
|
|
275
|
+
|
|
276
|
+
# Loop through the tree to get the sha of submodules
|
|
277
|
+
submodules = [item for item in ramls_tree["tree"] if item["mode"] == "160000"]
|
|
278
|
+
|
|
279
|
+
return submodules
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def build_extended_object(object_schema, submodule_path, github_headers):
|
|
283
|
+
"""
|
|
284
|
+
Takes an object schema (for example an organization) and the path to a
|
|
285
|
+
submodule repository and returns the same schema with the full schemas
|
|
286
|
+
of subordinate objects (for example aliases).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
object_schema (_type_): _description_
|
|
290
|
+
submodule_path (_type_): _description_
|
|
291
|
+
github_headers (_type_): _description_
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
_type_: _description_
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
supported_types = ["string", "boolean", "number", "integer", "text", "object", "array"]
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
for property_name_level1, property_level1 in object_schema["properties"].items():
|
|
301
|
+
# For now, treat references to UUIDs like strings
|
|
302
|
+
# It's not great practice, but it's the way FOLIO mostly handles it
|
|
303
|
+
if "../../common/schemas/uuid.json" in property_level1.get("$ref", ""):
|
|
304
|
+
property_level1["type"] = "string"
|
|
305
|
+
|
|
306
|
+
# Preliminary implementation of tags
|
|
307
|
+
# https://github.com/folio-org/raml/blob/master/schemas/tags.schema
|
|
308
|
+
elif property_name_level1 == "tags":
|
|
309
|
+
property_level1["properties"] = {
|
|
310
|
+
"tagList": {
|
|
311
|
+
"description": "List of tags",
|
|
312
|
+
"type": "array",
|
|
313
|
+
"items": {"type": "string"},
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
elif property_name_level1 == "contacts":
|
|
318
|
+
contact_schema = OrganizationMapper.fetch_additional_schema("contact")
|
|
319
|
+
property_level1["items"] = contact_schema
|
|
320
|
+
|
|
321
|
+
elif property_name_level1 == "interfaces":
|
|
322
|
+
interface_schema = OrganizationMapper.fetch_additional_schema("interface")
|
|
323
|
+
interface_schema["required"] = ["name"]
|
|
324
|
+
|
|
325
|
+
# Temporarily add the credential object as a subproperty
|
|
326
|
+
interface_credential_schema = OrganizationMapper.fetch_additional_schema(
|
|
327
|
+
"interface_credential"
|
|
328
|
+
)
|
|
329
|
+
interface_credential_schema["required"] = (
|
|
330
|
+
["username", "password", "interfaceId"],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
interface_schema["properties"][
|
|
334
|
+
"interfaceCredential"
|
|
335
|
+
] = interface_credential_schema
|
|
336
|
+
|
|
337
|
+
property_level1["items"] = interface_schema
|
|
338
|
+
|
|
339
|
+
elif (
|
|
340
|
+
property_level1.get("type") == "array"
|
|
341
|
+
and property_level1.get("items").get("$ref")
|
|
342
|
+
== "../../common/schemas/uuid.json"
|
|
343
|
+
):
|
|
344
|
+
property_level1["items"]["type"] = "string"
|
|
345
|
+
|
|
346
|
+
# Report and discard unhandled properties
|
|
347
|
+
elif (
|
|
348
|
+
property_level1.get("type") not in supported_types
|
|
349
|
+
or "../" in property_level1.get("$ref", "")
|
|
350
|
+
or "../" in property_level1.get("items", {}).get("$ref", "")
|
|
351
|
+
):
|
|
352
|
+
logging.info(f"Property not yet supported: {property_name_level1}")
|
|
353
|
+
|
|
354
|
+
# Handle object properties
|
|
355
|
+
elif property_level1.get("type") == "object" and property_level1.get("$ref"):
|
|
356
|
+
logging.info("Fecthing referenced schema for object %s", property_name_level1)
|
|
357
|
+
|
|
358
|
+
ref_object = property_level1["$ref"]
|
|
359
|
+
schema_url = f"{submodule_path}/{ref_object}"
|
|
360
|
+
|
|
361
|
+
req = httpx.get(schema_url, headers=github_headers, timeout=None)
|
|
362
|
+
req.raise_for_status()
|
|
363
|
+
|
|
364
|
+
property_level1 = dict(property_level1, **json.loads(req.text))
|
|
365
|
+
|
|
366
|
+
elif property_level1.get("type") == "array" and property_level1.get("items").get(
|
|
367
|
+
"$ref"
|
|
368
|
+
):
|
|
369
|
+
ref_object = property_level1["items"]["$ref"]
|
|
370
|
+
schema_url = f"{submodule_path}/{ref_object}"
|
|
371
|
+
|
|
372
|
+
req = httpx.get(schema_url, headers=github_headers, timeout=None)
|
|
373
|
+
req.raise_for_status()
|
|
374
|
+
|
|
375
|
+
property_level1["items"] = dict(
|
|
376
|
+
property_level1["items"], **json.loads(req.text)
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return object_schema
|
|
380
|
+
|
|
381
|
+
except httpx.HTTPError as he:
|
|
382
|
+
logging.error(he)
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def fetch_additional_schema(folio_object):
|
|
386
|
+
additional_schema = OrganizationMapper.get_latest_acq_schemas_from_github(
|
|
387
|
+
"folio-org", "mod-organizations-storage", "mod-orgs", folio_object
|
|
388
|
+
)
|
|
389
|
+
return additional_schema
|
|
@@ -3,8 +3,8 @@ import logging
|
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
5
|
from folioclient import FolioClient
|
|
6
|
+
|
|
6
7
|
from folio_migration_tools.custom_exceptions import TransformationProcessError
|
|
7
|
-
from folio_migration_tools.report_blurbs import Blurbs
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class RefDataMapping(object):
|
|
@@ -15,18 +15,16 @@ class RefDataMapping(object):
|
|
|
15
15
|
array_name,
|
|
16
16
|
the_map,
|
|
17
17
|
key_type,
|
|
18
|
-
|
|
18
|
+
blurb_id,
|
|
19
19
|
):
|
|
20
20
|
self.name = array_name
|
|
21
|
-
self.cache = {}
|
|
22
|
-
self.
|
|
21
|
+
self.cache: dict = {}
|
|
22
|
+
self.blurb_id = blurb_id
|
|
23
23
|
logging.info("%s reference data mapping. Initializing", self.name)
|
|
24
24
|
logging.info("Fetching %s reference data from FOLIO", self.name)
|
|
25
|
-
self.ref_data = list(
|
|
26
|
-
folio_client.folio_get_all(ref_data_path, array_name, "", 1000)
|
|
27
|
-
)
|
|
25
|
+
self.ref_data = list(folio_client.folio_get_all(ref_data_path, array_name, "", 1000))
|
|
28
26
|
self.map = the_map
|
|
29
|
-
self.regular_mappings = []
|
|
27
|
+
self.regular_mappings: list = []
|
|
30
28
|
self.key_type = key_type
|
|
31
29
|
self.hybrid_mappings = []
|
|
32
30
|
self.mapped_legacy_keys = []
|
|
@@ -45,6 +43,9 @@ class RefDataMapping(object):
|
|
|
45
43
|
return self.cached_dict.get(key_value.lower().strip(), ())
|
|
46
44
|
|
|
47
45
|
def setup_mappings(self):
|
|
46
|
+
if not self.map:
|
|
47
|
+
logging.info("%s legacy map file is empty or not provided", self.name)
|
|
48
|
+
return
|
|
48
49
|
self.pre_validate_map()
|
|
49
50
|
for idx, mapping in enumerate(self.map):
|
|
50
51
|
try:
|
|
@@ -64,7 +65,8 @@ class RefDataMapping(object):
|
|
|
64
65
|
x = mapping.get(f"folio_{self.key_type}", "")
|
|
65
66
|
raise TransformationProcessError(
|
|
66
67
|
"",
|
|
67
|
-
f"No {self.name} - {x} - set up in map or tenant. Check for
|
|
68
|
+
f"No {self.name} - {x} - set up in map or tenant. Check for "
|
|
69
|
+
f"inconstencies in {self.name} naming. "
|
|
68
70
|
f"Add a row to mapping file with *:s and a valid {self.name}",
|
|
69
71
|
)
|
|
70
72
|
else:
|
|
@@ -74,9 +76,7 @@ class RefDataMapping(object):
|
|
|
74
76
|
self.regular_mappings.append(mapping)
|
|
75
77
|
t = self.get_ref_data_tuple(mapping[f"folio_{self.key_type}"])
|
|
76
78
|
if not t:
|
|
77
|
-
raise TransformationProcessError(
|
|
78
|
-
"", f"Mapping not found for {mapping}"
|
|
79
|
-
)
|
|
79
|
+
raise TransformationProcessError("", f"Mapping not found for {mapping}")
|
|
80
80
|
mapping["folio_id"] = t[0]
|
|
81
81
|
except TransformationProcessError as transformation_process_error:
|
|
82
82
|
raise transformation_process_error from transformation_process_error
|
|
@@ -90,10 +90,12 @@ class RefDataMapping(object):
|
|
|
90
90
|
|
|
91
91
|
self.post_validate_map()
|
|
92
92
|
logging.info(
|
|
93
|
-
f"Loaded {len(self.regular_mappings)} mappings for {len(self.ref_data)} {self.name}
|
|
93
|
+
f"Loaded {len(self.regular_mappings)} mappings for {len(self.ref_data)} {self.name} "
|
|
94
|
+
"in FOLIO"
|
|
94
95
|
)
|
|
95
96
|
logging.info(
|
|
96
|
-
f"loaded {len(self.hybrid_mappings)} hybrid mappings for {len(self.ref_data)}
|
|
97
|
+
f"loaded {len(self.hybrid_mappings)} hybrid mappings for {len(self.ref_data)} "
|
|
98
|
+
f"{self.name} in FOLIO"
|
|
97
99
|
)
|
|
98
100
|
|
|
99
101
|
def get_hybrid_mapping(self, legacy_object):
|
|
@@ -128,24 +130,18 @@ class RefDataMapping(object):
|
|
|
128
130
|
return self.cache[obj_key]
|
|
129
131
|
prepped_props = {k: legacy_object[k].strip() for k in self.mapped_legacy_keys}
|
|
130
132
|
for mapping in self.regular_mappings:
|
|
131
|
-
match_number = sum(
|
|
132
|
-
prepped_props[k] == mapping[k] for k in self.mapped_legacy_keys
|
|
133
|
-
)
|
|
133
|
+
match_number = sum(prepped_props[k] == mapping[k] for k in self.mapped_legacy_keys)
|
|
134
134
|
if match_number == len(self.mapped_legacy_keys):
|
|
135
135
|
self.cache[obj_key] = mapping
|
|
136
136
|
return mapping
|
|
137
137
|
return None
|
|
138
138
|
|
|
139
139
|
def is_hybrid_default_mapping(self, mapping):
|
|
140
|
-
legacy_values = [
|
|
141
|
-
value for key, value in mapping.items() if key in self.mapped_legacy_keys
|
|
142
|
-
]
|
|
140
|
+
legacy_values = [value for key, value in mapping.items() if key in self.mapped_legacy_keys]
|
|
143
141
|
return "*" in legacy_values and not self.is_default_mapping(mapping)
|
|
144
142
|
|
|
145
143
|
def is_default_mapping(self, mapping):
|
|
146
|
-
legacy_values = [
|
|
147
|
-
value for key, value in mapping.items() if key in self.mapped_legacy_keys
|
|
148
|
-
]
|
|
144
|
+
legacy_values = [value for key, value in mapping.items() if key in self.mapped_legacy_keys]
|
|
149
145
|
return all(f == "*" for f in legacy_values)
|
|
150
146
|
|
|
151
147
|
def pre_validate_map(self):
|
|
@@ -163,12 +159,17 @@ class RefDataMapping(object):
|
|
|
163
159
|
)
|
|
164
160
|
if any(folio_values_not_in_map):
|
|
165
161
|
logging.info(
|
|
166
|
-
|
|
162
|
+
"Values from %s ref data in FOLIO that are not in the map: %s",
|
|
163
|
+
self.name,
|
|
164
|
+
folio_values_not_in_map,
|
|
167
165
|
)
|
|
168
166
|
if any(map_values_not_in_folio):
|
|
169
167
|
raise TransformationProcessError(
|
|
170
168
|
"",
|
|
171
|
-
|
|
169
|
+
(
|
|
170
|
+
f"Values ({self.key_type}) from {self.name} map are not in "
|
|
171
|
+
f"FOLIO: {map_values_not_in_folio}"
|
|
172
|
+
),
|
|
172
173
|
)
|
|
173
174
|
|
|
174
175
|
def post_validate_map(self):
|
|
@@ -176,7 +177,8 @@ class RefDataMapping(object):
|
|
|
176
177
|
raise TransformationProcessError(
|
|
177
178
|
"",
|
|
178
179
|
f"No fallback {self.name} set up in map."
|
|
179
|
-
f"Add a row to mapping file with *:s in all legacy
|
|
180
|
+
f"Add a row to mapping file with *:s in all legacy "
|
|
181
|
+
f"columns and a valid {self.name} value",
|
|
180
182
|
)
|
|
181
183
|
for mapping in self.map:
|
|
182
184
|
if f"folio_{self.key_type}" not in mapping:
|
|
@@ -209,5 +211,14 @@ def get_mapped_legacy_keys(mapping):
|
|
|
209
211
|
k.strip()
|
|
210
212
|
for k in mapping.keys()
|
|
211
213
|
if k
|
|
212
|
-
not in [
|
|
214
|
+
not in [
|
|
215
|
+
"folio_group",
|
|
216
|
+
"folio_code",
|
|
217
|
+
"folio_id",
|
|
218
|
+
"folio_name",
|
|
219
|
+
"legacy_code",
|
|
220
|
+
"folio_value",
|
|
221
|
+
"folio_owner",
|
|
222
|
+
"folio_feeFineType",
|
|
223
|
+
]
|
|
213
224
|
]
|