folio-migration-tools 1.2.1__py3-none-any.whl → 1.9.10__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/__init__.py +11 -0
- folio_migration_tools/__main__.py +169 -85
- folio_migration_tools/circulation_helper.py +96 -59
- folio_migration_tools/config_file_load.py +66 -0
- folio_migration_tools/custom_dict.py +6 -4
- folio_migration_tools/custom_exceptions.py +21 -19
- folio_migration_tools/extradata_writer.py +46 -0
- folio_migration_tools/folder_structure.py +63 -66
- folio_migration_tools/helper.py +29 -21
- folio_migration_tools/holdings_helper.py +57 -34
- folio_migration_tools/i18n_config.py +9 -0
- folio_migration_tools/library_configuration.py +173 -13
- folio_migration_tools/mapper_base.py +317 -106
- folio_migration_tools/mapping_file_transformation/courses_mapper.py +203 -0
- folio_migration_tools/mapping_file_transformation/holdings_mapper.py +83 -69
- folio_migration_tools/mapping_file_transformation/item_mapper.py +98 -94
- folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +352 -0
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +702 -223
- folio_migration_tools/mapping_file_transformation/notes_mapper.py +90 -0
- folio_migration_tools/mapping_file_transformation/order_mapper.py +492 -0
- folio_migration_tools/mapping_file_transformation/organization_mapper.py +389 -0
- folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +38 -27
- folio_migration_tools/mapping_file_transformation/user_mapper.py +149 -361
- folio_migration_tools/marc_rules_transformation/conditions.py +650 -246
- folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +292 -130
- folio_migration_tools/marc_rules_transformation/hrid_handler.py +244 -0
- folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +20846 -0
- folio_migration_tools/marc_rules_transformation/marc_file_processor.py +300 -0
- folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +136 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +241 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +681 -201
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +395 -429
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +531 -100
- folio_migration_tools/migration_report.py +85 -38
- folio_migration_tools/migration_tasks/__init__.py +1 -3
- folio_migration_tools/migration_tasks/authority_transformer.py +119 -0
- folio_migration_tools/migration_tasks/batch_poster.py +911 -198
- folio_migration_tools/migration_tasks/bibs_transformer.py +121 -116
- folio_migration_tools/migration_tasks/courses_migrator.py +192 -0
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py +252 -247
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py +321 -115
- folio_migration_tools/migration_tasks/items_transformer.py +264 -84
- folio_migration_tools/migration_tasks/loans_migrator.py +506 -195
- folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +187 -0
- folio_migration_tools/migration_tasks/migration_task_base.py +364 -74
- folio_migration_tools/migration_tasks/orders_transformer.py +373 -0
- folio_migration_tools/migration_tasks/organization_transformer.py +451 -0
- folio_migration_tools/migration_tasks/requests_migrator.py +130 -62
- folio_migration_tools/migration_tasks/reserves_migrator.py +253 -0
- folio_migration_tools/migration_tasks/user_transformer.py +180 -139
- folio_migration_tools/task_configuration.py +46 -0
- folio_migration_tools/test_infrastructure/__init__.py +0 -0
- folio_migration_tools/test_infrastructure/mocked_classes.py +406 -0
- folio_migration_tools/transaction_migration/legacy_loan.py +148 -34
- folio_migration_tools/transaction_migration/legacy_request.py +65 -25
- folio_migration_tools/transaction_migration/legacy_reserve.py +47 -0
- folio_migration_tools/transaction_migration/transaction_result.py +12 -1
- folio_migration_tools/translations/en.json +476 -0
- folio_migration_tools-1.9.10.dist-info/METADATA +169 -0
- folio_migration_tools-1.9.10.dist-info/RECORD +67 -0
- {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info}/WHEEL +1 -2
- folio_migration_tools-1.9.10.dist-info/entry_points.txt +3 -0
- folio_migration_tools/generate_schemas.py +0 -46
- folio_migration_tools/mapping_file_transformation/mapping_file_mapping_base_impl.py +0 -44
- folio_migration_tools/mapping_file_transformation/user_mapper_base.py +0 -212
- folio_migration_tools/marc_rules_transformation/bibs_processor.py +0 -163
- folio_migration_tools/marc_rules_transformation/holdings_processor.py +0 -284
- folio_migration_tools/report_blurbs.py +0 -219
- folio_migration_tools/transaction_migration/legacy_fee_fine.py +0 -36
- folio_migration_tools-1.2.1.dist-info/METADATA +0 -134
- folio_migration_tools-1.2.1.dist-info/RECORD +0 -50
- folio_migration_tools-1.2.1.dist-info/top_level.txt +0 -1
- {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,119 +1,203 @@
|
|
|
1
|
+
from importlib import metadata
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
import sys
|
|
5
|
+
from os import environ
|
|
6
|
+
from pathlib import Path
|
|
4
7
|
|
|
8
|
+
import httpx
|
|
5
9
|
import humps
|
|
6
|
-
import
|
|
10
|
+
import i18n
|
|
7
11
|
from argparse_prompt import PromptParser
|
|
12
|
+
from folioclient import FolioClient
|
|
8
13
|
from pydantic import ValidationError
|
|
9
14
|
|
|
15
|
+
from folio_migration_tools.config_file_load import merge_load
|
|
16
|
+
from folio_migration_tools.custom_exceptions import TransformationProcessError
|
|
10
17
|
from folio_migration_tools.library_configuration import LibraryConfiguration
|
|
11
|
-
from folio_migration_tools.migration_tasks import * #
|
|
18
|
+
from folio_migration_tools.migration_tasks import * # noqa: F403, F401
|
|
12
19
|
from folio_migration_tools.migration_tasks import migration_task_base
|
|
13
20
|
|
|
14
21
|
|
|
15
|
-
def parse_args():
|
|
16
|
-
"""Parse CLI Arguments"""
|
|
22
|
+
def parse_args(args):
|
|
17
23
|
task_classes = iter(inheritors(migration_task_base.MigrationTaskBase))
|
|
18
24
|
parser = PromptParser()
|
|
19
|
-
parser.add_argument(
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"configuration_path",
|
|
27
|
+
help="Path to configuration file",
|
|
28
|
+
nargs="?" if "FOLIO_MIGRATION_TOOLS_CONFIGURATION_PATH" in environ else None,
|
|
29
|
+
prompt="FOLIO_MIGRATION_TOOLS_CONFIGURATION_PATH" not in environ,
|
|
30
|
+
default=environ.get("FOLIO_MIGRATION_TOOLS_CONFIGURATION_PATH"),
|
|
31
|
+
)
|
|
32
|
+
tasks_string = ", ".join(sorted(tc.__name__ for tc in task_classes))
|
|
33
|
+
|
|
20
34
|
parser.add_argument(
|
|
21
35
|
"task_name",
|
|
22
|
-
help=(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
),
|
|
36
|
+
help=("Task name. Use one of: " f"{tasks_string}"),
|
|
37
|
+
nargs="?" if "FOLIO_MIGRATION_TOOLS_TASK_NAME" in environ else None,
|
|
38
|
+
prompt="FOLIO_MIGRATION_TOOLS_TASK_NAME" not in environ,
|
|
39
|
+
default=environ.get("FOLIO_MIGRATION_TOOLS_TASK_NAME"),
|
|
26
40
|
)
|
|
27
41
|
parser.add_argument(
|
|
28
|
-
"--okapi_password",
|
|
29
|
-
help="
|
|
42
|
+
"--folio_password", "--okapi_password",
|
|
43
|
+
help="password for the tenant in the configuration file",
|
|
44
|
+
prompt="FOLIO_MIGRATION_TOOLS_OKAPI_PASSWORD" not in environ,
|
|
45
|
+
default=environ.get("FOLIO_MIGRATION_TOOLS_OKAPI_PASSWORD"),
|
|
30
46
|
secure=True,
|
|
31
47
|
)
|
|
32
48
|
parser.add_argument(
|
|
33
49
|
"--base_folder_path",
|
|
34
50
|
help=(
|
|
35
|
-
"path to the base folder for this library. "
|
|
36
|
-
|
|
51
|
+
"path to the base folder for this library. Built on migration_repo_template"
|
|
52
|
+
),
|
|
53
|
+
prompt="FOLIO_MIGRATION_TOOLS_BASE_FOLDER_PATH" not in environ,
|
|
54
|
+
default=environ.get("FOLIO_MIGRATION_TOOLS_BASE_FOLDER_PATH"),
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--report_language",
|
|
58
|
+
help=(
|
|
59
|
+
"Language to write the reports. Defaults english for untranslated languages/strings."
|
|
37
60
|
),
|
|
61
|
+
default=environ.get("FOLIO_MIGRATION_TOOLS_REPORT_LANGUAGE", "en"),
|
|
62
|
+
prompt=False,
|
|
38
63
|
)
|
|
39
|
-
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--version", "-V",
|
|
66
|
+
help="Show the version of the FOLIO Migration Tools",
|
|
67
|
+
action="store_true",
|
|
68
|
+
prompt=False,
|
|
69
|
+
)
|
|
70
|
+
return parser.parse_args(args)
|
|
71
|
+
|
|
72
|
+
def prep_library_config(args):
|
|
73
|
+
config_file_humped = merge_load(args.configuration_path)
|
|
74
|
+
config_file_humped["libraryInformation"]["okapiPassword"] = args.folio_password
|
|
75
|
+
config_file_humped["libraryInformation"]["baseFolder"] = args.base_folder_path
|
|
76
|
+
config_file = humps.decamelize(config_file_humped)
|
|
77
|
+
library_config = LibraryConfiguration(**config_file["library_information"])
|
|
78
|
+
if library_config.ecs_tenant_id:
|
|
79
|
+
library_config.is_ecs = True
|
|
80
|
+
if library_config.ecs_tenant_id and not library_config.ecs_central_iteration_identifier:
|
|
81
|
+
print(
|
|
82
|
+
"ECS tenant ID is set, but no central iteration identifier is provided. "
|
|
83
|
+
"Please provide the central iteration identifier in the configuration file."
|
|
84
|
+
)
|
|
85
|
+
sys.exit("ECS Central Iteration Identifier Not Found")
|
|
86
|
+
return config_file, library_config
|
|
87
|
+
|
|
88
|
+
def print_version(args):
|
|
89
|
+
if "-V" in args or "--version" in args:
|
|
90
|
+
print(
|
|
91
|
+
f"FOLIO Migration Tools: {metadata.version('folio_migration_tools')}"
|
|
92
|
+
)
|
|
93
|
+
sys.exit(0)
|
|
94
|
+
return None
|
|
40
95
|
|
|
41
96
|
|
|
42
97
|
def main():
|
|
43
98
|
try:
|
|
44
|
-
task_classes =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
config_file_humped = json.load(config_file_path)
|
|
49
|
-
config_file_humped["libraryInformation"][
|
|
50
|
-
"okapiPassword"
|
|
51
|
-
] = args.okapi_password
|
|
52
|
-
config_file_humped["libraryInformation"][
|
|
53
|
-
"baseFolder"
|
|
54
|
-
] = args.base_folder_path
|
|
55
|
-
config_file = humps.decamelize(config_file_humped)
|
|
56
|
-
library_config = LibraryConfiguration(
|
|
57
|
-
**config_file["library_information"]
|
|
58
|
-
)
|
|
59
|
-
try:
|
|
60
|
-
migration_task_config = next(
|
|
61
|
-
t
|
|
62
|
-
for t in config_file["migration_tasks"]
|
|
63
|
-
if t["name"] == args.task_name
|
|
64
|
-
)
|
|
65
|
-
except StopIteration:
|
|
66
|
-
task_names = [
|
|
67
|
-
t.get("name", "") for t in config_file["migration_tasks"]
|
|
68
|
-
]
|
|
69
|
-
print(
|
|
70
|
-
f"Referenced task name {args.task_name} not found in the "
|
|
71
|
-
f'configuration file. Use one of {", ".join(task_names)}'
|
|
72
|
-
"\nHalting..."
|
|
73
|
-
)
|
|
74
|
-
sys.exit(2)
|
|
75
|
-
try:
|
|
76
|
-
task_class = next(
|
|
77
|
-
tc
|
|
78
|
-
for tc in task_classes
|
|
79
|
-
if tc.__name__ == migration_task_config["migration_task_type"]
|
|
80
|
-
)
|
|
81
|
-
task_config = task_class.TaskConfiguration(**migration_task_config)
|
|
82
|
-
task_obj = task_class(task_config, library_config)
|
|
83
|
-
task_obj.do_work()
|
|
84
|
-
task_obj.wrap_up()
|
|
85
|
-
except StopIteration:
|
|
86
|
-
print(
|
|
87
|
-
f'Referenced task {migration_task_config["migration_task_type"]} '
|
|
88
|
-
"is not a valid option. Update your task to incorporate "
|
|
89
|
-
f"one of {json.dumps([tc.__name__ for tc in task_classes], indent=4)}"
|
|
90
|
-
)
|
|
91
|
-
sys.exit(2)
|
|
92
|
-
except json.decoder.JSONDecodeError as json_error:
|
|
93
|
-
logging.critical(json_error)
|
|
94
|
-
print(
|
|
95
|
-
f"\nError parsing configuration file {config_file_path.name}. Halting. "
|
|
96
|
-
)
|
|
97
|
-
sys.exit(2)
|
|
98
|
-
except ValidationError as e:
|
|
99
|
-
print(e.json())
|
|
100
|
-
print("Validation errors in configuration file:")
|
|
101
|
-
print("==========================================")
|
|
99
|
+
task_classes = list(inheritors(migration_task_base.MigrationTaskBase))
|
|
100
|
+
# Check if the script is run with the --version or -V flag
|
|
101
|
+
print_version(sys.argv)
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
# Parse command line arguments
|
|
104
|
+
args = parse_args(sys.argv[1:])
|
|
105
|
+
try:
|
|
106
|
+
i18n.load_config(
|
|
107
|
+
Path(
|
|
108
|
+
environ.get("FOLIO_MIGRATION_TOOLS_I18N_CONFIG_BASE_PATH")
|
|
109
|
+
or args.base_folder_path
|
|
110
|
+
)
|
|
111
|
+
/ "i18n_config.py"
|
|
112
|
+
)
|
|
113
|
+
except i18n.I18nFileLoadError:
|
|
114
|
+
i18n.load_config(Path(__file__).parent / "i18n_config.py")
|
|
115
|
+
i18n.set("locale", args.report_language)
|
|
116
|
+
config_file, library_config = prep_library_config(args)
|
|
117
|
+
try:
|
|
118
|
+
migration_task_config = next(
|
|
119
|
+
t for t in config_file["migration_tasks"] if t["name"] == args.task_name
|
|
120
|
+
)
|
|
121
|
+
except StopIteration:
|
|
122
|
+
task_names = [t.get("name", "") for t in config_file["migration_tasks"]]
|
|
123
|
+
print(
|
|
124
|
+
f"Referenced task name {args.task_name} not found in the "
|
|
125
|
+
f'configuration file. Use one of {", ".join(task_names)}'
|
|
126
|
+
"\nHalting..."
|
|
127
|
+
)
|
|
128
|
+
sys.exit("Task Name Not Found")
|
|
129
|
+
try:
|
|
130
|
+
task_class = next(
|
|
131
|
+
tc
|
|
132
|
+
for tc in task_classes
|
|
133
|
+
if tc.__name__ == migration_task_config["migration_task_type"]
|
|
134
|
+
)
|
|
135
|
+
except StopIteration:
|
|
136
|
+
print(
|
|
137
|
+
f'Referenced task {migration_task_config["migration_task_type"]} '
|
|
138
|
+
"is not a valid option. Update your task to incorporate "
|
|
139
|
+
f"one of {json.dumps([tc.__name__ for tc in task_classes], indent=4)}"
|
|
140
|
+
)
|
|
141
|
+
sys.exit("Task Type Not Found")
|
|
142
|
+
try:
|
|
143
|
+
logging.getLogger("httpx").setLevel(logging.WARNING) # Exclude info messages from httpx
|
|
144
|
+
with FolioClient(
|
|
145
|
+
library_config.gateway_url,
|
|
146
|
+
library_config.tenant_id,
|
|
147
|
+
library_config.folio_username,
|
|
148
|
+
library_config.folio_password,
|
|
149
|
+
) as folio_client:
|
|
150
|
+
task_config = task_class.TaskConfiguration(**migration_task_config)
|
|
151
|
+
task_obj = task_class(task_config, library_config, folio_client)
|
|
152
|
+
task_obj.do_work()
|
|
153
|
+
task_obj.wrap_up()
|
|
154
|
+
except TransformationProcessError as tpe:
|
|
155
|
+
logging.critical(tpe.message)
|
|
156
|
+
print(f"\n{tpe.message}: {tpe.data_value}")
|
|
157
|
+
print("Task failure. Halting.")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
logging.info("Work done. Shutting down")
|
|
160
|
+
except json.decoder.JSONDecodeError as json_error:
|
|
161
|
+
logging.critical(json_error)
|
|
162
|
+
print(json_error.doc)
|
|
163
|
+
print(
|
|
164
|
+
f"\n{json_error}"
|
|
165
|
+
f"\nError parsing the above JSON mapping or configruation file. Halting."
|
|
166
|
+
)
|
|
167
|
+
sys.exit("Invalid JSON")
|
|
168
|
+
except ValidationError as e:
|
|
169
|
+
print(e.json())
|
|
170
|
+
print("Validation errors in configuration file:")
|
|
171
|
+
print("==========================================")
|
|
116
172
|
|
|
173
|
+
for validation_message in json.loads(e.json()):
|
|
174
|
+
print(
|
|
175
|
+
f"{validation_message['msg']}\t"
|
|
176
|
+
f"{', '.join(humps.camelize(str(x)) for x in validation_message['loc'])}"
|
|
177
|
+
)
|
|
178
|
+
print("Halting")
|
|
179
|
+
sys.exit("JSON Not Matching Spec")
|
|
180
|
+
except httpx.HTTPError as connection_error:
|
|
181
|
+
if hasattr(connection_error, "response"):
|
|
182
|
+
print(
|
|
183
|
+
f"\nHTTP Error when connecting to {connection_error.request.url}. "
|
|
184
|
+
f"Status code: {connection_error.response.status_code}. "
|
|
185
|
+
f"\nResponse: {connection_error.response.text}"
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
print(
|
|
189
|
+
f"\nConnection Error when connecting to {connection_error.request.url}. "
|
|
190
|
+
"Are you connected to the Internet/VPN? Do you need to update DNS settings?"
|
|
191
|
+
)
|
|
192
|
+
sys.exit("HTTP Not Connecting")
|
|
193
|
+
except FileNotFoundError as fnf_error:
|
|
194
|
+
print(f"\n{fnf_error.strerror}: {fnf_error.filename}")
|
|
195
|
+
sys.exit("File not found")
|
|
196
|
+
except Exception as ee:
|
|
197
|
+
logging.exception("Unhandled exception")
|
|
198
|
+
print(f"\n{ee}")
|
|
199
|
+
sys.exit(ee.__class__.__name__)
|
|
200
|
+
sys.exit(0)
|
|
117
201
|
|
|
118
202
|
def inheritors(base_class):
|
|
119
203
|
subclasses = set()
|
|
@@ -3,18 +3,20 @@ import json
|
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
5
|
import time
|
|
6
|
+
from typing import Set
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import i18n
|
|
10
|
+
from folioclient import FolioClient
|
|
11
|
+
from httpx import HTTPError
|
|
12
|
+
|
|
6
13
|
from folio_migration_tools.helper import Helper
|
|
7
14
|
from folio_migration_tools.migration_report import MigrationReport
|
|
8
|
-
from folio_migration_tools.report_blurbs import Blurbs
|
|
9
15
|
from folio_migration_tools.transaction_migration.legacy_loan import LegacyLoan
|
|
16
|
+
from folio_migration_tools.transaction_migration.legacy_request import LegacyRequest
|
|
10
17
|
from folio_migration_tools.transaction_migration.transaction_result import (
|
|
11
18
|
TransactionResult,
|
|
12
19
|
)
|
|
13
|
-
from folio_migration_tools.transaction_migration.legacy_request import LegacyRequest
|
|
14
|
-
|
|
15
|
-
import requests
|
|
16
|
-
from folioclient import FolioClient
|
|
17
|
-
from requests import HTTPError
|
|
18
20
|
|
|
19
21
|
date_time_format = "%Y-%m-%dT%H:%M:%S.%f+0000"
|
|
20
22
|
|
|
@@ -28,12 +30,15 @@ class CirculationHelper:
|
|
|
28
30
|
):
|
|
29
31
|
self.folio_client = folio_client
|
|
30
32
|
self.service_point_id = service_point_id
|
|
31
|
-
self.missing_patron_barcodes = set()
|
|
32
|
-
self.missing_item_barcodes = set()
|
|
33
|
+
self.missing_patron_barcodes: Set[str] = set()
|
|
34
|
+
self.missing_item_barcodes: Set[str] = set()
|
|
33
35
|
self.migration_report: MigrationReport = migration_report
|
|
34
36
|
|
|
35
37
|
def get_user_by_barcode(self, user_barcode):
|
|
36
38
|
if user_barcode in self.missing_patron_barcodes:
|
|
39
|
+
self.migration_report.add_general_statistics(
|
|
40
|
+
i18n.t("Users already detected as missing")
|
|
41
|
+
)
|
|
37
42
|
logging.info("User is already detected as missing")
|
|
38
43
|
return {}
|
|
39
44
|
user_path = f"/users?query=barcode=={user_barcode}"
|
|
@@ -49,6 +54,9 @@ class CirculationHelper:
|
|
|
49
54
|
|
|
50
55
|
def get_item_by_barcode(self, item_barcode):
|
|
51
56
|
if item_barcode in self.missing_item_barcodes:
|
|
57
|
+
self.migration_report.add_general_statistics(
|
|
58
|
+
i18n.t("Items already detected as missing")
|
|
59
|
+
)
|
|
52
60
|
logging.info("Item is already detected as missing")
|
|
53
61
|
return {}
|
|
54
62
|
item_path = f"/item-storage/items?query=barcode=={item_barcode}"
|
|
@@ -62,21 +70,64 @@ class CirculationHelper:
|
|
|
62
70
|
logging.error(f"{ee} {item_path}")
|
|
63
71
|
return {}
|
|
64
72
|
|
|
73
|
+
def is_checked_out(self, legacy_loan: LegacyLoan) -> bool:
|
|
74
|
+
"""Makes a deeper check to find out if the loan is already processed.
|
|
75
|
+
Looks up the item id, and then searches Loan Storage for any open loans.
|
|
76
|
+
If there are open loans, returns True. Else False.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
legacy_loan (LegacyLoan): _description_
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
bool: _description_
|
|
83
|
+
"""
|
|
84
|
+
if item := self.get_item_by_barcode(legacy_loan.item_barcode):
|
|
85
|
+
if self.get_active_loan_by_item_id(item["id"]):
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def get_active_loan_by_item_id(self, item_id: str) -> dict:
|
|
90
|
+
"""Queries FOLIO for the first open loan.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
item_id (str): the item ID. A uuid string.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
dict: The open loan, if found. Else an empty dictionary
|
|
97
|
+
"""
|
|
98
|
+
loan_path = f'/loan-storage/loans?query=(itemId=="{item_id}")'
|
|
99
|
+
try:
|
|
100
|
+
loans = self.folio_client.folio_get(loan_path, "loans")
|
|
101
|
+
return next((loan for loan in loans if loan["status"]["name"] == "Open"), {})
|
|
102
|
+
except Exception as ee:
|
|
103
|
+
logging.error(f"{ee} {loan_path}")
|
|
104
|
+
return {}
|
|
105
|
+
|
|
106
|
+
def get_holding_by_uuid(self, holdings_uuid):
|
|
107
|
+
holdings_path = f"/holdings-storage/holdings/{holdings_uuid}"
|
|
108
|
+
try:
|
|
109
|
+
return self.folio_client.folio_get_single_object(holdings_path)
|
|
110
|
+
except Exception as ee:
|
|
111
|
+
logging.error(f"{ee} {holdings_path}")
|
|
112
|
+
return {}
|
|
113
|
+
|
|
65
114
|
def check_out_by_barcode(self, legacy_loan: LegacyLoan) -> TransactionResult:
|
|
66
115
|
"""Checks out a legacy loan using the Endpoint /circulation/check-out-by-barcode
|
|
67
116
|
Adds all possible overrides in order to make the transaction go through
|
|
117
|
+
|
|
68
118
|
Args:
|
|
69
|
-
legacy_loan (LegacyLoan):
|
|
119
|
+
legacy_loan (LegacyLoan): _description_
|
|
70
120
|
|
|
71
121
|
Returns:
|
|
72
|
-
TransactionResult:
|
|
122
|
+
TransactionResult: _description_
|
|
73
123
|
"""
|
|
124
|
+
|
|
74
125
|
t0_function = time.time()
|
|
75
126
|
data = {
|
|
76
127
|
"itemBarcode": legacy_loan.item_barcode,
|
|
77
128
|
"userBarcode": legacy_loan.patron_barcode,
|
|
78
129
|
"loanDate": legacy_loan.out_date.isoformat(),
|
|
79
|
-
"servicePointId":
|
|
130
|
+
"servicePointId": legacy_loan.service_point_id,
|
|
80
131
|
"overrideBlocks": {
|
|
81
132
|
"itemNotLoanableBlock": {"dueDate": legacy_loan.due_date.isoformat()},
|
|
82
133
|
"patronBlock": {},
|
|
@@ -84,21 +135,19 @@ class CirculationHelper:
|
|
|
84
135
|
"comment": "Migrated from legacy system",
|
|
85
136
|
},
|
|
86
137
|
}
|
|
138
|
+
if legacy_loan.proxy_patron_barcode:
|
|
139
|
+
data.update({"proxyUserBarcode": legacy_loan.proxy_patron_barcode})
|
|
87
140
|
path = "/circulation/check-out-by-barcode"
|
|
88
|
-
url = f"{self.folio_client.
|
|
141
|
+
url = f"{self.folio_client.gateway_url}{path}"
|
|
89
142
|
try:
|
|
90
143
|
if legacy_loan.patron_barcode in self.missing_patron_barcodes:
|
|
91
|
-
error_message = "Patron barcode already detected as missing"
|
|
144
|
+
error_message = i18n.t("Patron barcode already detected as missing")
|
|
92
145
|
logging.error(
|
|
93
146
|
f"{error_message} Patron barcode: {legacy_loan.patron_barcode} "
|
|
94
147
|
f"Item Barcode:{legacy_loan.item_barcode}"
|
|
95
148
|
)
|
|
96
|
-
return TransactionResult(
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
req = requests.post(
|
|
100
|
-
url, headers=self.folio_client.okapi_headers, data=json.dumps(data)
|
|
101
|
-
)
|
|
149
|
+
return TransactionResult(False, False, "", error_message, error_message)
|
|
150
|
+
req = httpx.post(url, headers=self.folio_client.okapi_headers, json=data, timeout=None)
|
|
102
151
|
if req.status_code == 422:
|
|
103
152
|
error_message_from_folio = json.loads(req.text)["errors"][0]["message"]
|
|
104
153
|
stat_message = error_message_from_folio
|
|
@@ -109,8 +158,7 @@ class CirculationHelper:
|
|
|
109
158
|
error_message_from_folio,
|
|
110
159
|
)[0]
|
|
111
160
|
error_message = (
|
|
112
|
-
f"{stat_message} for item with "
|
|
113
|
-
f"barcode {legacy_loan.item_barcode}"
|
|
161
|
+
f"{stat_message} for item with barcode {legacy_loan.item_barcode}"
|
|
114
162
|
)
|
|
115
163
|
return TransactionResult(
|
|
116
164
|
False,
|
|
@@ -120,9 +168,7 @@ class CirculationHelper:
|
|
|
120
168
|
stat_message,
|
|
121
169
|
)
|
|
122
170
|
elif "No item with barcode" in error_message_from_folio:
|
|
123
|
-
error_message =
|
|
124
|
-
f"No item with barcode {legacy_loan.item_barcode} in FOLIO"
|
|
125
|
-
)
|
|
171
|
+
error_message = f"No item with barcode {legacy_loan.item_barcode} in FOLIO"
|
|
126
172
|
stat_message = "Item barcode not in FOLIO"
|
|
127
173
|
self.missing_item_barcodes.add(legacy_loan.item_barcode)
|
|
128
174
|
return TransactionResult(
|
|
@@ -135,10 +181,8 @@ class CirculationHelper:
|
|
|
135
181
|
|
|
136
182
|
elif "find user with matching barcode" in error_message_from_folio:
|
|
137
183
|
self.missing_patron_barcodes.add(legacy_loan.patron_barcode)
|
|
138
|
-
error_message =
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
stat_message = "Patron barcode not in FOLIO"
|
|
184
|
+
error_message = f"No patron with barcode {legacy_loan.patron_barcode} in FOLIO"
|
|
185
|
+
stat_message = i18n.t("Patron barcode not in FOLIO")
|
|
142
186
|
return TransactionResult(
|
|
143
187
|
False,
|
|
144
188
|
False,
|
|
@@ -146,10 +190,7 @@ class CirculationHelper:
|
|
|
146
190
|
error_message_from_folio,
|
|
147
191
|
stat_message,
|
|
148
192
|
)
|
|
149
|
-
elif
|
|
150
|
-
"Cannot check out item that already has an open"
|
|
151
|
-
in error_message_from_folio
|
|
152
|
-
):
|
|
193
|
+
elif "Cannot check out item that already has an open" in error_message_from_folio:
|
|
153
194
|
return TransactionResult(
|
|
154
195
|
False,
|
|
155
196
|
False,
|
|
@@ -157,12 +198,20 @@ class CirculationHelper:
|
|
|
157
198
|
error_message_from_folio,
|
|
158
199
|
error_message_from_folio,
|
|
159
200
|
)
|
|
201
|
+
elif "Item is already checked out" in error_message_from_folio:
|
|
202
|
+
return TransactionResult(
|
|
203
|
+
False,
|
|
204
|
+
True,
|
|
205
|
+
None,
|
|
206
|
+
error_message_from_folio,
|
|
207
|
+
error_message_from_folio,
|
|
208
|
+
)
|
|
160
209
|
logging.error(
|
|
161
210
|
f"{error_message} "
|
|
162
211
|
f"Patron barcode: {legacy_loan.patron_barcode} "
|
|
163
212
|
f"Item Barcode:{legacy_loan.item_barcode}"
|
|
164
213
|
)
|
|
165
|
-
self.migration_report.add(
|
|
214
|
+
self.migration_report.add("Details", stat_message)
|
|
166
215
|
return TransactionResult(
|
|
167
216
|
False, True, None, error_message, f"Check out error: {stat_message}"
|
|
168
217
|
)
|
|
@@ -174,7 +223,7 @@ class CirculationHelper:
|
|
|
174
223
|
legacy_loan.item_barcode,
|
|
175
224
|
f"{(time.time() - t0_function):.2f}",
|
|
176
225
|
)
|
|
177
|
-
return TransactionResult(True, False, json.loads(req.text),
|
|
226
|
+
return TransactionResult(True, False, json.loads(req.text), "", stats)
|
|
178
227
|
elif req.status_code == 204:
|
|
179
228
|
stats = "Successfully checked out by barcode"
|
|
180
229
|
logging.debug(
|
|
@@ -183,7 +232,7 @@ class CirculationHelper:
|
|
|
183
232
|
legacy_loan.item_barcode,
|
|
184
233
|
req.status_code,
|
|
185
234
|
)
|
|
186
|
-
return TransactionResult(True, False, None,
|
|
235
|
+
return TransactionResult(True, False, None, "", stats)
|
|
187
236
|
else:
|
|
188
237
|
req.raise_for_status()
|
|
189
238
|
except HTTPError:
|
|
@@ -199,19 +248,17 @@ class CirculationHelper:
|
|
|
199
248
|
False,
|
|
200
249
|
None,
|
|
201
250
|
"5XX",
|
|
202
|
-
|
|
251
|
+
i18n.t("Failed checkout http status %{code}", code=req.status_code),
|
|
203
252
|
)
|
|
204
253
|
|
|
205
254
|
@staticmethod
|
|
206
255
|
def create_request(
|
|
207
|
-
folio_client: FolioClient,
|
|
208
|
-
legacy_request: LegacyRequest,
|
|
209
|
-
migration_report: MigrationReport,
|
|
256
|
+
folio_client: FolioClient, legacy_request: LegacyRequest, migration_report: MigrationReport
|
|
210
257
|
):
|
|
211
258
|
try:
|
|
212
259
|
path = "/circulation/requests"
|
|
213
|
-
url = f"{folio_client.
|
|
214
|
-
data = legacy_request.
|
|
260
|
+
url = f"{folio_client.gateway_url}{path}"
|
|
261
|
+
data = legacy_request.serialize()
|
|
215
262
|
data["requestProcessingParameters"] = {
|
|
216
263
|
"overrideBlocks": {
|
|
217
264
|
"itemNotLoanableBlock": {
|
|
@@ -222,20 +269,15 @@ class CirculationHelper:
|
|
|
222
269
|
"comment": "Migrated from legacy system",
|
|
223
270
|
}
|
|
224
271
|
}
|
|
225
|
-
req =
|
|
226
|
-
url, headers=folio_client.okapi_headers, data=json.dumps(data)
|
|
227
|
-
)
|
|
272
|
+
req = httpx.post(url, headers=folio_client.okapi_headers, json=data, timeout=None)
|
|
228
273
|
logging.debug(f"POST {req.status_code}\t{url}\t{json.dumps(data)}")
|
|
229
274
|
if str(req.status_code) == "422":
|
|
230
275
|
message = json.loads(req.text)["errors"][0]["message"]
|
|
231
|
-
logging.error(f"{message}
|
|
276
|
+
logging.error(f"{message}")
|
|
232
277
|
migration_report.add_general_statistics(message)
|
|
233
278
|
return False
|
|
234
279
|
else:
|
|
235
280
|
req.raise_for_status()
|
|
236
|
-
migration_report.add_general_statistics(
|
|
237
|
-
f"Created {legacy_request.request_type}"
|
|
238
|
-
)
|
|
239
281
|
logging.info(
|
|
240
282
|
"%s Successfully created %s",
|
|
241
283
|
req.status_code,
|
|
@@ -244,7 +286,7 @@ class CirculationHelper:
|
|
|
244
286
|
return True
|
|
245
287
|
except Exception as exception:
|
|
246
288
|
logging.error(exception, exc_info=True)
|
|
247
|
-
migration_report.add(
|
|
289
|
+
migration_report.add("Details", exception)
|
|
248
290
|
Helper.log_data_issue(
|
|
249
291
|
legacy_request.item_barcode,
|
|
250
292
|
exception,
|
|
@@ -252,9 +294,7 @@ class CirculationHelper:
|
|
|
252
294
|
)
|
|
253
295
|
return False
|
|
254
296
|
|
|
255
|
-
def load_migrated_user_barcodes(
|
|
256
|
-
self, user_barcodes, patron_files, folder_structure
|
|
257
|
-
):
|
|
297
|
+
def load_migrated_user_barcodes(self, user_barcodes, patron_files, folder_structure):
|
|
258
298
|
if any(patron_files):
|
|
259
299
|
for filedef in patron_files:
|
|
260
300
|
my_path = folder_structure.results_folder / filedef.file_name
|
|
@@ -275,19 +315,16 @@ class CirculationHelper:
|
|
|
275
315
|
logging.info("Loaded %s barcodes from items", len(item_barcodes))
|
|
276
316
|
|
|
277
317
|
@staticmethod
|
|
278
|
-
def extend_open_loan(
|
|
279
|
-
folio_client: FolioClient, loan, extension_due_date, extend_out_date
|
|
280
|
-
):
|
|
281
|
-
# TODO: add logging instead of print out
|
|
318
|
+
def extend_open_loan(folio_client: FolioClient, loan, extension_due_date, extend_out_date):
|
|
282
319
|
try:
|
|
283
320
|
loan_to_put = copy.deepcopy(loan)
|
|
284
321
|
del loan_to_put["metadata"]
|
|
285
322
|
loan_to_put["dueDate"] = extension_due_date.isoformat()
|
|
286
323
|
loan_to_put["loanDate"] = extend_out_date.isoformat()
|
|
287
|
-
url = f"{folio_client.
|
|
324
|
+
url = f"{folio_client.gateway_url}/circulation/loans/{loan_to_put['id']}"
|
|
288
325
|
|
|
289
|
-
req =
|
|
290
|
-
url, headers=folio_client.okapi_headers,
|
|
326
|
+
req = httpx.put(
|
|
327
|
+
url, headers=folio_client.okapi_headers, json=loan_to_put, timeout=None
|
|
291
328
|
)
|
|
292
329
|
logging.info(
|
|
293
330
|
"%s\tPUT Extend loan %s to %s\t %s",
|