folio-migration-tools 1.9.0rc7__tar.gz → 1.9.0rc9__tar.gz

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 (66) hide show
  1. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/PKG-INFO +2 -1
  2. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/pyproject.toml +3 -3
  3. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/__main__.py +17 -5
  4. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/folder_structure.py +5 -0
  5. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/library_configuration.py +41 -3
  6. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapper_base.py +11 -38
  7. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/courses_mapper.py +1 -1
  8. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/item_mapper.py +0 -4
  9. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +1 -1
  10. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/user_mapper.py +1 -1
  11. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +1 -1
  12. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +11 -6
  13. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +1 -1
  14. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +52 -1
  15. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/holdings_csv_transformer.py +1 -13
  16. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/holdings_marc_transformer.py +12 -3
  17. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/items_transformer.py +22 -17
  18. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/loans_migrator.py +2 -9
  19. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/migration_task_base.py +51 -6
  20. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/orders_transformer.py +1 -1
  21. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/user_transformer.py +2 -10
  22. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/test_infrastructure/mocked_classes.py +63 -0
  23. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/LICENSE +0 -0
  24. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/README.md +0 -0
  25. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/__init__.py +0 -0
  26. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/circulation_helper.py +0 -0
  27. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/colors.py +0 -0
  28. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/config_file_load.py +0 -0
  29. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/custom_dict.py +0 -0
  30. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/custom_exceptions.py +0 -0
  31. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/extradata_writer.py +0 -0
  32. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/helper.py +0 -0
  33. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/holdings_helper.py +0 -0
  34. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/i18n_config.py +0 -0
  35. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/__init__.py +0 -0
  36. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/holdings_mapper.py +0 -0
  37. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +0 -0
  38. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/notes_mapper.py +0 -0
  39. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/order_mapper.py +0 -0
  40. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/organization_mapper.py +0 -0
  41. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +0 -0
  42. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/__init__.py +0 -0
  43. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/conditions.py +0 -0
  44. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +0 -0
  45. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/hrid_handler.py +0 -0
  46. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +0 -0
  47. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/marc_file_processor.py +0 -0
  48. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +0 -0
  49. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_report.py +0 -0
  50. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/__init__.py +0 -0
  51. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/authority_transformer.py +0 -0
  52. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/batch_poster.py +0 -0
  53. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/bibs_transformer.py +0 -0
  54. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/courses_migrator.py +0 -0
  55. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +0 -0
  56. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/organization_transformer.py +0 -0
  57. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/requests_migrator.py +0 -0
  58. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/migration_tasks/reserves_migrator.py +0 -0
  59. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/task_configuration.py +0 -0
  60. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/test_infrastructure/__init__.py +0 -0
  61. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/transaction_migration/__init__.py +0 -0
  62. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/transaction_migration/legacy_loan.py +0 -0
  63. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/transaction_migration/legacy_request.py +0 -0
  64. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/transaction_migration/legacy_reserve.py +0 -0
  65. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/transaction_migration/transaction_result.py +0 -0
  66. {folio_migration_tools-1.9.0rc7 → folio_migration_tools-1.9.0rc9}/src/folio_migration_tools/translations/en.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: folio_migration_tools
3
- Version: 1.9.0rc7
3
+ Version: 1.9.0rc9
4
4
  Summary: A tool allowing you to migrate data from legacy ILS:s (Library systems) into FOLIO LSP
5
5
  License: MIT
6
6
  Keywords: FOLIO,ILS,LSP,Library Systems,MARC21,Library data
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Provides-Extra: docs
18
18
  Requires-Dist: argparse-prompt (>=0.0.5,<0.0.6)
19
+ Requires-Dist: art (>=6.5,<7.0)
19
20
  Requires-Dist: deepdiff (>=6.2.3,<7.0.0)
20
21
  Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
21
22
  Requires-Dist: folio-uuid (>=0.2.8,<0.3.0)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "folio_migration_tools"
3
- version = "1.9.0rc7"
3
+ version = "1.9.0rc9"
4
4
  description = "A tool allowing you to migrate data from legacy ILS:s (Library systems) into FOLIO LSP"
5
5
  authors = [
6
6
  {name = "Theodor Tolstoy", email = "github.teddes@tolstoy.se"},
@@ -55,10 +55,11 @@ argparse-prompt = "^0.0.5"
55
55
  deepdiff = "^6.2.3"
56
56
  pyaml = "^21.10.1"
57
57
  python-i18n = "^0.3.9"
58
+ art = "^6.5"
58
59
 
59
60
  [tool.poetry.group.dev.dependencies]
60
61
  pytest = "^7.1.3"
61
- lxml = "^4.9.1"
62
+ lxml = ">4.9"
62
63
  coverage = {extras = ["toml"], version = "^6.5.0"}
63
64
  pytest-cov = "^4.0.0"
64
65
  black = "^22.10.0"
@@ -73,7 +74,6 @@ darglint = "^1.8.1"
73
74
  sphinx = "^5.3.0"
74
75
  sphinx-autodoc-typehints = "^1.19.4"
75
76
  myst-parser = "^0.18.1"
76
- pandas = "^1.5.3"
77
77
  types-requests = "^2.28.11.17"
78
78
  types-python-dateutil = "^2.8.19.11"
79
79
  ipykernel = "^6.29.5"
@@ -62,6 +62,22 @@ def parse_args(args):
62
62
  )
