folio-migration-tools 1.9.0rc6__py3-none-any.whl → 1.9.0rc8__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 (28) hide show
  1. folio_migration_tools/__main__.py +17 -5
  2. folio_migration_tools/custom_exceptions.py +7 -7
  3. folio_migration_tools/folder_structure.py +5 -0
  4. folio_migration_tools/library_configuration.py +41 -3
  5. folio_migration_tools/mapper_base.py +11 -38
  6. folio_migration_tools/mapping_file_transformation/courses_mapper.py +1 -1
  7. folio_migration_tools/mapping_file_transformation/item_mapper.py +2 -8
  8. folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +4 -8
  9. folio_migration_tools/mapping_file_transformation/user_mapper.py +1 -1
  10. folio_migration_tools/marc_rules_transformation/conditions.py +1 -1
  11. folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +1 -1
  12. folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +11 -6
  13. folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +1 -1
  14. folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +56 -2
  15. folio_migration_tools/migration_tasks/holdings_csv_transformer.py +1 -13
  16. folio_migration_tools/migration_tasks/holdings_marc_transformer.py +12 -3
  17. folio_migration_tools/migration_tasks/items_transformer.py +22 -17
  18. folio_migration_tools/migration_tasks/loans_migrator.py +2 -9
  19. folio_migration_tools/migration_tasks/migration_task_base.py +50 -6
  20. folio_migration_tools/migration_tasks/orders_transformer.py +1 -1
  21. folio_migration_tools/migration_tasks/user_transformer.py +2 -10
  22. folio_migration_tools/test_infrastructure/mocked_classes.py +63 -0
  23. folio_migration_tools/transaction_migration/legacy_loan.py +25 -27
  24. {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/METADATA +2 -1
  25. {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/RECORD +28 -28
  26. {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/LICENSE +0 -0
  27. {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/WHEEL +0 -0
  28. {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -5,12 +5,12 @@ import i18n
5
5
  from folio_migration_tools import StrCoercible
6
6
 
7
7
 
8
- class TransfomationError(Exception):
8
+ class TransformationError(Exception):
9
9
  pass
10
10
 
11
11
 
12
- class TransformationFieldMappingError(TransfomationError):
13
- """Raised when the a field mapping fails, but the error is not critical.
12
+ class TransformationFieldMappingError(TransformationError):
13
+ """Raised when the field mapping fails, but the error is not critical.
14
14
  The issue should be logged for the library to act upon it"""
15
15
 
16
16
  def __init__(self, index_or_id="", message="", data_value: Union[str, StrCoercible]=""):
@@ -35,8 +35,8 @@ class TransformationFieldMappingError(TransfomationError):
35
35
  )
36
36
 
37
37
 
38
- class TransformationRecordFailedError(TransfomationError):
39
- """Raised when the a field mapping fails, Error is critical and means tranformation fails"""
38
+ class TransformationRecordFailedError(TransformationError):
39
+ """Raised when the field mapping fails, Error is critical and means transformation fails"""
40
40
 
41
41
  def __init__(self, index_or_id, message="", data_value=""):
42
42
  self.index_or_id = index_or_id
@@ -61,8 +61,8 @@ class TransformationRecordFailedError(TransfomationError):
61
61
  )
62
62
 
63
63
 
64
- class TransformationProcessError(TransfomationError):
65
- """Raised when the transformation fails due to incorrect configuraiton,
64
+ class TransformationProcessError(TransformationError):
65
+ """Raised when the transformation fails due to incorrect configuration,
66
66
  mapping or reference data. This error should take the process to a halt."""
67
67
 
68
68
  def __init__(
@@ -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()
@@ -75,9 +74,7 @@ class ItemMapper(MappingFileMapperBase):
75
74
  self.folio_client,
76
75
  "/locations",
77
76
  "locations",
78
- self.validate_location_map(
79
- temporary_location_mapping, self.folio_client.locations
80
- ),
77
+ temporary_location_mapping,
81
78
  "code",
82
79
  "TemporaryLocationMapping",
83
80
  )
@@ -101,9 +98,6 @@ class ItemMapper(MappingFileMapperBase):
101
98
  "name",
102
99
  "PermanentLoanTypeMapping",
103
100
  )
104
- self.boundwith_relationship_map = self.setup_boundwith_relationship_map(
105
- boundwith_relationship_map
106
- )
107
101
 
108
102
  self.material_type_mapping = RefDataMapping(
109
103
  self.folio_client,
@@ -118,7 +112,7 @@ class ItemMapper(MappingFileMapperBase):
118
112
  self.folio_client,
119
113
  "/locations",
120
114
  "locations",
121
- self.validate_location_map(location_map, self.folio_client.locations),
115
+ location_map,
122
116
  "code",
123
117
  "LocationMapping",
124
118
  )
@@ -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
  )
@@ -755,12 +755,8 @@ class MappingFileMapperBase(MapperBase):
755
755
  for k in data
756
756
  if k["folio_field"] == folio_prop_name
757
757
  and any(
758
- [
759
- is_set_or_bool_or_numeric(k.get("value", "")),
760
- is_set_or_bool_or_numeric(k.get("legacy_field", "")),
761
- is_set_or_bool_or_numeric(k.get("fallback_legacy_field", "")),
762
- is_set_or_bool_or_numeric(k.get("fallback_value", "")),
763
- ]
758
+ is_set_or_bool_or_numeric(k.get(key, ""))
759
+ for key in ("value", "legacy_field", "fallback_legacy_field", "fallback_value")
764
760
  )
765
761
  )
766
762
 
@@ -972,4 +968,4 @@ def in_deep(dictionary, keys):
972
968
 
973
969
 
974
970
  def is_set_or_bool_or_numeric(any_value):
975
- return any(isinstance(any_value, t) for t in [int, bool, float, complex]) or any_value.strip()
971
+ return (isinstance(any_value, str) and (any_value.strip() not in empty_vals)) or isinstance(any_value, (int, float, complex))
@@ -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)
@@ -818,7 +818,7 @@ class Conditions:
818
818
  """
819
819
  This method handles the mapping of electronic access relationship IDs.
820
820
  If the record type being mapped is FOLIO holdings, it provides an (optional) alternative
821
- mapping baseed on a provided name parameter, bypassing the FOLIO MARC-to-Holdings mapping
821
+ mapping based on a provided name parameter, bypassing the FOLIO MARC-to-Holdings mapping
822
822
  engine behavior. This requires use of a supplemental mapping rules file in the
823
823
  HoldingsMarcTransformer task definition containing the name parameter.
824
824
  """
@@ -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
  )
