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.
- folio_migration_tools/__main__.py +17 -5
- folio_migration_tools/custom_exceptions.py +7 -7
- folio_migration_tools/folder_structure.py +5 -0
- folio_migration_tools/library_configuration.py +41 -3
- folio_migration_tools/mapper_base.py +11 -38
- folio_migration_tools/mapping_file_transformation/courses_mapper.py +1 -1
- folio_migration_tools/mapping_file_transformation/item_mapper.py +2 -8
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +4 -8
- folio_migration_tools/mapping_file_transformation/user_mapper.py +1 -1
- folio_migration_tools/marc_rules_transformation/conditions.py +1 -1
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +1 -1
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +11 -6
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +1 -1
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +56 -2
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py +1 -13
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py +12 -3
- folio_migration_tools/migration_tasks/items_transformer.py +22 -17
- folio_migration_tools/migration_tasks/loans_migrator.py +2 -9
- folio_migration_tools/migration_tasks/migration_task_base.py +50 -6
- folio_migration_tools/migration_tasks/orders_transformer.py +1 -1
- folio_migration_tools/migration_tasks/user_transformer.py +2 -10
- folio_migration_tools/test_infrastructure/mocked_classes.py +63 -0
- folio_migration_tools/transaction_migration/legacy_loan.py +25 -27
- {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/METADATA +2 -1
- {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/RECORD +28 -28
- {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/LICENSE +0 -0
- {folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
|
8
|
+
class TransformationError(Exception):
|
|
9
9
|
pass
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class TransformationFieldMappingError(
|
|
13
|
-
"""Raised when the
|
|
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(
|
|
39
|
-
"""Raised when the
|
|
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(
|
|
65
|
-
"""Raised when the transformation fails due to incorrect
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
"""
|
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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):
|
|
@@ -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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
359
|
+
if folio_rec["holdingsRecordId"] in self.boundwith_relationship_map:
|
|
371
360
|
for idx_, instance_id in enumerate(
|
|
372
|
-
self.
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
118
|
-
if self.due_date.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
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)
|
{folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: folio_migration_tools
|
|
3
|
-
Version: 1.9.
|
|
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)
|
{folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/RECORD
RENAMED
|
@@ -1,67 +1,67 @@
|
|
|
1
1
|
folio_migration_tools/__init__.py,sha256=DXvzUKFSpSZjflFWaNm0L8yhFk0u7RVIvQMskwMmKFc,238
|
|
2
|
-
folio_migration_tools/__main__.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
14
|
-
folio_migration_tools/mapper_base.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
34
|
-
folio_migration_tools/marc_rules_transformation/rules_mapper_base.py,sha256
|
|
35
|
-
folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=
|
|
36
|
-
folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=
|
|
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=
|
|
44
|
-
folio_migration_tools/migration_tasks/holdings_marc_transformer.py,sha256=
|
|
45
|
-
folio_migration_tools/migration_tasks/items_transformer.py,sha256=
|
|
46
|
-
folio_migration_tools/migration_tasks/loans_migrator.py,sha256=
|
|
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=
|
|
49
|
-
folio_migration_tools/migration_tasks/orders_transformer.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
64
|
-
folio_migration_tools-1.9.
|
|
65
|
-
folio_migration_tools-1.9.
|
|
66
|
-
folio_migration_tools-1.9.
|
|
67
|
-
folio_migration_tools-1.9.
|
|
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,,
|
{folio_migration_tools-1.9.0rc6.dist-info → folio_migration_tools-1.9.0rc8.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|