63
63
  return parser.parse_args(args)
64
64
 
65
+ def prep_library_config(args):
66
+ config_file_humped = merge_load(args.configuration_path)
67
+ config_file_humped["libraryInformation"]["okapiPassword"] = args.okapi_password
68
+ config_file_humped["libraryInformation"]["baseFolder"] = args.base_folder_path
69
+ config_file = humps.decamelize(config_file_humped)
70
+ library_config = LibraryConfiguration(**config_file["library_information"])
71
+ if library_config.ecs_tenant_id:
72
+ library_config.is_ecs = True
73
+ if library_config.ecs_tenant_id and not library_config.ecs_central_iteration_identifier:
74
+ print(
75
+ "ECS tenant ID is set, but no central iteration identifier is provided. "
76
+ "Please provide the central iteration identifier in the configuration file."
77
+ )
78
+ sys.exit("ECS Central Iteration Identifier Not Found")
79
+ return config_file, library_config
80
+
65
81
 
66
82
  def main():
67
83
  try:
@@ -79,11 +95,7 @@ def main():
79
95
  except i18n.I18nFileLoadError:
80
96
  i18n.load_config(Path(__file__).parent / "i18n_config.py")
81
97
  i18n.set("locale", args.report_language)
82
- config_file_humped = merge_load(args.configuration_path)
83
- config_file_humped["libraryInformation"]["okapiPassword"] = args.okapi_password
84
- config_file_humped["libraryInformation"]["baseFolder"] = args.base_folder_path
85
- config_file = humps.decamelize(config_file_humped)
86
- library_config = LibraryConfiguration(**config_file["library_information"])
98
+ config_file, library_config = prep_library_config(args)
87
99
  try:
88
100
  migration_task_config = next(
89
101
  t for t in config_file["migration_tasks"] if t["name"] == args.task_name
@@ -120,6 +120,9 @@ class FolderStructure:
120
120
  self.id_map_path = (
121
121
  self.results_folder / f"{str(self.object_type.name).lower()}_id_map.json"
122
122
  )
123
+ self.boundwith_relationships_map_path = (
124
+ self.results_folder / "boundwith_relationships_map.json"
125
+ )
123
126
  # Mapping files
124
127
  self.material_type_map_path = self.mapping_files_folder / "material_types.tsv"
125
128
  self.loan_type_map_path = self.mapping_files_folder / "loan_types.tsv"
@@ -139,6 +142,8 @@ class FolderStructure:
139
142
  def verify_git_ignore(gitignore: Path):
140
143
  with open(gitignore, "r+") as f:
141
144
  contents = f.read()
145
+ if "reports/" not in contents:
146
+ f.write("reports/\n")
142
147
  if "results/" not in contents:
143
148
  f.write("results/\n")
144
149
  if "archive/" not in contents:
@@ -90,13 +90,20 @@ class LibraryConfiguration(BaseModel):
90
90
  )
91
91
  multi_field_delimiter: Optional[str] = "<delimiter>"
92
92
  failed_records_threshold: Annotated[
93
- int, Field(description=("Number of failed records until the process shuts down"))
93
+ int,
94
+ Field(description=("Number of failed records until the process shuts down")),
94
95
  ] = 5000
95
96
  failed_percentage_threshold: Annotated[
96
- int, Field(description=("Percentage of failed records until the process shuts down"))
97
+ int,
98
+ Field(
99
+ description=("Percentage of failed records until the process shuts down")
100
+ ),
97
101
  ] = 20
98
102
  generic_exception_threshold: Annotated[
99
- int, Field(description=("Number of generic exceptions until the process shuts down"))
103
+ int,
104
+ Field(
105
+ description=("Number of generic exceptions until the process shuts down")
106
+ ),
100
107
  ] = 50
101
108
  library_name: str
102
109
  log_level_debug: bool
@@ -111,3 +118,34 @@ class LibraryConfiguration(BaseModel):
111
118
  add_time_stamp_to_file_names: Annotated[
112
119
  bool, Field(title="Add time stamp to file names")
113
120
  ] = False
