folio-migration-tools 1.9.9__py3-none-any.whl → 1.10.0__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 (58) hide show
  1. folio_migration_tools/__init__.py +3 -4
  2. folio_migration_tools/__main__.py +53 -31
  3. folio_migration_tools/circulation_helper.py +118 -108
  4. folio_migration_tools/custom_dict.py +2 -2
  5. folio_migration_tools/custom_exceptions.py +4 -5
  6. folio_migration_tools/folder_structure.py +17 -7
  7. folio_migration_tools/helper.py +8 -7
  8. folio_migration_tools/holdings_helper.py +4 -3
  9. folio_migration_tools/i18n_cache.py +79 -0
  10. folio_migration_tools/library_configuration.py +77 -37
  11. folio_migration_tools/mapper_base.py +45 -31
  12. folio_migration_tools/mapping_file_transformation/courses_mapper.py +1 -1
  13. folio_migration_tools/mapping_file_transformation/holdings_mapper.py +7 -3
  14. folio_migration_tools/mapping_file_transformation/item_mapper.py +13 -26
  15. folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +1 -2
  16. folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +13 -11
  17. folio_migration_tools/mapping_file_transformation/order_mapper.py +23 -5
  18. folio_migration_tools/mapping_file_transformation/organization_mapper.py +3 -3
  19. folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +3 -0
  20. folio_migration_tools/mapping_file_transformation/user_mapper.py +47 -28
  21. folio_migration_tools/marc_rules_transformation/conditions.py +82 -97
  22. folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +13 -5
  23. folio_migration_tools/marc_rules_transformation/hrid_handler.py +3 -2
  24. folio_migration_tools/marc_rules_transformation/marc_file_processor.py +26 -24
  25. folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +56 -51
  26. folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +28 -17
  27. folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +68 -37
  28. folio_migration_tools/migration_report.py +18 -7
  29. folio_migration_tools/migration_tasks/batch_poster.py +285 -354
  30. folio_migration_tools/migration_tasks/bibs_transformer.py +14 -9
  31. folio_migration_tools/migration_tasks/courses_migrator.py +2 -3
  32. folio_migration_tools/migration_tasks/holdings_csv_transformer.py +23 -24
  33. folio_migration_tools/migration_tasks/holdings_marc_transformer.py +14 -24
  34. folio_migration_tools/migration_tasks/items_transformer.py +23 -34
  35. folio_migration_tools/migration_tasks/loans_migrator.py +67 -144
  36. folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +3 -3
  37. folio_migration_tools/migration_tasks/migration_task_base.py +43 -52
  38. folio_migration_tools/migration_tasks/orders_transformer.py +25 -41
  39. folio_migration_tools/migration_tasks/organization_transformer.py +9 -18
  40. folio_migration_tools/migration_tasks/requests_migrator.py +21 -24
  41. folio_migration_tools/migration_tasks/reserves_migrator.py +6 -5
  42. folio_migration_tools/migration_tasks/user_transformer.py +25 -20
  43. folio_migration_tools/task_configuration.py +6 -7
  44. folio_migration_tools/transaction_migration/legacy_loan.py +15 -27
  45. folio_migration_tools/transaction_migration/legacy_request.py +1 -1
  46. folio_migration_tools/translations/en.json +3 -8
  47. {folio_migration_tools-1.9.9.dist-info → folio_migration_tools-1.10.0.dist-info}/METADATA +19 -28
  48. folio_migration_tools-1.10.0.dist-info/RECORD +63 -0
  49. folio_migration_tools-1.10.0.dist-info/WHEEL +4 -0
  50. folio_migration_tools-1.10.0.dist-info/entry_points.txt +3 -0
  51. folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +0 -241
  52. folio_migration_tools/migration_tasks/authority_transformer.py +0 -119
  53. folio_migration_tools/test_infrastructure/__init__.py +0 -0
  54. folio_migration_tools/test_infrastructure/mocked_classes.py +0 -406
  55. folio_migration_tools-1.9.9.dist-info/RECORD +0 -67
  56. folio_migration_tools-1.9.9.dist-info/WHEEL +0 -4
  57. folio_migration_tools-1.9.9.dist-info/entry_points.txt +0 -3
  58. folio_migration_tools-1.9.9.dist-info/licenses/LICENSE +0 -21
