folio_data_import 0.6.0__tar.gz → 0.6.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/PKG-INFO +2 -2
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/pyproject.toml +2 -2
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/UserImport.py +51 -23
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/marc_preprocessors/_preprocessors.py +102 -2
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/README.md +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/BatchPoster.py +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/DILogRetriever.py +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/MARCDataImport.py +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/__init__.py +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/__main__.py +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/_postgres.py +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/_progress.py +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/custom_exceptions.py +0 -0
- {folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/marc_preprocessors/__init__.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: folio_data_import
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A python module to perform bulk import of data into a FOLIO environment. Currently supports MARC and user data import.
|
|
5
5
|
Author: Brooks Travis
|
|
6
6
|
Author-email: Brooks Travis <brooks.travis@gmail.com>
|
|
7
7
|
License-Expression: MIT
|
|
8
|
-
Requires-Dist: folioclient>=1.0.
|
|
8
|
+
Requires-Dist: folioclient>=1.0.8,<2.0
|
|
9
9
|
Requires-Dist: pymarc>=5.2.2
|
|
10
10
|
Requires-Dist: pyhumps>=3.8.0
|
|
11
11
|
Requires-Dist: tabulate>=0.9.0
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "folio_data_import"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.2"
|
|
4
4
|
description = "A python module to perform bulk import of data into a FOLIO environment. Currently supports MARC and user data import."
|
|
5
5
|
authors = [{ name = "Brooks Travis", email = "brooks.travis@gmail.com" }]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.10,<4.0"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"folioclient>=1.0.
|
|
10
|
+
"folioclient>=1.0.8,<2.0",
|
|
11
11
|
"pymarc>=5.2.2",
|
|
12
12
|
"pyhumps>=3.8.0",
|
|
13
13
|
"tabulate>=0.9.0",
|
|
@@ -152,6 +152,15 @@ class UserImporter: # noqa: R0902
|
|
|
152
152
|
description="Whether to delete existing users, rather than create/update.",
|
|
153
153
|
),
|
|
154
154
|
] = False
|
|
155
|
+
delete_pus: Annotated[
|
|
156
|
+
bool,
|
|
157
|
+
Field(
|
|
158
|
+
title="Delete permission user objects when deleting users",
|
|
159
|
+
description=(
|
|
160
|
+
"Whether to delete associated permission user objects when deleting users."
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
] = False
|
|
155
164
|
|
|
156
165
|
logfile: AsyncTextIOWrapper
|
|
157
166
|
errorfile: AsyncTextIOWrapper
|
|
@@ -174,6 +183,7 @@ class UserImporter: # noqa: R0902
|
|
|
174
183
|
)
|
|
175
184
|
else:
|
|
176
185
|
self.reporter = reporter
|
|
186
|
+
self.delete_pus = config.delete_pus
|
|
177
187
|
self.limit_simultaneous_requests = asyncio.Semaphore(config.limit_simultaneous_requests)
|
|
178
188
|
# Build reference data maps (these need processing)
|
|
179
189
|
self.patron_group_map: dict = self.build_ref_data_id_map(
|
|
@@ -800,36 +810,54 @@ class UserImporter: # noqa: R0902
|
|
|
800
810
|
"skipping deletion\n"
|
|
801
811
|
)
|
|
802
812
|
return
|
|
803
|
-
|
|
804
|
-
|
|
813
|
+
if existing_user:
|
|
814
|
+
try:
|
|
805
815
|
await self.folio_client.folio_delete_async(
|
|
806
816
|
f"/users/{existing_user['id']}",
|
|
807
817
|
)
|
|
808
|
-
if existing_rp:
|
|
809
|
-
await self.folio_client.folio_delete_async(
|
|
810
|
-
f"/request-preference-storage/request-preference/{existing_rp.get('id', '')}"
|
|
811
|
-
)
|
|
812
|
-
if existing_pu:
|
|
813
|
-
await self.folio_client.folio_delete_async(
|
|
814
|
-
f"/perms/users/{existing_pu.get('id', '')}"
|
|
815
|
-
)
|
|
816
|
-
if existing_spu:
|
|
817
|
-
await self.folio_client.folio_delete_async(
|
|
818
|
-
f"/service-points-users/{existing_spu.get('id', '')}"
|
|
819
|
-
)
|
|
820
|
-
|
|
821
|
-
if existing_user:
|
|
822
818
|
logger.debug(f"Row {line_number}: Deleted user {existing_user['id']}\n")
|
|
823
819
|
|
|
824
|
-
async with self.lock:
|
|
825
820
|
self.stats.deleted += 1
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
async with self.lock:
|
|
821
|
+
except folioclient.FolioError as ee:
|
|
822
|
+
logger.exception(
|
|
823
|
+
f"Row {line_number}: Failed to delete user {existing_user['id']}: "
|
|
824
|
+
f"{str(getattr(getattr(ee, 'response', str(ee)), 'text', str(ee)))}\n"
|
|
825
|
+
)
|
|
832
826
|
self.stats.failed += 1
|
|
827
|
+
try:
|
|
828
|
+
if existing_rp:
|
|
829
|
+
await self.folio_client.folio_delete_async(
|
|
830
|
+
"/request-preference-storage/request-preference/"
|
|
831
|
+
f"{existing_rp.get('id', '')}"
|
|
832
|
+
)
|
|
833
|
+
except folioclient.FolioError as ee:
|
|
834
|
+
logger.exception(
|
|
835
|
+
f"Row {line_number}: Failed to delete request preference for user "
|
|
836
|
+
f"{existing_user['id']}: "
|
|
837
|
+
f"{str(getattr(getattr(ee, 'response', str(ee)), 'text', str(ee)))}\n"
|
|
838
|
+
)
|
|
839
|
+
try:
|
|
840
|
+
if existing_pu and self.delete_pus:
|
|
841
|
+
await self.folio_client.folio_delete_async(
|
|
842
|
+
f"/perms/users/{existing_pu.get('id', '')}"
|
|
843
|
+
)
|
|
844
|
+
except folioclient.FolioError as ee:
|
|
845
|
+
logger.exception(
|
|
846
|
+
f"Row {line_number}: Failed to delete permission user for user "
|
|
847
|
+
f"{existing_user['id']}: "
|
|
848
|
+
f"{str(getattr(getattr(ee, 'response', str(ee)), 'text', str(ee)))}\n"
|
|
849
|
+
)
|
|
850
|
+
try:
|
|
851
|
+
if existing_spu:
|
|
852
|
+
await self.folio_client.folio_delete_async(
|
|
853
|
+
f"/service-points-users/{existing_spu.get('id', '')}"
|
|
854
|
+
)
|
|
855
|
+
except folioclient.FolioError as ee:
|
|
856
|
+
logger.exception(
|
|
857
|
+
f"Row {line_number}: Failed to delete service points user for user "
|
|
858
|
+
f"{existing_user['id']}: "
|
|
859
|
+
f"{str(getattr(getattr(ee, 'response', str(ee)), 'text', str(ee)))}\n"
|
|
860
|
+
)
|
|
833
861
|
|
|
834
862
|
async def process_line(
|
|
835
863
|
self,
|
|
@@ -2,6 +2,7 @@ import importlib
|
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
4
|
import sys
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
from typing import Callable, Dict, List, Tuple, Union
|
|
6
7
|
|
|
7
8
|
import pymarc
|
|
@@ -106,6 +107,29 @@ class MARCPreprocessor:
|
|
|
106
107
|
return record
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
def remove_sudoc_prefixes(record: Record) -> Record:
|
|
111
|
+
"""Remove common ABES/SUDOC prefixes from the 001 of a record.
|
|
112
|
+
Useful when importing records from the ABES SUDOC catalog that
|
|
113
|
+
you want to match to existing records in FOLIO without the prefix.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
record (Record): The MARC record to preprocess.
|
|
117
|
+
"""
|
|
118
|
+
sudoc_prefixes = ["PPN", "ABES"]
|
|
119
|
+
if "001" in record:
|
|
120
|
+
if any(record["001"].data.startswith(prefix) for prefix in sudoc_prefixes):
|
|
121
|
+
logger.debug("Field '001' starts with a known SUDOC prefix. Removing prefix from 001.")
|
|
122
|
+
for prefix in sudoc_prefixes:
|
|
123
|
+
record["001"].data = record["001"].data.replace(prefix, "").strip()
|
|
124
|
+
else:
|
|
125
|
+
logger.debug(
|
|
126
|
+
"Field '001' does not start with a known SUDOC prefix. No changes made to 001."
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
logger.warning("Field '001' not found in record. Skipping prefix removal.")
|
|
130
|
+
return record
|
|
131
|
+
|
|
132
|
+
|
|
109
133
|
def prepend_prefix_001(record: Record, prefix: str) -> Record:
|
|
110
134
|
"""
|
|
111
135
|
Prepend a prefix to the record's 001 field.
|
|
@@ -117,9 +141,11 @@ def prepend_prefix_001(record: Record, prefix: str) -> Record:
|
|
|
117
141
|
Returns:
|
|
118
142
|
Record: The preprocessed MARC record.
|
|
119
143
|
"""
|
|
120
|
-
if "001" in record:
|
|
144
|
+
if "001" in record and record["001"].data:
|
|
121
145
|
record["001"].data = (
|
|
122
|
-
f"({prefix})" + record["001"].data
|
|
146
|
+
f"({prefix})" + record["001"].data
|
|
147
|
+
if not record["001"].data.startswith(f"({prefix})")
|
|
148
|
+
else record["001"].data
|
|
123
149
|
)
|
|
124
150
|
else:
|
|
125
151
|
logger.warning("Field '001' not found in record. Skipping prefix prepend.")
|
|
@@ -426,6 +452,35 @@ def clean_empty_fields(record: Record, **kwargs) -> Record:
|
|
|
426
452
|
return record
|
|
427
453
|
|
|
428
454
|
|
|
455
|
+
def clean_empty_contributors(record: Record, **kwargs) -> Record:
|
|
456
|
+
"""
|
|
457
|
+
Remove contributor fields (100, 110, 111, 700, 710, 711) that have empty subfield a. These can
|
|
458
|
+
cause data import mapping issues in FOLIO. Removals are logged at custom log level 26, which is
|
|
459
|
+
used by folio_migration_tools to populate the data issues report.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
record (Record): The MARC record to preprocess.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Record: The preprocessed MARC record.
|
|
466
|
+
"""
|
|
467
|
+
for field in record.get_fields("100", "110", "111", "700", "710", "711"):
|
|
468
|
+
if all(
|
|
469
|
+
not subfield.value.strip()
|
|
470
|
+
for subfield in field.subfields
|
|
471
|
+
if subfield.code in ["a", "b", "c", "d", "q"]
|
|
472
|
+
):
|
|
473
|
+
logger.log(
|
|
474
|
+
26,
|
|
475
|
+
"DATA ISSUE\t%s\t%s\t%s",
|
|
476
|
+
record["001"].value(),
|
|
477
|
+
f"{field.tag} contributor field has empty name subfields, removing field",
|
|
478
|
+
field,
|
|
479
|
+
)
|
|
480
|
+
record.remove_field(field)
|
|
481
|
+
return record
|
|
482
|
+
|
|
483
|
+
|
|
429
484
|
def fix_bib_leader(record: Record, **kwargs) -> Record:
|
|
430
485
|
"""
|
|
431
486
|
Fixes the leader of the record by setting the record status to 'c' (modified
|
|
@@ -546,6 +601,51 @@ def remove_non_numeric_fields(record: Record, **kwargs) -> Record:
|
|
|
546
601
|
return record
|
|
547
602
|
|
|
548
603
|
|
|
604
|
+
def populate_blank_008_0_5(record: Record, **kwargs) -> Record:
|
|
605
|
+
"""
|
|
606
|
+
Populate the first 6 characters of the 008 with today's date in the format YYMMDD if they are
|
|
607
|
+
blank. Workaround for FOLIO issue: https://folio-org.atlassian.net/browse/MODQM-515
|
|
608
|
+
"""
|
|
609
|
+
if "008" in record and record["008"].data:
|
|
610
|
+
if len(record["008"].data) >= 6 and record["008"].data[:6] == " ":
|
|
611
|
+
today = datetime.today().strftime("%y%m%d")
|
|
612
|
+
record["008"].data = today + record["008"].data[6:]
|
|
613
|
+
logger.log(
|
|
614
|
+
26,
|
|
615
|
+
"DATA ISSUE\t%s\t%s\t%s",
|
|
616
|
+
record["001"].value(),
|
|
617
|
+
"First 6 characters of 008 were blank, populated with today's date",
|
|
618
|
+
record["008"],
|
|
619
|
+
)
|
|
620
|
+
else:
|
|
621
|
+
logger.warning("Field '008' not found in record. Skipping 008 population.")
|
|
622
|
+
return record
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def move_856z_to_856y(record: Record, **kwargs) -> Record:
|
|
626
|
+
"""
|
|
627
|
+
Move subfield z from 856 fields to subfield y. This is useful when
|
|
628
|
+
importing records from the ABES SUDOC catalog.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
record (Record): The MARC record to preprocess.
|
|
632
|
+
Returns:
|
|
633
|
+
Record: The preprocessed MARC record.
|
|
634
|
+
"""
|
|
635
|
+
for field in record.get_fields("856"):
|
|
636
|
+
if "z" in field:
|
|
637
|
+
field.add_subfield("y", field["z"])
|
|
638
|
+
field.delete_subfield("z")
|
|
639
|
+
logger.log(
|
|
640
|
+
26,
|
|
641
|
+
"DATA ISSUE\t%s\t%s\t%s",
|
|
642
|
+
record["001"].value(),
|
|
643
|
+
f"Subfield z moved to subfield y in {field.tag}",
|
|
644
|
+
field,
|
|
645
|
+
)
|
|
646
|
+
return record
|
|
647
|
+
|
|
648
|
+
|
|
549
649
|
def ordinal(n: int) -> str:
|
|
550
650
|
s = ("th", "st", "nd", "rd") + ("th",) * 10
|
|
551
651
|
v = n % 100
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{folio_data_import-0.6.0 → folio_data_import-0.6.2}/src/folio_data_import/custom_exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|