121
+ use_gateway_url_for_uuids: Annotated[
122
+ bool,
123
+ Field(
124
+ title="Use gateway URL for UUIDs",
125
+ description=(
126
+ "If set to true, folio_uuid will use the gateway URL when generating deterministic UUIDs for FOLIO records. "
127
+ "If set to false (default), the UUIDs will be generated using the tenant_id (or ecs_tenant_id)."
128
+ ),
129
+ ),
130
+ ] = False
131
+ is_ecs: Annotated[
132
+ bool,
133
+ Field(
134
+ title="Library is running ECS FOLIO",
135
+ description=(
136
+ "If set to true, the migration is running in an ECS environment. "
137
+ "If set to false (default), the migration is running in a non-ECS environment. "
138
+ "If ecs_tenant_id is set, this will be set to true, regardless of the value here."
139
+ ),
140
+ ),
141
+ ] = False
142
+ ecs_central_iteration_identifier: Annotated[
143
+ str,
144
+ Field(
145
+ title="ECS central iteration identifier",
146
+ description=(
147
+ "The iteration_identifier value from the central tenant configuration that corresponds "
148
+ "to this configuration's iteration_identifier. Used to access the central instances_id_map."
149
+ ),
150
+ ),
151
+ ] = ""
@@ -293,42 +293,6 @@ class MapperBase:
293
293
  )
294
294
  sys.exit(1)
295
295
 
296
- def setup_boundwith_relationship_map(self, boundwith_relationship_map):
297
- new_map = {}
298
- for entry in boundwith_relationship_map:
299
- if "MFHD_ID" not in entry or not entry.get("MFHD_ID", ""):
300
- raise TransformationProcessError(
301
- "", "Column MFHD_ID missing from Boundwith relationship map", ""
302
- )
303
- if "BIB_ID" not in entry or not entry.get("BIB_ID", ""):
304
- raise TransformationProcessError(
305
- "", "Column BIB_ID missing from Boundwith relationship map", ""
306
- )
307
- instance_uuid = str(
308
- FolioUUID(
309
- str(self.folio_client.okapi_url),
310
- FOLIONamespaces.instances,
311
- entry["BIB_ID"],
312
- )
313
- )
314
- mfhd_uuid = str(
315
- FolioUUID(
316
- str(self.folio_client.okapi_url),
317
- FOLIONamespaces.holdings,
318
- entry["MFHD_ID"],
319
- )
320
- )
321
- if entry["BIB_ID"] in self.parent_id_map:
322
- new_map[mfhd_uuid] = new_map.get(mfhd_uuid, []) + [instance_uuid]
323
- else:
324
- raise TransformationRecordFailedError(
325
- entry["MFHD_ID"],
326
- "Boundwith relationship map contains a BIB_ID id not in the instance id map. No boundwith holdings created.",
327
- entry["BIB_ID"],
328
- )
329
-
330
- return new_map
331
-
332
296
  def save_id_map_file(self, path, legacy_map: dict):
333
297
  with open(path, "w") as legacy_map_file:
334
298
  for id_string in legacy_map.values():
@@ -417,7 +381,7 @@ class MapperBase:
417
381
  "holdingsRecordId": bound_with_holding_uuid,
418
382
  "itemId": str(
419
383
  FolioUUID(
420
- self.folio_client.okapi_url,
384
+ self.base_string_for_folio_uuid,
421
385
  FOLIONamespaces.items,
422
386
  legacy_item_id,
423
387
  )
@@ -470,12 +434,21 @@ class MapperBase:
470
434
  def generate_boundwith_holding_uuid(self, holding_uuid, instance_uuid):
471
435
  return str(
472
436
  FolioUUID(
473
- self.folio_client.okapi_url,
437
+ self.base_string_for_folio_uuid,
474
438
  FOLIONamespaces.holdings,
475
439
  f"{holding_uuid}-{instance_uuid}",
476
440
  )
477
441
  )
478
442
 
443
+ @property
444
+ def base_string_for_folio_uuid(self):
445
+ if self.library_configuration.use_gateway_url_for_uuids and not self.library_configuration.is_ecs:
446
+ return str(self.folio_client.okapi_url)
447
+ elif self.library_configuration.ecs_tenant_id:
448
+ return str(self.library_configuration.ecs_tenant_id)
449
+ else:
450
+ return str(self.library_configuration.tenant_id)
451
+
479
452
  @staticmethod
480
453
  def validate_location_map(location_map: List[Dict], locations: List[Dict]) -> List[Dict]:
481
454
  mapped_codes = [x['folio_code'] for x in location_map]
@@ -121,7 +121,7 @@ class CoursesMapper(MappingFileMapperBase):
121
121
  def get_uuid(self, composite_course, object_type: FOLIONamespaces, idx: int = 0):
122
122
  return str(
123
123
  FolioUUID(
124
- self.folio_client.okapi_url,
124
+ self.base_string_for_folio_uuid,
125
125
  object_type,
126
126
  composite_course[1] if idx == 0 else f"{composite_course[1]}_{idx}",
127
127
  )
@@ -42,7 +42,6 @@ class ItemMapper(MappingFileMapperBase):
42
42
  temporary_loan_type_mapping,
43
43
  temporary_location_mapping,
44
44
  library_configuration: LibraryConfiguration,
45
- boundwith_relationship_map,
46
45
  task_configuration: AbstractTaskConfiguration,
47
46
  ):
48
47
  item_schema = folio_client.get_item_schema()
@@ -99,9 +98,6 @@ class ItemMapper(MappingFileMapperBase):
99
98
  "name",
100
99
  "PermanentLoanTypeMapping",
101
100
  )
