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.
Files changed (73) hide show
  1. folio_migration_tools/__init__.py +11 -0
  2. folio_migration_tools/__main__.py +169 -85
  3. folio_migration_tools/circulation_helper.py +96 -59
  4. folio_migration_tools/config_file_load.py +66 -0
  5. folio_migration_tools/custom_dict.py +6 -4
  6. folio_migration_tools/custom_exceptions.py +21 -19
  7. folio_migration_tools/extradata_writer.py +46 -0
  8. folio_migration_tools/folder_structure.py +63 -66
  9. folio_migration_tools/helper.py +29 -21
  10. folio_migration_tools/holdings_helper.py +57 -34
  11. folio_migration_tools/i18n_config.py +9 -0
  12. folio_migration_tools/library_configuration.py +173 -13
  13. folio_migration_tools/mapper_base.py +317 -106
  14. folio_migration_tools/mapping_file_transformation/courses_mapper.py +203 -0
  15. folio_migration_tools/mapping_file_transformation/holdings_mapper.py +83 -69
  16. folio_migration_tools/mapping_file_transformation/item_mapper.py +98 -94
  17. folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +352 -0
  18. folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +702 -223
  19. folio_migration_tools/mapping_file_transformation/notes_mapper.py +90 -0
  20. folio_migration_tools/mapping_file_transformation/order_mapper.py +492 -0
  21. folio_migration_tools/mapping_file_transformation/organization_mapper.py +389 -0
  22. folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +38 -27
  23. folio_migration_tools/mapping_file_transformation/user_mapper.py +149 -361
  24. folio_migration_tools/marc_rules_transformation/conditions.py +650 -246
  25. folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +292 -130
  26. folio_migration_tools/marc_rules_transformation/hrid_handler.py +244 -0
  27. folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +20846 -0
  28. folio_migration_tools/marc_rules_transformation/marc_file_processor.py +300 -0
  29. folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +136 -0
  30. folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +241 -0
  31. folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +681 -201
  32. folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +395 -429
  33. folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +531 -100
  34. folio_migration_tools/migration_report.py +85 -38
  35. folio_migration_tools/migration_tasks/__init__.py +1 -3
  36. folio_migration_tools/migration_tasks/authority_transformer.py +119 -0
  37. folio_migration_tools/migration_tasks/batch_poster.py +911 -198
  38. folio_migration_tools/migration_tasks/bibs_transformer.py +121 -116
  39. folio_migration_tools/migration_tasks/courses_migrator.py +192 -0
  40. folio_migration_tools/migration_tasks/holdings_csv_transformer.py +252 -247
  41. folio_migration_tools/migration_tasks/holdings_marc_transformer.py +321 -115
  42. folio_migration_tools/migration_tasks/items_transformer.py +264 -84
  43. folio_migration_tools/migration_tasks/loans_migrator.py +506 -195
  44. folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +187 -0
  45. folio_migration_tools/migration_tasks/migration_task_base.py +364 -74
  46. folio_migration_tools/migration_tasks/orders_transformer.py +373 -0
  47. folio_migration_tools/migration_tasks/organization_transformer.py +451 -0
  48. folio_migration_tools/migration_tasks/requests_migrator.py +130 -62
  49. folio_migration_tools/migration_tasks/reserves_migrator.py +253 -0
  50. folio_migration_tools/migration_tasks/user_transformer.py +180 -139
  51. folio_migration_tools/task_configuration.py +46 -0
  52. folio_migration_tools/test_infrastructure/__init__.py +0 -0
  53. folio_migration_tools/test_infrastructure/mocked_classes.py +406 -0
  54. folio_migration_tools/transaction_migration/legacy_loan.py +148 -34
  55. folio_migration_tools/transaction_migration/legacy_request.py +65 -25
  56. folio_migration_tools/transaction_migration/legacy_reserve.py +47 -0
  57. folio_migration_tools/transaction_migration/transaction_result.py +12 -1
  58. folio_migration_tools/translations/en.json +476 -0
  59. folio_migration_tools-1.9.10.dist-info/METADATA +169 -0
  60. folio_migration_tools-1.9.10.dist-info/RECORD +67 -0
  61. {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info}/WHEEL +1 -2
  62. folio_migration_tools-1.9.10.dist-info/entry_points.txt +3 -0
  63. folio_migration_tools/generate_schemas.py +0 -46
  64. folio_migration_tools/mapping_file_transformation/mapping_file_mapping_base_impl.py +0 -44
  65. folio_migration_tools/mapping_file_transformation/user_mapper_base.py +0 -212
  66. folio_migration_tools/marc_rules_transformation/bibs_processor.py +0 -163
  67. folio_migration_tools/marc_rules_transformation/holdings_processor.py +0 -284
  68. folio_migration_tools/report_blurbs.py +0 -219
  69. folio_migration_tools/transaction_migration/legacy_fee_fine.py +0 -36
  70. folio_migration_tools-1.2.1.dist-info/METADATA +0 -134
  71. folio_migration_tools-1.2.1.dist-info/RECORD +0 -50
  72. folio_migration_tools-1.2.1.dist-info/top_level.txt +0 -1
  73. {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
- from datetime import datetime, timedelta, timezone
1
+ import json
2
2
  import logging
3
- from dateutil.parser import parse
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__(self, legacy_loan_dict, utc_difference=0, row=0):
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
- self.utc_difference = utc_difference
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 prop != "next_item_status" and not legacy_loan_dict[prop].strip():
31
- self.errors.append(("Empty properties in legacy data", prop))
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
- except Exception as ee:
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(("Parse date failure. Setting UTC NOW", "due_date"))
37
- temp_date_due = datetime.now(timezone.utc)
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
- except Exception as ee:
41
- temp_date_out = datetime.now(timezone.utc)
42
- self.errors.append(("Parse date failure. Setting UTC NOW", "out_date"))
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
- try:
50
- self.make_loan_utc()
51
- if self.due_date <= self.out_date:
52
- if self.due_date.hour == 0:
53
- self.due_date = self.due_date.replace(hour=23, minute=59)
54
- if self.out_date.hour == 0:
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 make_loan_utc(self):
74
- if self.utc_difference != 0:
75
- self.due_date = self.due_date + timedelta(hours=self.utc_difference)
76
- self.out_date = self.out_date + timedelta(hours=self.utc_difference)
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)