folio-migration-tools 1.9.3__py3-none-any.whl → 1.9.4__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/marc_rules_transformation/rules_mapper_bibs.py +2 -2
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +7 -1
- folio_migration_tools/migration_tasks/batch_poster.py +119 -18
- folio_migration_tools/migration_tasks/items_transformer.py +4 -1
- folio_migration_tools/migration_tasks/loans_migrator.py +221 -76
- folio_migration_tools/migration_tasks/organization_transformer.py +1 -0
- folio_migration_tools/transaction_migration/legacy_loan.py +23 -9
- folio_migration_tools/translations/en.json +19 -10
- {folio_migration_tools-1.9.3.dist-info → folio_migration_tools-1.9.4.dist-info}/METADATA +2 -1
- {folio_migration_tools-1.9.3.dist-info → folio_migration_tools-1.9.4.dist-info}/RECORD +13 -13
- {folio_migration_tools-1.9.3.dist-info → folio_migration_tools-1.9.4.dist-info}/LICENSE +0 -0
- {folio_migration_tools-1.9.3.dist-info → folio_migration_tools-1.9.4.dist-info}/WHEEL +0 -0
- {folio_migration_tools-1.9.3.dist-info → folio_migration_tools-1.9.4.dist-info}/entry_points.txt +0 -0
|
@@ -174,11 +174,11 @@ class BibsRulesMapper(RulesMapperBase):
|
|
|
174
174
|
self.process_marc_field(folio_instance, main_entry_fields[0], ignored_subsequent_fields, legacy_ids)
|
|
175
175
|
try:
|
|
176
176
|
self.process_marc_field(folio_instance, marc_record['245'], ignored_subsequent_fields, legacy_ids)
|
|
177
|
-
except KeyError:
|
|
177
|
+
except KeyError as ke:
|
|
178
178
|
raise TransformationRecordFailedError(
|
|
179
179
|
legacy_ids,
|
|
180
180
|
"No 245 field in MARC record"
|
|
181
|
-
)
|
|
181
|
+
) from ke
|
|
182
182
|
|
|
183
183
|
def perform_additional_parsing(
|
|
184
184
|
self,
|
|
@@ -196,9 +196,15 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
196
196
|
if "004" not in marc_record:
|
|
197
197
|
raise TransformationProcessError(
|
|
198
198
|
"",
|
|
199
|
-
("No 004 in record. The tools only support bib-mfhd linking
|
|
199
|
+
("No 004 in record. The tools only support bib-mfhd linking through 004"),
|
|
200
200
|
legacy_ids,
|
|
201
201
|
)
|
|
202
|
+
if len(marc_record.get_fields("004")) > 1:
|
|
203
|
+
Helper.log_data_issue(
|
|
204
|
+
legacy_ids,
|
|
205
|
+
"More than one linked bib (004) found in record. Using the first one",
|
|
206
|
+
[str(x) for x in marc_record.get_fields("004")],
|
|
207
|
+
)
|
|
202
208
|
legacy_instance_id = marc_record["004"].data.strip()
|
|
203
209
|
folio_holding["formerIds"].append(f"{self.bib_id_template}{legacy_instance_id}")
|
|
204
210
|
if legacy_instance_id in self.parent_id_map:
|
|
@@ -131,6 +131,56 @@ class BatchPoster(MigrationTaskBase):
|
|
|
131
131
|
),
|
|
132
132
|
),
|
|
133
133
|
] = False
|
|
134
|
+
preserve_statistical_codes: Annotated[
|
|
135
|
+
bool,
|
|
136
|
+
Field(
|
|
137
|
+
title="Preserve statistical codes",
|
|
138
|
+
description=(
|
|
139
|
+
"Toggles whether or not to preserve statistical codes "
|
|
140
|
+
"during the upsert process. Defaults to False"
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
] = False
|
|
144
|
+
preserve_administrative_notes: Annotated[
|
|
145
|
+
bool,
|
|
146
|
+
Field(
|
|
147
|
+
title="Preserve administrative notes",
|
|
148
|
+
description=(
|
|
149
|
+
"Toggles whether or not to preserve administrative notes "
|
|
150
|
+
"during the upsert process. Defaults to False"
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
] = False
|
|
154
|
+
preserve_temporary_locations: Annotated[
|
|
155
|
+
bool,
|
|
156
|
+
Field(
|
|
157
|
+
title="Preserve temporary locations",
|
|
158
|
+
description=(
|
|
159
|
+
"Toggles whether or not to preserve temporary locations "
|
|
160
|
+
"on items during the upsert process. Defaults to False"
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
] = False
|
|
164
|
+
preserve_temporary_loan_types: Annotated[
|
|
165
|
+
bool,
|
|
166
|
+
Field(
|
|
167
|
+
title="Preserve temporary loan types",
|
|
168
|
+
description=(
|
|
169
|
+
"Toggles whether or not to preserve temporary loan types "
|
|
170
|
+
"on items during the upsert process. Defaults to False"
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
] = False
|
|
174
|
+
preserve_item_status: Annotated[
|
|
175
|
+
bool,
|
|
176
|
+
Field(
|
|
177
|
+
title="Preserve item status",
|
|
178
|
+
description=(
|
|
179
|
+
"Toggles whether or not to preserve item status "
|
|
180
|
+
"on items during the upsert process. Defaults to False"
|
|
181
|
+
),
|
|
182
|
+
),
|
|
183
|
+
] = True
|
|
134
184
|
|
|
135
185
|
task_configuration: TaskConfiguration
|
|
136
186
|
|
|
@@ -292,7 +342,7 @@ class BatchPoster(MigrationTaskBase):
|
|
|
292
342
|
"""
|
|
293
343
|
fetch_batch_size = 90
|
|
294
344
|
fetch_tasks = []
|
|
295
|
-
|
|
345
|
+
existing_records = {}
|
|
296
346
|
async with httpx.AsyncClient(base_url=self.folio_client.gateway_url) as client:
|
|
297
347
|
for i in range(0, len(batch), fetch_batch_size):
|
|
298
348
|
batch_slice = batch[i:i + fetch_batch_size]
|
|
@@ -313,31 +363,82 @@ class BatchPoster(MigrationTaskBase):
|
|
|
313
363
|
responses = await asyncio.gather(*fetch_tasks)
|
|
314
364
|
|
|
315
365
|
for response in responses:
|
|
316
|
-
self.
|
|
366
|
+
self.collect_existing_records_for_upsert(object_type, response, existing_records)
|
|
317
367
|
for record in batch:
|
|
318
|
-
if record["id"] in
|
|
319
|
-
|
|
368
|
+
if record["id"] in existing_records:
|
|
369
|
+
self.prepare_record_for_upsert(record, existing_records[record["id"]])
|
|
370
|
+
|
|
371
|
+
def handle_source_marc(self, new_record: dict, existing_record: dict):
|
|
372
|
+
updates = {}
|
|
373
|
+
updates.update(existing_record)
|
|
374
|
+
self.handle_upsert_for_administrative_notes(updates)
|
|
375
|
+
self.handle_upsert_for_statistical_codes(updates)
|
|
376
|
+
keep_new = {k: v for k, v in new_record.items() if k in ["statisticalCodeIds", "administrativeNotes"]}
|
|
377
|
+
if "instanceStatusId" in new_record:
|
|
378
|
+
updates["instanceStatusId"] = new_record["instanceStatusId"]
|
|
379
|
+
for k, v in keep_new.items():
|
|
380
|
+
updates[k] = list(dict.fromkeys(updates.get(k, []) + v))
|
|
381
|
+
new_record.update(updates)
|
|
320
382
|
|
|
321
383
|
@staticmethod
|
|
322
|
-
def
|
|
384
|
+
def collect_existing_records_for_upsert(object_type: str, response: httpx.Response, existing_records: dict):
|
|
323
385
|
if response.status_code == 200:
|
|
324
386
|
response_json = response.json()
|
|
325
387
|
for record in response_json[object_type]:
|
|
326
|
-
|
|
327
|
-
"_version": record["_version"],
|
|
328
|
-
}
|
|
329
|
-
if "hrid" in record:
|
|
330
|
-
updates[record["id"]]["hrid"] = record["hrid"]
|
|
331
|
-
if "status" in record:
|
|
332
|
-
updates[record["id"]]["status"] = record["status"]
|
|
333
|
-
if "lastCheckIn" in record:
|
|
334
|
-
updates[record["id"]]["lastCheckIn"] = record["lastCheckIn"]
|
|
388
|
+
existing_records[record["id"]] = record
|
|
335
389
|
else:
|
|
336
390
|
logging.error(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
391
|
+
"Failed to fetch current records. HTTP %s\t%s",
|
|
392
|
+
response.status_code,
|
|
393
|
+
response.text,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def handle_upsert_for_statistical_codes(self, updates: dict):
|
|
397
|
+
if not self.task_configuration.preserve_statistical_codes:
|
|
398
|
+
updates.pop("statisticalCodeIds", None)
|
|
399
|
+
|
|
400
|
+
def handle_upsert_for_administrative_notes(self, updates: dict):
|
|
401
|
+
if not self.task_configuration.preserve_administrative_notes:
|
|
402
|
+
updates.pop("administrativeNotes", None)
|
|
403
|
+
|
|
404
|
+
def handle_upsert_for_temporary_locations(self, updates: dict):
|
|
405
|
+
if not self.task_configuration.preserve_temporary_locations:
|
|
406
|
+
updates.pop("temporaryLocationId", None)
|
|
407
|
+
|
|
408
|
+
def handle_upsert_for_temporary_loan_types(self, updates: dict):
|
|
409
|
+
if not self.task_configuration.preserve_temporary_loan_types:
|
|
410
|
+
updates.pop("temporaryLoanTypeId", None)
|
|
411
|
+
|
|
412
|
+
def keep_existing_fields(self, updates: dict, existing_record: dict):
|
|
413
|
+
keep_existing_fields = ["hrid", "lastCheckIn"]
|
|
414
|
+
if self.task_configuration.preserve_item_status:
|
|
415
|
+
keep_existing_fields.append("status")
|
|
416
|
+
for key in keep_existing_fields:
|
|
417
|
+
if key in existing_record:
|
|
418
|
+
updates[key] = existing_record[key]
|
|
419
|
+
|
|
420
|
+
def prepare_record_for_upsert(self, new_record: dict, existing_record: dict):
|
|
421
|
+
if "source" in existing_record and "MARC" in existing_record["source"]:
|
|
422
|
+
self.handle_source_marc(new_record, existing_record)
|
|
423
|
+
else:
|
|
424
|
+
updates = {
|
|
425
|
+
"_version": existing_record["_version"],
|
|
426
|
+
}
|
|
427
|
+
self.keep_existing_fields(updates, existing_record)
|
|
428
|
+
keep_new = {k: v for k, v in new_record.items() if k in ["statisticalCodeIds", "administrativeNotes"]}
|
|
429
|
+
self.handle_upsert_for_statistical_codes(existing_record)
|
|
430
|
+
self.handle_upsert_for_administrative_notes(existing_record)
|
|
431
|
+
self.handle_upsert_for_temporary_locations(existing_record)
|
|
432
|
+
self.handle_upsert_for_temporary_loan_types(existing_record)
|
|
433
|
+
for k, v in keep_new.items():
|
|
434
|
+
updates[k] = list(dict.fromkeys(existing_record.get(k, []) + v))
|
|
435
|
+
for key in [
|
|
436
|
+
"temporaryLocationId",
|
|
437
|
+
"temporaryLoanTypeId",
|
|
438
|
+
]:
|
|
439
|
+
if key in existing_record:
|
|
440
|
+
updates[key] = existing_record[key]
|
|
441
|
+
new_record.update(updates)
|
|
341
442
|
|
|
342
443
|
async def get_with_retry(self, client: httpx.AsyncClient, url: str, params=None):
|
|
343
444
|
if params is None:
|
|
@@ -218,7 +218,10 @@ class ItemsTransformer(MigrationTaskBase):
|
|
|
218
218
|
self.folio_keys = MappingFileMapperBase.get_mapped_folio_properties_from_map(
|
|
219
219
|
self.items_map
|
|
220
220
|
)
|
|
221
|
-
if
|
|
221
|
+
if (
|
|
222
|
+
any(k for k in self.folio_keys if k.startswith("statisticalCodeIds"))
|
|
223
|
+
or any(getattr(k, "statistical_code", "") for k in self.task_configuration.files)
|
|
224
|
+
):
|
|
222
225
|
statcode_mapping = self.load_ref_data_mapping_file(
|
|
223
226
|
"statisticalCodeIds",
|
|
224
227
|
self.folder_structure.mapping_files_folder
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
import time
|
|
7
7
|
import traceback
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
|
-
from typing import Annotated, Optional
|
|
9
|
+
from typing import Annotated, Literal, Optional
|
|
10
10
|
from urllib.error import HTTPError
|
|
11
11
|
from zoneinfo import ZoneInfo
|
|
12
12
|
from pydantic import Field
|
|
@@ -55,7 +55,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
55
55
|
Optional[list[FileDefinition]],
|
|
56
56
|
Field(
|
|
57
57
|
title="Open loans files",
|
|
58
|
-
description="List of files containing open loan data."
|
|
58
|
+
description="List of files containing open loan data.",
|
|
59
59
|
),
|
|
60
60
|
]
|
|
61
61
|
fallback_service_point_id: Annotated[
|
|
@@ -69,10 +69,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
69
69
|
Optional[int],
|
|
70
70
|
Field(
|
|
71
71
|
title="Starting row",
|
|
72
|
-
description=(
|
|
73
|
-
"The starting row for data processing. "
|
|
74
|
-
"By default is 1."
|
|
75
|
-
),
|
|
72
|
+
description=("The starting row for data processing. By default is 1."),
|
|
76
73
|
),
|
|
77
74
|
] = 1
|
|
78
75
|
item_files: Annotated[
|
|
@@ -80,8 +77,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
80
77
|
Field(
|
|
81
78
|
title="Item files",
|
|
82
79
|
description=(
|
|
83
|
-
"List of files containing item data. "
|
|
84
|
-
"By default is empty list."
|
|
80
|
+
"List of files containing item data. By default is empty list."
|
|
85
81
|
),
|
|
86
82
|
),
|
|
87
83
|
] = []
|
|
@@ -90,8 +86,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
90
86
|
Field(
|
|
91
87
|
title="Patron files",
|
|
92
88
|
description=(
|
|
93
|
-
"List of files containing patron data. "
|
|
94
|
-
"By default is empty list."
|
|
89
|
+
"List of files containing patron data. By default is empty list."
|
|
95
90
|
),
|
|
96
91
|
),
|
|
97
92
|
] = []
|
|
@@ -104,7 +99,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
104
99
|
self,
|
|
105
100
|
task_configuration: TaskConfiguration,
|
|
106
101
|
library_config: LibraryConfiguration,
|
|
107
|
-
folio_client
|
|
102
|
+
folio_client,
|
|
108
103
|
):
|
|
109
104
|
csv.register_dialect("tsv", delimiter="\t")
|
|
110
105
|
self.patron_item_combos: set = set()
|
|
@@ -129,7 +124,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
129
124
|
my_path = "/configurations/entries?query=(module==ORG%20and%20configName==localeSettings)"
|
|
130
125
|
try:
|
|
131
126
|
self.tenant_timezone_str = json.loads(
|
|
132
|
-
self.folio_client.folio_get_single_object(my_path)["configs"][0][
|
|
127
|
+
self.folio_client.folio_get_single_object(my_path)["configs"][0][
|
|
128
|
+
"value"
|
|
129
|
+
]
|
|
133
130
|
)["timezone"]
|
|
134
131
|
logging.info("Tenant timezone is: %s", self.tenant_timezone_str)
|
|
135
132
|
except Exception:
|
|
@@ -138,10 +135,14 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
138
135
|
self.tenant_timezone = ZoneInfo(self.tenant_timezone_str)
|
|
139
136
|
self.semi_valid_legacy_loans = []
|
|
140
137
|
for file_def in task_configuration.open_loans_files:
|
|
141
|
-
loans_file_path =
|
|
138
|
+
loans_file_path = (
|
|
139
|
+
self.folder_structure.legacy_records_folder / file_def.file_name
|
|
140
|
+
)
|
|
142
141
|
with open(loans_file_path, "r", encoding="utf-8") as loans_file:
|
|
143
|
-
total_rows, empty_rows, reader =
|
|
144
|
-
|
|
142
|
+
total_rows, empty_rows, reader = (
|
|
143
|
+
MappingFileMapperBase._get_delimited_file_reader(
|
|
144
|
+
loans_file, loans_file_path
|
|
145
|
+
)
|
|
145
146
|
)
|
|
146
147
|
logging.info("Source data file contains %d rows", total_rows)
|
|
147
148
|
logging.info("Source data file contains %d empty rows", empty_rows)
|
|
@@ -158,7 +159,8 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
158
159
|
self.semi_valid_legacy_loans.extend(
|
|
159
160
|
self.load_and_validate_legacy_loans(
|
|
160
161
|
reader,
|
|
161
|
-
file_def.service_point_id
|
|
162
|
+
file_def.service_point_id
|
|
163
|
+
or task_configuration.fallback_service_point_id,
|
|
162
164
|
)
|
|
163
165
|
)
|
|
164
166
|
|
|
@@ -167,8 +169,12 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
167
169
|
len(self.semi_valid_legacy_loans),
|
|
168
170
|
file_def.file_name,
|
|
169
171
|
)
|
|
170
|
-
logging.info(
|
|
171
|
-
|
|
172
|
+
logging.info(
|
|
173
|
+
"Loaded and validated %s loans in total", len(self.semi_valid_legacy_loans)
|
|
174
|
+
)
|
|
175
|
+
if any(self.task_configuration.item_files) or any(
|
|
176
|
+
self.task_configuration.patron_files
|
|
177
|
+
):
|
|
172
178
|
self.valid_legacy_loans = list(self.check_barcodes())
|
|
173
179
|
logging.info(
|
|
174
180
|
"Loaded and validated %s loans against barcodes",
|
|
@@ -185,9 +191,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
185
191
|
|
|
186
192
|
def check_smtp_config(self):
|
|
187
193
|
try:
|
|
188
|
-
smtp_config = self.folio_client.folio_get_single_object(
|
|
189
|
-
"
|
|
190
|
-
][0]
|
|
194
|
+
smtp_config = self.folio_client.folio_get_single_object(
|
|
195
|
+
"/smtp-configuration"
|
|
196
|
+
)["smtpConfigurations"][0]
|
|
191
197
|
smtp_config_disabled = "disabled" in smtp_config["host"].lower()
|
|
192
198
|
except IndexError:
|
|
193
199
|
smtp_config_disabled = True
|
|
@@ -195,7 +201,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
195
201
|
if not smtp_config_disabled:
|
|
196
202
|
logging.warn("SMTP connection not disabled...")
|
|
197
203
|
for i in range(10, 0, -1):
|
|
198
|
-
sys.stdout.write(
|
|
204
|
+
sys.stdout.write(
|
|
205
|
+
"Pausing for {:02d} seconds. Press Ctrl+C to exit...\r".format(i)
|
|
206
|
+
)
|
|
199
207
|
time.sleep(1)
|
|
200
208
|
else:
|
|
201
209
|
logging.info("SMTP connection is disabled...")
|
|
@@ -225,7 +233,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
225
233
|
f"Patron barcode: {legacy_loan.patron_barcode} {ee}"
|
|
226
234
|
)
|
|
227
235
|
if num_loans % 25 == 0:
|
|
228
|
-
logging.info(
|
|
236
|
+
logging.info(
|
|
237
|
+
f"{timings(self.t0, t0_migration, num_loans)} {num_loans}"
|
|
238
|
+
)
|
|
229
239
|
|
|
230
240
|
def checkout_single_loan(self, legacy_loan: LegacyLoan):
|
|
231
241
|
"""Checks a legacy loan out. Retries once if it fails.
|
|
@@ -234,17 +244,23 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
234
244
|
legacy_loan (LegacyLoan): The Legacy loan
|
|
235
245
|
"""
|
|
236
246
|
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
237
|
-
|
|
247
|
+
|
|
238
248
|
if res_checkout.was_successful:
|
|
239
249
|
self.migration_report.add("Details", i18n.t("Checked out on first try"))
|
|
240
|
-
self.migration_report.add_general_statistics(
|
|
250
|
+
self.migration_report.add_general_statistics(
|
|
251
|
+
i18n.t("Successfully checked out")
|
|
252
|
+
)
|
|
241
253
|
self.set_renewal_count(legacy_loan, res_checkout)
|
|
242
254
|
self.set_new_status(legacy_loan, res_checkout)
|
|
243
255
|
elif res_checkout.should_be_retried:
|
|
244
256
|
res_checkout2 = self.handle_checkout_failure(legacy_loan, res_checkout)
|
|
245
257
|
if res_checkout2.was_successful and res_checkout2.folio_loan:
|
|
246
|
-
self.migration_report.add(
|
|
247
|
-
|
|
258
|
+
self.migration_report.add(
|
|
259
|
+
"Details", i18n.t("Checked out on second try")
|
|
260
|
+
)
|
|
261
|
+
self.migration_report.add_general_statistics(
|
|
262
|
+
i18n.t("Successfully checked out")
|
|
263
|
+
)
|
|
248
264
|
logging.info("Checked out on second try")
|
|
249
265
|
self.set_renewal_count(legacy_loan, res_checkout2)
|
|
250
266
|
self.set_new_status(legacy_loan, res_checkout2)
|
|
@@ -252,7 +268,8 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
252
268
|
if res_checkout2.error_message == "Aged to lost and checked out":
|
|
253
269
|
self.migration_report.add(
|
|
254
270
|
"Details",
|
|
255
|
-
i18n.t("Second failure")
|
|
271
|
+
i18n.t("Second failure")
|
|
272
|
+
+ f": {res_checkout2.migration_report_message}",
|
|
256
273
|
)
|
|
257
274
|
logging.error(
|
|
258
275
|
f"{res_checkout2.error_message}. Item barcode: {legacy_loan.item_barcode}"
|
|
@@ -261,15 +278,22 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
261
278
|
self.failed[legacy_loan.item_barcode] = legacy_loan
|
|
262
279
|
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
263
280
|
Helper.log_data_issue(
|
|
264
|
-
"",
|
|
281
|
+
"",
|
|
282
|
+
"Loans failing during checkout",
|
|
283
|
+
json.dumps(legacy_loan.to_dict()),
|
|
284
|
+
)
|
|
285
|
+
logging.error(
|
|
286
|
+
"Failed on second try: %s", res_checkout2.error_message
|
|
265
287
|
)
|
|
266
|
-
logging.error("Failed on second try: %s", res_checkout2.error_message)
|
|
267
288
|
self.migration_report.add(
|
|
268
289
|
"Details",
|
|
269
|
-
i18n.t("Second failure")
|
|
290
|
+
i18n.t("Second failure")
|
|
291
|
+
+ f": {res_checkout2.migration_report_message}",
|
|
270
292
|
)
|
|
271
293
|
elif not res_checkout.should_be_retried:
|
|
272
|
-
logging.error(
|
|
294
|
+
logging.error(
|
|
295
|
+
"Failed first time. No retries: %s", res_checkout.error_message
|
|
296
|
+
)
|
|
273
297
|
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
274
298
|
self.migration_report.add(
|
|
275
299
|
"Details",
|
|
@@ -296,10 +320,14 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
296
320
|
elif legacy_loan.next_item_status not in ["Available", "", "Checked out"]:
|
|
297
321
|
self.set_item_status(legacy_loan)
|
|
298
322
|
|
|
299
|
-
def set_renewal_count(
|
|
323
|
+
def set_renewal_count(
|
|
324
|
+
self, legacy_loan: LegacyLoan, res_checkout: TransactionResult
|
|
325
|
+
):
|
|
300
326
|
if legacy_loan.renewal_count > 0:
|
|
301
327
|
self.update_open_loan(res_checkout.folio_loan, legacy_loan)
|
|
302
|
-
self.migration_report.add_general_statistics(
|
|
328
|
+
self.migration_report.add_general_statistics(
|
|
329
|
+
i18n.t("Updated renewal count for loan")
|
|
330
|
+
)
|
|
303
331
|
|
|
304
332
|
def wrap_up(self):
|
|
305
333
|
for k, v in self.failed.items():
|
|
@@ -316,15 +344,19 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
316
344
|
|
|
317
345
|
def write_failed_loans_to_file(self):
|
|
318
346
|
csv_columns = [
|
|
319
|
-
"
|
|
347
|
+
"patron_barcode",
|
|
348
|
+
"proxy_patron_barcode",
|
|
320
349
|
"item_barcode",
|
|
321
|
-
"
|
|
350
|
+
"due_date",
|
|
322
351
|
"out_date",
|
|
323
|
-
"
|
|
352
|
+
"next_item_status",
|
|
324
353
|
"renewal_count",
|
|
354
|
+
"service_point_id",
|
|
325
355
|
]
|
|
326
356
|
with open(self.folder_structure.failed_recs_path, "w+") as failed_loans_file:
|
|
327
|
-
writer = csv.DictWriter(
|
|
357
|
+
writer = csv.DictWriter(
|
|
358
|
+
failed_loans_file, fieldnames=csv_columns, dialect="tsv"
|
|
359
|
+
)
|
|
328
360
|
writer.writeheader()
|
|
329
361
|
for _k, failed_loan in self.failed_and_not_dupe.items():
|
|
330
362
|
writer.writerow(failed_loan[0])
|
|
@@ -339,12 +371,16 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
339
371
|
user_barcodes, self.task_configuration.patron_files, self.folder_structure
|
|
340
372
|
)
|
|
341
373
|
for loan in self.semi_valid_legacy_loans:
|
|
342
|
-
has_item_barcode = loan.item_barcode in item_barcodes or not any(
|
|
343
|
-
|
|
374
|
+
has_item_barcode = loan.item_barcode in item_barcodes or not any(
|
|
375
|
+
item_barcodes
|
|
376
|
+
)
|
|
377
|
+
has_patron_barcode = loan.patron_barcode in user_barcodes or not any(
|
|
378
|
+
user_barcodes
|
|
379
|
+
)
|
|
344
380
|
has_proxy_barcode = True
|
|
345
381
|
if loan.proxy_patron_barcode:
|
|
346
|
-
has_proxy_barcode =
|
|
347
|
-
user_barcodes
|
|
382
|
+
has_proxy_barcode = (
|
|
383
|
+
loan.proxy_patron_barcode in user_barcodes or not any(user_barcodes)
|
|
348
384
|
)
|
|
349
385
|
if has_item_barcode and has_patron_barcode and has_proxy_barcode:
|
|
350
386
|
self.migration_report.add_general_statistics(
|
|
@@ -375,10 +411,14 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
375
411
|
)
|
|
376
412
|
if not has_proxy_barcode:
|
|
377
413
|
Helper.log_data_issue(
|
|
378
|
-
"",
|
|
414
|
+
"",
|
|
415
|
+
"Loan without matched proxy patron barcode",
|
|
416
|
+
json.dumps(loan.to_dict()),
|
|
379
417
|
)
|
|
380
418
|
|
|
381
|
-
def load_and_validate_legacy_loans(
|
|
419
|
+
def load_and_validate_legacy_loans(
|
|
420
|
+
self, loans_reader, service_point_id: str
|
|
421
|
+
) -> list:
|
|
382
422
|
results = []
|
|
383
423
|
num_bad = 0
|
|
384
424
|
logging.info("Validating legacy loans in file...")
|
|
@@ -398,7 +438,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
398
438
|
)
|
|
399
439
|
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
400
440
|
for error in legacy_loan.errors:
|
|
401
|
-
self.migration_report.add(
|
|
441
|
+
self.migration_report.add(
|
|
442
|
+
"DiscardedLoans", f"{error[0]} - {error[1]}"
|
|
443
|
+
)
|
|
402
444
|
# Add this loan to failed loans for later correction and re-run.
|
|
403
445
|
self.failed[
|
|
404
446
|
legacy_loan.item_barcode or f"no_barcode_{legacy_loan_count}"
|
|
@@ -416,7 +458,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
416
458
|
)
|
|
417
459
|
trfe.log_it()
|
|
418
460
|
self.failed[
|
|
419
|
-
legacy_loan_dict.get(
|
|
461
|
+
legacy_loan_dict.get(
|
|
462
|
+
"item_barcode", f"no_barcode_{legacy_loan_count}"
|
|
463
|
+
)
|
|
420
464
|
] = legacy_loan_dict
|
|
421
465
|
except ValueError as ve:
|
|
422
466
|
logging.exception(ve)
|
|
@@ -457,13 +501,20 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
457
501
|
elif folio_checkout.error_message.startswith(
|
|
458
502
|
"Cannot check out item that already has an open loan"
|
|
459
503
|
):
|
|
460
|
-
return
|
|
504
|
+
return self.handle_checked_out_item(legacy_loan)
|
|
461
505
|
elif "Aged to lost" in folio_checkout.error_message:
|
|
462
|
-
return self.
|
|
506
|
+
return self.handle_lost_item(legacy_loan, "Aged to lost")
|
|
463
507
|
elif folio_checkout.error_message == "Declared lost":
|
|
464
|
-
return
|
|
465
|
-
elif folio_checkout.error_message.startswith(
|
|
508
|
+
return self.handle_lost_item(legacy_loan, "Declared lost")
|
|
509
|
+
elif folio_checkout.error_message.startswith(
|
|
510
|
+
"Cannot check out to inactive user"
|
|
511
|
+
):
|
|
466
512
|
return self.checkout_to_inactive_user(legacy_loan)
|
|
513
|
+
elif (
|
|
514
|
+
"has the item status Claimed returned and cannot be checked out"
|
|
515
|
+
in folio_checkout.error_message
|
|
516
|
+
):
|
|
517
|
+
return self.handle_claimed_returned_item(legacy_loan)
|
|
467
518
|
else:
|
|
468
519
|
self.migration_report.add(
|
|
469
520
|
"Details",
|
|
@@ -489,7 +540,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
489
540
|
f"Duplicate loans (or failed twice) Item barcode: "
|
|
490
541
|
f"{legacy_loan.item_barcode} Patron barcode: {legacy_loan.patron_barcode}"
|
|
491
542
|
)
|
|
492
|
-
self.migration_report.add(
|
|
543
|
+
self.migration_report.add(
|
|
544
|
+
"Details", i18n.t("Duplicate loans (or failed twice)")
|
|
545
|
+
)
|
|
493
546
|
del self.failed[legacy_loan.item_barcode]
|
|
494
547
|
return TransactionResult(False, False, "", "", "")
|
|
495
548
|
|
|
@@ -500,7 +553,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
500
553
|
user["expirationDate"] = datetime.isoformat(datetime.now() + timedelta(days=1))
|
|
501
554
|
self.activate_user(user)
|
|
502
555
|
logging.debug("Successfully Activated user")
|
|
503
|
-
res = self.circulation_helper.check_out_by_barcode(
|
|
556
|
+
res = self.circulation_helper.check_out_by_barcode(
|
|
557
|
+
legacy_loan
|
|
558
|
+
) # checkout_and_update
|
|
504
559
|
if res.should_be_retried:
|
|
505
560
|
res = self.handle_checkout_failure(legacy_loan, res)
|
|
506
561
|
self.migration_report.add("Details", res.migration_report_message)
|
|
@@ -509,28 +564,92 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
509
564
|
self.migration_report.add("Details", i18n.t("Handled inactive users"))
|
|
510
565
|
return res
|
|
511
566
|
|
|
512
|
-
def
|
|
567
|
+
def handle_checked_out_item(self, legacy_loan: LegacyLoan) -> TransactionResult:
|
|
513
568
|
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
514
569
|
return TransactionResult(
|
|
515
570
|
False,
|
|
516
571
|
False,
|
|
517
572
|
legacy_loan,
|
|
518
|
-
i18n.t(
|
|
519
|
-
|
|
573
|
+
i18n.t(
|
|
574
|
+
"Loan already exists for %{item_barcode}",
|
|
575
|
+
item_barcode=legacy_loan.item_barcode,
|
|
576
|
+
),
|
|
577
|
+
i18n.t(
|
|
578
|
+
"Loan already exists for %{item_barcode}",
|
|
579
|
+
item_barcode=legacy_loan.item_barcode,
|
|
580
|
+
),
|
|
520
581
|
)
|
|
521
|
-
|
|
522
582
|
else:
|
|
523
|
-
logging.debug(
|
|
583
|
+
logging.debug(
|
|
584
|
+
i18n.t(
|
|
585
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
586
|
+
item_barcode=legacy_loan.item_barcode,
|
|
587
|
+
)
|
|
588
|
+
)
|
|
524
589
|
legacy_loan.next_item_status = "Available"
|
|
525
590
|
self.set_item_status(legacy_loan)
|
|
526
591
|
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
527
|
-
legacy_loan.next_item_status = "
|
|
592
|
+
legacy_loan.next_item_status = "Checked out"
|
|
593
|
+
return res_checkout
|
|
594
|
+
|
|
595
|
+
def handle_lost_item(
|
|
596
|
+
self,
|
|
597
|
+
legacy_loan: LegacyLoan,
|
|
598
|
+
lost_type: Literal["Aged to lost", "Declared lost"],
|
|
599
|
+
) -> TransactionResult:
|
|
600
|
+
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
601
|
+
return TransactionResult(
|
|
602
|
+
False,
|
|
603
|
+
False,
|
|
604
|
+
legacy_loan,
|
|
605
|
+
i18n.t("%{lost_type} and checked out", lost_type=lost_type),
|
|
606
|
+
i18n.t("%{lost_type} and checked out", lost_type=lost_type),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
else:
|
|
610
|
+
logging.debug(
|
|
611
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
612
|
+
item_barcode=legacy_loan.item_barcode,
|
|
613
|
+
)
|
|
614
|
+
legacy_loan.next_item_status = "Available"
|
|
528
615
|
self.set_item_status(legacy_loan)
|
|
529
|
-
|
|
616
|
+
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
617
|
+
legacy_loan.next_item_status = lost_type
|
|
618
|
+
if lost_type == "Aged to lost":
|
|
619
|
+
self.set_item_status(legacy_loan)
|
|
620
|
+
s = i18n.t(
|
|
621
|
+
"Successfully Checked out %{lost_type} item and put the status back",
|
|
622
|
+
lost_type=lost_type,
|
|
623
|
+
)
|
|
624
|
+
else:
|
|
625
|
+
s = i18n.t(
|
|
626
|
+
"Successfully Checked out %{lost_type} item. Item will be declared lost.",
|
|
627
|
+
lost_type=lost_type,
|
|
628
|
+
)
|
|
530
629
|
logging.info(s)
|
|
531
630
|
self.migration_report.add("Details", s)
|
|
532
631
|
return res_checkout
|
|
533
632
|
|
|
633
|
+
def handle_claimed_returned_item(self, legacy_loan: LegacyLoan):
|
|
634
|
+
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
635
|
+
return TransactionResult(
|
|
636
|
+
False,
|
|
637
|
+
False,
|
|
638
|
+
legacy_loan,
|
|
639
|
+
i18n.t("Claimed returned and checked out"),
|
|
640
|
+
i18n.t("Claimed returned and checked out"),
|
|
641
|
+
)
|
|
642
|
+
else:
|
|
643
|
+
logging.debug(
|
|
644
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
645
|
+
item_barcode=legacy_loan.item_barcode,
|
|
646
|
+
)
|
|
647
|
+
legacy_loan.next_item_status = "Available"
|
|
648
|
+
self.set_item_status(legacy_loan)
|
|
649
|
+
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
650
|
+
legacy_loan.next_item_status = "Claimed returned"
|
|
651
|
+
return res_checkout
|
|
652
|
+
|
|
534
653
|
def update_open_loan(self, folio_loan: dict, legacy_loan: LegacyLoan):
|
|
535
654
|
due_date = du_parser.isoparse(str(legacy_loan.due_date))
|
|
536
655
|
out_date = du_parser.isoparse(str(legacy_loan.out_date))
|
|
@@ -541,7 +660,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
541
660
|
loan_to_put["dueDate"] = due_date.isoformat()
|
|
542
661
|
loan_to_put["loanDate"] = out_date.isoformat()
|
|
543
662
|
loan_to_put["renewalCount"] = renewal_count
|
|
544
|
-
url =
|
|
663
|
+
url = (
|
|
664
|
+
f"{self.folio_client.gateway_url}/circulation/loans/{loan_to_put['id']}"
|
|
665
|
+
)
|
|
545
666
|
req = self.http_client.put(
|
|
546
667
|
url,
|
|
547
668
|
headers=self.folio_client.okapi_headers,
|
|
@@ -562,7 +683,8 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
562
683
|
else:
|
|
563
684
|
self.migration_report.add(
|
|
564
685
|
"Details",
|
|
565
|
-
i18n.t("Update open loan error http status")
|
|
686
|
+
i18n.t("Update open loan error http status")
|
|
687
|
+
+ f": {req.status_code}",
|
|
566
688
|
)
|
|
567
689
|
req.raise_for_status()
|
|
568
690
|
logging.debug("Updating open loan was successful")
|
|
@@ -592,40 +714,59 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
592
714
|
"servicePointId": str(self.task_configuration.fallback_service_point_id),
|
|
593
715
|
}
|
|
594
716
|
logging.debug(f"Declare lost data: {json.dumps(data, indent=4)}")
|
|
595
|
-
if self.folio_put_post(
|
|
596
|
-
|
|
717
|
+
if self.folio_put_post(
|
|
718
|
+
declare_lost_url, data, "POST", i18n.t("Declare item as lost")
|
|
719
|
+
):
|
|
720
|
+
self.migration_report.add(
|
|
721
|
+
"Details", i18n.t("Successfully declared loan as lost")
|
|
722
|
+
)
|
|
597
723
|
else:
|
|
598
724
|
logging.error(f"Unsuccessfully declared loan {folio_loan} as lost")
|
|
599
|
-
self.migration_report.add(
|
|
725
|
+
self.migration_report.add(
|
|
726
|
+
"Details", i18n.t("Unsuccessfully declared loan as lost")
|
|
727
|
+
)
|
|
600
728
|
|
|
601
729
|
def claim_returned(self, folio_loan):
|
|
602
|
-
claim_returned_url =
|
|
730
|
+
claim_returned_url = (
|
|
731
|
+
f"/circulation/loans/{folio_loan['id']}/claim-item-returned"
|
|
732
|
+
)
|
|
603
733
|
logging.debug(f"Claim returned url:{claim_returned_url}")
|
|
604
734
|
due_date = du_parser.isoparse(folio_loan["dueDate"])
|
|
605
735
|
data = {
|
|
606
|
-
"itemClaimedReturnedDateTime": datetime.isoformat(
|
|
736
|
+
"itemClaimedReturnedDateTime": datetime.isoformat(
|
|
737
|
+
due_date + timedelta(days=1)
|
|
738
|
+
),
|
|
607
739
|
"comment": "Created at migration. Date is due date + 1 day",
|
|
608
740
|
}
|
|
609
741
|
logging.debug(f"Claim returned data:\t{json.dumps(data)}")
|
|
610
|
-
if self.folio_put_post(
|
|
742
|
+
if self.folio_put_post(
|
|
743
|
+
claim_returned_url, data, "POST", i18n.t("Claim item returned")
|
|
744
|
+
):
|
|
611
745
|
self.migration_report.add(
|
|
612
746
|
"Details", i18n.t("Successfully declared loan as Claimed returned")
|
|
613
747
|
)
|
|
614
748
|
else:
|
|
615
|
-
logging.error(
|
|
749
|
+
logging.error(
|
|
750
|
+
f"Unsuccessfully declared loan {folio_loan} as Claimed returned"
|
|
751
|
+
)
|
|
616
752
|
self.migration_report.add(
|
|
617
753
|
"Details",
|
|
618
754
|
i18n.t(
|
|
619
|
-
"Unsuccessfully declared loan %{loan} as Claimed returned",
|
|
755
|
+
"Unsuccessfully declared loan %{loan} as Claimed returned",
|
|
756
|
+
loan=folio_loan,
|
|
620
757
|
),
|
|
621
758
|
)
|
|
622
759
|
|
|
623
760
|
def set_item_status(self, legacy_loan: LegacyLoan):
|
|
624
761
|
try:
|
|
625
762
|
# Get Item by barcode, update status.
|
|
626
|
-
item_path =
|
|
763
|
+
item_path = (
|
|
764
|
+
f'item-storage/items?query=(barcode=="{legacy_loan.item_barcode}")'
|
|
765
|
+
)
|
|
627
766
|
item_url = f"{self.folio_client.gateway_url}/{item_path}"
|
|
628
|
-
resp = self.http_client.get(
|
|
767
|
+
resp = self.http_client.get(
|
|
768
|
+
item_url, headers=self.folio_client.okapi_headers
|
|
769
|
+
)
|
|
629
770
|
resp.raise_for_status()
|
|
630
771
|
data = resp.json()
|
|
631
772
|
folio_item = data["items"][0]
|
|
@@ -675,11 +816,11 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
675
816
|
self.migration_report.add("Details", i18n.t("Successfully deactivated user"))
|
|
676
817
|
|
|
677
818
|
def update_item(self, item):
|
|
678
|
-
url = f
|
|
819
|
+
url = f"/item-storage/items/{item['id']}"
|
|
679
820
|
return self.folio_put_post(url, item, "PUT", i18n.t("Update item"))
|
|
680
821
|
|
|
681
822
|
def update_user(self, user):
|
|
682
|
-
url = f
|
|
823
|
+
url = f"/users/{user['id']}"
|
|
683
824
|
self.folio_put_post(url, user, "PUT", i18n.t("Update user"))
|
|
684
825
|
|
|
685
826
|
def get_user_by_barcode(self, barcode):
|
|
@@ -746,7 +887,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
746
887
|
try:
|
|
747
888
|
api_path = f"{folio_loan['id']}/change-due-date"
|
|
748
889
|
api_url = f"{self.folio_client.gateway_url}/circulation/loans/{api_path}"
|
|
749
|
-
body = {
|
|
890
|
+
body = {
|
|
891
|
+
"dueDate": du_parser.isoparse(str(legacy_loan.due_date)).isoformat()
|
|
892
|
+
}
|
|
750
893
|
req = self.http_client.post(
|
|
751
894
|
api_url, headers=self.folio_client.okapi_headers, json=body
|
|
752
895
|
)
|
|
@@ -762,12 +905,14 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
762
905
|
return False
|
|
763
906
|
elif req.status_code == 201:
|
|
764
907
|
self.migration_report.add(
|
|
765
|
-
"Details",
|
|
908
|
+
"Details",
|
|
909
|
+
i18n.t("Successfully changed due date") + f" ({req.status_code})",
|
|
766
910
|
)
|
|
767
911
|
return True, json.loads(req.text), None
|
|
768
912
|
elif req.status_code == 204:
|
|
769
913
|
self.migration_report.add(
|
|
770
|
-
"Details",
|
|
914
|
+
"Details",
|
|
915
|
+
i18n.t("Successfully changed due date") + f" ({req.status_code})",
|
|
771
916
|
)
|
|
772
917
|
return True, None, None
|
|
773
918
|
else:
|
|
@@ -148,6 +148,7 @@ class OrganizationTransformer(MigrationTaskBase):
|
|
|
148
148
|
self.mapper = OrganizationMapper(
|
|
149
149
|
self.folio_client,
|
|
150
150
|
self.library_configuration,
|
|
151
|
+
self.task_configuration,
|
|
151
152
|
self.organization_map,
|
|
152
153
|
self.load_ref_data_mapping_file(
|
|
153
154
|
"organizationTypes",
|
|
@@ -29,11 +29,13 @@ class LegacyLoan(object):
|
|
|
29
29
|
"patron_barcode",
|
|
30
30
|
"due_date",
|
|
31
31
|
"out_date",
|
|
32
|
+
]
|
|
33
|
+
optional_headers = [
|
|
34
|
+
"service_point_id",
|
|
35
|
+
"proxy_patron_barcode",
|
|
32
36
|
"renewal_count",
|
|
33
37
|
"next_item_status",
|
|
34
|
-
"service_point_id",
|
|
35
38
|
]
|
|
36
|
-
optional_headers = ["service_point_id", "proxy_patron_barcode"]
|
|
37
39
|
legal_statuses = [
|
|
38
40
|
"",
|
|
39
41
|
"Aged to lost",
|
|
@@ -72,7 +74,9 @@ class LegacyLoan(object):
|
|
|
72
74
|
)
|
|
73
75
|
except (ParserError, OverflowError) as ee:
|
|
74
76
|
logging.error(ee)
|
|
75
|
-
self.errors.append(
|
|
77
|
+
self.errors.append(
|
|
78
|
+
(f"Parse date failure in {row=}. Setting UTC NOW", "due_date")
|
|
79
|
+
)
|
|
76
80
|
temp_date_due = datetime.now(ZoneInfo("UTC"))
|
|
77
81
|
try:
|
|
78
82
|
temp_date_out: datetime = parse(self.legacy_loan_dict["out_date"])
|
|
@@ -86,18 +90,24 @@ class LegacyLoan(object):
|
|
|
86
90
|
temp_date_out = datetime.now(
|
|
87
91
|
ZoneInfo("UTC")
|
|
88
92
|
) # TODO: Consider moving this assignment block above the temp_date_due
|
|
89
|
-
self.errors.append(
|
|
93
|
+
self.errors.append(
|
|
94
|
+
(f"Parse date failure in {row=}. Setting UTC NOW", "out_date")
|
|
95
|
+
)
|
|
90
96
|
|
|
91
97
|
# good to go, set properties
|
|
92
98
|
self.item_barcode: str = self.legacy_loan_dict["item_barcode"].strip()
|
|
93
99
|
self.patron_barcode: str = self.legacy_loan_dict["patron_barcode"].strip()
|
|
94
|
-
self.proxy_patron_barcode: str = self.legacy_loan_dict.get(
|
|
100
|
+
self.proxy_patron_barcode: str = self.legacy_loan_dict.get(
|
|
101
|
+
"proxy_patron_barcode", ""
|
|
102
|
+
)
|
|
95
103
|
self.due_date: datetime = temp_date_due
|
|
96
104
|
self.out_date: datetime = temp_date_out
|
|
97
105
|
self.correct_for_1_day_loans()
|
|
98
106
|
self.make_utc()
|
|
99
107
|
self.renewal_count = self.set_renewal_count(self.legacy_loan_dict)
|
|
100
|
-
self.next_item_status = self.legacy_loan_dict.get(
|
|
108
|
+
self.next_item_status = self.legacy_loan_dict.get(
|
|
109
|
+
"next_item_status", ""
|
|
110
|
+
).strip()
|
|
101
111
|
if self.next_item_status not in legal_statuses:
|
|
102
112
|
self.errors.append((f"Not an allowed status {row=}", self.next_item_status))
|
|
103
113
|
self.service_point_id = (
|
|
@@ -112,9 +122,11 @@ class LegacyLoan(object):
|
|
|
112
122
|
try:
|
|
113
123
|
return int(renewal_count)
|
|
114
124
|
except ValueError:
|
|
115
|
-
self.report(
|
|
125
|
+
self.report(
|
|
126
|
+
i18n.t("Unresolvable %{renewal_count=} was replaced with 0.")
|
|
127
|
+
)
|
|
116
128
|
else:
|
|
117
|
-
self.report(
|
|
129
|
+
self.report(i18n.t("Missing renewal count was replaced with 0."))
|
|
118
130
|
return 0
|
|
119
131
|
|
|
120
132
|
def correct_for_1_day_loans(self):
|
|
@@ -129,17 +141,19 @@ class LegacyLoan(object):
|
|
|
129
141
|
i18n.t(
|
|
130
142
|
"Due date is before out date, or date information is missing from both"
|
|
131
143
|
),
|
|
132
|
-
json.dumps(self.legacy_loan_dict, indent=2)
|
|
144
|
+
json.dumps(self.legacy_loan_dict, indent=2),
|
|
133
145
|
)
|
|
134
146
|
|
|
135
147
|
def to_dict(self):
|
|
136
148
|
return {
|
|
137
149
|
"item_barcode": self.item_barcode,
|
|
138
150
|
"patron_barcode": self.patron_barcode,
|
|
151
|
+
"proxy_patron_barcode": self.proxy_patron_barcode,
|
|
139
152
|
"due_date": self.due_date.isoformat(),
|
|
140
153
|
"out_date": self.out_date.isoformat(),
|
|
141
154
|
"renewal_count": self.renewal_count,
|
|
142
155
|
"next_item_status": self.next_item_status,
|
|
156
|
+
"service_point_id": self.service_point_id,
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
def make_utc(self):
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"%{field} a, x and z are missing or empty": "%{field} a, x and z are missing or empty",
|
|
7
7
|
"%{field} subfields a, x, and z missing from field": "%{field} subfields a, x, and z missing from field",
|
|
8
8
|
"%{fro} mapped from %{record}": "%{fro} mapped from %{record}",
|
|
9
|
+
"%{lost_type} and checked out": "%{lost_type} and checked out",
|
|
9
10
|
"%{props} were concatenated": "%{props} were concatenated",
|
|
10
11
|
"%{schema_value} added to %{prop_name}": "%{schema_value} added to %{prop_name}",
|
|
11
12
|
"%{tag} subfield %{subfield} not in field": "%{tag} subfield %{subfield} not in field",
|
|
@@ -36,6 +37,8 @@
|
|
|
36
37
|
"Check mapping file against the schema.": "Check mapping file against the schema.",
|
|
37
38
|
"Checked out on first try": "Checked out on first try",
|
|
38
39
|
"Checked out on second try": "Checked out on second try",
|
|
40
|
+
"Claim item returned": "Claim item returned",
|
|
41
|
+
"Claimed returned and checked out": "Claimed returned and checked out",
|
|
39
42
|
"Click to expand all %{count} things": {
|
|
40
43
|
"many": "Click to expand all %{count} things",
|
|
41
44
|
"one": "Click to expand one thing"
|
|
@@ -57,6 +60,7 @@
|
|
|
57
60
|
"DATA ISSUE Users not in FOLIO": "DATA ISSUE Users not in FOLIO",
|
|
58
61
|
"Data issue. Consider fixing the record. ": "Data issue. Consider fixing the record. ",
|
|
59
62
|
"Declare item as lost": "Declare item as lost",
|
|
63
|
+
"Declared lost and checked out": "Declared lost and checked out",
|
|
60
64
|
"Discarded reserves": "Discarded reserves",
|
|
61
65
|
"Due date is before out date, or date information is missing from both": "Due date is before out date, or date information is missing from both",
|
|
62
66
|
"Duplicate 001. Creating HRID instead.\n Previous 001 will be stored in a new 035 field": "Duplicate 001. Creating HRID instead.\n Previous 001 will be stored in a new 035 field",
|
|
@@ -88,6 +92,7 @@
|
|
|
88
92
|
"Instances HRID starting number": "Instances HRID starting number",
|
|
89
93
|
"Instances linked using instances_id_map": "Instances linked using instances_id_map",
|
|
90
94
|
"Interfaces": "Interfaces",
|
|
95
|
+
"Invalid specific retention policy in 008/13-15: %{value}": "Invalid specific retention policy in 008/13-15: %{value}",
|
|
91
96
|
"Inventory records written to disk": "Inventory records written to disk",
|
|
92
97
|
"Item lookups performed": "Item lookups performed",
|
|
93
98
|
"Item transformation report": "Item transformation report",
|
|
@@ -95,6 +100,7 @@
|
|
|
95
100
|
"Legacy Field": "Legacy Field",
|
|
96
101
|
"Legacy bib records without 001": "Legacy bib records without 001",
|
|
97
102
|
"Legacy id is empty": "Legacy id is empty",
|
|
103
|
+
"Loan already exists for %{item_barcode}": "Loan already exists for %{item_barcode}",
|
|
98
104
|
"Loan already in failed.": "Loan already in failed.",
|
|
99
105
|
"Loans discarded. Had migrated item barcode": "Loans discarded. Had migrated item barcode",
|
|
100
106
|
"Loans failed pre-validation": "Loans failed pre-validation",
|
|
@@ -176,6 +182,7 @@
|
|
|
176
182
|
"Reserve discarded. Could not find migrated barcode": "Reserve discarded. Could not find migrated barcode",
|
|
177
183
|
"Reserve verified against migrated item": "Reserve verified against migrated item",
|
|
178
184
|
"Reserves migration report": "Reserves migration report",
|
|
185
|
+
"Retention policy 6 indicates a limited period. Specific retention period will be mapped from 008/13-15": "Retention policy 6 indicates a limited period. Specific retention period will be mapped from 008/13-15",
|
|
179
186
|
"Rows merged to create Purchase Orders": "Rows merged to create Purchase Orders",
|
|
180
187
|
"SRS records written to disk": "SRS records written to disk",
|
|
181
188
|
"Second failure": "Second failure",
|
|
@@ -186,6 +193,7 @@
|
|
|
186
193
|
"Set leader 11 (Subfield code count) from %{record} to 2": "Set leader 11 (Subfield code count) from %{record} to 2",
|
|
187
194
|
"Set leader 20-23 from %{field} to 4500": "Set leader 20-23 from %{field} to 4500",
|
|
188
195
|
"Set up statistical code id mapping...": "Set up statistical code id mapping...",
|
|
196
|
+
"Setting item %{item_barcode} to status \"Available\"": "Setting item %{item_barcode} to status \"Available\"",
|
|
189
197
|
"Source digits": "Source digits",
|
|
190
198
|
"Source of heading or term": "Source of heading or term",
|
|
191
199
|
"Staff suppressed": "Staff suppressed",
|
|
@@ -201,6 +209,8 @@
|
|
|
201
209
|
"Successful matching on %{criteria}": "Successful matching on %{criteria}",
|
|
202
210
|
"Successful user transformations": "Successful user transformations",
|
|
203
211
|
"Successfully %{action}": "Successfully %{action}",
|
|
212
|
+
"Successfully Checked out %{lost_type} item and put the status back": "Successfully Checked out %{lost_type} item and put the status back",
|
|
213
|
+
"Successfully Checked out %{lost_type} item. Item will be declared lost.": "Successfully Checked out %{lost_type} item. Item will be declared lost.",
|
|
204
214
|
"Successfully activated user": "Successfully activated user",
|
|
205
215
|
"Successfully changed due date": "Successfully changed due date",
|
|
206
216
|
"Successfully checked out": "Successfully checked out",
|
|
@@ -280,6 +290,8 @@
|
|
|
280
290
|
"blurbs.Details.title": "Details",
|
|
281
291
|
"blurbs.DiffsBetweenOrders.description": "This is a technical report that helps you to identify differences in the mapped order fields. ",
|
|
282
292
|
"blurbs.DiffsBetweenOrders.title": "Differences between generated orders with same Legacy Identifier",
|
|
293
|
+
"blurbs.DigitizationPolicyMapping.description": "Digitization policies mapped from `008[21]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
|
|
294
|
+
"blurbs.DigitizationPolicyMapping.title": "Digitization policy",
|
|
283
295
|
"blurbs.DiscardedLoans.description": "List of loans discarded for various resons",
|
|
284
296
|
"blurbs.DiscardedLoans.title": "Discarded loans",
|
|
285
297
|
"blurbs.DiscardedRequests.description": "List of requests discarded for various resons",
|
|
@@ -320,6 +332,8 @@
|
|
|
320
332
|
"blurbs.HoldingsTypeMapping.title": "Holdings type mapping",
|
|
321
333
|
"blurbs.HridHandling.description": "There are two ways of handling HRIDs. The default behaviour is to take the current 001 and move that to a new 035. This will also emerge as an Identifier on the Inventory Instances. The 001 and Instance HRID will be generated from the HRID settings in FOLIO. The second option is to maintain the 001s in the records, and also add this as the Instance HRID",
|
|
322
334
|
"blurbs.HridHandling.title": "HRID and 001/035 handling",
|
|
335
|
+
"blurbs.ILLPolicyMapping.description": "ILL policies mapped from `008[20]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
|
|
336
|
+
"blurbs.ILLPolicyMapping.title": "ILL policy",
|
|
323
337
|
"blurbs.IncompleteEntityMapping.description": "**NO ACTION REQUIRED** <br/>This is a coding anomaly that FSE will look into. <br/>Usually, the library does not have to do anything about it.<br/> One thing to look at is if there are many repeated subfields or unexpected patterns of subfields in the table.",
|
|
324
338
|
"blurbs.IncompleteEntityMapping.title": "Incomplete entity mapping adding entity",
|
|
325
339
|
"blurbs.IncompleteSubPropertyRemoved.description": "Add the missing required information to the record in your current ILS to ensure that it can be migrated over.",
|
|
@@ -376,6 +390,8 @@
|
|
|
376
390
|
"blurbs.MatchedModesOfIssuanceCode.title": "Matched Modes of issuance code",
|
|
377
391
|
"blurbs.MaterialTypeMapping.description": "",
|
|
378
392
|
"blurbs.MaterialTypeMapping.title": "Mapped Material Types",
|
|
393
|
+
"blurbs.MethodOfAcquisitionMapping.description": "Acquisition methods mapped from `008[7]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
|
|
394
|
+
"blurbs.MethodOfAcquisitionMapping.title": "Method of acquisition",
|
|
379
395
|
"blurbs.MissingInstanceTypeIds.description": "**IC ACTION REQUIRED** These reords should get an instance type ID mapped from 336, or a default of Undefined, or they will not be transformed.",
|
|
380
396
|
"blurbs.MissingInstanceTypeIds.title": "Records without Instance Type Ids",
|
|
381
397
|
"blurbs.MissingRequiredProperties.description": "",
|
|
@@ -408,6 +424,8 @@
|
|
|
408
424
|
"blurbs.RecourceTypeMapping.title": "Resource Type Mapping (336)",
|
|
409
425
|
"blurbs.ReferenceDataMapping.description": "",
|
|
410
426
|
"blurbs.ReferenceDataMapping.title": "Reference Data Mapping",
|
|
427
|
+
"blurbs.RetentionPolicyMapping.description": "Retention policies mapped from `008[12-15]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
|
|
428
|
+
"blurbs.RetentionPolicyMapping.title": "Retention policy",
|
|
411
429
|
"blurbs.Section1.description": "This entries below seem to be related to instances",
|
|
412
430
|
"blurbs.Section1.title": "__Section 1: instances",
|
|
413
431
|
"blurbs.Section2.description": "The entries below seem to be related to holdings",
|
|
@@ -446,15 +464,6 @@
|
|
|
446
464
|
"blurbs.ValueSetInMappingFile.title": "Value set in mapping file",
|
|
447
465
|
"blurbs.ValuesMappedFromLegacyFields.description": "A list fo the values and what they were mapped to",
|
|
448
466
|
"blurbs.ValuesMappedFromLegacyFields.title": "Values mapped from legacy fields",
|
|
449
|
-
"blurbs.MethodOfAcquisitionMapping.title": "Method of acquisition",
|
|
450
|
-
"blurbs.MethodOfAcquisitionMapping.description": "Acquisition methods mapped from `008[7]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
|
|
451
|
-
"blurbs.RetentionPolicyMapping.title": "Retention policy",
|
|
452
|
-
"blurbs.RetentionPolicyMapping.description": "Retention policies mapped from `008[12-15]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
|
|
453
|
-
"blurbs.ILLPolicyMapping.title": "ILL policy",
|
|
454
|
-
"blurbs.ILLPolicyMapping.description": "ILL policies mapped from `008[20]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
|
|
455
|
-
"blurbs.DigitizationPolicyMapping.title": "Digitization policy",
|
|
456
|
-
"blurbs.DigitizationPolicyMapping.description": "Digitization policies mapped from `008[21]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
|
|
457
|
-
"Invalid specific retention policy in 008/13-15: %{value}": "Invalid specific retention policy in 008/13-15: %{value}",
|
|
458
467
|
"created": "created",
|
|
459
468
|
"instance type code (%{code}) not found in FOLIO": "instance type code (%{code}) not found in FOLIO",
|
|
460
469
|
"item barcode": "item barcode",
|
|
@@ -462,4 +471,4 @@
|
|
|
462
471
|
"naturalId mapped from %{fro}": "naturalId mapped from %{fro}",
|
|
463
472
|
"no matching identifier_types in %{names}": "no matching identifier_types in %{names}",
|
|
464
473
|
"subfield present in %{linked_value_tag} but not in %{pattern_field}": "subfield present in %{linked_value_tag} but not in %{pattern_field}"
|
|
465
|
-
}
|
|
474
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: folio_migration_tools
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.4
|
|
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
|
|
@@ -19,6 +19,7 @@ Requires-Dist: argparse-prompt (>=0.0.5,<0.0.6)
|
|
|
19
19
|
Requires-Dist: art (>=6.5,<7.0)
|
|
20
20
|
Requires-Dist: deepdiff (>=6.2.3,<7.0.0)
|
|
21
21
|
Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
|
|
22
|
+
Requires-Dist: folio-data-import (>=0.3.2,<0.4.0)
|
|
22
23
|
Requires-Dist: folio-uuid (>=0.2.8,<0.3.0)
|
|
23
24
|
Requires-Dist: folioclient (>=0.70.1,<0.71.0)
|
|
24
25
|
Requires-Dist: pyaml (>=21.10.1,<22.0.0)
|
|
@@ -32,22 +32,22 @@ folio_migration_tools/marc_rules_transformation/marc_file_processor.py,sha256=Qh
|
|
|
32
32
|
folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py,sha256=9ATjYMRAjy0QcXtmNZaHVhHLJ5hE1WUgOcF6KMJjbgo,5309
|
|
33
33
|
folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py,sha256=PGt2w8h2pj8_8sGjQe3L-odFDlquURtKnoNFRWQB3GI,9621
|
|
34
34
|
folio_migration_tools/marc_rules_transformation/rules_mapper_base.py,sha256=loNZ9gEYaAwjkP2_wLlXGedjWvSdHoGF_oJN9g6gI3s,45928
|
|
35
|
-
folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=
|
|
36
|
-
folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=
|
|
35
|
+
folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=RD46EzS0NQArn5LCGbrxDm9vbbW9PO_6iNUQwJBAbSg,30364
|
|
36
|
+
folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=ZgyDxmNE7LwW8Cd55wRIEE-u6iyMKCRRXdq2ZRjm2nc,28779
|
|
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
|
-
folio_migration_tools/migration_tasks/batch_poster.py,sha256=
|
|
40
|
+
folio_migration_tools/migration_tasks/batch_poster.py,sha256=7gH9KSdtTSbPIS3eXK6_JBi0OshUAupV8AEew9QBSoU,45327
|
|
41
41
|
folio_migration_tools/migration_tasks/bibs_transformer.py,sha256=46d44pcDAodFXDYbrTCMRASISbDciXmA0CXYfhP2IaE,6298
|
|
42
42
|
folio_migration_tools/migration_tasks/courses_migrator.py,sha256=CzXnsu-KGP7B4zcINJzLYUqz47D16NuFfzu_DPqRlTQ,7061
|
|
43
43
|
folio_migration_tools/migration_tasks/holdings_csv_transformer.py,sha256=kMhtHE8DJjA4d6kXBcfflueha3R3nwlBQjdec8CaY8c,21926
|
|
44
44
|
folio_migration_tools/migration_tasks/holdings_marc_transformer.py,sha256=c_ruhOgidyJdSnnRwWUs3wwFMiLqbVMPOhhCaYuH_TI,14343
|
|
45
|
-
folio_migration_tools/migration_tasks/items_transformer.py,sha256=
|
|
46
|
-
folio_migration_tools/migration_tasks/loans_migrator.py,sha256=
|
|
45
|
+
folio_migration_tools/migration_tasks/items_transformer.py,sha256=oTbFX2saF7-ZCb1mO3baLvODnBSEbbN5F_GtSth3iG4,19755
|
|
46
|
+
folio_migration_tools/migration_tasks/loans_migrator.py,sha256=9DQf58CZpgTM1m_miZTzsfVOGBWLsHrSFalQBO7g9DE,39406
|
|
47
47
|
folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py,sha256=CnmlTge7nChUJ10EiUkriQtJlVxWqglgfhjgneh2_yM,7247
|
|
48
48
|
folio_migration_tools/migration_tasks/migration_task_base.py,sha256=Q-57h6rmt74bC9LidA9ZoagEcwVd_ytq8IUWelVOm2E,22521
|
|
49
49
|
folio_migration_tools/migration_tasks/orders_transformer.py,sha256=6SnzU_rUTu2B5hQykI2nRA7vI1rg-uxuF9Ncupe0AEY,14302
|
|
50
|
-
folio_migration_tools/migration_tasks/organization_transformer.py,sha256=
|
|
50
|
+
folio_migration_tools/migration_tasks/organization_transformer.py,sha256=Kuxkh1sKyUVBqm5qAK1Jrq-4xcyNz2JPZvvFRqfwI8s,16922
|
|
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=4sSPer6_6yMwiiY1VYJmYZske_Ah1XG4KAM3NDadPhg,9952
|
|
53
53
|
folio_migration_tools/migration_tasks/user_transformer.py,sha256=aylrMC9n47fdStgsNfW4ZbJh2E4FDSPypsaNv52ynKc,12330
|
|
@@ -55,13 +55,13 @@ folio_migration_tools/task_configuration.py,sha256=73OWc8TX--fwPRptv3eQVEVv0-XmN
|
|
|
55
55
|
folio_migration_tools/test_infrastructure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
56
|
folio_migration_tools/test_infrastructure/mocked_classes.py,sha256=BurU3NGU_Q8as_BGmW98q9O6bujZDkOfFmvKKdVw9t8,15056
|
|
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=2GHqHDVShdZuoUlW2yvePcsesJZ5-Huq3leD71tNtaE,6618
|
|
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=qzw0okg4axAE_ezXopP9gFsQ_e60o0zh7zqRzFBSWHY,1806
|
|
61
61
|
folio_migration_tools/transaction_migration/transaction_result.py,sha256=cTdCN0BnlI9_ZJB2Z3Fdkl9gpymIi-9mGZsRFlQcmDk,656
|
|
62
|
-
folio_migration_tools/translations/en.json,sha256=
|
|
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.
|
|
62
|
+
folio_migration_tools/translations/en.json,sha256=6IpYYNFCtQoXACndPM0d1Oa25GYuaF-G-b4YpzTjQH0,41656
|
|
63
|
+
folio_migration_tools-1.9.4.dist-info/LICENSE,sha256=PhIEkitVi3ejgq56tt6sWoJIG_zmv82cjjd_aYPPGdI,1072
|
|
64
|
+
folio_migration_tools-1.9.4.dist-info/METADATA,sha256=L36bpdFWAhsV2r8FBIjpZ-1N4Vl0u4EMV0f7g5ObNgg,7494
|
|
65
|
+
folio_migration_tools-1.9.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
66
|
+
folio_migration_tools-1.9.4.dist-info/entry_points.txt,sha256=Hbe-HjqMcU8FwVshVIkeWyZd9XwgT1CCMNf06EpHQu8,77
|
|
67
|
+
folio_migration_tools-1.9.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{folio_migration_tools-1.9.3.dist-info → folio_migration_tools-1.9.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|