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
@@ -1,5 +1,3 @@
1
- import ast
2
- import copy
3
1
  import csv
4
2
  import ctypes
5
3
  import json
@@ -7,13 +5,13 @@ import logging
7
5
  import sys
8
6
  import time
9
7
  import traceback
10
- import uuid
11
- from os.path import isfile
12
- from typing import List, Optional
13
- from folioclient import FolioClient
8
+ from typing import Annotated, List, Optional
14
9
 
15
- from folio_uuid import FolioUUID
10
+ import i18n
16
11
  from folio_uuid.folio_namespaces import FOLIONamespaces
12
+ from httpx import HTTPError
13
+ from pydantic import Field
14
+
17
15
  from folio_migration_tools.custom_exceptions import (
18
16
  TransformationProcessError,
19
17
  TransformationRecordFailedError,
@@ -22,7 +20,6 @@ from folio_migration_tools.helper import Helper
22
20
  from folio_migration_tools.holdings_helper import HoldingsHelper
23
21
  from folio_migration_tools.library_configuration import (
24
22
  FileDefinition,
25
- FolioRelease,
26
23
  HridHandling,
27
24
  LibraryConfiguration,
28
25
  )
@@ -32,32 +29,147 @@ from folio_migration_tools.mapping_file_transformation.holdings_mapper import (
32
29
  from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
33
30
  MappingFileMapperBase,
34
31
  )
32
+ from folio_migration_tools.marc_rules_transformation.hrid_handler import HRIDHandler
35
33
  from folio_migration_tools.migration_tasks.migration_task_base import MigrationTaskBase
36
- from pydantic.main import BaseModel
37
-
34
+ from folio_migration_tools.task_configuration import AbstractTaskConfiguration
38
35
 
39
36
  csv.field_size_limit(int(ctypes.c_ulong(-1).value // 2))
40
37
  csv.register_dialect("tsv", delimiter="\t")
41
38
 
42
39
 
43
40
  class HoldingsCsvTransformer(MigrationTaskBase):
44
- class TaskConfiguration(BaseModel):
45
- name: str
46
- migration_task_type: str
47
- hrid_handling: HridHandling
48
- files: List[FileDefinition]
49
- holdings_map_file_name: str
50
- location_map_file_name: str
51
- default_call_number_type_name: str
52
- previously_generated_holdings_files: Optional[list[str]] = []
53
- fallback_holdings_type_id: str
54
- holdings_type_uuid_for_boundwiths: str
55
- call_number_type_map_file_name: Optional[str]
56
- holdings_merge_criteria: Optional[list[str]] = [
41
+ class TaskConfiguration(AbstractTaskConfiguration):
42
+ name: Annotated[
43
+ str,
44
+ Field(
45
+ title="Task name",
46
+ description="Name of the task",
47
+ ),
48
+ ]
49
+ migration_task_type: Annotated[
50
+ str,
51
+ Field(
52
+ title="Migration task type",
53
+ description="Type of migration task",
54
+ ),
55
+ ]
56
+ hrid_handling: Annotated[
57
+ HridHandling,
58
+ Field(
59
+ title="HRID handling",
60
+ description=(
61
+ "Determining how the HRID generation "
62
+ "should be handled."
63
+ ),
64
+ ),
65
+ ]
66
+ files: Annotated[
67
+ List[FileDefinition],
68
+ Field(
69
+ title="Files",
70
+ description="List of files",
71
+ ),
72
+ ]
73
+ holdings_map_file_name: Annotated[
74
+ str,
75
+ Field(
76
+ title="Holdings map file name",
77
+ description="File name for holdings map",
78
+ ),
79
+ ]
80
+ location_map_file_name: Annotated[
81
+ str,
82
+ Field(
83
+ title="Location map file name",
84
+ description="File name for location map",
85
+ ),
86
+ ]
87
+ default_call_number_type_name: Annotated[
88
+ str,
89
+ Field(
90
+ title="Default call number type name",
91
+ description="Default name for call number type",
92
+ ),
93
+ ]
94
+ previously_generated_holdings_files: Annotated[
95
+ Optional[list[str]],
96
+ Field(
97
+ title="Previously generated holdings files",
98
+ description=(
99
+ "List of previously generated holdings files. "
100
+ "By default is empty list."
101
+ ),
102
+ ),
103
+ ] = []
104
+ fallback_holdings_type_id: Annotated[
105
+ str,
106
+ Field(
107
+ title="Fallback holdings type ID",
108
+ description="ID for fallback holdings type",
109
+ ),
110
+ ]
111
+ holdings_type_uuid_for_boundwiths: Annotated[
112
+ str,
113
+ Field(
114
+ title="Holdings Type for Boundwith Holdings",
115
+ description=(
116
+ "UUID for a Holdings type (set in Settings->Inventory) "
117
+ "for Bound-with Holdings. Default is empty string."
118
+ ),
119
+ ),
120
+ ] = ""
121
+ call_number_type_map_file_name: Annotated[
122
+ Optional[str],
123
+ Field(
124
+ title="Call number type map file name",
125
+ description="File name for call number type map",
126
+ ),
127
+ ]
128
+ holdings_merge_criteria: Annotated[
129
+ Optional[list[str]],
130
+ Field(
131
+ title="Holdings merge criteria",
132
+ description=(
133
+ "List of holdings merge criteria. "
134
+ "Default value is "
135
+ "['instanceId', 'permanentLocationId', 'callNumber']."
136
+ ),
137
+ ),
138
+ ] = [
57
139
  "instanceId",
58
140
  "permanentLocationId",
59
141
  "callNumber",
60
142
  ]
143
+ reset_hrid_settings: Annotated[
144
+ Optional[bool],
145
+ Field(
146
+ title="Reset HRID settings",
147
+ description=(
148
+ "At the end of the run reset "
149
+ "FOLIO with the HRID settings. Default is FALSE."
150
+ ),
151
+ ),
152
+ ] = False
153
+ update_hrid_settings: Annotated[
154
+ bool,
155
+ Field(
156
+ title="Update HRID settings",
157
+ description=(
158
+ "At the end of the run update "
159
+ "FOLIO with the HRID settings. Default is TRUE."
160
+ ),
161
+ ),
162
+ ] = True
163
+ statistical_codes_map_file_name: Annotated[
164
+ Optional[str],
165
+ Field(
166
+ title="Statistical code map file name",
167
+ description=(
168
+ "Path to the file containing the mapping of statistical codes. "
169
+ "The file should be in TSV format with legacy_stat_code and folio_code columns."
170
+ ),
171
+ ),
172
+ ] = ""
61
173
 
62
174
  @staticmethod
63
175
  def get_object_type() -> FOLIONamespaces:
@@ -67,46 +179,57 @@ class HoldingsCsvTransformer(MigrationTaskBase):
67
179
  self,
68
180
  task_config: TaskConfiguration,
69
181
  library_config: LibraryConfiguration,
182
+ folio_client,
70
183
  use_logging: bool = True,
71
184
  ):
72
- super().__init__(library_config, task_config, use_logging)
185
+ super().__init__(library_config, task_config, folio_client, use_logging)
73
186
  self.fallback_holdings_type = None
187
+ self.folio_keys, self.holdings_field_map = self.load_mapped_fields()
188
+ if any(k for k in self.folio_keys if k.startswith("statisticalCodeIds")):
189
+ statcode_mapping = self.load_ref_data_mapping_file(
190
+ "statisticalCodeIds",
191
+ self.folder_structure.mapping_files_folder
192
+ / self.task_configuration.statistical_codes_map_file_name,
193
+ self.folio_keys,
194
+ False,
195
+ )
196
+ else:
197
+ statcode_mapping = None
74
198
  try:
75
- self.task_config = task_config
76
199
  self.bound_with_keys = set()
77
- self.files = self.list_source_files()
78
200
  self.mapper = HoldingsMapper(
79
201
  self.folio_client,
80
- self.load_mapped_fields(),
202
+ self.holdings_field_map,
81
203
  self.load_location_map(),
82
204
  self.load_call_number_type_map(),
83
- self.load_id_map(self.folder_structure.instance_id_map_path, True),
205
+ self.load_instance_id_map(True),
84
206
  library_config,
207
+ task_config,
208
+ statcode_mapping,
85
209
  )
86
210
  self.holdings = {}
87
211
  self.total_records = 0
88
- self.holdings_id_map = self.load_id_map(
89
- self.folder_structure.holdings_id_map_path
90
- )
91
- self.holdings_sources = self.get_holdings_sources()
212
+ self.holdings_id_map = self.load_id_map(self.folder_structure.holdings_id_map_path)
92
213
  self.results_path = self.folder_structure.created_objects_path
93
214
  self.holdings_types = list(
94
215
  self.folio_client.folio_get_all("/holdings-types", "holdingsTypes")
95
216
  )
96
217
  logging.info("%s\tholdings types in tenant", len(self.holdings_types))
97
218
  self.validate_merge_criterias()
98
-
219
+ self.check_source_files(
220
+ self.folder_structure.data_folder / "items", self.task_configuration.files
221
+ )
99
222
  self.fallback_holdings_type = next(
100
223
  h
101
224
  for h in self.holdings_types
102
- if h["id"] == self.task_config.fallback_holdings_type_id
225
+ if h["id"] == self.task_configuration.fallback_holdings_type_id
103
226
  )
104
227
  if not self.fallback_holdings_type:
105
228
  raise TransformationProcessError(
106
229
  "",
107
230
  (
108
231
  "Holdings type with ID "
109
- f"{self.task_config.fallback_holdings_type_id} "
232
+ f"{self.task_configuration.fallback_holdings_type_id} "
110
233
  "not found in FOLIO."
111
234
  ),
112
235
  )
@@ -114,35 +237,51 @@ class HoldingsCsvTransformer(MigrationTaskBase):
114
237
  "%s will be used as default holdings type",
115
238
  self.fallback_holdings_type["name"],
116
239
  )
117
- if any(self.task_config.previously_generated_holdings_files):
118
- for file_name in self.task_config.previously_generated_holdings_files:
240
+ if any(self.task_configuration.previously_generated_holdings_files):
241
+ for file_name in self.task_configuration.previously_generated_holdings_files:
119
242
  logging.info("Processing %s", file_name)
120
243
  self.holdings.update(
121
244
  HoldingsHelper.load_previously_generated_holdings(
122
245
  self.folder_structure.results_folder / file_name,
123
- self.task_config.holdings_merge_criteria,
246
+ self.task_configuration.holdings_merge_criteria,
124
247
  self.mapper.migration_report,
125
- self.task_config.holdings_type_uuid_for_boundwiths,
248
+ self.task_configuration.holdings_type_uuid_for_boundwiths,
126
249
  )
127
250
  )
128
251
 
129
252
  else:
130
253
  logging.info("No file of legacy holdings setup.")
131
- except TransformationProcessError as process_error:
254
+
255
+ if (
256
+ self.task_configuration.reset_hrid_settings
257
+ and self.task_configuration.update_hrid_settings
258
+ ):
259
+ hrid_handler = HRIDHandler(
260
+ self.folio_client, HridHandling.default, self.mapper.migration_report, True
261
+ )
262
+ hrid_handler.reset_holdings_hrid_counter()
263
+
264
+ except HTTPError as http_error:
265
+ logging.critical(http_error)
266
+ sys.exit(1)
267
+ except (FileNotFoundError, TransformationProcessError) as process_error:
132
268
  logging.critical(process_error)
133
269
  logging.critical("Halting.")
134
270
  sys.exit(1)
271
+ except json.JSONDecodeError as jde:
272
+ raise jde
135
273
  except Exception as exception:
136
274
  logging.info("\n=======ERROR===========")
137
275
  logging.info(exception)
138
276
  logging.info("\n=======Stack Trace===========")
139
277
  traceback.print_exc()
278
+ sys.exit(1)
140
279
  logging.info("Init done")
141
280
 
142
281
  def load_call_number_type_map(self):
143
282
  with open(
144
283
  self.folder_structure.mapping_files_folder
145
- / self.task_config.call_number_type_map_file_name,
284
+ / self.task_configuration.call_number_type_map_file_name,
146
285
  "r",
147
286
  ) as callnumber_type_map_f:
148
287
  return self.load_ref_data_map_from_file(
@@ -151,8 +290,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
151
290
 
152
291
  def load_location_map(self):
153
292
  with open(
154
- self.folder_structure.mapping_files_folder
155
- / self.task_config.location_map_file_name
293
+ self.folder_structure.mapping_files_folder / self.task_configuration.location_map_file_name
156
294
  ) as location_map_f:
157
295
  return self.load_ref_data_map_from_file(
158
296
  location_map_f, "Found %s rows in location map"
@@ -166,13 +304,10 @@ class HoldingsCsvTransformer(MigrationTaskBase):
166
304
 
167
305
  def load_mapped_fields(self):
168
306
  with open(
169
- self.folder_structure.mapping_files_folder
170
- / self.task_config.holdings_map_file_name
307
+ self.folder_structure.mapping_files_folder / self.task_configuration.holdings_map_file_name
171
308
  ) as holdings_mapper_f:
172
309
  holdings_map = json.load(holdings_mapper_f)
173
- logging.info(
174
- "%s fields in holdings mapping file map", len(holdings_map["data"])
175
- )
310
+ logging.info("%s fields in holdings mapping file map", len(holdings_map["data"]))
176
311
  mapped_fields = MappingFileMapperBase.get_mapped_folio_properties_from_map(
177
312
  holdings_map
178
313
  )
@@ -180,91 +315,56 @@ class HoldingsCsvTransformer(MigrationTaskBase):
180
315
  "%s mapped fields in holdings mapping file map",
181
316
  len(list(mapped_fields)),
182
317
  )
183
- return holdings_map
184
-
185
- def list_source_files(self):
186
- # Source data files
187
- files = [
188
- self.folder_structure.data_folder / "items" / f.file_name
189
- for f in self.task_config.files
190
- if isfile(self.folder_structure.data_folder / "items" / f.file_name)
191
- ]
192
- if not any(files):
193
- ret_str = ",".join(f.file_name for f in self.task_config.files)
194
- raise TransformationProcessError(
195
- "",
196
- f"Files {ret_str} not found in {self.folder_structure.data_folder / 'items'}",
197
- )
198
- logging.info("Files to process:")
199
- for filename in files:
200
- logging.info("\t%s", filename)
201
- return files
202
-
203
- def load_instance_id_map(self):
204
- res = {}
205
- with open(
206
- self.folder_structure.instance_id_map_path, "r"
207
- ) as instance_id_map_file:
208
- for index, json_string in enumerate(instance_id_map_file):
209
- # Format:{"legacy_id", "folio_id","instanceLevelCallNumber"}
210
- if index % 100000 == 0:
211
- print(f"{index} instance ids loaded to map", end="\r")
212
- map_object = json.loads(json_string)
213
- res[map_object["legacy_id"]] = map_object
214
- logging.info("Loaded %s migrated instance IDs", (index + 1))
215
- return res
318
+ return mapped_fields, holdings_map
216
319
 
217
320
  def do_work(self):
218
321
  logging.info("Starting....")
219
- for file_name in self.files:
220
- logging.info("Processing %s", file_name)
322
+ for file_def in self.task_configuration.files:
323
+ logging.info("Processing %s", file_def.file_name)
221
324
  try:
222
- self.process_single_file(file_name)
325
+ self.process_single_file(file_def)
223
326
  except Exception as ee:
224
327
  error_str = (
225
- f"Processing of {file_name} failed:\n{ee}."
226
- "\nCheck source files for empty lines or missing reference data"
328
+ f"Processing of {file_def.file_name} failed:\n{ee}."
329
+ "\nCheck source files for empty rows or missing reference data"
227
330
  )
228
331
  logging.critical(error_str)
229
332
  print(f"\n{error_str}\nHalting")
230
333
  sys.exit(1)
231
334
  logging.info(
232
- f"processed {self.total_records:,} records in {len(self.files)} files"
335
+ f"processed {self.total_records:,} records in {len(self.task_configuration.files)} files"
233
336
  )
234
337
 
235
338
  def wrap_up(self):
236
- logging.info("Work done. Wrapping up...")
339
+ logging.info("Done. Transformer wrapping up...")
340
+ self.extradata_writer.flush()
237
341
  if any(self.holdings):
238
342
  logging.info(
239
343
  "Saving holdings created to %s",
240
344
  self.folder_structure.created_objects_path,
241
345
  )
242
- with open(
243
- self.folder_structure.created_objects_path, "w+"
244
- ) as holdings_file:
346
+ with open(self.folder_structure.created_objects_path, "w+") as holdings_file:
245
347
  for holding in self.holdings.values():
246
348
  for legacy_id in holding["formerIds"]:
247
349
  # Prevent the first item in a boundwith to be overwritten
248
350
  # TODO: Find out why not
249
351
  # if legacy_id not in self.holdings_id_map:
250
- self.holdings_id_map[legacy_id] = self.mapper.get_id_map_dict(
251
- legacy_id, holding
352
+ self.holdings_id_map[legacy_id] = self.mapper.get_id_map_tuple(
353
+ legacy_id, holding, self.object_type
252
354
  )
253
355
  Helper.write_to_file(holdings_file, holding)
254
356
  self.mapper.migration_report.add_general_statistics(
255
- "Holdings Records Written to disk"
357
+ i18n.t("Holdings Records Written to disk")
256
358
  )
257
359
  self.mapper.save_id_map_file(
258
360
  self.folder_structure.holdings_id_map_path, self.holdings_id_map
259
361
  )
260
- with open(
261
- self.folder_structure.migration_reports_file, "w"
262
- ) as migration_report_file:
263
- logging.info(
264
- "Writing migration- and mapping report to %s",
265
- self.folder_structure.migration_reports_file,
362
+ with open(self.folder_structure.migration_reports_file, "w") as migration_report_file:
363
+ self.mapper.migration_report.write_migration_report(
364
+ i18n.t("Holdings transformation report"),
365
+ migration_report_file,
366
+ self.mapper.start_datetime,
266
367
  )
267
- self.mapper.migration_report.write_migration_report(migration_report_file)
268
368
  Helper.print_mapping_report(
269
369
  migration_report_file,
270
370
  self.total_records,
@@ -272,17 +372,14 @@ class HoldingsCsvTransformer(MigrationTaskBase):
272
372
  self.mapper.mapped_legacy_fields,
273
373
  )
274
374
  logging.info("All done!")
375
+ self.clean_out_empty_logs()
275
376
 
276
377
  def validate_merge_criterias(self):
277
378
  holdings_schema = self.folio_client.get_holdings_schema()
278
379
  properties = holdings_schema["properties"].keys()
279
380
  logging.info(properties)
280
- logging.info(self.task_config.holdings_merge_criteria)
281
- res = [
282
- mc
283
- for mc in self.task_config.holdings_merge_criteria
284
- if mc not in properties
285
- ]
381
+ logging.info(self.task_configuration.holdings_merge_criteria)
382
+ res = [mc for mc in self.task_configuration.holdings_merge_criteria if mc not in properties]
286
383
  if any(res):
287
384
  logging.critical(
288
385
  (
@@ -293,23 +390,22 @@ class HoldingsCsvTransformer(MigrationTaskBase):
293
390
  )
294
391
  sys.exit(1)
295
392
 
296
- def process_single_file(self, file_name):
297
- with open(file_name, encoding="utf-8-sig") as records_file:
393
+ def process_single_file(self, file_def: FileDefinition):
394
+ full_path = self.folder_structure.data_folder / "items" / file_def.file_name
395
+ with open(full_path, encoding="utf-8-sig") as records_file:
298
396
  self.mapper.migration_report.add_general_statistics(
299
- "Number of files processed"
397
+ i18n.t("Number of files processed")
300
398
  )
301
399
  start = time.time()
302
400
  records_processed = 0
303
- for idx, legacy_record in enumerate(
304
- self.mapper.get_objects(records_file, file_name)
305
- ):
401
+ for idx, legacy_record in enumerate(self.mapper.get_objects(records_file, full_path)):
306
402
  records_processed = idx + 1
307
403
  try:
308
404
  self.mapper.verify_legacy_record(legacy_record, idx)
309
405
  folio_rec, legacy_id = self.mapper.do_map(
310
406
  legacy_record, f"row # {idx}", FOLIONamespaces.holdings
311
407
  )
312
- self.post_process_holding(folio_rec, legacy_id)
408
+ self.post_process_holding(folio_rec, legacy_id, file_def)
313
409
  except TransformationProcessError as process_error:
314
410
  self.mapper.handle_transformation_process_error(idx, process_error)
315
411
  except TransformationRecordFailedError as error:
@@ -317,27 +413,25 @@ class HoldingsCsvTransformer(MigrationTaskBase):
317
413
  except Exception as excepion:
318
414
  self.mapper.handle_generic_exception(idx, excepion)
319
415
  self.mapper.migration_report.add_general_statistics(
320
- "Number of Legacy items in file"
416
+ i18n.t("Number of Legacy items in file")
321
417
  )
322
418
  if idx > 1 and idx % 10000 == 0:
323
419
  elapsed = idx / (time.time() - start)
324
420
  elapsed_formatted = "{0:.4g}".format(elapsed)
325
- logging.info(
326
- f"{idx:,} records processed. Recs/sec: {elapsed_formatted} "
327
- )
421
+ logging.info(f"{idx:,} records processed. Recs/sec: {elapsed_formatted} ")
328
422
  self.total_records = records_processed
329
423
  logging.info(
330
- f"Done processing {file_name} containing {self.total_records:,} records. "
424
+ f"Done processing {file_def.file_name} containing {self.total_records:,} records. "
331
425
  f"Total records processed: {self.total_records:,}"
332
426
  )
333
427
 
334
- def post_process_holding(self, folio_rec: dict, legacy_id: str):
428
+ def post_process_holding(self, folio_rec: dict, legacy_id: str, file_def: FileDefinition):
335
429
  HoldingsHelper.handle_notes(folio_rec)
430
+ HoldingsHelper.remove_empty_holdings_statements(folio_rec)
431
+
336
432
  if not folio_rec.get("holdingsTypeId", ""):
337
433
  folio_rec["holdingsTypeId"] = self.fallback_holdings_type["id"]
338
434
 
339
- folio_rec["sourceId"] = self.holdings_sources.get("FOLIO")
340
-
341
435
  holdings_from_row = []
342
436
  all_instance_ids = folio_rec.get("instanceId", [])
343
437
  if len(all_instance_ids) == 1:
@@ -346,152 +440,77 @@ class HoldingsCsvTransformer(MigrationTaskBase):
346
440
  holdings_from_row.append(folio_rec)
347
441
 
348
442
  elif len(folio_rec.get("instanceId", [])) > 1: # Bound-with.
349
- holdings_from_row.extend(
350
- self.create_bound_with_holdings(folio_rec, legacy_id)
351
- )
443
+ holdings_from_row.extend(self.create_bound_with_holdings(folio_rec, legacy_id))
352
444
  else:
353
- raise TransformationRecordFailedError(
354
- legacy_id, "No instance id in parsed record", ""
355
- )
445
+ raise TransformationRecordFailedError(legacy_id, "No instance id in parsed record", "")
356
446
 
357
447
  for folio_holding in holdings_from_row:
448
+ self.mapper.perform_additional_mappings(legacy_id, folio_holding, file_def)
358
449
  self.merge_holding_in(folio_holding, all_instance_ids, legacy_id)
359
450
  self.mapper.report_folio_mapping(folio_holding, self.mapper.schema)
360
451
 
361
452
  def create_bound_with_holdings(self, folio_holding, legacy_id: str):
362
- if not self.task_config.holdings_type_uuid_for_boundwiths:
363
- raise TransformationProcessError(
364
- "Missing task setting holdingsTypeUuidForBoundwiths. Add a "
365
- "holdingstype specifically for boundwith holdings and reference "
366
- "the UUID in this parameter."
367
- )
368
-
369
- # Add former ids
370
- temp_ids = []
371
- for former_id in folio_holding.get("formerIds", []):
372
- if (
373
- former_id.startswith("[")
374
- and former_id.endswith("]")
375
- and "," in former_id
376
- ):
377
- ids = list(
378
- former_id[1:-1]
379
- .replace('"', "")
380
- .replace(" ", "")
381
- .replace("'", "")
382
- .split(",")
383
- )
384
- temp_ids.extend(ids)
385
- else:
386
- temp_ids.append(former_id)
387
- folio_holding["formerIds"] = temp_ids
388
- for bwidx, instance_id in enumerate(folio_holding["instanceId"]):
389
- if not instance_id:
390
- raise ValueError(f"No ID for record {folio_holding}")
391
-
392
- bound_with_holding = copy.deepcopy(folio_holding)
393
- bound_with_holding["instanceId"] = instance_id
394
- if folio_holding.get("callNumber", None):
395
- call_numbers = ast.literal_eval(folio_holding["callNumber"])
396
- if isinstance(call_numbers, str):
397
- call_numbers = [call_numbers]
398
- bound_with_holding["callNumber"] = call_numbers[bwidx]
399
- if not self.task_config.holdings_type_uuid_for_boundwiths:
400
- raise TransformationProcessError(
401
- "",
402
- (
403
- "Boundwith UUID not added to task configuration."
404
- "Add a property to holdingsTypeUuidForBoundwiths to "
405
- "the task configuration"
406
- ),
407
- "",
408
- )
409
- bound_with_holding[
410
- "holdingsTypeId"
411
- ] = self.task_config.holdings_type_uuid_for_boundwiths
412
- bound_with_holding["id"] = str(
413
- FolioUUID(
414
- self.folio_client.okapi_url,
415
- FOLIONamespaces.holdings,
416
- f'{folio_holding["id"]}-{instance_id}',
417
- )
453
+ folio_holding["formerIds"] = explode_former_ids(folio_holding)
454
+ return list(
455
+ self.mapper.create_bound_with_holdings(
456
+ folio_holding,
457
+ folio_holding["instanceId"],
458
+ self.task_configuration.holdings_type_uuid_for_boundwiths,
418
459
  )
419
- self.mapper.migration_report.add_general_statistics(
420
- "Bound-with holdings created"
421
- )
422
- yield bound_with_holding
423
-
424
- @staticmethod
425
- def generate_boundwith_part(
426
- folio_client: FolioClient, legacy_item_id: str, bound_with_holding: dict
427
- ):
428
- part = {
429
- "id": str(uuid.uuid4()),
430
- "holdingsRecordId": bound_with_holding["id"],
431
- "itemId": str(
432
- FolioUUID(
433
- folio_client.okapi_url,
434
- FOLIONamespaces.items,
435
- legacy_item_id,
436
- )
437
- ),
438
- }
439
- logging.log(25, f"boundwithPart\t{json.dumps(part)}")
460
+ )
440
461
 
441
462
  def merge_holding_in(
442
463
  self, incoming_holding: dict, instance_ids: list[str], legacy_item_id: str
443
- ):
464
+ ) -> None:
444
465
  """Determines what newly generated holdingsrecords are to be merged with
445
466
  previously created ones. When that is done, it generates the correct boundwith
446
467
  parts needed.
447
468
 
448
469
  Args:
449
- new_folio_holding (dict): The newly created FOLIO Holding
470
+ incoming_holding (dict): The newly created FOLIO Holding
450
471
  instance_ids (list): the instance IDs tied to the current item
451
472
  legacy_item_id (str): Id of the Item the holding was generated from
452
473
  """
453
-
454
474
  if len(instance_ids) > 1:
455
475
  # Is boundwith
456
476
  bw_key = (
457
- f"bw_{incoming_holding['instanceId']}_{'_'.join(sorted(instance_ids))}"
477
+ f"bw_{incoming_holding['instanceId']}_{incoming_holding['permanentLocationId']}_"
478
+ f"{incoming_holding.get('callNumber', '')}_{'_'.join(sorted(instance_ids))}"
458
479
  )
459
480
  if bw_key not in self.bound_with_keys:
460
481
  self.bound_with_keys.add(bw_key)
461
482
  self.holdings[bw_key] = incoming_holding
462
- self.generate_boundwith_part(
463
- self.folio_client, legacy_item_id, incoming_holding
464
- )
483
+ self.mapper.create_and_write_boundwith_part(legacy_item_id, incoming_holding["id"])
465
484
  self.mapper.migration_report.add_general_statistics(
466
- "Unique BW Holdings created from Items"
485
+ i18n.t("Unique BW Holdings created from Items")
467
486
  )
468
487
  else:
469
488
  self.merge_holding(bw_key, incoming_holding)
470
- self.generate_boundwith_part(
471
- self.folio_client, legacy_item_id, self.holdings[bw_key]
489
+ self.mapper.create_and_write_boundwith_part(
490
+ legacy_item_id, self.holdings[bw_key]["id"]
472
491
  )
473
- self.holdings_id_map[legacy_item_id] = self.mapper.get_id_map_dict(
474
- legacy_item_id, self.holdings[bw_key]
492
+ self.holdings_id_map[legacy_item_id] = self.mapper.get_id_map_tuple(
493
+ legacy_item_id, self.holdings[bw_key], self.object_type
475
494
  )
476
495
  self.mapper.migration_report.add_general_statistics(
477
- "BW Items found tied to previously created BW Holdings"
496
+ i18n.t("BW Items found tied to previously created BW Holdings")
478
497
  )
479
498
  else:
480
499
  # Regular holding. Merge according to criteria
481
500
  new_holding_key = HoldingsHelper.to_key(
482
501
  incoming_holding,
483
- self.task_config.holdings_merge_criteria,
502
+ self.task_configuration.holdings_merge_criteria,
484
503
  self.mapper.migration_report,
485
- self.task_config.holdings_type_uuid_for_boundwiths,
504
+ self.task_configuration.holdings_type_uuid_for_boundwiths,
486
505
  )
487
506
  if self.holdings.get(new_holding_key, None):
488
507
  self.mapper.migration_report.add_general_statistics(
489
- "Holdings already created from Item"
508
+ i18n.t("Holdings already created from Item")
490
509
  )
491
510
  self.merge_holding(new_holding_key, incoming_holding)
492
511
  else:
493
512
  self.mapper.migration_report.add_general_statistics(
494
- "Unique Holdings created from Items"
513
+ i18n.t("Unique Holdings created from Items")
495
514
  )
496
515
  self.holdings[new_holding_key] = incoming_holding
497
516
 
@@ -500,29 +519,15 @@ class HoldingsCsvTransformer(MigrationTaskBase):
500
519
  self.holdings[holdings_key], new_holdings_record
501
520
  )
502
521
 
503
- def get_holdings_sources(self):
504
- res = {}
505
- if self.library_configuration.folio_release != FolioRelease.juniper:
506
- holdings_sources = list(
507
- self.mapper.folio_client.folio_get_all(
508
- "/holdings-sources", "holdingsRecordsSources"
509
- )
510
- )
511
- logging.info(
512
- "Fetched %s holdingsRecordsSources from tenant", len(holdings_sources)
513
- )
514
- res = {n["name"].upper(): n["id"] for n in holdings_sources}
515
- if "FOLIO" not in res:
516
- raise TransformationProcessError(
517
- "", "No holdings source with name FOLIO in tenant"
518
- )
519
- if "MARC" not in res:
520
- raise TransformationProcessError(
521
- "", "No holdings source with name MARC in tenant"
522
- )
523
- return res
524
-
525
522
 
526
- def dedupe(list_of_dicts):
527
- # TODO: Move to interface or parent class
528
- return [dict(t) for t in {tuple(d.items()) for d in list_of_dicts}]
523
+ def explode_former_ids(folio_holding: dict):
524
+ temp_ids = []
525
+ for former_id in folio_holding.get("formerIds", []):
526
+ if former_id.startswith("[") and former_id.endswith("]") and "," in former_id:
527
+ ids = list(
528
+ former_id[1:-1].replace('"', "").replace(" ", "").replace("'", "").split(",")
529
+ )
530
+ temp_ids.extend(ids)
531
+ else:
532
+ temp_ids.append(former_id)
533
+ return temp_ids