102
- self.boundwith_relationship_map = self.setup_boundwith_relationship_map(
103
- boundwith_relationship_map
104
- )
105
101
 
106
102
  self.material_type_mapping = RefDataMapping(
107
103
  self.folio_client,
@@ -208,7 +208,7 @@ class MappingFileMapperBase(MapperBase):
208
208
  )
209
209
  generated_id = str(
210
210
  FolioUUID(
211
- self.folio_client.okapi_url,
211
+ self.base_string_for_folio_uuid,
212
212
  object_type,
213
213
  legacy_id,
214
214
  )
@@ -158,7 +158,7 @@ class UserMapper(MappingFileMapperBase):
158
158
  self.departments_mapping,
159
159
  legacy_user,
160
160
  index_or_id,
161
- False,
161
+ True,
162
162
  )
163
163
  elif folio_prop_name in ["expirationDate", "enrollmentDate", "personal.dateOfBirth"]:
164
164
  return self.get_parsed_date(mapped_value, folio_prop_name)
@@ -133,7 +133,7 @@ class AuthorityMapper(RulesMapperBase):
133
133
  folio_authority = {}
134
134
  folio_authority["id"] = str(
135
135
  FolioUUID(
136
- str(self.folio_client.okapi_url),
136
+ self.base_string_for_folio_uuid,
137
137
  FOLIONamespaces.authorities,
138
138
  str(legacy_ids[-1]),
139
139
  )
@@ -810,8 +810,8 @@ class RulesMapperBase(MapperBase):
810
810
  )
811
811
  data_import_marc_file.write(marc_record.as_marc())
812
812
 
