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.
Files changed (73) hide show
  1. folio_migration_tools/__init__.py +11 -0
  2. folio_migration_tools/__main__.py +169 -85
  3. folio_migration_tools/circulation_helper.py +96 -59
  4. folio_migration_tools/config_file_load.py +66 -0
  5. folio_migration_tools/custom_dict.py +6 -4
  6. folio_migration_tools/custom_exceptions.py +21 -19
  7. folio_migration_tools/extradata_writer.py +46 -0
  8. folio_migration_tools/folder_structure.py +63 -66
  9. folio_migration_tools/helper.py +29 -21
  10. folio_migration_tools/holdings_helper.py +57 -34
  11. folio_migration_tools/i18n_config.py +9 -0
  12. folio_migration_tools/library_configuration.py +173 -13
  13. folio_migration_tools/mapper_base.py +317 -106
  14. folio_migration_tools/mapping_file_transformation/courses_mapper.py +203 -0
  15. folio_migration_tools/mapping_file_transformation/holdings_mapper.py +83 -69
  16. folio_migration_tools/mapping_file_transformation/item_mapper.py +98 -94
  17. folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +352 -0
  18. folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +702 -223
  19. folio_migration_tools/mapping_file_transformation/notes_mapper.py +90 -0
  20. folio_migration_tools/mapping_file_transformation/order_mapper.py +492 -0
  21. folio_migration_tools/mapping_file_transformation/organization_mapper.py +389 -0
  22. folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +38 -27
  23. folio_migration_tools/mapping_file_transformation/user_mapper.py +149 -361
  24. folio_migration_tools/marc_rules_transformation/conditions.py +650 -246
  25. folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +292 -130
  26. folio_migration_tools/marc_rules_transformation/hrid_handler.py +244 -0
  27. folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +20846 -0
  28. folio_migration_tools/marc_rules_transformation/marc_file_processor.py +300 -0
  29. folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +136 -0
  30. folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +241 -0
  31. folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +681 -201
  32. folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +395 -429
  33. folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +531 -100
  34. folio_migration_tools/migration_report.py +85 -38
  35. folio_migration_tools/migration_tasks/__init__.py +1 -3
  36. folio_migration_tools/migration_tasks/authority_transformer.py +119 -0
  37. folio_migration_tools/migration_tasks/batch_poster.py +911 -198
  38. folio_migration_tools/migration_tasks/bibs_transformer.py +121 -116
  39. folio_migration_tools/migration_tasks/courses_migrator.py +192 -0
  40. folio_migration_tools/migration_tasks/holdings_csv_transformer.py +252 -247
  41. folio_migration_tools/migration_tasks/holdings_marc_transformer.py +321 -115
  42. folio_migration_tools/migration_tasks/items_transformer.py +264 -84
  43. folio_migration_tools/migration_tasks/loans_migrator.py +506 -195
  44. folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +187 -0
  45. folio_migration_tools/migration_tasks/migration_task_base.py +364 -74
  46. folio_migration_tools/migration_tasks/orders_transformer.py +373 -0
  47. folio_migration_tools/migration_tasks/organization_transformer.py +451 -0
  48. folio_migration_tools/migration_tasks/requests_migrator.py +130 -62
  49. folio_migration_tools/migration_tasks/reserves_migrator.py +253 -0
  50. folio_migration_tools/migration_tasks/user_transformer.py +180 -139
  51. folio_migration_tools/task_configuration.py +46 -0
  52. folio_migration_tools/test_infrastructure/__init__.py +0 -0
  53. folio_migration_tools/test_infrastructure/mocked_classes.py +406 -0
  54. folio_migration_tools/transaction_migration/legacy_loan.py +148 -34
  55. folio_migration_tools/transaction_migration/legacy_request.py +65 -25
  56. folio_migration_tools/transaction_migration/legacy_reserve.py +47 -0
  57. folio_migration_tools/transaction_migration/transaction_result.py +12 -1
  58. folio_migration_tools/translations/en.json +476 -0
  59. folio_migration_tools-1.9.10.dist-info/METADATA +169 -0
  60. folio_migration_tools-1.9.10.dist-info/RECORD +67 -0
  61. {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info}/WHEEL +1 -2
  62. folio_migration_tools-1.9.10.dist-info/entry_points.txt +3 -0
  63. folio_migration_tools/generate_schemas.py +0 -46
  64. folio_migration_tools/mapping_file_transformation/mapping_file_mapping_base_impl.py +0 -44
  65. folio_migration_tools/mapping_file_transformation/user_mapper_base.py +0 -212
  66. folio_migration_tools/marc_rules_transformation/bibs_processor.py +0 -163
  67. folio_migration_tools/marc_rules_transformation/holdings_processor.py +0 -284
  68. folio_migration_tools/report_blurbs.py +0 -219
  69. folio_migration_tools/transaction_migration/legacy_fee_fine.py +0 -36
  70. folio_migration_tools-1.2.1.dist-info/METADATA +0 -134
  71. folio_migration_tools-1.2.1.dist-info/RECORD +0 -50
  72. folio_migration_tools-1.2.1.dist-info/top_level.txt +0 -1
  73. {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,11 @@
1
+ import importlib.metadata
2
+ from typing import Protocol
3
+
4
+ __version__ = importlib.metadata.version("folio_migration_tools")
5
+
6
+ class StrCoercible(Protocol):
7
+ def __repr__(self) -> str:
8
+ ...
9
+
10
+ def __str__(self) -> str:
11
+ ...
@@ -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 requests.exceptions
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 * # pylint: disable=wildcard-import, unused-wildcard-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("configuration_path", help="Path to configuration file")
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
- "Task name. One of one of "
24
- f'{", ".join((tc.__name__ for tc in task_classes))}'
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="pasword for the tenant in the configuration file",
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
- " Built on migration_repo_template"
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
- return parser.parse_args()
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 = [t for t in inheritors(migration_task_base.MigrationTaskBase)]
45
- args = parse_args()
46
- with open(args.configuration_path) as config_file_path:
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
- for validation_message in json.loads(e.json()):
104
- print(
105
- f"{validation_message['msg']}\t"
106
- f"{', '.join(humps.camelize(str(x)) for x in validation_message['loc'])}"
107
- )
108
- print("Halting")
109
-
110
- # task_obj.do_work()
111
- logging.info("Work done, wrapping up")
112
- sys.exit(0)
113
- # task_obj.wrap_up()
114
- except requests.exceptions.SSLError:
115
- print("\nSSL error. Are you connected to the Internet and the VPN?")
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): Legacy loan to be posted
119
+ legacy_loan (LegacyLoan): _description_
70
120
 
71
121
  Returns:
72
- TransactionResult: the result of the transaction
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": self.service_point_id,
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.okapi_url}{path}"
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
- False, False, None, error_message, error_message
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
- f"No patron with barcode {legacy_loan.patron_barcode} in FOLIO"
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(Blurbs.Details, stat_message)
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), None, stats)
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, None, stats)
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
- f"Failed checkout http status {req.status_code}",
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.okapi_url}{path}"
214
- data = legacy_request.to_dict()
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 = requests.post(
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}\t{json.dumps(data)}")
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(Blurbs.Details, exception)
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.okapi_url}/circulation/loans/{loan_to_put['id']}"
324
+ url = f"{folio_client.gateway_url}/circulation/loans/{loan_to_put['id']}"
288
325
 
289
- req = requests.put(
290
- url, headers=folio_client.okapi_headers, data=json.dumps(loan_to_put)
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",