@@ -61,7 +61,10 @@ class RulesMapperHoldings(RulesMapperBase):
61
61
  self.boundwith_relationship_map = self.setup_boundwith_relationship_map(
62
62
  boundwith_relationship_map
63
63
  )
64
- self.location_map = location_map
64
+ self.location_map = self.validate_location_map(
65
+ location_map,
66
+ self.folio_client.locations,
67
+ )
65
68
  self.holdings_id_map: dict = {}
66
69
  self.ref_data_dicts: dict = {}
67
70
  self.fallback_holdings_type_id = self.task_configuration.fallback_holdings_type_id
@@ -208,7 +211,7 @@ class RulesMapperHoldings(RulesMapperBase):
208
211
  folio_holding: dict = {}
209
212
  folio_holding["id"] = str(
210
213
  FolioUUID(
211
- str(self.folio_client.okapi_url),
214
+ self.base_string_for_folio_uuid,
212
215
  FOLIONamespaces.holdings,
213
216
  str(legacy_ids[0]),
214
217
  )
@@ -458,3 +461,54 @@ class RulesMapperHoldings(RulesMapperBase):
458
461
  idx, f"No legacy id found in record from {marc_path}", ""
459
462
  )
460
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,47 @@ 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
+ if self.library_configuration.is_ecs and self.central_folder_structure:
167
+ logging.info(
168
+ "Loading ECS central tenant instance id map from %s", self.central_folder_structure.instance_id_map_path
169
+ )
170
+ instance_id_map = self.load_id_map(
171
+ self.central_folder_structure.instance_id_map_path,
172
+ raise_if_empty=False,
173
+ )
174
+ map_files.append(str(self.central_folder_structure.instance_id_map_path))
175
+ logging.info(
176
+ "Loading member tenant isntance id map from %s",
177
+ self.folder_structure.instance_id_map_path
178
+ )
179
+ instance_id_map = self.load_id_map(
180
+ self.folder_structure.instance_id_map_path,
181
+ raise_if_empty=False,
182
+ existing_id_map=instance_id_map,
183
+ )
184
+ map_files.append(str(self.folder_structure.instance_id_map_path))
185
+ if not any(instance_id_map) and raise_if_empty:
186
+ map_file_paths = ", ".join(map_files)
187
+ raise TransformationProcessError("", "Instance id map is empty", map_file_paths)
188
+ return instance_id_map
189
+
146
190
  @staticmethod