813
- @staticmethod
814
813
  def save_source_record(
814
+ self,
815
815
  srs_records_file,
816
816
  record_type: FOLIONamespaces,
817
817
  folio_client: FolioClient,
@@ -831,7 +831,7 @@ class RulesMapperBase(MapperBase):
831
831
  legacy_ids (List[str]): _description_
832
832
  suppress (bool): _description_
833
833
  """
834
- srs_id = RulesMapperBase.create_srs_id(record_type, folio_client.okapi_url, legacy_ids[-1])
834
+ srs_id = self.create_srs_id(record_type, legacy_ids[-1])
835
835
 
836
836
  marc_record.add_ordered_field(
837
837
  Field(
@@ -850,7 +850,7 @@ class RulesMapperBase(MapperBase):
850
850
  logging.exception(
851
851
  "Something is wrong with the marc record's leader: %s, %s", marc_record.leader, ee
852
852
  )
853
- srs_record_string = RulesMapperBase.get_srs_string(
853
+ srs_record_string = self.get_srs_string(
854
854
  marc_record,
855
855
  folio_record,
856
856
  srs_id,
@@ -859,8 +859,7 @@ class RulesMapperBase(MapperBase):
859
859
  )
860
860
  srs_records_file.write(f"{srs_record_string}\n")
861
861
 
862
- @staticmethod
863
- def create_srs_id(record_type, okapi_url: str, legacy_id: str):
862
+ def create_srs_id(self, record_type, legacy_id: str):
864
863
  srs_types = {
865
864
  FOLIONamespaces.holdings: FOLIONamespaces.srs_records_holdingsrecord,
866
865
  FOLIONamespaces.instances: FOLIONamespaces.srs_records_bib,
@@ -868,7 +867,13 @@ class RulesMapperBase(MapperBase):
868
867
  FOLIONamespaces.edifact: FOLIONamespaces.srs_records_edifact,
869
868
  }
870
869
 
871
- return str(FolioUUID(okapi_url, srs_types.get(record_type), legacy_id))
870
+ return str(
871
+ FolioUUID(
872
+ self.base_string_for_folio_uuid,
873
+ srs_types.get(record_type),
874
+ legacy_id
875
+ )
876
+ )
872
877
 
873
878
  @staticmethod
874
879
  def get_bib_id_from_907y(marc_record: Record, index_or_legacy_id):
@@ -71,7 +71,7 @@ class BibsRulesMapper(RulesMapperBase):
71
71
  folio_instance = {}
72
72
  folio_instance["id"] = str(
73
73
  FolioUUID(
74
- str(self.folio_client.okapi_url),
74
+ self.base_string_for_folio_uuid,
75
75
  FOLIONamespaces.instances,
76
76
  str(legacy_ids[-1]),
77
77
  )
@@ -211,7 +211,7 @@ class RulesMapperHoldings(RulesMapperBase):
211
211
  folio_holding: dict = {}
212
212
  folio_holding["id"] = str(
213
213
  FolioUUID(
214
- str(self.folio_client.okapi_url),
214
+ self.base_string_for_folio_uuid,
215
215
  FOLIONamespaces.holdings,
216
216
  str(legacy_ids[0]),
217
217
  )
@@ -461,3 +461,54 @@ class RulesMapperHoldings(RulesMapperBase):
461
461
  idx, f"No legacy id found in record from {marc_path}", ""
462
462
  )
463
463
  return results
464
+
465
+ def verity_boundwith_map_entry(self, entry):
466
+ if "MFHD_ID" not in entry or not entry.get("MFHD_ID", ""):
467
+ raise TransformationProcessError(
468
+ "", "Column MFHD_ID missing from Boundwith relationship map", ""
469
+ )
470
+ if "BIB_ID" not in entry or not entry.get("BIB_ID", ""):
471
+ raise TransformationProcessError(
472
+ "", "Column BIB_ID missing from Boundwith relationship map", ""
473
+ )
474
+
475
+ def setup_boundwith_relationship_map(self, boundwith_relationship_map):
476
+ """
477
+ Creates a map of MFHD_ID to BIB_ID for boundwith relationships.
478
+
479
+ Arguments:
480
+ boundwith_relationship_map: A list of dictionaries containing the MFHD_ID and BIB_ID.
481
+
482
+ Returns:
483
+ A dictionary mapping MFHD_ID to a list of BIB_IDs.
484
+
485
+ Raises:
486
+ TransformationProcessError: If MFHD_ID or BIB_ID is missing from the entry or if the instance_uuid is not in the parent_id_map.
487
+ TransformationRecordFailedError: If BIB_ID is not in the instance id map.
488
+ """
489
+ new_map = {}
490
+ for idx, entry in enumerate(boundwith_relationship_map):
491
+ self.verity_boundwith_map_entry(entry)
492
+ mfhd_uuid = str(
493
+ FolioUUID(
494
+ self.base_string_for_folio_uuid,
495
+ FOLIONamespaces.holdings,
496
+ entry["MFHD_ID"],
497
+ )
498
+ )
499
+ try:
500
+ parent_id_tuple = self.get_bw_instance_id_map_tuple(entry)
501
+ new_map[mfhd_uuid] = new_map.get(mfhd_uuid, []) + [parent_id_tuple[1]]
502
+ except TransformationRecordFailedError as trfe:
503
+ self.handle_transformation_record_failed_error(idx, trfe)
504
+ return new_map
505
+
506
+ def get_bw_instance_id_map_tuple(self, entry):
507
+ try:
508
+ return self.parent_id_map[entry["BIB_ID"]]
509
+ except KeyError:
510
+ raise TransformationRecordFailedError(
511
+ entry["MFHD_ID"],
512
+ "Boundwith relationship map contains a BIB_ID id not in the instance id map. No boundwith holdings created for this BIB_ID.",
513
+ entry["BIB_ID"],
514
+ )
@@ -182,7 +182,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
182
182
  self.load_mapped_fields(),
183
183
  self.load_location_map(),
184
184
  self.load_call_number_type_map(),
185
- self.load_id_map(self.folder_structure.instance_id_map_path, True),
185
+ self.load_instance_id_map(True),
186
186
  library_config,
187
187
  )
188
188
  self.holdings = {}
@@ -296,18 +296,6 @@ class HoldingsCsvTransformer(MigrationTaskBase):
296
296
  )
297
297
  return holdings_map
298
298
 
299
- def load_instance_id_map(self):
300
- res = {}
301
- with open(self.folder_structure.instance_id_map_path, "r") as instance_id_map_file:
302
- for index, json_string in enumerate(instance_id_map_file):
303
- # Format:{"legacy_id", "folio_id","instanceLevelCallNumber"}
304
- if index % 500000 == 0:
305
- print(f"{index} instance ids loaded to map", end="\r")
306
- map_object = json.loads(json_string)
307
- res[map_object["legacy_id"]] = map_object
308
- logging.info("Loaded %s migrated instance IDs", (index + 1))
309
- return res
310
-
311
299
  def do_work(self):
312
300
  logging.info("Starting....")
313
301
  for file_def in self.task_config.files:
@@ -229,9 +229,7 @@ class HoldingsMarcTransformer(MigrationTaskBase):
229
229
  self.check_source_files(
230
230
  self.folder_structure.legacy_records_folder, self.task_config.files
231
231
  )
232
- self.instance_id_map = self.load_id_map(
233
- self.folder_structure.instance_id_map_path, True
234
- )
232
+ self.instance_id_map = self.load_instance_id_map(True)
235
233
  self.mapper = RulesMapperHoldings(
236
234
  self.folio_client,
237
235
  self.location_map,
@@ -283,6 +281,17 @@ class HoldingsMarcTransformer(MigrationTaskBase):
283
281
  logging.info("Done. Transformer Wrapping up...")
284
282
  self.extradata_writer.flush()
285
283
  self.processor.wrap_up()
284
+ if self.mapper.boundwith_relationship_map:
285
+ with open(
286
+ self.folder_structure.boundwith_relationships_map_path, "w+"
287
+ ) as boundwith_relationship_file:
288
+ logging.info(
289
+ "Writing boundwiths relationship map to %s",
290
+ boundwith_relationship_file.name,
291
+ )
292
+ for key, val in self.mapper.boundwith_relationship_map.items():
293
+ boundwith_relationship_file.write(json.dumps((key, val)) + "\n")
294
+
286
295
  with open(self.folder_structure.migration_reports_file, "w+") as report_file:
287
296
  self.mapper.migration_report.write_migration_report(
288
297
  i18n.t("Bibliographic records transformation report"),
@@ -233,7 +233,8 @@ class ItemsTransformer(MigrationTaskBase):
233
233
  ).is_file():
234
234
  temporary_loan_type_mapping = self.load_ref_data_mapping_file(
235
235
  "temporaryLoanTypeId",
236
- self.folder_structure.temp_loan_type_map_path,
236
+ self.folder_structure.mapping_files_folder
237
+ / self.task_config.temp_loan_types_map_file_name,
237
238
  self.folio_keys,
238
239
  )
239
240
  else:
@@ -243,20 +244,9 @@ class ItemsTransformer(MigrationTaskBase):
243
244
  )
244
245
  temporary_loan_type_mapping = None
245
246
  # Load Boundwith relationship map
246
- self.boundwith_relationship_map = []
247
+ self.boundwith_relationship_map = {}
247
248
  if self.task_config.boundwith_relationship_file_path:
248
- with open(
249
- self.folder_structure.data_folder
250
- / FOLIONamespaces.holdings.name
251
- / self.task_config.boundwith_relationship_file_path
252
- ) as boundwith_relationship_file:
253
- self.boundwith_relationship_map = list(
254
- csv.DictReader(boundwith_relationship_file, dialect="tsv")
255
- )
256
- logging.info(
257
- "Rows in Bound with relationship map: %s", len(self.boundwith_relationship_map)
258
- )
259
-
249
+ self.load_boundwith_relationships()
260
250
  if (
261
251
  self.folder_structure.mapping_files_folder
262
252
  / self.task_config.temp_location_map_file_name
@@ -312,7 +302,6 @@ class ItemsTransformer(MigrationTaskBase):
312
302
  temporary_loan_type_mapping,
313
303
  temporary_location_mapping,
314
304
  self.library_configuration,
315
- self.boundwith_relationship_map,
316
305
  self.task_configuration
317
306
  )
318
307
  if (
@@ -367,9 +356,9 @@ class ItemsTransformer(MigrationTaskBase):
367
356
  self.mapper.perform_additional_mappings(folio_rec, file_def)
368
357
  self.handle_circiulation_notes(folio_rec, self.folio_client.current_user)
369
358
  self.handle_notes(folio_rec)
370
- if folio_rec["holdingsRecordId"] in self.mapper.boundwith_relationship_map:
359
+ if folio_rec["holdingsRecordId"] in self.boundwith_relationship_map:
371
360
  for idx_, instance_id in enumerate(
372
- self.mapper.boundwith_relationship_map.get(
361
+ self.boundwith_relationship_map.get(
373
362
  folio_rec["holdingsRecordId"]
374
363
  )
375
364
  ):
@@ -456,6 +445,22 @@ class ItemsTransformer(MigrationTaskBase):
456
445
  else:
457
446
  del folio_rec["circulationNotes"]
458
447
 
448
+ def load_boundwith_relationships(self):
449
+ try:
450
+ with open(
451
+ self.folder_structure.boundwith_relationships_map_path
452
+ ) as boundwith_relationship_file:
453
+ self.boundwith_relationship_map = dict(
454
+ json.loads(x) for x in boundwith_relationship_file
455
+ )
456
+ logging.info(
457
+ "Rows in Bound with relationship map: %s", len(self.boundwith_relationship_map)
458
+ )
459
+ except FileNotFoundError:
460
+ raise TransformationProcessError(
461
+ "", "Boundwith relationship file specified, but relationships file from holdings transformation not found. ", self.folder_structure.boundwith_relationships_map_path
462
+ )
463
+
459
464
  def wrap_up(self):
460
465
  logging.info("Done. Transformer wrapping up...")
461
466
  self.extradata_writer.flush()
@@ -14,12 +14,12 @@ from pydantic import Field
14
14
  import i18n
15
15
  from dateutil import parser as du_parser
16
16
  from folio_uuid.folio_namespaces import FOLIONamespaces
17
+ from art import tprint
17
18
 
18
19
  from folio_migration_tools.circulation_helper import CirculationHelper
19
20
  from folio_migration_tools.helper import Helper
20
21
  from folio_migration_tools.library_configuration import (
21
22
  FileDefinition,
22
- FolioRelease,
23
23
  LibraryConfiguration,
24
24
  )
25
25
  from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
@@ -781,11 +781,4 @@ def timings(t0, t0func, num_objects):
781
781
 
782
782
 
783
783
  def print_smtp_warning():
784
- s = r"""
785
- _____ __ __ _____ ______ ___
786
- / ____| | \/ | |_ _| | __ | |__ \\
787
- | (___ | \ / | | | | |__|_| ) |
788
- \___ \ | |\/| | | | | | / /
789
- |_____/ |_| |_| |_| |_| (_)
790
- """ # noqa: E501, W605
791
- print(s)
784
+ tprint("\nSMTP?\n", space=2)
@@ -9,6 +9,7 @@ from abc import abstractmethod
9
9
  from datetime import datetime, timezone
10
10
  from genericpath import isfile
11
11
  from pathlib import Path
12
+ from typing import Optional
12
13
 
13
14
  import folioclient
14
15
  from folio_uuid.folio_namespaces import FOLIONamespaces
@@ -54,6 +55,15 @@ class MigrationTaskBase:
54
55
  {"x-okapi-tenant": self.ecs_tenant_id} if self.ecs_tenant_id else {}
55
56
  )
56
57
  self.folio_client.okapi_headers.update(self.ecs_tenant_header)
58
+ self.central_folder_structure: Optional[FolderStructure] = None
59
+ if library_configuration.is_ecs and library_configuration.ecs_central_iteration_identifier:
60
+ self.central_folder_structure = FolderStructure(
61
+ library_configuration.base_folder,
62
+ FOLIONamespaces.instances,
63
+ task_configuration.name,
64
+ library_configuration.ecs_central_iteration_identifier,
65
+ library_configuration.add_time_stamp_to_file_names,
66
+ )
57
67
  self.folder_structure: FolderStructure = FolderStructure(
58
68
  library_configuration.base_folder,
59
69
  self.get_object_type(),
@@ -66,6 +76,8 @@ class MigrationTaskBase:
66
76
  self.object_type = self.get_object_type()
67
77
  try:
68
78
  self.folder_structure.setup_migration_file_structure()
79
+ if self.central_folder_structure:
80
+ self.central_folder_structure.setup_migration_file_structure()
69
81
  # Initiate Worker
70
82
  except FileNotFoundError as fne:
71
83
  logging.error(fne)
@@ -143,15 +155,48 @@ class MigrationTaskBase:
143
155
  for filename in files:
144
156
  logging.info("\t%s", filename)
145
157
 
158
+ def load_instance_id_map(self, raise_if_empty=True) -> dict:
159
+ """
160
+ This method handles loading instance id maps for holdings and other transformations that require it.
161
+ This is in the base class because multiple tasks need it. It exists because instances in an ECS environment
162
+ are transformed for the central and data tenants separately, but the data tenants need to know about
163
+ the central tenant instance ids. This is a bit of a hack, but it works for now.
164
+ """
165
+ map_files = []
166
+ instance_id_map = {}
167
+ if self.library_configuration.is_ecs and self.central_folder_structure:
168
+ logging.info(
169
+ "Loading ECS central tenant instance id map from %s", self.central_folder_structure.instance_id_map_path
170
+ )
171
+ instance_id_map = self.load_id_map(
172
+ self.central_folder_structure.instance_id_map_path,
173
+ raise_if_empty=False,
174
+ )
175
+ map_files.append(str(self.central_folder_structure.instance_id_map_path))
176
+ logging.info(
177
+ "Loading member tenant isntance id map from %s",
178
+ self.folder_structure.instance_id_map_path
179
+ )
180
+ instance_id_map = self.load_id_map(
181
+ self.folder_structure.instance_id_map_path,
182
+ raise_if_empty=False,
183
+ existing_id_map=instance_id_map,
184
+ )
185
+ map_files.append(str(self.folder_structure.instance_id_map_path))
186
+ if not any(instance_id_map) and raise_if_empty:
187
+ map_file_paths = ", ".join(map_files)
188
+ raise TransformationProcessError("", "Instance id map is empty", map_file_paths)
189
+ return instance_id_map
190
+
146
191
  @staticmethod
147
- def load_id_map(map_path, raise_if_empty=False):
192
+ def load_id_map(map_path, raise_if_empty=False, existing_id_map={}):
148
193
  if not isfile(map_path):
149
- logging.warn(
194
+ logging.warning(
150
195
  "No legacy id map found at %s. Will build one from scratch", map_path
151
196
  )
152
197
  return {}
153
- id_map = {}
154
- loaded_rows = 0
198
+ id_map = existing_id_map
199
+ loaded_rows = len(id_map)
155
200
  with open(map_path) as id_map_file:
156
201
  for index, json_string in enumerate(id_map_file, start=1):
157
202
  loaded_rows = index
@@ -159,12 +204,12 @@ class MigrationTaskBase:
159
204
  map_tuple = json.loads(json_string)
160
205
  if loaded_rows % 500000 == 0:
161
206
  print(
162
- f"{loaded_rows + 1} ids loaded to map. Last Id: {map_tuple[0]}",
207
+ f"{loaded_rows + 1} ids loaded to map. Last Id: {map_tuple[0]} ",
163
208
  end="\r",
164
209
  )
165
210
 
166
211
  id_map[map_tuple[0]] = map_tuple
167
- logging.info("Loaded %s migrated IDs", loaded_rows)
212
+ logging.info("Loaded %s migrated IDs from %s", loaded_rows, id_map_file.name)
168
213
  if not any(id_map) and raise_if_empty:
169
214
  raise TransformationProcessError("", "Legacy id map is empty", map_path)
170
215
  return id_map
@@ -177,7 +177,7 @@ class OrdersTransformer(MigrationTaskBase):
177
177
  self.library_configuration,
178
178
  self.orders_map,
179
179
  self.load_id_map(self.folder_structure.organizations_id_map_path, True),
180
- self.load_id_map(self.folder_structure.instance_id_map_path, True),
180
+ self.load_instance_id_map(True),
181
181
  self.load_ref_data_mapping_file(
182
182
  "acquisitionMethod",
183
183
  self.folder_structure.mapping_files_folder
@@ -6,6 +6,7 @@ from pydantic import Field
6
6
 
7
7
  import i18n
8
8
  from folio_uuid.folio_namespaces import FOLIONamespaces
9
+ from art import tprint
9
10
 
10
11
  from folio_migration_tools.custom_exceptions import (
11
12
  TransformationProcessError,
@@ -282,16 +283,7 @@ class UserTransformer(MigrationTaskBase):
282
283
 
283
284
 
284
285
  def print_email_warning():
285
- s = (
286
- " ______ __ __ _____ _ _____ ___ \n" # noqa: E501, W605
287
- " | ____| | \\/ | /\\ |_ _| | | / ____| |__ \\ \n" # noqa: E501, W605
288
- " | |__ | \\ / | / \\ | | | | | (___ ) |\n" # noqa: E501, W605
289
- " | __| | |\\/| | / /\\ \\ | | | | \\___ \\ / / \n" # noqa: E501, W605
290
- " |______| |_| |_| /_/ \\_\\ |_____| |______| |_____/ (_) \n" # noqa: E501, W605
291
- " \n" # noqa: E501, W605
292
- " \n"
293
- )
294
- print(s)
286
+ tprint("\nEMAILS?\n", space=2)
295
287
 
296
288
 
297
289
  def remove_empty_addresses(folio_user):
@@ -12,6 +12,10 @@ from folio_migration_tools.mapping_file_transformation.holdings_mapper import (
12
12
  HoldingsMapper,
13
13
  )
14
14
  from folio_migration_tools.migration_report import MigrationReport
15
+ from folio_migration_tools.library_configuration import (
16
+ LibraryConfiguration,
17
+ FolioRelease,
18
+ )
15
19
 
16
20
 
17
21
  def mocked_holdings_mapper() -> Mock:
@@ -242,3 +246,62 @@ def folio_get_single_object_mocked(*args, **kwargs):
242
246
 
243
247
  def folio_get_from_github(owner, repo, file_path):
244
248
  return FolioClient.get_latest_from_github(owner, repo, file_path, "")
249
+
250
+ OKAPI_URL = "http://localhost:9130"
251
+ LIBRARY_NAME = "Test Library"
252
+
253
+ def get_mocked_library_config():
254
+ return LibraryConfiguration(
255
+ okapi_url=OKAPI_URL,
256
+ tenant_id="test_tenant",
257
+ okapi_username="test_user",
258
+ okapi_password="test_password",
259
+ base_folder=Path("."),
260
+ library_name=LIBRARY_NAME,
261
+ log_level_debug=False,
262
+ folio_release=FolioRelease.sunflower,
263
+ iteration_identifier="test_iteration"
264
+ )
265
+
266
+ def get_mocked_ecs_central_libarary_config():
267
+ return LibraryConfiguration(
268
+ okapi_url=OKAPI_URL,
269
+ tenant_id="test_tenant",
270
+ okapi_username="test_user",
271
+ okapi_password="test_password",
272
+ base_folder=Path("."),
273
+ library_name=LIBRARY_NAME,
274
+ log_level_debug=False,
275
+ folio_release=FolioRelease.sunflower,
276
+ iteration_identifier="central_iteration",
277
+ is_ecs=True,
278
+ )
279
+
280
+ def get_mocked_ecs_member_libarary_config():
281
+ return LibraryConfiguration(
282
+ okapi_url=OKAPI_URL,
283
+ tenant_id="test_tenant",
284
+ ecs_tenant_id="test_ecs_tenant",
285
+ okapi_username="test_user",
286
+ okapi_password="test_password",
287
+ base_folder=Path("."),
288
+ library_name=LIBRARY_NAME,
289
+ log_level_debug=False,
290
+ folio_release=FolioRelease.sunflower,
291
+ iteration_identifier="member_iteration",
292
+ ecs_central_iteration_identifier="central_iteration",
293
+ is_ecs=True,
294
+ )
295
+
296
+ def get_mocked_folder_structure():
297
+ mock_fs = MagicMock()
298
+ mock_fs.mapping_files = Path("mapping_files")
299
+ mock_fs.results_folder = Path("results")
300
+ mock_fs.legacy_records_folder = Path("source_files")
301
+ mock_fs.logs_folder = Path("logs")
302
+ mock_fs.migration_reports_file = Path("/dev/null")
303
+ mock_fs.transformation_extra_data_path = Path("transformation_extra_data")
304
+ mock_fs.transformation_log_path = Path("/dev/null")
305
+ mock_fs.data_issue_file_path = Path("/dev/null")
306
+ mock_fs.failed_marc_recs_file = Path("failed_marc_recs.txt")
307
+ return mock_fs