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,373 @@
1
+ import csv
2
+ import ctypes
3
+ import json
4
+ import logging
5
+ import sys
6
+ import time
7
+ from os.path import isfile
8
+ from typing import List, Optional, Annotated
9
+ from pydantic import Field
10
+
11
+ import i18n
12
+ from deepdiff import DeepDiff
13
+ from folio_uuid.folio_namespaces import FOLIONamespaces
14
+
15
+ from folio_migration_tools.custom_exceptions import (
16
+ TransformationProcessError,
17
+ TransformationRecordFailedError,
18
+ )
19
+ from folio_migration_tools.helper import Helper
20
+ from folio_migration_tools.library_configuration import (
21
+ FileDefinition,
22
+ LibraryConfiguration,
23
+ )
24
+ from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
25
+ MappingFileMapperBase,
26
+ )
27
+ from folio_migration_tools.mapping_file_transformation.order_mapper import (
28
+ CompositeOrderMapper,
29
+ )
30
+ from folio_migration_tools.migration_tasks.migration_task_base import (
31
+ MigrationTaskBase,
32
+ )
33
+ from folio_migration_tools.task_configuration import AbstractTaskConfiguration
34
+
35
+ csv.field_size_limit(int(ctypes.c_ulong(-1).value // 2))
36
+
37
+
38
+ # Read files and do some work
39
+ class OrdersTransformer(MigrationTaskBase):
40
+ class TaskConfiguration(AbstractTaskConfiguration):
41
+ name: Annotated[
42
+ str,
43
+ Field(
44
+ title="Task name",
45
+ description="The name of the task.",
46
+ ),
47
+ ]
48
+ migration_task_type: Annotated[
49
+ str,
50
+ Field(
51
+ title="Migration task type",
52
+ description="Type of the migration task.",
53
+ ),
54
+ ]
55
+ files: Annotated[
56
+ List[FileDefinition],
57
+ Field(
58
+ title="Files",
59
+ description="List of the files.",
60
+ ),
61
+ ]
62
+ orders_mapping_file_name: Annotated[
63
+ str,
64
+ Field(
65
+ title="Orders Mapping File Name",
66
+ description="File name for orders mapping.",
67
+ ),
68
+ ]
69
+ organizations_code_map_file_name: Annotated[
70
+ str,
71
+ Field(
72
+ title="Organizations Code Map File Name",
73
+ description="File name for organizations code mapping.",
74
+ ),
75
+ ]
76
+ acquisition_method_map_file_name: Annotated[
77
+ str,
78
+ Field(
79
+ title="Acquisition Method Map File Name",
80
+ description="File name for acquisition method mapping.",
81
+ ),
82
+ ]
83
+ payment_status_map_file_name: Annotated[
84
+ Optional[str],
85
+ Field(
86
+ title="Payment Status Map File Name",
87
+ description=(
88
+ "File name for payment status mapping. "
89
+ "By default is empty string."
90
+ ),
91
+ ),
92
+ ] = ""
93
+ receipt_status_map_file_name: Annotated[
94
+ Optional[str],
95
+ Field(
96
+ title="Receipt Status Map File Name",
97
+ description=(
98
+ "File name for receipt status mapping. "
99
+ "By default is empty string."
100
+ ),
101
+ ),
102
+ ] = ""
103
+ workflow_status_map_file_name: Annotated[
104
+ Optional[str],
105
+ Field(
106
+ title="Workflow Status Map File Name",
107
+ description=(
108
+ "File name for workflow status mapping. "
109
+ "By default is empty string."
110
+ ),
111
+ ),
112
+ ] = ""
113
+ location_map_file_name: Annotated[
114
+ Optional[str],
115
+ Field(
116
+ title="Location Map File Name",
117
+ description=(
118
+ "File name for location mapping. "
119
+ "By default is empty string."
120
+ ),
121
+ ),
122
+ ] = ""
123
+ funds_map_file_name: Annotated[
124
+ Optional[str],
125
+ Field(
126
+ title="Funds Map File Name",
127
+ description=(
128
+ "File name for funds mapping. "
129
+ "By default is empty string."
130
+ ),
131
+ ),
132
+ ] = ""
133
+ funds_expense_class_map_file_name: Annotated[
134
+ Optional[str],
135
+ Field(
136
+ title="Funds Expense Class Map File Name",
137
+ description=(
138
+ "File name for funds expense class mapping. "
139
+ "By default is empty string."
140
+ ),
141
+ ),
142
+ ] = ""
143
+
144
+ @staticmethod
145
+ def get_object_type() -> FOLIONamespaces:
146
+ return FOLIONamespaces.orders
147
+
148
+ def __init__(
149
+ self,
150
+ task_config: TaskConfiguration,
151
+ library_config: LibraryConfiguration,
152
+ folio_client,
153
+ use_logging: bool = True,
154
+ ):
155
+ csv.register_dialect("tsv", delimiter="\t")
156
+
157
+ super().__init__(library_config, task_config, folio_client, use_logging)
158
+ self.object_type_name = self.get_object_type().name
159
+ self.task_config = task_config
160
+ self.task_configuration = self.task_config
161
+ self.files = self.list_source_files()
162
+ self.total_records = 0
163
+ self.current_folio_record: dict = {}
164
+ self.orders_map = self.setup_records_map(
165
+ self.folder_structure.mapping_files_folder / self.task_config.orders_mapping_file_name
166
+ )
167
+ self.results_path = self.folder_structure.created_objects_path
168
+ self.failed_files: List[str] = []
169
+
170
+ self.folio_keys = []
171
+ self.folio_keys = MappingFileMapperBase.get_mapped_folio_properties_from_map(
172
+ self.orders_map
173
+ )
174
+ self.minted_ids: set = set()
175
+
176
+ self.mapper = CompositeOrderMapper(
177
+ self.folio_client,
178
+ self.library_configuration,
179
+ self.task_configuration,
180
+ self.orders_map,
181
+ self.load_id_map(self.folder_structure.organizations_id_map_path, True),
182
+ self.load_instance_id_map(True),
183
+ self.load_ref_data_mapping_file(
184
+ "acquisitionMethod",
185
+ self.folder_structure.mapping_files_folder
186
+ / self.task_config.acquisition_method_map_file_name,
187
+ self.folio_keys,
188
+ ),
189
+ self.load_ref_data_mapping_file( # Not required, on POL
190
+ "paymentStatus",
191
+ self.folder_structure.mapping_files_folder
192
+ / self.task_config.payment_status_map_file_name,
193
+ self.folio_keys,
194
+ False,
195
+ ),
196
+ self.load_ref_data_mapping_file( # Not required, on POL
197
+ "receiptStatus",
198
+ self.folder_structure.mapping_files_folder
199
+ / self.task_config.receipt_status_map_file_name,
200
+ self.folio_keys,
201
+ False,
202
+ ),
203
+ self.load_ref_data_mapping_file( # Not required
204
+ "workflowStatus",
205
+ self.folder_structure.mapping_files_folder
206
+ / self.task_config.workflow_status_map_file_name,
207
+ self.folio_keys,
208
+ False,
209
+ ),
210
+ self.load_ref_data_mapping_file(
211
+ "locationMap",
212
+ self.folder_structure.mapping_files_folder
213
+ / self.task_config.location_map_file_name,
214
+ self.folio_keys,
215
+ False,
216
+ ),
217
+ self.load_ref_data_mapping_file( # Required if there was is a fund.
218
+ "fundsMap",
219
+ self.folder_structure.mapping_files_folder
220
+ / self.task_config.funds_map_file_name,
221
+ self.folio_keys,
222
+ True,
223
+ ),
224
+ self.load_ref_data_mapping_file( # Todo: The property in the schema has no type
225
+ "fundsExpenseClassMap",
226
+ self.folder_structure.mapping_files_folder
227
+ / self.task_config.funds_expense_class_map_file_name,
228
+ self.folio_keys,
229
+ False,
230
+ ),
231
+ )
232
+
233
+ def list_source_files(self):
234
+ files = [
235
+ self.folder_structure.data_folder / self.object_type_name / f.file_name
236
+ for f in self.task_config.files
237
+ if isfile(self.folder_structure.data_folder / self.object_type_name / f.file_name)
238
+ ]
239
+ if not any(files):
240
+ ret_str = ",".join(f.file_name for f in self.task_config.files)
241
+ raise TransformationProcessError(
242
+ f"Files {ret_str} not found in"
243
+ "{self.folder_structure.data_folder} / {self.object_type_name}"
244
+ )
245
+ logging.info("Files to process:")
246
+ for filename in files:
247
+ logging.info("\t%s", filename)
248
+ return files
249
+
250
+ def process_single_file(self, filename):
251
+ with open(filename, encoding="utf-8-sig") as records_file, open(
252
+ self.folder_structure.created_objects_path, "w+"
253
+ ) as results_file:
254
+ self.mapper.migration_report.add_general_statistics(
255
+ i18n.t("Number of files processed")
256
+ )
257
+ start = time.time()
258
+ records_processed = 0
259
+ for idx, record in enumerate(self.mapper.get_objects(records_file, filename)):
260
+ records_processed += 1
261
+
262
+ try:
263
+ # Print first legacy record, then first transformed record
264
+ if idx == 0:
265
+ logging.info("First legacy record:")
266
+ logging.info(json.dumps(record, indent=4))
267
+
268
+ folio_rec, legacy_id = self.mapper.do_map(
269
+ record, f"row {idx}", FOLIONamespaces.orders, True
270
+ )
271
+ self.mapper.perform_additional_mapping(legacy_id, folio_rec)
272
+
273
+ self.mapper.migration_report.add_general_statistics(
274
+ i18n.t("TOTAL Purchase Order Lines created")
275
+ )
276
+ self.mapper.report_folio_mapping(folio_rec, self.mapper.composite_order_schema)
277
+ self.mapper.notes_mapper.map_notes(
278
+ record,
279
+ legacy_id,
280
+ folio_rec["id"],
281
+ FOLIONamespaces.orders,
282
+ )
283
+
284
+ self.merge_into_orders_with_embedded_pols(folio_rec, results_file)
285
+
286
+ except TransformationProcessError as process_error:
287
+ self.mapper.handle_transformation_process_error(idx, process_error)
288
+ except TransformationRecordFailedError as error:
289
+ self.mapper.handle_transformation_record_failed_error(idx, error)
290
+ except Exception as excepion:
291
+ self.mapper.handle_generic_exception(idx, excepion)
292
+
293
+ # TODO Rewrite to base % value on number of rows in file
294
+ if idx > 1 and idx % 50 == 0:
295
+ elapsed = idx / (time.time() - start)
296
+ elapsed_formatted = "{0:.4g}".format(elapsed)
297
+ logging.info( # pylint: disable=logging-fstring-interpolation
298
+ f"{idx:,} records processed. Recs/sec: {elapsed_formatted} "
299
+ )
300
+
301
+ self.total_records = records_processed
302
+
303
+ logging.info( # pylint: disable=logging-fstring-interpolation
304
+ f"Done processing {filename} containing {self.total_records:,} records. "
305
+ f"Total records processed: {self.total_records:,}"
306
+ )
307
+ logging.info("Storing last record to disk")
308
+ Helper.write_to_file(results_file, self.current_folio_record)
309
+ self.mapper.migration_report.add_general_statistics(
310
+ i18n.t("TOTAL Purchase Orders created")
311
+ )
312
+
313
+ def do_work(self):
314
+ logging.info("Getting started!")
315
+ for file in self.files:
316
+ logging.info("Processing %s", file)
317
+ try:
318
+ print(file)
319
+ self.process_single_file(file)
320
+ except Exception as ee:
321
+ error_str = (
322
+ f"Processing of {file} failed:\n{ee}."
323
+ "Check source files for empty lines or missing reference data"
324
+ )
325
+ logging.exception(error_str)
326
+ self.mapper.migration_report.add("FailedFiles", f"{file} - {ee}")
327
+ sys.exit()
328
+
329
+ def wrap_up(self):
330
+ logging.info("Done. Wrapping up...")
331
+ with open(self.folder_structure.migration_reports_file, "w") as migration_report_file:
332
+ logging.info(
333
+ "Writing migration- and mapping report to %s",
334
+ self.folder_structure.migration_reports_file,
335
+ )
336
+ self.mapper.migration_report.write_migration_report(
337
+ i18n.t("Pruchase Orders and Purchase Order Lines Transformation Report"),
338
+ migration_report_file,
339
+ self.start_datetime,
340
+ )
341
+
342
+ Helper.print_mapping_report(
343
+ migration_report_file,
344
+ self.total_records,
345
+ self.mapper.mapped_folio_fields,
346
+ self.mapper.mapped_legacy_fields,
347
+ )
348
+ logging.info("All done!")
349
+
350
+ def merge_into_orders_with_embedded_pols(self, folio_rec, results_file):
351
+ # Handle merging and storage
352
+ if not self.current_folio_record:
353
+ self.current_folio_record = folio_rec
354
+ if folio_rec["id"] != self.current_folio_record["id"]:
355
+ # Writes record to file
356
+ Helper.write_to_file(results_file, self.current_folio_record)
357
+ self.mapper.migration_report.add_general_statistics(
358
+ i18n.t("TOTAL Purchase Orders created")
359
+ )
360
+ self.current_folio_record = folio_rec
361
+
362
+ else:
363
+ # Merge if possible
364
+ diff = DeepDiff(self.current_folio_record, folio_rec)
365
+ if "compositePoLines" in diff.affected_root_keys:
366
+ self.current_folio_record.get("compositePoLines", []).extend(
367
+ folio_rec.get("compositePoLines", [])
368
+ )
369
+ self.mapper.migration_report.add_general_statistics(
370
+ i18n.t("Rows merged to create Purchase Orders")
371
+ )
372
+ for key in diff.affected_paths:
373
+ self.mapper.migration_report.add("DiffsBetweenOrders", key)