147
- def load_id_map(map_path, raise_if_empty=False):
191
+ def load_id_map(map_path, raise_if_empty=False, existing_id_map={}):
148
192
  if not isfile(map_path):
149
- logging.warn(
193
+ logging.warning(
150
194
  "No legacy id map found at %s. Will build one from scratch", map_path
151
195
  )
152
196
  return {}
153
- id_map = {}
154
- loaded_rows = 0
197
+ id_map = existing_id_map
198
+ loaded_rows = len(id_map)
155
199
  with open(map_path) as id_map_file:
156
200
  for index, json_string in enumerate(id_map_file, start=1):
157
201
  loaded_rows = index
@@ -159,12 +203,12 @@ class MigrationTaskBase:
159
203
  map_tuple = json.loads(json_string)
160
204
  if loaded_rows % 500000 == 0:
161
205
  print(
162
- f"{loaded_rows + 1} ids loaded to map. Last Id: {map_tuple[0]}",
206
+ f"{loaded_rows + 1} ids loaded to map. Last Id: {map_tuple[0]} ",
163
207
  end="\r",
164
208
  )
165
209
 
166
210
  id_map[map_tuple[0]] = map_tuple
167
- logging.info("Loaded %s migrated IDs", loaded_rows)
211
+ logging.info("Loaded %s migrated IDs from %s", loaded_rows, id_map_file.name)
168
212
  if not any(id_map) and raise_if_empty:
169
213
  raise TransformationProcessError("", "Legacy id map is empty", map_path)
170
214
  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
@@ -3,9 +3,10 @@ from datetime import datetime
3
3
  from zoneinfo import ZoneInfo
4
4
 
5
5
  from dateutil import tz
6
- from dateutil.parser import parse
6
+ from dateutil.parser import parse, ParserError
7
7
 
8
8
  from folio_migration_tools.migration_report import MigrationReport
9
+ from folio_migration_tools.custom_exceptions import TransformationProcessError
9
10
 
10
11
  utc = ZoneInfo("UTC")
11
12
 
@@ -42,46 +43,47 @@ class LegacyLoan(object):
42
43
 
43
44
  self.tenant_timezone = tenant_timezone
44
45
  self.errors = []
46
+ self.row = row
45
47
  for prop in correct_headers:
46
48
  if prop not in legacy_loan_dict and prop not in optional_headers:
47
- self.errors.append(("Missing properties in legacy data", prop))
49
+ self.errors.append((f"Missing properties in legacy data {row=}", prop))
48
50
  if (
49
51
  prop != "next_item_status"
50
52
  and not legacy_loan_dict.get(prop, "").strip()
51
53
  and prop not in optional_headers
52
54
  ):
53
- self.errors.append(("Empty properties in legacy data", prop))
55
+ self.errors.append((f"Empty properties in legacy data {row=}", prop))
54
56
  try:
55
57
  temp_date_due: datetime = parse(legacy_loan_dict["due_date"])
56
58
  if temp_date_due.tzinfo != tz.UTC:
57
59
  temp_date_due = temp_date_due.replace(tzinfo=self.tenant_timezone)
58
60
  self.report(
59
- f"Provided due_date is not UTC, "
60
- f"setting tzinfo to tenant timezone ({self.tenant_timezone})"
61
+ f"Provided due_date is not UTC in {row=}, "
62
+ f"setting tz-info to tenant timezone ({self.tenant_timezone})"
61
63
  )
62
64
  if temp_date_due.hour == 0 and temp_date_due.minute == 0:
63
65
  temp_date_due = temp_date_due.replace(hour=23, minute=59)
64
66
  self.report(
65
- "Hour and minute not specified for due date. "
67
+ f"Hour and minute not specified for due date in {row=}. "
66
68
  "Assuming end of local calendar day (23:59)..."
67
69
  )
68
- except Exception as ee:
70
+ except (ParserError, OverflowError) as ee:
69
71
  logging.error(ee)
70
- self.errors.append(("Parse date failure. Setting UTC NOW", "due_date"))
72
+ self.errors.append((f"Parse date failure in {row=}. Setting UTC NOW", "due_date"))
71
73
  temp_date_due = datetime.now(ZoneInfo("UTC"))
72
74
  try:
73
75
  temp_date_out: datetime = parse(legacy_loan_dict["out_date"])
74
76
  if temp_date_out.tzinfo != tz.UTC:
75
77
  temp_date_out = temp_date_out.replace(tzinfo=self.tenant_timezone)
76
78
  self.report(
77
- f"Provided out_date is not UTC, "
78
- f"setting tzinfo to tenant timezone ({self.tenant_timezone})"
79
+ f"Provided out_date is not UTC in {row=}, "
80
+ f"setting tz-info to tenant timezone ({self.tenant_timezone})"
79
81
  )
80
- except Exception:
82
+ except (ParserError, OverflowError):
81
83
  temp_date_out = datetime.now(
82
84
  ZoneInfo("UTC")
83
85
  ) # TODO: Consider moving this assignment block above the temp_date_due
84
- self.errors.append(("Parse date failure. Setting UTC NOW", "out_date"))
86
+ self.errors.append((f"Parse date failure in {row=}. Setting UTC NOW", "out_date"))
85
87
 
86
88
  # good to go, set properties
87
89
  self.item_barcode: str = legacy_loan_dict["item_barcode"].strip()
@@ -94,7 +96,7 @@ class LegacyLoan(object):
94
96
  self.renewal_count = self.set_renewal_count(legacy_loan_dict)
95
97
  self.next_item_status = legacy_loan_dict.get("next_item_status", "").strip()
96
98
  if self.next_item_status not in legal_statuses:
97
- self.errors.append(("Not an allowed status", self.next_item_status))
99
+ self.errors.append((f"Not an allowed status {row=}", self.next_item_status))
98
100
  self.service_point_id = (
99
101
  legacy_loan_dict["service_point_id"]
100
102
  if legacy_loan_dict.get("service_point_id", "")
@@ -107,23 +109,19 @@ class LegacyLoan(object):
107
109
  try:
108
110
  return int(renewal_count)
109
111
  except ValueError:
110
- self.report(
111
- f"Unresolvable {renewal_count=} was replaced with 0.")
112
+ self.report(f"Unresolvable {renewal_count=} was replaced with 0.")
112
113
  else:
113
114
  self.report(f"Missing renewal count was replaced with 0.")
114
115
  return 0
115
116
 
116
117
  def correct_for_1_day_loans(self):
117
- try:
118
- if self.due_date.date() <= self.out_date.date():
119
- if self.due_date.hour == 0:
120
- self.due_date = self.due_date.replace(hour=23, minute=59)
121
- if self.out_date.hour == 0:
122
- self.out_date = self.out_date.replace(hour=0, minute=1)
123
- if self.due_date <= self.out_date:
124
- raise ValueError("Due date is before out date")
125
- except Exception:
126
- self.errors.append(("Time alignment issues", "both dates"))
118
+ if self.due_date.date() <= self.out_date.date():
119
+ if self.due_date.hour == 0:
120
+ self.due_date = self.due_date.replace(hour=23, minute=59)
121
+ if self.out_date.hour == 0:
122
+ self.out_date = self.out_date.replace(hour=0, minute=1)
123
+ if self.due_date <= self.out_date:
124
+ raise TransformationProcessError(self.row, "Due date is before out date")
127
125
 
128
126
  def to_dict(self):
129
127
  return {
@@ -140,8 +138,8 @@ class LegacyLoan(object):
140
138
  if self.tenant_timezone != ZoneInfo("UTC"):
141
139
  self.due_date = self.due_date.astimezone(ZoneInfo("UTC"))
142
140
  self.out_date = self.out_date.astimezone(ZoneInfo("UTC"))
143
- except Exception:
144
- self.errors.append(("UTC correction issues", "both dates"))
141
+ except TypeError:
142
+ self.errors.append((f"UTC correction issues {self.row}", "both dates"))
145
143
 
146
144
  def report(self, what_to_report: str):
147
145
  self.migration_report.add("Details", what_to_report)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: folio_migration_tools
3
- Version: 1.9.0rc6
3
+ Version: 1.9.0rc8
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,67 +1,67 @@
1
1
  folio_migration_tools/__init__.py,sha256=DXvzUKFSpSZjflFWaNm0L8yhFk0u7RVIvQMskwMmKFc,238
2
- folio_migration_tools/__main__.py,sha256=0rbCmTq4HTxj8M3UjvX3rOEkq6-YqvaSGEMmuCORbho,7282
2
+ folio_migration_tools/__main__.py,sha256=_0el5EyJhG8lPj--gM5zMfVJgTt9RhrJo7rmuOY20sM,7883
3
3
  folio_migration_tools/circulation_helper.py,sha256=2kAkLM6caPiep0ZtBkMICbRDh53KdfdH21oEX1eMRDI,14193
4
4
  folio_migration_tools/colors.py,sha256=GP0wdI_GZ2WD5SjrbPN-S3u8vvN_u6rGQIBBcWv_0ZM,227
5
5
  folio_migration_tools/config_file_load.py,sha256=zHHa6NDkN6EJiQE4DgjrFQPVKsd70POsfbGkB8308jg,2822
6
6
  folio_migration_tools/custom_dict.py,sha256=-FUnhKp90Dg8EHlY6twx-PYQxBUWEO7FgxL2b7pf-xk,678
7
- folio_migration_tools/custom_exceptions.py,sha256=fRdMt5AwH_XddZ-bts5ByslN5gYthkLdh7o22JroQeE,2686
7
+ folio_migration_tools/custom_exceptions.py,sha256=1zgOKy3NBUVGG6i9YxK6w2Hntlea8MHmm7mdnjBtzvQ,2687
8
8
  folio_migration_tools/extradata_writer.py,sha256=fuchNcMc6BYb9IyfAcvXg7X4J2TfX6YiROfT2hr0JMw,1678
9
- folio_migration_tools/folder_structure.py,sha256=yyVvbkM9PbczSHNI8vK0Ru7i0x4nbYGzrRriXrnIh38,6715
9
+ folio_migration_tools/folder_structure.py,sha256=bZlmKGtxdytWcqjnM2lE4Vpx4nHyYRk7CNL1tZhLtXY,6917
10
10
  folio_migration_tools/helper.py,sha256=KkOkNAGO_fuYqxdLrsbLzCJLQHUrFZG1NzD4RmpQ-KM,2804
11
11
  folio_migration_tools/holdings_helper.py,sha256=yJpz6aJrKRBiJ1MtT5bs2vXAc88uJuGh2_KDuCySOKc,7559
12
12
  folio_migration_tools/i18n_config.py,sha256=3AH_2b9zTsxE4XTe4isM_zYtPJSlK0ix6eBmV7kAYUM,228
13
- folio_migration_tools/library_configuration.py,sha256=JE23VSUKDeCxzj_fOeQwkXF4fwd_y9s_hNy91YtEy7A,3514
14
- folio_migration_tools/mapper_base.py,sha256=ftYQJ6UDsP96EvCr5t3yMJemsmSNXsSH5zChDLu4Pp8,21224
13
+ folio_migration_tools/library_configuration.py,sha256=UhHNiz9SI2nEnm6XME2ESD33LNwqdRzIgCU9kjYPHQQ,4863
14
+ folio_migration_tools/mapper_base.py,sha256=WnUA2KBJrvAWRuq7KsTPi9YXD76pXfX7lyI5pExEwLI,20139
15
15
  folio_migration_tools/mapping_file_transformation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- folio_migration_tools/mapping_file_transformation/courses_mapper.py,sha256=mJQxxeTn1bCYb2zwFYyXJ6EGZpJ0DsmwOY3nED7D_gQ,8091
16
+ folio_migration_tools/mapping_file_transformation/courses_mapper.py,sha256=RuNkdG9XumpgPO3Zvcx_JYzZ598Xle_AMNf18zLR2UM,8095
17
17
  folio_migration_tools/mapping_file_transformation/holdings_mapper.py,sha256=nJS-xx1LszvbYfw0qdTUHX9xXHlxS7wP5mYmixFMh8A,7221
18
- folio_migration_tools/mapping_file_transformation/item_mapper.py,sha256=CkpxQhwn-ZUpZu4nvybAyrQhdRVX2-wtCgT35-8JLh0,11005
18
+ folio_migration_tools/mapping_file_transformation/item_mapper.py,sha256=YYZFgNoDVuSO_mRuaDNZ6-6bYbEtYFtfbIZ1MFPBAgc,10687
19
19
  folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py,sha256=nCkqbxaDHKxMuqQHh_afxQp48YrVD-SeCZ0L1iGvnkk,13402
20
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py,sha256=RacwSOP6r6i28EOywaepq5K5FimD8Ld5MlBo89FYO7c,37963
20
+ folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py,sha256=bLL6tTqqv2MOjZlowjL8lngYP09F_iwfFikEpjB4nmI,37816
21
21
  folio_migration_tools/mapping_file_transformation/notes_mapper.py,sha256=auLQZqa4rSJo_MIV4Lc5-LG8RcBpp2bnKH243qNYq_0,3470
22
22
  folio_migration_tools/mapping_file_transformation/order_mapper.py,sha256=k-kIuf2ceXrPWe3oVnfhuQlE7eglcx6PDLVJtddkeiM,17680
23
23
  folio_migration_tools/mapping_file_transformation/organization_mapper.py,sha256=0zjw0-C-qTYH9GC6FDBElucWCZWdoOiTHOY7q9_4NQg,14571
24
24
  folio_migration_tools/mapping_file_transformation/ref_data_mapping.py,sha256=qFsn_LwKZeKFdOudfEQnNA3DEHOdNQVKzTPdZAlDPX0,8864
25
- folio_migration_tools/mapping_file_transformation/user_mapper.py,sha256=oWuIPRQL0anF_qTVFibHtc1oOaqyKCBH4O1hX5rQAZQ,7806
25
+ folio_migration_tools/mapping_file_transformation/user_mapper.py,sha256=Q8418BdXdCuEDxfoXqLCjWy1lUxhQNLRwSE5Gi1lqoA,7805
26
26
  folio_migration_tools/marc_rules_transformation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- folio_migration_tools/marc_rules_transformation/conditions.py,sha256=am_VQ5P8vx-AFbVNBVeu0WkIbIh4cQJBLROxNTj_yUE,39143
27
+ folio_migration_tools/marc_rules_transformation/conditions.py,sha256=ttTZISieqveu3YpvpnawHh3In1_DNQMTziI5yasfmWU,39142
28
28
  folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py,sha256=lTb5QWEAgwyFHy5vdSK6oDl1Q5v2GnzuV04xWV3p4rc,12401
29
29
  folio_migration_tools/marc_rules_transformation/hrid_handler.py,sha256=Ihdv0_1q7gL_pZ3HWU3GcfV_jjpIfOLithWk9z_uH3Y,9997
30
30
  folio_migration_tools/marc_rules_transformation/loc_language_codes.xml,sha256=ztn2_yKws6qySL4oSsZh7sOjxq5bCC1PhAnXJdtgmJ0,382912
31
31
  folio_migration_tools/marc_rules_transformation/marc_file_processor.py,sha256=WkOQRDi7f4PZ5qmVH3Q-1_zdGEKYSvOGC6jixDwDp98,12349
32
32
  folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py,sha256=9ATjYMRAjy0QcXtmNZaHVhHLJ5hE1WUgOcF6KMJjbgo,5309
33
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py,sha256=GFw8j9UtCxnUdLShmPzJa1MpCK8a0NkQIN5C3jyouRs,9604
34
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py,sha256=-vJDMNZe-7JbARgybVj6lLtZM79PgWU9V_k23330uLM,41195
35
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=ckVeysbpW9s19pmHvogdRFOCouzz17Y6oKJD0_QfQAk,28924
36
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=gPJaWcvt-CKIJxrEzheRzohnc3mFEnsznuZIXLPhyZM,18954
33
+ folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py,sha256=e-wwJs8s8qEgIp8NvQgjx9lEyv7uvt08Fp6fPsy1GK8,9603
34
+ folio_migration_tools/marc_rules_transformation/rules_mapper_base.py,sha256=C2jTzdXGkGjE3EWHxUh8jJeqE9tVk0qwRWVxFPZUj-Y,41223
35
+ folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=ZZHsuxlrHRcxkWPeiTjze0SahkNW_rhY3vkOQKnm1cU,28923
36
+ folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=wTZ2x8VIvCuLuNJLsbUAMCVjbMN7SS1teq0G6LAcOhU,21240
37
37
  folio_migration_tools/migration_report.py,sha256=BkRspM1hwTBnWeqsHamf7yVEofzLj560Q-9G--O00hw,4258
38
38
  folio_migration_tools/migration_tasks/__init__.py,sha256=ZkbY_yGyB84Ke8OMlYUzyyBj4cxxNrhMTwQlu_GbdDs,211
39
39
  folio_migration_tools/migration_tasks/authority_transformer.py,sha256=AoXg9s-GLO3yEEDCrQV7hc4YVXxwxsdxDdpj1zhHydE,4251
40
40
  folio_migration_tools/migration_tasks/batch_poster.py,sha256=wI4lCXU5BQDbKErF6pQxT6srq_Wf_nfFAJc4f1sRCoo,36388
41
41
  folio_migration_tools/migration_tasks/bibs_transformer.py,sha256=XzlPo-0uuugJA4SM80xOlOj5nDK6OMDXFnAYg80hOBc,7791
42
42
  folio_migration_tools/migration_tasks/courses_migrator.py,sha256=CzXnsu-KGP7B4zcINJzLYUqz47D16NuFfzu_DPqRlTQ,7061
43
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py,sha256=WT-RlDRm2ILr2-2shfG3TZ3nlSfqxEXT3TklZSqtJCM,22311
44
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py,sha256=8dtrhxyA9hbISISzpvMJGYaMaDbtZ1MOZeoJJF5lk24,11164
45
- folio_migration_tools/migration_tasks/items_transformer.py,sha256=NNG7AdS_iuWurdySnWZ0o8kw3KNWPAvkRaHpW-18nV0,18779
46
- folio_migration_tools/migration_tasks/loans_migrator.py,sha256=4n7zbwljX_jgj9ltnxZAegjN3e8QJMjr6JJa5XfVueY,34771
43
+ folio_migration_tools/migration_tasks/holdings_csv_transformer.py,sha256=NtysoayEIqQ8c_GNcRi6LXDYR-7OLmqFCfciMwzsyT4,21668
44
+ folio_migration_tools/migration_tasks/holdings_marc_transformer.py,sha256=gL2LoXgavVQDpIH-t2vF2za04W8IjBul7MiVifuzvD8,11637
45
+ folio_migration_tools/migration_tasks/items_transformer.py,sha256=qk0sLPBxE5MtPnpLzO_gEhVVe1BqHHnpn2Zaz_vo1RY,19083
46
+ folio_migration_tools/migration_tasks/loans_migrator.py,sha256=JE1e0i2HFzhYl05SqEkg79p9KzwSq_hPboVT9mJhgmk,34510
47
47
  folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py,sha256=CnmlTge7nChUJ10EiUkriQtJlVxWqglgfhjgneh2_yM,7247
48
- folio_migration_tools/migration_tasks/migration_task_base.py,sha256=8enHPNrgOHZs5sDGsz0yMPXap0HBprz8-1HVr9udvf0,16704
49
- folio_migration_tools/migration_tasks/orders_transformer.py,sha256=Q9YU8DUtVBqXfmvwa2LFxsO09h3IRRXXh3xWF5hUNg8,14249
48
+ folio_migration_tools/migration_tasks/migration_task_base.py,sha256=WEKsO8fBsbtA5jkXe_tn1tP9QaVtSzP1_AJR5m41bII,19148
49
+ folio_migration_tools/migration_tasks/orders_transformer.py,sha256=ry3oUUVQTFKCDUbGF5Zjo5ppa6AseKQwpF-wb1sb5UY,14214
50
50
  folio_migration_tools/migration_tasks/organization_transformer.py,sha256=vcCjhN1sS55c_a0LXi1Yw1eq3zpDn5E4BGbm2zDQ_Z4,16885
51
51
  folio_migration_tools/migration_tasks/requests_migrator.py,sha256=QP9OBezC3FfcKpI78oMmydxcPaUIYAgHyKevyLwC-WQ,14841
52
52
  folio_migration_tools/migration_tasks/reserves_migrator.py,sha256=SA3b7FQWHMHb7bEO8ZqOlblQ9m65zWUMH71uRk-zOKw,9950
53
- folio_migration_tools/migration_tasks/user_transformer.py,sha256=g-0etM5MpW3gjOrJ4pHotJHfubH8nxsZqUI8ZDi_TC0,12912
53
+ folio_migration_tools/migration_tasks/user_transformer.py,sha256=cNBT-wn_xx1OQXiB-vMLZmvyzkg1X562AJXUcYfThaE,12279
54
54
  folio_migration_tools/task_configuration.py,sha256=C5-OQtZLH7b4lVeyj5v8OXsqKNN4tzfp9F3b4vhthN4,632
55
55
  folio_migration_tools/test_infrastructure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
- folio_migration_tools/test_infrastructure/mocked_classes.py,sha256=rNes6UlRqIWGwPurfiQK97IvgB5OPwnZTbv1T28jHzk,9150
56
+ folio_migration_tools/test_infrastructure/mocked_classes.py,sha256=trK1ZvxTdebc8qHtFtLtc-6SLlNdGDtX2z4zhP8GMcI,11278
57
57
  folio_migration_tools/transaction_migration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
- folio_migration_tools/transaction_migration/legacy_loan.py,sha256=M48ZU45yEL1h93Jbld5qKwF1DEKt6sGYPc-t_JgLYX0,5896
58
+ folio_migration_tools/transaction_migration/legacy_loan.py,sha256=3PDyC1wbJzF0CcNWelvZ0tC8hjl3p5hbLVJHrz78ORM,6006
59
59
  folio_migration_tools/transaction_migration/legacy_request.py,sha256=1ulyFzPQw_InOjyPzkWpGnNptgXdQ18nmri0J8Nlpkc,6124
60
60
  folio_migration_tools/transaction_migration/legacy_reserve.py,sha256=d0qbh2fWpwlVSYRL6wZyZG20__NAYNxh7sPSsB-LAes,1804
61
61
  folio_migration_tools/transaction_migration/transaction_result.py,sha256=cTdCN0BnlI9_ZJB2Z3Fdkl9gpymIi-9mGZsRFlQcmDk,656
62
62
  folio_migration_tools/translations/en.json,sha256=HOVpkb_T-SN_x0NpDp8gyvV1hMLCui3SsG7ByyIv0OU,38669
63
- folio_migration_tools-1.9.0rc6.dist-info/LICENSE,sha256=PhIEkitVi3ejgq56tt6sWoJIG_zmv82cjjd_aYPPGdI,1072
64
- folio_migration_tools-1.9.0rc6.dist-info/METADATA,sha256=EL9MObYfZSyJ1otUDkUQEL0T2P8yxU5E77qZ4HhKFrk,7415
65
- folio_migration_tools-1.9.0rc6.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
66
- folio_migration_tools-1.9.0rc6.dist-info/entry_points.txt,sha256=Hbe-HjqMcU8FwVshVIkeWyZd9XwgT1CCMNf06EpHQu8,77
67
- folio_migration_tools-1.9.0rc6.dist-info/RECORD,,
63
+ folio_migration_tools-1.9.0rc8.dist-info/LICENSE,sha256=PhIEkitVi3ejgq56tt6sWoJIG_zmv82cjjd_aYPPGdI,1072
64
+ folio_migration_tools-1.9.0rc8.dist-info/METADATA,sha256=BelPrPeeo24CEE-5O1AmPhHODf5NOMUAmQWe1TXZf3U,7447
65
+ folio_migration_tools-1.9.0rc8.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
66
+ folio_migration_tools-1.9.0rc8.dist-info/entry_points.txt,sha256=Hbe-HjqMcU8FwVshVIkeWyZd9XwgT1CCMNf06EpHQu8,77
67
+ folio_migration_tools-1.9.0rc8.dist-info/RECORD,,