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,406 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
from unittest.mock import Mock
|
|
7
|
+
|
|
8
|
+
from folioclient import FolioClient
|
|
9
|
+
|
|
10
|
+
from folio_migration_tools.extradata_writer import ExtradataWriter
|
|
11
|
+
from folio_migration_tools.mapping_file_transformation.holdings_mapper import (
|
|
12
|
+
HoldingsMapper,
|
|
13
|
+
)
|
|
14
|
+
from folio_migration_tools.migration_report import MigrationReport
|
|
15
|
+
from folio_migration_tools.library_configuration import (
|
|
16
|
+
LibraryConfiguration,
|
|
17
|
+
FolioRelease,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def mocked_holdings_mapper() -> Mock:
|
|
22
|
+
mock_mapper = Mock(spec=HoldingsMapper)
|
|
23
|
+
mock_mapper.migration_report = MigrationReport()
|
|
24
|
+
mock_mapper.extradata_writer = ExtradataWriter(Path(""))
|
|
25
|
+
|
|
26
|
+
return mock_mapper
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mocked_folio_client() -> FolioClient:
|
|
30
|
+
try:
|
|
31
|
+
FolioClient.login = MagicMock(name="login", return_value=None)
|
|
32
|
+
FolioClient.okapi_token = "token" # noqa:S105
|
|
33
|
+
mocked_folio = FolioClient("okapi_url", "tenant_id", "username", "password")
|
|
34
|
+
mocked_folio.folio_get_single_object = folio_get_single_object_mocked
|
|
35
|
+
mocked_folio.folio_get_all = folio_get_all_mocked
|
|
36
|
+
mocked_folio.get_from_github = folio_get_from_github
|
|
37
|
+
mocked_folio.current_user = str(uuid.uuid4())
|
|
38
|
+
return mocked_folio
|
|
39
|
+
except Exception as ee:
|
|
40
|
+
logging.error(ee)
|
|
41
|
+
raise ee
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def folio_get_all_mocked(ref_data_path, array_name, query="", limit=10):
|
|
45
|
+
with open("./static/reference_data.json", "r") as super_schema_file:
|
|
46
|
+
super_schema = json.load(super_schema_file)
|
|
47
|
+
if ref_data_path == "/coursereserves/terms":
|
|
48
|
+
yield from [
|
|
49
|
+
{"name": "Fall 2022", "id": "42093be3-d1e7-4bb6-b2b9-18e153d109b2"},
|
|
50
|
+
{"name": "Summer 2022", "id": "415b14a8-c94c-4aa1-a0a8-d397efae343e"},
|
|
51
|
+
]
|
|
52
|
+
elif ref_data_path == "/coursereserves/departments":
|
|
53
|
+
yield from [
|
|
54
|
+
{
|
|
55
|
+
"id": "7532e5ab-9812-496c-ab77-4fbb6a7e5dbf",
|
|
56
|
+
"name": "Department_t",
|
|
57
|
+
"description": "Art & Art History",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "af7ae6be-c0b2-444d-b76f-4061098d17cd",
|
|
61
|
+
"name": "Department_FALLBACK",
|
|
62
|
+
"description": "FALLBACK",
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
elif ref_data_path == "/organizations-storage/categories":
|
|
66
|
+
yield from [
|
|
67
|
+
{"id": "c78640d5-a1ec-4721-9a1f-c6f876d4c179", "value": "Returns"},
|
|
68
|
+
{"id": "604c2c9d-ed3a-46cd-bec4-69926c303b22", "value": "Sales"},
|
|
69
|
+
{"id": "c5b175bd-34a0-4a4d-9bd9-8eddae8e67f8", "value": "General"},
|
|
70
|
+
{"id": "97dcb23df-1aba-444e-b88d-804d17c715a5", "value": "Technical Support"},
|
|
71
|
+
{"id": "e193b0d1-4674-4a9e-818b-375f013d963f", "value": "Moral Support"},
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
elif ref_data_path == "/organizations-storage/organization-types":
|
|
75
|
+
yield from [
|
|
76
|
+
{"id": "837d04b6-d81c-4c49-9efd-2f62515999b3", "name": "Consortium"},
|
|
77
|
+
{"id": "fc54327d-fd60-4f6a-ba37-a4375511b91b", "name": "Unspecified"},
|
|
78
|
+
]
|
|
79
|
+
elif (
|
|
80
|
+
ref_data_path == "/organizations-storage/organizations"
|
|
81
|
+
and query == '?query=(code=="EBSCO")'
|
|
82
|
+
):
|
|
83
|
+
yield from [{"id": "some id", "code": "some code", "name": "EBSCO Information Services"}]
|
|
84
|
+
|
|
85
|
+
elif (
|
|
86
|
+
ref_data_path == "/organizations-storage/organizations"
|
|
87
|
+
and query == '?query=(code=="LisasAwesomeStartup")'
|
|
88
|
+
):
|
|
89
|
+
yield from []
|
|
90
|
+
|
|
91
|
+
elif ref_data_path == "/organizations-storage/organizations":
|
|
92
|
+
yield from [
|
|
93
|
+
{"id": "837d04b6-d81c-4c49-9efd-2f62515999b3", "code": "GOBI"},
|
|
94
|
+
{"id": "fc54327d-fd60-4f6a-ba37-a4375511b91b", "code": "EBSCO"},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
elif ref_data_path == "/orders/acquisition-methods":
|
|
98
|
+
yield from [
|
|
99
|
+
{"id": "837d04b6-d81c-4c49-9efd-2f62515999b3", "value": "Purchase"},
|
|
100
|
+
{"id": "fc54327d-fd60-4f6a-ba37-a4375511b91b", "value": "Theft"},
|
|
101
|
+
{"id": "fc54327d-fd60-4f6a-ba37-a437551sarfs91b", "value": "Other"},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
elif ref_data_path == "/groups":
|
|
105
|
+
yield from [
|
|
106
|
+
{
|
|
107
|
+
"group": "FOLIO fallback group name",
|
|
108
|
+
"desc": "Mocked response",
|
|
109
|
+
"id": "27ab99d3-0e17-41f0-a20a-99e05acc0e6f",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"group": "FOLIO group name",
|
|
113
|
+
"desc": "Mocked response",
|
|
114
|
+
"id": "5fc96cbd-a860-42a7-8d2b-72af30206712",
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
elif ref_data_path == "/departments":
|
|
118
|
+
yield from [
|
|
119
|
+
{
|
|
120
|
+
"id": "12a2ad12-951d-4124-9fb2-58c70f0b7f71",
|
|
121
|
+
"name": "FOLIO user department name",
|
|
122
|
+
"code": "fdp",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "12a2ad12-951d-4124-9fb2-58c70f0b7f72",
|
|
126
|
+
"name": "FOLIO user department name 2",
|
|
127
|
+
"code": "fdp2",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "2f452d21-507d-4b32-a89d-8ea9753cc946",
|
|
131
|
+
"name": "FOLIO fallback user department name",
|
|
132
|
+
"code": "fb",
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
elif ref_data_path == "/owners":
|
|
136
|
+
yield from [
|
|
137
|
+
{
|
|
138
|
+
"owner": "The Best Fee Fine Owner",
|
|
139
|
+
"desc": "She really is!",
|
|
140
|
+
"servicePointOwner": [
|
|
141
|
+
{"value": "a77b55e7-f9f3-40a1-83e0-241bc606a826", "label": "lisatest"}
|
|
142
|
+
],
|
|
143
|
+
"id": "5abfff3f-50eb-432a-9a43-21f8f7a70194",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"owner": "The Other Fee Fine Owner",
|
|
147
|
+
"desc": "heeey",
|
|
148
|
+
"servicePointOwner": [
|
|
149
|
+
{"value": "1543c345-dcaf-4367-84a8-853d95837a3b", "label": "lisatest2 :) <3 "}
|
|
150
|
+
],
|
|
151
|
+
"id": "62a0eb54-de96-46ee-b184-5be6c8114a19",
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
elif ref_data_path == "/feefines":
|
|
155
|
+
yield from [
|
|
156
|
+
{
|
|
157
|
+
"automatic": False,
|
|
158
|
+
"feeFineType": "Coffee spill",
|
|
159
|
+
"ownerId": "5abfff3f-50eb-432a-9a43-21f8f7a70194",
|
|
160
|
+
"id": "6e8dc178-f667-45cd-90b5-338c78c3a85c",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"automatic": False,
|
|
164
|
+
"feeFineType": "Coffee spill",
|
|
165
|
+
"ownerId": "62a0eb54-de96-46ee-b184-5be6c8114a19",
|
|
166
|
+
"id": "031836ec-521a-4493-9f76-0e02c2e7d241",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"automatic": False,
|
|
170
|
+
"feeFineType": "Replacement library card",
|
|
171
|
+
"ownerId": "5abfff3f-50eb-432a-9a43-21f8f7a70194",
|
|
172
|
+
"id": "8936606d-223b-428e-9a70-4b8105f60cdb",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"automatic": True,
|
|
176
|
+
"feeFineType": "Replacement processing fee",
|
|
177
|
+
"id": "d20df2fb-45fd-4184-b238-0d25747ffdd9",
|
|
178
|
+
},
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
elif ref_data_path == "/service-points":
|
|
182
|
+
yield from [
|
|
183
|
+
{
|
|
184
|
+
"id": "finance_office_uuid",
|
|
185
|
+
"name": "Finance Office",
|
|
186
|
+
"code": "fo",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "library_main_desk_uuid",
|
|
190
|
+
"name": "Library Main Desk",
|
|
191
|
+
"code": "lmd",
|
|
192
|
+
},
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
elif ref_data_path == "/users" and query == '?query=(externalSystemId=="Some external id")':
|
|
196
|
+
yield from [{"id": "some id", "barcode": "some barcode", "patronGroup": "some group"}]
|
|
197
|
+
elif ref_data_path == "/users" and query == '?query=(barcode=="u123")':
|
|
198
|
+
yield from [{"id": "user123", "barcode": "u123", "patronGroup": "some group"}]
|
|
199
|
+
elif ref_data_path == "/users" and query == '?query=(barcode=="u456")':
|
|
200
|
+
yield from [{"id": "user456", "barcode": "u456", "patronGroup": "some group"}]
|
|
201
|
+
|
|
202
|
+
elif ref_data_path == "/inventory/items" and query == '?query=(barcode=="some barcode")':
|
|
203
|
+
yield from [
|
|
204
|
+
{
|
|
205
|
+
"id": "a FOLIO item uuid",
|
|
206
|
+
"title": "Döda fallen i Avesta.",
|
|
207
|
+
"barcode": "some barcode",
|
|
208
|
+
"callNumber": "QB611 .C44",
|
|
209
|
+
"materialType": {
|
|
210
|
+
"id": "4eea3f27-8910-46fc-9666-e2b44326c2b8",
|
|
211
|
+
"name": "sound recording",
|
|
212
|
+
},
|
|
213
|
+
"effectiveLocation": {
|
|
214
|
+
"id": "2e48e713-17f3-4c13-a9f8-23845bb210a4",
|
|
215
|
+
"name": "Reading room",
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
elif ref_data_path == "/holdings-note-types":
|
|
221
|
+
yield from [
|
|
222
|
+
{
|
|
223
|
+
"id": "88914775-f677-4759-b57b-1a33b90b24e0",
|
|
224
|
+
"name": "Electronic bookplate",
|
|
225
|
+
"source": "folio",
|
|
226
|
+
"metadata": {
|
|
227
|
+
"createdDate": "2024-09-04T01:54:20.719+00:00",
|
|
228
|
+
"updatedDate": "2024-09-04T01:54:20.719+00:00"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"id": "c4407cc7-d79f-4609-95bd-1cefb2e2b5c5",
|
|
233
|
+
"name": "Copy note",
|
|
234
|
+
"source": "folio",
|
|
235
|
+
"metadata": {
|
|
236
|
+
"createdDate": "2024-09-04T01:54:20.722+00:00",
|
|
237
|
+
"updatedDate": "2024-09-04T01:54:20.722+00:00"
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"id": "d6510242-5ec3-42ed-b593-3585d2e48fd6",
|
|
242
|
+
"name": "Action note",
|
|
243
|
+
"source": "folio",
|
|
244
|
+
"metadata": {
|
|
245
|
+
"createdDate": "2024-09-04T01:54:20.723+00:00",
|
|
246
|
+
"updatedDate": "2024-09-04T01:54:20.723+00:00"
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"id": "e19eabab-a85c-4aef-a7b2-33bd9acef24e",
|
|
251
|
+
"name": "Binding",
|
|
252
|
+
"source": "folio",
|
|
253
|
+
"metadata": {
|
|
254
|
+
"createdDate": "2024-09-04T01:54:20.724+00:00",
|
|
255
|
+
"updatedDate": "2024-09-04T01:54:20.724+00:00"
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"id": "db9b4787-95f0-4e78-becf-26748ce6bdeb",
|
|
260
|
+
"name": "Provenance",
|
|
261
|
+
"source": "folio",
|
|
262
|
+
"metadata": {
|
|
263
|
+
"createdDate": "2024-09-04T01:54:20.725+00:00",
|
|
264
|
+
"updatedDate": "2024-09-04T01:54:20.725+00:00"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"id": "6a41b714-8574-4084-8d64-a9373c3fbb59",
|
|
269
|
+
"name": "Reproduction",
|
|
270
|
+
"source": "folio",
|
|
271
|
+
"metadata": {
|
|
272
|
+
"createdDate": "2024-09-04T01:54:20.728+00:00",
|
|
273
|
+
"updatedDate": "2024-09-04T01:54:20.728+00:00"
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"id": "b160f13a-ddba-4053-b9c4-60ec5ea45d56",
|
|
278
|
+
"name": "Note",
|
|
279
|
+
"source": "folio",
|
|
280
|
+
"metadata": {
|
|
281
|
+
"createdDate": "2024-09-04T01:54:20.728+00:00",
|
|
282
|
+
"updatedDate": "2024-09-04T01:54:20.728+00:00"
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"id": "841d1873-015b-4bfb-a69f-6cbb41d925ba",
|
|
287
|
+
"name": "Original MARC holdings statements",
|
|
288
|
+
"source": "local",
|
|
289
|
+
"metadata": {
|
|
290
|
+
"createdDate": "2025-05-02T01:54:20.728+00:00",
|
|
291
|
+
"updatedDate": "2025-05-02T01:54:20.728+00:00"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"id": "09c1e5c9-6f11-432e-bcbe-b9e733ccce57",
|
|
296
|
+
"name": "Original MFHD Record",
|
|
297
|
+
"source": "local",
|
|
298
|
+
"metadata": {
|
|
299
|
+
"createdDate": "2025-05-02T01:54:20.728+00:00",
|
|
300
|
+
"updatedDate": "2025-05-02T01:54:20.728+00:00"
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"id": "474120b0-d64e-4a6f-9c9c-e7d3e76f3cf5",
|
|
305
|
+
"name": "Original MFHD (MARC21)",
|
|
306
|
+
"source": "local",
|
|
307
|
+
"metadata": {
|
|
308
|
+
"createdDate": "2025-05-02T01:54:20.728+00:00",
|
|
309
|
+
"updatedDate": "2025-05-02T01:54:20.728+00:00"
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
elif ref_data_path in super_schema:
|
|
315
|
+
yield from super_schema.get(ref_data_path)
|
|
316
|
+
else:
|
|
317
|
+
yield {}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def folio_get_single_object_mocked(*args, **kwargs):
|
|
321
|
+
with open("./static/reference_data.json", "r") as super_schema_file:
|
|
322
|
+
super_schema = json.load(super_schema_file)
|
|
323
|
+
if args[0] == "/hrid-settings-storage/hrid-settings":
|
|
324
|
+
return {
|
|
325
|
+
"instances": {"prefix": "pref", "startNumber": 1},
|
|
326
|
+
"holdings": {"prefix": "pref", "startNumber": 1},
|
|
327
|
+
"items": {"prefix": "pref", "startNumber": 1},
|
|
328
|
+
"commonRetainLeadingZeroes": True,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
elif (
|
|
332
|
+
args[0] == "/configurations/entries?query=(module==ORG%20and%20configName==localeSettings)"
|
|
333
|
+
):
|
|
334
|
+
return {
|
|
335
|
+
"configs": [
|
|
336
|
+
{
|
|
337
|
+
"value": '{"timezone":"America/New_York"}',
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
elif args[0] in super_schema:
|
|
343
|
+
return super_schema.get(args[0])
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def folio_get_from_github(owner, repo, file_path):
|
|
347
|
+
return FolioClient.get_latest_from_github(owner, repo, file_path, "")
|
|
348
|
+
|
|
349
|
+
OKAPI_URL = "http://localhost:9130"
|
|
350
|
+
LIBRARY_NAME = "Test Library"
|
|
351
|
+
|
|
352
|
+
def get_mocked_library_config():
|
|
353
|
+
return LibraryConfiguration(
|
|
354
|
+
okapi_url=OKAPI_URL,
|
|
355
|
+
tenant_id="test_tenant",
|
|
356
|
+
okapi_username="test_user",
|
|
357
|
+
okapi_password="test_password",
|
|
358
|
+
base_folder=Path("."),
|
|
359
|
+
library_name=LIBRARY_NAME,
|
|
360
|
+
log_level_debug=False,
|
|
361
|
+
folio_release=FolioRelease.sunflower,
|
|
362
|
+
iteration_identifier="test_iteration"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def get_mocked_ecs_central_libarary_config():
|
|
366
|
+
return LibraryConfiguration(
|
|
367
|
+
okapi_url=OKAPI_URL,
|
|
368
|
+
tenant_id="test_tenant",
|
|
369
|
+
okapi_username="test_user",
|
|
370
|
+
okapi_password="test_password",
|
|
371
|
+
base_folder=Path("."),
|
|
372
|
+
library_name=LIBRARY_NAME,
|
|
373
|
+
log_level_debug=False,
|
|
374
|
+
folio_release=FolioRelease.sunflower,
|
|
375
|
+
iteration_identifier="central_iteration",
|
|
376
|
+
is_ecs=True,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def get_mocked_ecs_member_libarary_config():
|
|
380
|
+
return LibraryConfiguration(
|
|
381
|
+
okapi_url=OKAPI_URL,
|
|
382
|
+
tenant_id="test_tenant",
|
|
383
|
+
ecs_tenant_id="test_ecs_tenant",
|
|
384
|
+
okapi_username="test_user",
|
|
385
|
+
okapi_password="test_password",
|
|
386
|
+
base_folder=Path("."),
|
|
387
|
+
library_name=LIBRARY_NAME,
|
|
388
|
+
log_level_debug=False,
|
|
389
|
+
folio_release=FolioRelease.sunflower,
|
|
390
|
+
iteration_identifier="member_iteration",
|
|
391
|
+
ecs_central_iteration_identifier="central_iteration",
|
|
392
|
+
is_ecs=True,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def get_mocked_folder_structure():
|
|
396
|
+
mock_fs = MagicMock()
|
|
397
|
+
mock_fs.mapping_files = Path("mapping_files")
|
|
398
|
+
mock_fs.results_folder = Path("results")
|
|
399
|
+
mock_fs.legacy_records_folder = Path("source_files")
|
|
400
|
+
mock_fs.logs_folder = Path("logs")
|
|
401
|
+
mock_fs.migration_reports_file = Path("/dev/null")
|
|
402
|
+
mock_fs.transformation_extra_data_path = Path("transformation_extra_data")
|
|
403
|
+
mock_fs.transformation_log_path = Path("/dev/null")
|
|
404
|
+
mock_fs.data_issue_file_path = Path("/dev/null")
|
|
405
|
+
mock_fs.failed_marc_recs_file = Path("failed_marc_recs.txt")
|
|
406
|
+
return mock_fs
|
|
@@ -1,16 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
import json
|
|
2
2
|
import logging
|
|
3
|
-
|
|
3
|
+
import i18n
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from zoneinfo import ZoneInfo
|
|
6
|
+
|
|
7
|
+
from dateutil import tz
|
|
8
|
+
from dateutil.parser import parse, ParserError
|
|
9
|
+
|
|
10
|
+
from folio_migration_tools.helper import Helper
|
|
11
|
+
from folio_migration_tools.migration_report import MigrationReport
|
|
12
|
+
from folio_migration_tools.custom_exceptions import TransformationRecordFailedError
|
|
13
|
+
|
|
14
|
+
utc = ZoneInfo("UTC")
|
|
4
15
|
|
|
5
16
|
|
|
6
17
|
class LegacyLoan(object):
|
|
7
|
-
def __init__(
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
legacy_loan_dict,
|
|
21
|
+
fallback_service_point_id: str,
|
|
22
|
+
migration_report: MigrationReport,
|
|
23
|
+
tenant_timezone=utc,
|
|
24
|
+
row=0,
|
|
25
|
+
):
|
|
26
|
+
self.migration_report: MigrationReport = migration_report
|
|
8
27
|
# validate
|
|
9
28
|
correct_headers = [
|
|
10
29
|
"item_barcode",
|
|
11
30
|
"patron_barcode",
|
|
12
31
|
"due_date",
|
|
13
32
|
"out_date",
|
|
33
|
+
]
|
|
34
|
+
optional_headers = [
|
|
35
|
+
"service_point_id",
|
|
36
|
+
"proxy_patron_barcode",
|
|
14
37
|
"renewal_count",
|
|
15
38
|
"next_item_status",
|
|
16
39
|
]
|
|
@@ -22,55 +45,146 @@ class LegacyLoan(object):
|
|
|
22
45
|
"Declared lost",
|
|
23
46
|
"Lost and paid",
|
|
24
47
|
]
|
|
25
|
-
|
|
48
|
+
|
|
49
|
+
self.legacy_loan_dict = legacy_loan_dict
|
|
50
|
+
self.tenant_timezone = tenant_timezone
|
|
26
51
|
self.errors = []
|
|
52
|
+
self.row = row
|
|
27
53
|
for prop in correct_headers:
|
|
28
|
-
if prop not in legacy_loan_dict:
|
|
29
|
-
self.errors.append(("Missing properties in legacy data", prop))
|
|
30
|
-
if
|
|
31
|
-
|
|
54
|
+
if prop not in self.legacy_loan_dict and prop not in optional_headers:
|
|
55
|
+
self.errors.append((f"Missing properties in legacy data {row=}", prop))
|
|
56
|
+
if (
|
|
57
|
+
prop != "next_item_status"
|
|
58
|
+
and not self.legacy_loan_dict.get(prop, "").strip()
|
|
59
|
+
and prop not in optional_headers
|
|
60
|
+
):
|
|
61
|
+
self.errors.append((f"Empty properties in legacy data {row=}", prop))
|
|
32
62
|
try:
|
|
33
|
-
temp_date_due: datetime = parse(legacy_loan_dict["due_date"])
|
|
34
|
-
|
|
63
|
+
temp_date_due: datetime = parse(self.legacy_loan_dict["due_date"])
|
|
64
|
+
if temp_date_due.tzinfo != tz.UTC:
|
|
65
|
+
temp_date_due = temp_date_due.replace(tzinfo=self.tenant_timezone)
|
|
66
|
+
Helper.log_data_issue(
|
|
67
|
+
self.row,
|
|
68
|
+
f"Provided due_date is not UTC in {row=}, "
|
|
69
|
+
f"setting tz-info to tenant timezone ({self.tenant_timezone})",
|
|
70
|
+
json.dumps(self.legacy_loan_dict)
|
|
71
|
+
)
|
|
72
|
+
self.report(
|
|
73
|
+
f"Provided due_date is not UTC, setting tz-info to tenant timezone ({self.tenant_timezone})"
|
|
74
|
+
)
|
|
75
|
+
if temp_date_due.hour == 0 and temp_date_due.minute == 0:
|
|
76
|
+
temp_date_due = temp_date_due.replace(hour=23, minute=59)
|
|
77
|
+
Helper.log_data_issue(
|
|
78
|
+
self.row,
|
|
79
|
+
f"Hour and minute not specified for due date in {row=}. "
|
|
80
|
+
"Assuming end of local calendar day (23:59)...",
|
|
81
|
+
json.dumps(self.legacy_loan_dict)
|
|
82
|
+
)
|
|
83
|
+
self.report(
|
|
84
|
+
"Hour and minute not specified for due date"
|
|
85
|
+
)
|
|
86
|
+
except (ParserError, OverflowError) as ee:
|
|
35
87
|
logging.error(ee)
|
|
36
|
-
self.errors.append(
|
|
37
|
-
|
|
88
|
+
self.errors.append(
|
|
89
|
+
(f"Parse date failure in {row=}. Setting UTC NOW", "due_date")
|
|
90
|
+
)
|
|
91
|
+
temp_date_due = datetime.now(ZoneInfo("UTC"))
|
|
38
92
|
try:
|
|
39
|
-
temp_date_out: datetime = parse(legacy_loan_dict["out_date"])
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
93
|
+
temp_date_out: datetime = parse(self.legacy_loan_dict["out_date"])
|
|
94
|
+
if temp_date_out.tzinfo != tz.UTC:
|
|
95
|
+
temp_date_out = temp_date_out.replace(tzinfo=self.tenant_timezone)
|
|
96
|
+
Helper.log_data_issue(
|
|
97
|
+
self.row,
|
|
98
|
+
f"Provided out_date is not UTC in {row=}, "
|
|
99
|
+
f"setting tz-info to tenant timezone ({self.tenant_timezone})",
|
|
100
|
+
json.dumps(self.legacy_loan_dict)
|
|
101
|
+
)
|
|
102
|
+
self.report(
|
|
103
|
+
f"Provided out_date is not UTC, setting tz-info to tenant timezone ({self.tenant_timezone})"
|
|
104
|
+
)
|
|
105
|
+
except (ParserError, OverflowError):
|
|
106
|
+
temp_date_out = datetime.now(
|
|
107
|
+
ZoneInfo("UTC")
|
|
108
|
+
) # TODO: Consider moving this assignment block above the temp_date_due
|
|
109
|
+
self.errors.append(
|
|
110
|
+
(f"Parse date failure in {row=}. Setting UTC NOW", "out_date")
|
|
111
|
+
)
|
|
43
112
|
|
|
44
113
|
# good to go, set properties
|
|
45
|
-
self.item_barcode: str = legacy_loan_dict["item_barcode"].strip()
|
|
46
|
-
self.patron_barcode: str = legacy_loan_dict["patron_barcode"].strip()
|
|
114
|
+
self.item_barcode: str = self.legacy_loan_dict["item_barcode"].strip()
|
|
115
|
+
self.patron_barcode: str = self.legacy_loan_dict["patron_barcode"].strip()
|
|
116
|
+
self.proxy_patron_barcode: str = self.legacy_loan_dict.get(
|
|
117
|
+
"proxy_patron_barcode", ""
|
|
118
|
+
)
|
|
47
119
|
self.due_date: datetime = temp_date_due
|
|
48
120
|
self.out_date: datetime = temp_date_out
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.out_date = self.out_date.replace(hour=0, minute=1)
|
|
56
|
-
except Exception as ee:
|
|
57
|
-
self.errors.append(("Time alignment issues", "both dates"))
|
|
58
|
-
self.renewal_count = int(legacy_loan_dict["renewal_count"])
|
|
59
|
-
self.next_item_status = legacy_loan_dict.get("next_item_status", "").strip()
|
|
121
|
+
self.correct_for_1_day_loans()
|
|
122
|
+
self.make_utc()
|
|
123
|
+
self.renewal_count = self.set_renewal_count(self.legacy_loan_dict)
|
|
124
|
+
self.next_item_status = self.legacy_loan_dict.get(
|
|
125
|
+
"next_item_status", ""
|
|
126
|
+
).strip()
|
|
60
127
|
if self.next_item_status not in legal_statuses:
|
|
61
|
-
self.errors.append(("Not an allowed status", self.next_item_status))
|
|
128
|
+
self.errors.append((f"Not an allowed status {row=}", self.next_item_status))
|
|
129
|
+
self.service_point_id = (
|
|
130
|
+
self.legacy_loan_dict["service_point_id"]
|
|
131
|
+
if self.legacy_loan_dict.get("service_point_id", "")
|
|
132
|
+
else fallback_service_point_id
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def set_renewal_count(self, loan: dict) -> int:
|
|
136
|
+
if "renewal_count" in loan:
|
|
137
|
+
renewal_count = loan["renewal_count"]
|
|
138
|
+
try:
|
|
139
|
+
return int(renewal_count)
|
|
140
|
+
except ValueError:
|
|
141
|
+
Helper.log_data_issue(
|
|
142
|
+
self.row,
|
|
143
|
+
i18n.t("Unresolvable %{renewal_count=} was replaced with 0."),
|
|
144
|
+
json.dumps(loan)
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
Helper.log_data_issue(
|
|
148
|
+
self.row,
|
|
149
|
+
i18n.t("Missing renewal count was replaced with 0."),
|
|
150
|
+
json.dumps(loan)
|
|
151
|
+
)
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
def correct_for_1_day_loans(self):
|
|
155
|
+
if self.due_date.date() <= self.out_date.date():
|
|
156
|
+
if self.due_date.hour == 0:
|
|
157
|
+
self.due_date = self.due_date.replace(hour=23, minute=59)
|
|
158
|
+
if self.out_date.hour == 0:
|
|
159
|
+
self.out_date = self.out_date.replace(hour=0, minute=1)
|
|
160
|
+
if self.due_date <= self.out_date:
|
|
161
|
+
raise TransformationRecordFailedError(
|
|
162
|
+
self.row,
|
|
163
|
+
i18n.t(
|
|
164
|
+
"Due date is before out date, or date information is missing from both"
|
|
165
|
+
),
|
|
166
|
+
json.dumps(self.legacy_loan_dict, indent=2),
|
|
167
|
+
)
|
|
62
168
|
|
|
63
169
|
def to_dict(self):
|
|
64
170
|
return {
|
|
65
171
|
"item_barcode": self.item_barcode,
|
|
66
172
|
"patron_barcode": self.patron_barcode,
|
|
173
|
+
"proxy_patron_barcode": self.proxy_patron_barcode,
|
|
67
174
|
"due_date": self.due_date.isoformat(),
|
|
68
175
|
"out_date": self.out_date.isoformat(),
|
|
69
176
|
"renewal_count": self.renewal_count,
|
|
70
177
|
"next_item_status": self.next_item_status,
|
|
178
|
+
"service_point_id": self.service_point_id,
|
|
71
179
|
}
|
|
72
180
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
181
|
+
def make_utc(self):
|
|
182
|
+
try:
|
|
183
|
+
if self.tenant_timezone != ZoneInfo("UTC"):
|
|
184
|
+
self.due_date = self.due_date.astimezone(ZoneInfo("UTC"))
|
|
185
|
+
self.out_date = self.out_date.astimezone(ZoneInfo("UTC"))
|
|
186
|
+
except TypeError:
|
|
187
|
+
self.errors.append((f"UTC correction issues {self.row}", "both dates"))
|
|
188
|
+
|
|
189
|
+
def report(self, what_to_report: str):
|
|
190
|
+
self.migration_report.add("Details", what_to_report)
|