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,90 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import i18n
|
|
3
|
+
|
|
4
|
+
from folio_uuid.folio_namespaces import FOLIONamespaces
|
|
5
|
+
from folioclient import FolioClient
|
|
6
|
+
|
|
7
|
+
from folio_migration_tools.library_configuration import LibraryConfiguration
|
|
8
|
+
from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
|
|
9
|
+
MappingFileMapperBase,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotesMapper(MappingFileMapperBase):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
library_configuration: LibraryConfiguration,
|
|
17
|
+
task_configuration,
|
|
18
|
+
folio_client: FolioClient,
|
|
19
|
+
record_map: dict,
|
|
20
|
+
object_type: FOLIONamespaces,
|
|
21
|
+
ignore_legacy_identifier: bool = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.folio_client: FolioClient = folio_client
|
|
24
|
+
self.setup_notes_schema()
|
|
25
|
+
super().__init__(
|
|
26
|
+
folio_client,
|
|
27
|
+
self.notes_schema,
|
|
28
|
+
record_map,
|
|
29
|
+
None,
|
|
30
|
+
object_type,
|
|
31
|
+
library_configuration,
|
|
32
|
+
task_configuration,
|
|
33
|
+
ignore_legacy_identifier,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.noteprops = {
|
|
37
|
+
"data": [p for p in record_map["data"] if p["folio_field"].startswith("notes[")]
|
|
38
|
+
}
|
|
39
|
+
logging.info("Set %s props used for note mapping", len(self.noteprops["data"]))
|
|
40
|
+
logging.info("Initiated mapper for Notes")
|
|
41
|
+
|
|
42
|
+
def setup_notes_schema(self):
|
|
43
|
+
notes_schemas = self.get_notes_schema()
|
|
44
|
+
self.notes_schema = notes_schemas["noteCollection"]
|
|
45
|
+
self.notes_schema["properties"]["notes"]["items"] = notes_schemas["note"]
|
|
46
|
+
self.notes_schema["required"] = []
|
|
47
|
+
|
|
48
|
+
def map_notes(self, legacy_object, legacy_id, object_uuid: str, record_type: FOLIONamespaces):
|
|
49
|
+
if any(self.noteprops["data"]):
|
|
50
|
+
for note in self.do_map(legacy_object, legacy_id, FOLIONamespaces.note)[0].get(
|
|
51
|
+
"notes", []
|
|
52
|
+
):
|
|
53
|
+
if note.get("content", "").strip():
|
|
54
|
+
type_string = {
|
|
55
|
+
FOLIONamespaces.users: "user",
|
|
56
|
+
FOLIONamespaces.course: "course",
|
|
57
|
+
FOLIONamespaces.organizations: "organization",
|
|
58
|
+
FOLIONamespaces.orders: "orders",
|
|
59
|
+
}.get(record_type)
|
|
60
|
+
note["links"] = [{"id": object_uuid, "type": type_string}]
|
|
61
|
+
if "type" in note:
|
|
62
|
+
del note["type"]
|
|
63
|
+
self.extradata_writer.write("notes", note)
|
|
64
|
+
self.migration_report.add_general_statistics(
|
|
65
|
+
i18n.t("Number of linked notes created")
|
|
66
|
+
)
|
|
67
|
+
self.migration_report.add("MappedNoteTypes", note["typeId"])
|
|
68
|
+
else:
|
|
69
|
+
self.migration_report.add_general_statistics(
|
|
70
|
+
i18n.t("Number of discarded notes with no content")
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def get_notes_schema(self):
|
|
74
|
+
notes_schema = self.folio_client.get_from_github(
|
|
75
|
+
"folio-org",
|
|
76
|
+
"mod-notes",
|
|
77
|
+
"src/main/resources/swagger.api/schemas/note.yaml",
|
|
78
|
+
)
|
|
79
|
+
notes_common = self.folio_client.get_from_github(
|
|
80
|
+
"folio-org",
|
|
81
|
+
"mod-notes",
|
|
82
|
+
"src/main/resources/swagger.api/schemas/common.yaml",
|
|
83
|
+
)
|
|
84
|
+
for prop in notes_schema["note"]["properties"].items():
|
|
85
|
+
if prop[1].get("$ref", "") == "common.yaml#/uuid":
|
|
86
|
+
prop[1]["type"] = notes_common["uuid"]["type"]
|
|
87
|
+
|
|
88
|
+
for p in ["links", "metadata", "id"]:
|
|
89
|
+
del notes_schema["note"]["properties"][p]
|
|
90
|
+
return notes_schema
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import i18n
|
|
11
|
+
from folio_uuid.folio_uuid import FOLIONamespaces
|
|
12
|
+
from folioclient import FolioClient
|
|
13
|
+
from httpx import HTTPError
|
|
14
|
+
|
|
15
|
+
from folio_migration_tools.custom_exceptions import TransformationRecordFailedError
|
|
16
|
+
from folio_migration_tools.helper import Helper
|
|
17
|
+
from folio_migration_tools.library_configuration import LibraryConfiguration
|
|
18
|
+
from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
|
|
19
|
+
MappingFileMapperBase,
|
|
20
|
+
)
|
|
21
|
+
from folio_migration_tools.mapping_file_transformation.notes_mapper import NotesMapper
|
|
22
|
+
from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
|
|
23
|
+
RefDataMapping,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CompositeOrderMapper(MappingFileMapperBase):
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
folio_client: FolioClient,
|
|
32
|
+
library_configuration: LibraryConfiguration,
|
|
33
|
+
task_configuration,
|
|
34
|
+
composite_order_map: dict,
|
|
35
|
+
organizations_id_map: dict,
|
|
36
|
+
instance_id_map: dict,
|
|
37
|
+
acquisition_method_map,
|
|
38
|
+
payment_status_map,
|
|
39
|
+
receipt_status_map,
|
|
40
|
+
workflow_status_map,
|
|
41
|
+
location_map,
|
|
42
|
+
funds_map,
|
|
43
|
+
funds_expense_class_map=None,
|
|
44
|
+
):
|
|
45
|
+
# Get organization schema
|
|
46
|
+
self.composite_order_schema = CompositeOrderMapper.get_latest_acq_schemas_from_github(
|
|
47
|
+
"folio-org", "mod-orders", "mod-orders", "composite_purchase_order"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
super().__init__(
|
|
51
|
+
folio_client,
|
|
52
|
+
self.composite_order_schema,
|
|
53
|
+
composite_order_map,
|
|
54
|
+
None,
|
|
55
|
+
FOLIONamespaces.orders,
|
|
56
|
+
library_configuration,
|
|
57
|
+
task_configuration,
|
|
58
|
+
)
|
|
59
|
+
logging.info("Loading Instance ID map...")
|
|
60
|
+
self.instance_id_map = instance_id_map
|
|
61
|
+
self.organizations_id_map = organizations_id_map
|
|
62
|
+
self.folio_organization_cache = {}
|
|
63
|
+
|
|
64
|
+
self.acquisitions_methods_mapping = RefDataMapping(
|
|
65
|
+
self.folio_client,
|
|
66
|
+
"/orders/acquisition-methods",
|
|
67
|
+
"acquisitionMethods",
|
|
68
|
+
acquisition_method_map,
|
|
69
|
+
"value",
|
|
70
|
+
"AcquisitionMethodMapping",
|
|
71
|
+
)
|
|
72
|
+
logging.info("Init done")
|
|
73
|
+
self.location_mapping = RefDataMapping(
|
|
74
|
+
self.folio_client,
|
|
75
|
+
"/locations",
|
|
76
|
+
"locations",
|
|
77
|
+
location_map,
|
|
78
|
+
"code",
|
|
79
|
+
"OrderLineLocationMapping",
|
|
80
|
+
)
|
|
81
|
+
self.funds_mapping = RefDataMapping(
|
|
82
|
+
self.folio_client,
|
|
83
|
+
"/finance/funds",
|
|
84
|
+
"funds",
|
|
85
|
+
funds_map,
|
|
86
|
+
"code",
|
|
87
|
+
"FundsMapping",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
self.folio_client: FolioClient = folio_client
|
|
91
|
+
self.notes_mapper: NotesMapper = NotesMapper(
|
|
92
|
+
library_configuration,
|
|
93
|
+
None,
|
|
94
|
+
self.folio_client,
|
|
95
|
+
composite_order_map,
|
|
96
|
+
FOLIONamespaces.note,
|
|
97
|
+
True,
|
|
98
|
+
)
|
|
99
|
+
self.notes_mapper.migration_report = self.migration_report
|
|
100
|
+
|
|
101
|
+
def get_prop(self, legacy_order, folio_prop_name: str, index_or_id, schema_default_value):
|
|
102
|
+
if folio_prop_name.endswith(".acquisitionMethod"):
|
|
103
|
+
return self.get_mapped_ref_data_value(
|
|
104
|
+
self.acquisitions_methods_mapping,
|
|
105
|
+
legacy_order,
|
|
106
|
+
folio_prop_name,
|
|
107
|
+
index_or_id,
|
|
108
|
+
False,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
elif re.compile(r"compositePoLines\[(\d+)\]\.id").fullmatch(folio_prop_name):
|
|
112
|
+
return str(uuid.uuid4())
|
|
113
|
+
|
|
114
|
+
elif re.compile(r"notes\[\d+\]\.").match(folio_prop_name):
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
if folio_prop_name.endswith(".locationId"):
|
|
118
|
+
return self.get_mapped_ref_data_value(
|
|
119
|
+
self.location_mapping,
|
|
120
|
+
legacy_order,
|
|
121
|
+
folio_prop_name,
|
|
122
|
+
index_or_id,
|
|
123
|
+
False,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if folio_prop_name.endswith(".fundId"):
|
|
127
|
+
return self.get_mapped_ref_data_value(
|
|
128
|
+
self.location_mapping,
|
|
129
|
+
legacy_order,
|
|
130
|
+
folio_prop_name,
|
|
131
|
+
index_or_id,
|
|
132
|
+
False,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
mapped_value = super().get_prop(
|
|
136
|
+
legacy_order, folio_prop_name, index_or_id, schema_default_value
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return mapped_value
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def get_latest_acq_schemas_from_github(owner, repo, module, object):
|
|
143
|
+
"""
|
|
144
|
+
Given a repository owner, a repository, a module name and the name
|
|
145
|
+
of a FOLIO acquisition object, returns a schema for that object that
|
|
146
|
+
also includes the schemas of any other referenced acq objects.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
owner (_type_): _description_
|
|
150
|
+
repo (_type_): _description_
|
|
151
|
+
module (_type_): _description_
|
|
152
|
+
object (_type_): _description_
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
_type_: _description_
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
# Authenticate when calling GitHub, using an API key stored in .env
|
|
159
|
+
github_headers = {
|
|
160
|
+
"content-type": "application/json",
|
|
161
|
+
"User-Agent": "FOLIO Migration Tools (https://github.com/FOLIO-FSE/folio_migration_tools/)", # noqa:E501,B950
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if os.environ.get("GITHUB_TOKEN"):
|
|
165
|
+
logging.info("Using GITHUB_TOKEN environment variable for GitHub API Access")
|
|
166
|
+
github_headers["authorization"] = f"token {os.environ.get('GITHUB_TOKEN')}"
|
|
167
|
+
|
|
168
|
+
# Start talkign to GitHub...
|
|
169
|
+
github_path = "https://raw.githubusercontent.com"
|
|
170
|
+
submodules = CompositeOrderMapper.get_submodules_of_latest_release(
|
|
171
|
+
owner, repo, github_headers
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Get the sha's of submodules acq-models and raml_utils
|
|
175
|
+
acq_models_sha = next(
|
|
176
|
+
(item["sha"] for item in submodules if item["path"] == "acq-models")
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# # TODO Maybe - fetch raml_utils schemas if deemed necessary
|
|
180
|
+
# raml_utils_sha = next((item["sha"] for item in submodules
|
|
181
|
+
# if item["path"] == "raml-utils"))
|
|
182
|
+
|
|
183
|
+
acq_models_path = (
|
|
184
|
+
f"{github_path}/{owner}/acq-models/{acq_models_sha}/{module}/schemas/"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
req = httpx.get(
|
|
188
|
+
f"{acq_models_path}/{object}.json",
|
|
189
|
+
headers=github_headers,
|
|
190
|
+
follow_redirects=True,
|
|
191
|
+
timeout=None,
|
|
192
|
+
)
|
|
193
|
+
req.raise_for_status()
|
|
194
|
+
|
|
195
|
+
object_schema = json.loads(req.text)
|
|
196
|
+
|
|
197
|
+
return CompositeOrderMapper.build_extended_object(
|
|
198
|
+
object_schema, acq_models_path, github_headers
|
|
199
|
+
)
|
|
200
|
+
except httpx.HTTPError as http_error:
|
|
201
|
+
logging.critical(f"Halting! \t{http_error}")
|
|
202
|
+
sys.exit(2)
|
|
203
|
+
|
|
204
|
+
except json.decoder.JSONDecodeError as json_error:
|
|
205
|
+
logging.critical(json_error)
|
|
206
|
+
sys.exit(2)
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def get_submodules_of_latest_release(owner, repo, github_headers):
|
|
210
|
+
"""
|
|
211
|
+
Given a repository owner and a repository, identifies the latest
|
|
212
|
+
release of the repository and returns the submodules associated with
|
|
213
|
+
this release.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
owner (_type_): _description_
|
|
217
|
+
repo (_type_): _description_
|
|
218
|
+
github_headers (_type_): _description_
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
_type_: _description_
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
github_path = "https://api.github.com/repos"
|
|
227
|
+
|
|
228
|
+
# Get metadata for the latest release
|
|
229
|
+
latest_release_path = f"{github_path}/{owner}/{repo}/releases/latest"
|
|
230
|
+
req = httpx.get(
|
|
231
|
+
f"{latest_release_path}", headers=github_headers, follow_redirects=True, timeout=None
|
|
232
|
+
)
|
|
233
|
+
req.raise_for_status()
|
|
234
|
+
latest_release = json.loads(req.text)
|
|
235
|
+
|
|
236
|
+
# Get the tag assigned to the latest release
|
|
237
|
+
release_tag = latest_release["tag_name"]
|
|
238
|
+
logging.info(f"Using schemas from latest {repo} release: {release_tag}")
|
|
239
|
+
|
|
240
|
+
# Get the tree for the latest release
|
|
241
|
+
tree_path = f"{github_path}/{owner}/{repo}/git/trees/{release_tag}"
|
|
242
|
+
req = httpx.get(tree_path, headers=github_headers, follow_redirects=True, timeout=None)
|
|
243
|
+
req.raise_for_status()
|
|
244
|
+
release_tree = json.loads(req.text)
|
|
245
|
+
|
|
246
|
+
# Loop through the tree to get the sha of the folder with path "ramls"
|
|
247
|
+
ramls_sha = next((item["sha"] for item in release_tree["tree"] if item["path"] == "ramls"))
|
|
248
|
+
|
|
249
|
+
# Get the tree for the ramls folder
|
|
250
|
+
ramls_path = f"{github_path}/{owner}/{repo}/git/trees/{ramls_sha}"
|
|
251
|
+
req = httpx.get(ramls_path, headers=github_headers, follow_redirects=True, timeout=None)
|
|
252
|
+
req.raise_for_status()
|
|
253
|
+
ramls_tree = json.loads(req.text)
|
|
254
|
+
|
|
255
|
+
# Loop through the tree to get the sha of submodules
|
|
256
|
+
submodules = [item for item in ramls_tree["tree"] if item["mode"] == "160000"]
|
|
257
|
+
|
|
258
|
+
return submodules
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def build_extended_object(object_schema, submodule_path, github_headers):
|
|
262
|
+
"""
|
|
263
|
+
Takes an object schema (for example an organization) and the path to a
|
|
264
|
+
submodule repository and returns the same schema with the full schemas
|
|
265
|
+
of subordinate objects (for example aliases).
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
object_schema (_type_): _description_
|
|
269
|
+
submodule_path (_type_): _description_
|
|
270
|
+
github_headers (_type_): _description_
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
_type_: _description_
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
supported_types = [
|
|
277
|
+
"string",
|
|
278
|
+
"boolean",
|
|
279
|
+
"number",
|
|
280
|
+
"integer",
|
|
281
|
+
"text",
|
|
282
|
+
"object",
|
|
283
|
+
"array",
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
if (
|
|
288
|
+
"properties" not in object_schema
|
|
289
|
+
and "$ref" in object_schema
|
|
290
|
+
and object_schema["type"] == "object"
|
|
291
|
+
):
|
|
292
|
+
object_schema["properties"] = CompositeOrderMapper.inject_schema_by_ref(
|
|
293
|
+
submodule_path, github_headers, object_schema
|
|
294
|
+
).get("properties", {})#TODO: Investigate new CustomFields schema and figure out how to actually handle it
|
|
295
|
+
|
|
296
|
+
for property_name_level1, property_level1 in object_schema.get(
|
|
297
|
+
"properties", {}
|
|
298
|
+
).items():
|
|
299
|
+
# Report and discard unhandled properties
|
|
300
|
+
if property_level1.get("type") not in supported_types:
|
|
301
|
+
logging.info(f"Property not yet supported: {property_name_level1}")
|
|
302
|
+
property_level1["type"] = "Deprecated"
|
|
303
|
+
|
|
304
|
+
# Handle object properties
|
|
305
|
+
elif property_level1.get("type") == "object" and property_level1.get("$ref"):
|
|
306
|
+
logging.info("Fecthing referenced schema for object %s", property_name_level1)
|
|
307
|
+
actual_path = urllib.parse.urljoin(
|
|
308
|
+
f"{submodule_path}", object_schema.get("$ref", "")
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
p1 = CompositeOrderMapper.inject_schema_by_ref(
|
|
312
|
+
actual_path, github_headers, property_level1
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
p2 = CompositeOrderMapper.build_extended_object(
|
|
316
|
+
p1, actual_path, github_headers
|
|
317
|
+
)
|
|
318
|
+
object_schema["properties"][property_name_level1] = p2
|
|
319
|
+
|
|
320
|
+
# Handle arrays of items properties
|
|
321
|
+
elif property_level1.get("type") == "array" and property_level1.get("items").get(
|
|
322
|
+
"$ref"
|
|
323
|
+
):
|
|
324
|
+
logging.info(
|
|
325
|
+
"Fetching referenced schema for array object %s", property_name_level1
|
|
326
|
+
)
|
|
327
|
+
actual_path = urllib.parse.urljoin(
|
|
328
|
+
f"{submodule_path}", object_schema.get("$ref", "")
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
p1 = CompositeOrderMapper.inject_items_schema_by_ref(
|
|
332
|
+
actual_path, github_headers, property_level1
|
|
333
|
+
)
|
|
334
|
+
p2 = CompositeOrderMapper.build_extended_object(
|
|
335
|
+
p1, actual_path, github_headers
|
|
336
|
+
)
|
|
337
|
+
property_level1["items"] = p2
|
|
338
|
+
elif property_level1.get("type") == "string" and property_level1.get("$ref"):
|
|
339
|
+
logging.info("Fetching referenced schema for object %s", property_name_level1)
|
|
340
|
+
actual_path = urllib.parse.urljoin(
|
|
341
|
+
f"{submodule_path}", object_schema.get("$ref", "")
|
|
342
|
+
)
|
|
343
|
+
p1 = CompositeOrderMapper.inject_schema_by_ref(
|
|
344
|
+
actual_path, github_headers, property_level1
|
|
345
|
+
)
|
|
346
|
+
object_schema["properties"][property_name_level1] = p1
|
|
347
|
+
|
|
348
|
+
return object_schema
|
|
349
|
+
|
|
350
|
+
except HTTPError as he:
|
|
351
|
+
logging.error(he)
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def inject_schema_by_ref(submodule_path, github_headers, property: dict):
|
|
355
|
+
base_raml = "https://raw.githubusercontent.com/folio-org/raml/master/"
|
|
356
|
+
try:
|
|
357
|
+
u1 = urllib.parse.urlparse(submodule_path)
|
|
358
|
+
schema_url = urllib.parse.urljoin(u1.geturl(), property["$ref"])
|
|
359
|
+
if schema_url.endswith("tags.schema"):
|
|
360
|
+
schema_url = f"{base_raml}schemas/tags.schema"
|
|
361
|
+
if schema_url.endswith("metadata.schema"):
|
|
362
|
+
schema_url = f"{base_raml}schemas/metadata.schema"
|
|
363
|
+
|
|
364
|
+
req = httpx.get(
|
|
365
|
+
schema_url, headers=github_headers, follow_redirects=True, timeout=None
|
|
366
|
+
)
|
|
367
|
+
req.raise_for_status()
|
|
368
|
+
return dict(property, **json.loads(req.text))
|
|
369
|
+
except Exception as ee:
|
|
370
|
+
logging.error(ee)
|
|
371
|
+
return {}
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def inject_items_schema_by_ref(submodule_path, github_headers, property: dict):
|
|
375
|
+
try:
|
|
376
|
+
u1 = urllib.parse.urlparse(submodule_path)
|
|
377
|
+
schema_url = urllib.parse.urljoin(u1.geturl(), property["items"]["$ref"])
|
|
378
|
+
req = httpx.get(
|
|
379
|
+
schema_url, headers=github_headers, follow_redirects=True, timeout=None
|
|
380
|
+
)
|
|
381
|
+
req.raise_for_status()
|
|
382
|
+
return dict(property["items"], **json.loads(req.text))
|
|
383
|
+
except Exception as ee:
|
|
384
|
+
logging.error(ee)
|
|
385
|
+
return {}
|
|
386
|
+
|
|
387
|
+
def perform_additional_mapping(self, index_or_id, composite_order):
|
|
388
|
+
self.validate_po_number(index_or_id, composite_order.get("poNumber"))
|
|
389
|
+
|
|
390
|
+
# Get organization UUID from FOLIO
|
|
391
|
+
composite_order["vendor"] = self.get_folio_organization_uuid(
|
|
392
|
+
index_or_id, composite_order.get("vendor")
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Replace legacy bib ID with instance UUID from map
|
|
396
|
+
bib_id = composite_order["compositePoLines"][0].pop("instanceId", "")
|
|
397
|
+
if matching_instance := self.get_folio_instance_uuid(index_or_id, bib_id):
|
|
398
|
+
composite_order["compositePoLines"][0]["instanceId"] = matching_instance
|
|
399
|
+
|
|
400
|
+
return composite_order
|
|
401
|
+
|
|
402
|
+
def validate_po_number(
|
|
403
|
+
self,
|
|
404
|
+
index_or_id: str,
|
|
405
|
+
po_number: str,
|
|
406
|
+
):
|
|
407
|
+
if not self.is_valid_po_number(po_number):
|
|
408
|
+
self.migration_report.add(
|
|
409
|
+
"PurchaseOrderVendorLinking",
|
|
410
|
+
i18n.t("RECORD FAILED: PO number has invalid character(s)"),
|
|
411
|
+
)
|
|
412
|
+
raise TransformationRecordFailedError(
|
|
413
|
+
index_or_id,
|
|
414
|
+
"Purchase Order number has invalid character(s)",
|
|
415
|
+
po_number,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def is_valid_po_number(po_number: str) -> bool:
|
|
420
|
+
valid_po_number_characters = r"[A-Za-z0-9]"
|
|
421
|
+
return re.sub(valid_po_number_characters, "", po_number) == ""
|
|
422
|
+
|
|
423
|
+
def get_matching_record_from_folio(
|
|
424
|
+
self,
|
|
425
|
+
index_or_id,
|
|
426
|
+
cache: dict,
|
|
427
|
+
path: str,
|
|
428
|
+
match_property: str,
|
|
429
|
+
match_value: str,
|
|
430
|
+
result_type: str,
|
|
431
|
+
):
|
|
432
|
+
if match_value in cache:
|
|
433
|
+
return cache[match_value]
|
|
434
|
+
else:
|
|
435
|
+
query = f'?query=({match_property}=="{match_value}")'
|
|
436
|
+
if matching_record := next(
|
|
437
|
+
self.folio_client.folio_get_all(path, result_type, query), None
|
|
438
|
+
):
|
|
439
|
+
cache[match_value] = matching_record
|
|
440
|
+
return matching_record
|
|
441
|
+
|
|
442
|
+
def get_folio_organization_uuid(self, index_or_id, org_code):
|
|
443
|
+
if self.organizations_id_map:
|
|
444
|
+
self.migration_report.add(
|
|
445
|
+
"PurchaseOrderVendorLinking",
|
|
446
|
+
i18n.t("Organizations linked using organizations_id_map"),
|
|
447
|
+
)
|
|
448
|
+
if matching_org := self.organizations_id_map.get(org_code):
|
|
449
|
+
return matching_org[1]
|
|
450
|
+
|
|
451
|
+
if matching_org := self.get_matching_record_from_folio(
|
|
452
|
+
index_or_id,
|
|
453
|
+
self.folio_organization_cache,
|
|
454
|
+
"/organizations-storage/organizations",
|
|
455
|
+
"code",
|
|
456
|
+
org_code,
|
|
457
|
+
"organizations",
|
|
458
|
+
):
|
|
459
|
+
self.migration_report.add(
|
|
460
|
+
"PurchaseOrderVendorLinking",
|
|
461
|
+
i18n.t("Organizations not in ID map, linked using FOLIO lookup"),
|
|
462
|
+
)
|
|
463
|
+
return matching_org["id"]
|
|
464
|
+
|
|
465
|
+
else:
|
|
466
|
+
self.migration_report.add(
|
|
467
|
+
"PurchaseOrderVendorLinking",
|
|
468
|
+
i18n.t("RECORD FAILED Organization identifier not in ID map/FOLIO"),
|
|
469
|
+
)
|
|
470
|
+
raise TransformationRecordFailedError(
|
|
471
|
+
index_or_id,
|
|
472
|
+
"No matching Organization for organization identifier",
|
|
473
|
+
org_code,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
def get_folio_instance_uuid(self, index_or_id, bib_id):
|
|
477
|
+
if matching_instance := self.instance_id_map.get(bib_id):
|
|
478
|
+
self.migration_report.add(
|
|
479
|
+
"PurchaseOrderInstanceLinking",
|
|
480
|
+
i18n.t("Instances linked using instances_id_map"),
|
|
481
|
+
)
|
|
482
|
+
return matching_instance[1]
|
|
483
|
+
else:
|
|
484
|
+
self.migration_report.add(
|
|
485
|
+
"PurchaseOrderInstanceLinking",
|
|
486
|
+
i18n.t("Bib identifier not in instances_id_map, no instance linked"),
|
|
487
|
+
)
|
|
488
|
+
Helper.log_data_issue(
|
|
489
|
+
index_or_id,
|
|
490
|
+
"No matching instance for bib identifier",
|
|
491
|
+
bib_id,
|
|
492
|
+
)
|