@@ -2,7 +2,7 @@ import json
2
2
  import logging
3
3
  import sys
4
4
  from datetime import datetime, timezone
5
- from typing import Dict, List, Set, Union
5
+ from typing import Dict, List, Set
6
6
  from uuid import uuid4
7
7
 
8
8
  import i18n
@@ -117,7 +117,9 @@ class ItemMapper(MappingFileMapperBase):
117
117
  "LocationMapping",
118
118
  )
119
119
 
120
- def perform_additional_mappings(self, legacy_ids: Union[str, List[str]], folio_rec: Dict, file_def: FileDefinition):
120
+ def perform_additional_mappings(
121
+ self, legacy_ids: List[str] | str, folio_rec: Dict, file_def: FileDefinition
122
+ ):
121
123
  self.handle_suppression(folio_rec, file_def)
122
124
  self.map_statistical_codes(folio_rec, file_def)
123
125
  self.map_statistical_code_ids(legacy_ids, folio_rec)
@@ -126,24 +128,17 @@ class ItemMapper(MappingFileMapperBase):
126
128
  folio_record["discoverySuppress"] = file_def.discovery_suppressed
127
129
  self.migration_report.add(
128
130
  "Suppression",
129
- i18n.t("Suppressed from discovery")
130
- + f" = {folio_record['discoverySuppress']}",
131
+ i18n.t("Suppressed from discovery") + f" = {folio_record['discoverySuppress']}",
131
132
  )
132
133
 
133
134
  def setup_status_mapping(self, item_statuses_map):
134
- statuses = self.item_schema["properties"]["status"]["properties"]["name"][
135
- "enum"
136
- ]
135
+ statuses = self.item_schema["properties"]["status"]["properties"]["name"]["enum"]
137
136
  for mapping in item_statuses_map:
138
137
  if "folio_name" not in mapping:
139
- logging.critical(
140
- "folio_name is not a column in the status mapping file"
141
- )
138
+ logging.critical("folio_name is not a column in the status mapping file")
142
139
  sys.exit(1)
143
140
  elif "legacy_code" not in mapping:
144
- logging.critical(
145
- "legacy_code is not a column in the status mapping file"
146
- )
141
+ logging.critical("legacy_code is not a column in the status mapping file")
147
142
  sys.exit(1)
148
143
  elif mapping["folio_name"] not in statuses:
149
144
  logging.critical(
@@ -158,9 +153,7 @@ class ItemMapper(MappingFileMapperBase):
158
153
  )
159
154
  sys.exit(1)
160
155
  elif not all(mapping.values()):
161
- logging.critical(
162
- "empty value in mapping %s. Check mapping file", mapping.values()
163
- )
156
+ logging.critical("empty value in mapping %s. Check mapping file", mapping.values())
164
157
  sys.exit(1)
165
158
  else:
166
159
  self.status_mapping = {
@@ -168,7 +161,7 @@ class ItemMapper(MappingFileMapperBase):
168
161
  }
169
162
  logging.info(json.dumps(statuses, indent=True))
170
163
 
171
- def get_prop(self, legacy_item, folio_prop_name, index_or_id, schema_default_value):
164
+ def get_prop(self, legacy_item, folio_prop_name, index_or_id, schema_default_value): # noqa: C901
172
165
  if folio_prop_name == "permanentLocationId":
173
166
  return self.get_mapped_ref_data_value(
174
167
  self.location_mapping,
@@ -213,9 +206,7 @@ class ItemMapper(MappingFileMapperBase):
213
206
  index_or_id,
214
207
  True,
215
208
  )
216
- self.migration_report.add(
217
- "TemporaryLoanTypeMapping", f"{folio_prop_name} -> {ltid}"
218
- )
209
+ self.migration_report.add("TemporaryLoanTypeMapping", f"{folio_prop_name} -> {ltid}")
219
210
  return ltid
220
211
  elif folio_prop_name == "permanentLoanTypeId":
221
212
  return self.get_mapped_ref_data_value(
@@ -232,9 +223,7 @@ class ItemMapper(MappingFileMapperBase):
232
223
  normalized_barcode = barcode.strip().lower()
233
224
  if normalized_barcode and normalized_barcode in self.unique_barcodes:
234
225
  Helper.log_data_issue(index_or_id, "Duplicate barcode", mapped_value)
235
- self.migration_report.add_general_statistics(
236
- i18n.t("Duplicate barcodes")
237
- )
226
+ self.migration_report.add_general_statistics(i18n.t("Duplicate barcodes"))
238
227
  return f"{barcode}-{uuid4()}"
239
228
  else:
240
229
  if normalized_barcode:
@@ -259,9 +248,7 @@ class ItemMapper(MappingFileMapperBase):
259
248
  self.migration_report.add("UnmappedProperties", f"{folio_prop_name}")
260
249
  return ""
261
250
 
262
- def get_item_level_call_number_type_id(
263
- self, legacy_item, folio_prop_name: str, index_or_id
264
- ):
251
+ def get_item_level_call_number_type_id(self, legacy_item, folio_prop_name: str, index_or_id):
265
252
  if self.call_number_mapping:
266
253
  return self.get_mapped_ref_data_value(
267
254
  self.call_number_mapping, legacy_item, index_or_id, folio_prop_name
@@ -172,8 +172,7 @@ class ManualFeeFinesMapper(MappingFileMapperBase):
172
172
  except TypeError as te:
173
173
  raise TransformationProcessError(
174
174
  "",
175
- "Failed to fetch Tenant Locale Settings. "
176
- "Is your library configuration correct?",
175
+ "Failed to fetch Tenant Locale Settings. Is your library configuration correct?",
177
176
  ) from te
178
177
  except KeyError as ke:
179
178
  raise TransformationProcessError(
@@ -133,7 +133,7 @@ class MappingFileMapperBase(MapperBase):
133
133
  raise TransformationProcessError(
134
134
  "",
135
135
  f"property legacyIdentifier not setup in map: "
136
- f"{field_map.get('legacyIdentifier', '') ({exception})}",
136
+ f"{field_map.get('legacyIdentifier', '')({exception})}",
137
137
  ) from exception
138
138
  del field_map["legacyIdentifier"]
139
139
  return field_map
@@ -213,12 +213,10 @@ class MappingFileMapperBase(MapperBase):
213
213
  }
214
214
  )
215
215
  if object_type == FOLIONamespaces.holdings and hasattr(self, "holdings_sources"):
216
- folio_object['sourceId'] = self.holdings_sources.get("FOLIO")
216
+ folio_object["sourceId"] = self.holdings_sources.get("FOLIO")
217
217
  elif object_type == FOLIONamespaces.holdings and not hasattr(self, "holdings_sources"):
218
218
  raise TransformationProcessError(
219
- index_or_id,
220
- "Holdings source not set in the mapper",
221
- None
219
+ index_or_id, "Holdings source not set in the mapper", None
222
220
  )
223
221
  return folio_object, legacy_id
224
222
 
@@ -400,7 +398,7 @@ class MappingFileMapperBase(MapperBase):
400
398
  value = replaced_val
401
399
  if value and mapping_file_entry.get("rules", {}).get("regexGetFirstMatchOrEmpty", ""):
402
400
  my_pattern = (
403
- f'{mapping_file_entry.get("rules", {}).get("regexGetFirstMatchOrEmpty")}|$'
401
+ f"{mapping_file_entry.get('rules', {}).get('regexGetFirstMatchOrEmpty')}|$"
404
402
  )
405
403
  value = re.findall(my_pattern, value)[0]
406
404
  if not value and mapping_file_entry.get("fallback_legacy_field", ""):
@@ -498,7 +496,7 @@ class MappingFileMapperBase(MapperBase):
498
496
  set_deep(folio_object, schema_property_name, temp_object)
499
497
  # folio_object[schema_property_name] = temp_object
500
498
 
501
- def map_objects_array_props(
499
+ def map_objects_array_props( # noqa: C901
502
500
  self,
503
501
  legacy_object,
504
502
  prop_name: str,
@@ -553,7 +551,9 @@ class MappingFileMapperBase(MapperBase):
553
551
  )
554
552
  multi_field_props.append(sub_prop_name)
555
553
  else:
556
- self.validate_enums(res, sub_prop, sub_prop_name, index_or_id, required)
554
+ self.validate_enums(
555
+ res, sub_prop, sub_prop_name, index_or_id, required
556
+ )
557
557
 
558
558
  if res or isinstance(res, bool):
559
559
  temp_object[sub_prop_name] = res
@@ -619,8 +619,8 @@ class MappingFileMapperBase(MapperBase):
619
619
  @staticmethod
620
620
  def split_obj_by_delim(delimiter: str, folio_obj: dict, delimited_props: List[str]):
621
621
  non_split_props = [(k, v) for k, v in folio_obj.items() if k not in delimited_props]
622
- delimited_props = map(lambda x: [x, *folio_obj[x].split(delimiter)], delimited_props)
623
- zipped = list(zip(*delimited_props))
622
+ delimited_props = ([x, *folio_obj[x].split(delimiter)] for x in delimited_props)
623
+ zipped = list(zip(*delimited_props, strict=False))
624
624
  res = []
625
625
  for (prop_name_idx, prop_name), (value_idx, ra) in itertools.product(
626
626
  enumerate(zipped[0]), enumerate(zipped[1:])
@@ -973,4 +973,6 @@ def in_deep(dictionary, keys):
973
973
 
974
974
 
975
975
  def is_set_or_bool_or_numeric(any_value):
976
- return (isinstance(any_value, str) and (any_value.strip() not in empty_vals)) or isinstance(any_value, (int, float, complex))
976
+ return (isinstance(any_value, str) and (any_value.strip() not in empty_vals)) or isinstance(
977
+ any_value, (int, float, complex)
978
+ )
@@ -25,7 +25,6 @@ from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
25
25
 
26
26
 
27
27
  class CompositeOrderMapper(MappingFileMapperBase):
28
-
29
28
  def __init__(
30
29
  self,
31
30
  folio_client: FolioClient,
@@ -78,6 +77,14 @@ class CompositeOrderMapper(MappingFileMapperBase):
78
77
  "code",
79
78
  "OrderLineLocationMapping",
80
79
  )
80
+ self.funds_mapping = RefDataMapping(
81
+ self.folio_client,
82
+ "/finance/funds",
83
+ "funds",
84
+ funds_map,
85
+ "code",
86
+ "FundsMapping",
87
+ )
81
88
 
82
89
  self.folio_client: FolioClient = folio_client
83
90
  self.notes_mapper: NotesMapper = NotesMapper(
@@ -115,6 +122,15 @@ class CompositeOrderMapper(MappingFileMapperBase):
115
122
  False,
116
123
  )
117
124
 
125
+ if folio_prop_name.endswith(".fundId"):
126
+ return self.get_mapped_ref_data_value(
127
+ self.location_mapping,
128
+ legacy_order,
129
+ folio_prop_name,
130
+ index_or_id,
131
+ False,
132
+ )
133
+
118
134
  mapped_value = super().get_prop(
119
135
  legacy_order, folio_prop_name, index_or_id, schema_default_value
120
136
  )
@@ -274,7 +290,9 @@ class CompositeOrderMapper(MappingFileMapperBase):
274
290
  ):
275
291
  object_schema["properties"] = CompositeOrderMapper.inject_schema_by_ref(
276
292
  submodule_path, github_headers, object_schema
277
- ).get("properties", {})#TODO: Investigate new CustomFields schema and figure out how to actually handle it
293
+ ).get(
294
+ "properties", {}
295
+ ) # TODO: Investigate new CustomFields schema and figure out how to actually handle it # noqa: E501
278
296
 
279
297
  for property_name_level1, property_level1 in object_schema.get(
280
298
  "properties", {}
@@ -383,9 +401,9 @@ class CompositeOrderMapper(MappingFileMapperBase):
383
401
  return composite_order
384
402
 
385
403
  def validate_po_number(
386
- self,
387
- index_or_id: str,
388
- po_number: str,
404
+ self,
405
+ index_or_id: str,
406
+ po_number: str,
389
407
  ):
390
408
  if not self.is_valid_po_number(po_number):
391
409
  self.migration_report.add(
@@ -330,9 +330,9 @@ class OrganizationMapper(MappingFileMapperBase):
330
330
  ["username", "password", "interfaceId"],
331
331
  )
332
332
 
333
- interface_schema["properties"][
334
- "interfaceCredential"
335
- ] = interface_credential_schema
333
+ interface_schema["properties"]["interfaceCredential"] = (
334
+ interface_credential_schema
335
+ )
336
336
 
337
337
  property_level1["items"] = interface_schema
338
338
 
@@ -43,6 +43,9 @@ class RefDataMapping(object):
43
43
  return self.cached_dict.get(key_value.lower().strip(), ())
44
44
 
45
45
  def setup_mappings(self):
46
+ if not self.map:
47
+ logging.info("%s legacy map file is empty or not provided", self.name)
48
+ return
46
49
  self.pre_validate_map()
47
50
  for idx, mapping in enumerate(self.map):
48
51
  try:
@@ -47,7 +47,7 @@ class UserMapper(MappingFileMapperBase):
47
47
  None,
48
48
  FOLIONamespaces.users,
49
49
  library_config,
50
- task_config
50
+ task_config,
51
51
  )
52
52
  self.task_config = self.task_configuration
53
53
  self.notes_mapper: NotesMapper = NotesMapper(
@@ -115,6 +115,10 @@ class UserMapper(MappingFileMapperBase):
115
115
 
116
116
  if self.task_config.remove_request_preferences:
117
117
  del clean_folio_object["requestPreference"]
118
+
119
+ if self.task_config.remove_username:
120
+ del clean_folio_object["username"]
121
+
118
122
  self.report_folio_mapping_no_schema(clean_folio_object)
119
123
  self.report_legacy_mapping_no_schema(legacy_user)
120
124
 
@@ -157,18 +161,25 @@ class UserMapper(MappingFileMapperBase):
157
161
  "No Departments mapping set up. Set up a departments mapping file "
158
162
  " or remove the mapping of the Departments field",
159
163
  )
160
- if len(self.departments_mapping.mapped_legacy_keys) == 1 and self.library_configuration.multi_field_delimiter in legacy_user.get(self.departments_mapping.mapped_legacy_keys[0], ""):
161
- split_departments = legacy_user.get(self.departments_mapping.mapped_legacy_keys[0], "").split(
162
- self.library_configuration.multi_field_delimiter
164
+ if len(
165
+ self.departments_mapping.mapped_legacy_keys
166
+ ) == 1 and self.library_configuration.multi_field_delimiter in legacy_user.get(
167
+ self.departments_mapping.mapped_legacy_keys[0], ""
168
+ ):
169
+ split_departments = legacy_user.get(
170
+ self.departments_mapping.mapped_legacy_keys[0], ""
171
+ ).split(self.library_configuration.multi_field_delimiter)
172
+ return self.library_configuration.multi_field_delimiter.join(
173
+ [
174
+ self.get_mapped_name(
175
+ self.departments_mapping,
176
+ {self.departments_mapping.mapped_legacy_keys[0]: dept},
177
+ index_or_id,
178
+ True,
179
+ )
180
+ for dept in split_departments
181
+ ]
163
182
  )
164
- return self.library_configuration.multi_field_delimiter.join([
165
- self.get_mapped_name(
166
- self.departments_mapping,
167
- {self.departments_mapping.mapped_legacy_keys[0]: dept},
168
- index_or_id,
169
- True,
170
- ) for dept in split_departments
171
- ])
172
183
  else:
173
184
  return self.get_mapped_name(
174
185
  self.departments_mapping,
@@ -198,21 +209,29 @@ class UserMapper(MappingFileMapperBase):
198
209
  return ""
199
210
 
200
211
  def setup_groups_mapping(self, groups_map):
201
- return RefDataMapping(
202
- self.folio_client,
203
- "/groups",
204
- "usergroups",
205
- groups_map,
206
- "group",
207
- "UserGroupMapping",
208
- ) if groups_map else None
212
+ return (
213
+ RefDataMapping(
214
+ self.folio_client,
215
+ "/groups",
216
+ "usergroups",
217
+ groups_map,
218
+ "group",
219
+ "UserGroupMapping",
220
+ )
221
+ if groups_map
222
+ else None
223
+ )
209
224
 
210
225
  def setup_departments_mapping(self, departments_mapping):
211
- return RefDataMapping(
212
- self.folio_client,
213
- "/departments",
214
- "departments",
215
- departments_mapping,
216
- "name",
217
- "DepartmentsMapping",
218
- ) if departments_mapping else None
226
+ return (
227
+ RefDataMapping(
228
+ self.folio_client,
229
+ "/departments",
230
+ "departments",
231
+ departments_mapping,
232
+ "name",
233
+ "DepartmentsMapping",
234
+ )
235
+ if departments_mapping
236
+ else None
237
+ )