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
@@ -1,30 +1,34 @@
1
1
  import copy
2
2
  import csv
3
- from datetime import datetime, timedelta
4
3
  import json
5
- import sys
6
- from dateutil import parser as du_parser
7
- import requests
8
4
  import logging
5
+ import sys
9
6
  import time
10
7
  import traceback
8
+ from datetime import datetime, timedelta
9
+ from typing import Annotated, List, Literal, Optional
11
10
  from urllib.error import HTTPError
12
- from datetime import timezone
13
- from pydantic import BaseModel
14
- from folio_migration_tools.helper import Helper
11
+ from zoneinfo import ZoneInfo
12
+ from pydantic import Field
13
+
14
+ import i18n
15
+ from dateutil import parser as du_parser
15
16
  from folio_uuid.folio_namespaces import FOLIONamespaces
17
+ from art import tprint
18
+
16
19
  from folio_migration_tools.circulation_helper import CirculationHelper
17
- from folio_migration_tools.custom_dict import InsensitiveDictReader
20
+ from folio_migration_tools.custom_exceptions import TransformationRecordFailedError
21
+ from folio_migration_tools.helper import Helper
18
22
  from folio_migration_tools.library_configuration import (
19
23
  FileDefinition,
20
24
  LibraryConfiguration,
21
25
  )
26
+ from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
27
+ MappingFileMapperBase,
28
+ )
22
29
  from folio_migration_tools.migration_report import MigrationReport
23
30
  from folio_migration_tools.migration_tasks.migration_task_base import MigrationTaskBase
24
-
25
- from typing import Optional
26
- from folio_migration_tools.report_blurbs import Blurbs
27
-
31
+ from folio_migration_tools.task_configuration import AbstractTaskConfiguration
28
32
  from folio_migration_tools.transaction_migration.legacy_loan import LegacyLoan
29
33
  from folio_migration_tools.transaction_migration.transaction_result import (
30
34
  TransactionResult,
@@ -32,15 +36,60 @@ from folio_migration_tools.transaction_migration.transaction_result import (
32
36
 
33
37
 
34
38
  class LoansMigrator(MigrationTaskBase):
35
- class TaskConfiguration(BaseModel):
36
- name: str
37
- utc_difference: int
38
- migration_task_type: str
39
- open_loans_file: FileDefinition
40
- fallback_service_point_id: str
41
- starting_row: Optional[int] = 1
42
- item_files: Optional[list[FileDefinition]] = []
43
- patron_files: Optional[list[FileDefinition]] = []
39
+ class TaskConfiguration(AbstractTaskConfiguration):
40
+ name: Annotated[
41
+ str,
42
+ Field(
43
+ title="Task name",
44
+ description="The name of the task.",
45
+ ),
46
+ ]
47
+ migration_task_type: Annotated[
48
+ str,
49
+ Field(
50
+ title="Migration task type",
51
+ description="The type of the migration task.",
52
+ ),
53
+ ]
54
+ open_loans_files: Annotated[
55
+ Optional[list[FileDefinition]],
56
+ Field(
57
+ title="Open loans files",
58
+ description="List of files containing open loan data.",
59
+ ),
60
+ ]
61
+ fallback_service_point_id: Annotated[
62
+ str,
63
+ Field(
64
+ title="Fallback service point ID",
65
+ description="Identifier of the fallback service point.",
66
+ ),
67
+ ]
68
+ starting_row: Annotated[
69
+ Optional[int],
70
+ Field(
71
+ title="Starting row",
72
+ description=("The starting row for data processing. By default is 1."),
73
+ ),
74
+ ] = 1
75
+ item_files: Annotated[
76
+ Optional[list[FileDefinition]],
77
+ Field(
78
+ title="Item files",
79
+ description=(
80
+ "List of files containing item data. By default is empty list."
81
+ ),
82
+ ),
83
+ ] = []
84
+ patron_files: Annotated[
85
+ Optional[list[FileDefinition]],
86
+ Field(
87
+ title="Patron files",
88
+ description=(
89
+ "List of files containing patron data. By default is empty list."
90
+ ),
91
+ ),
92
+ ] = []
44
93
 
45
94
  @staticmethod
46
95
  def get_object_type() -> FOLIONamespaces:
@@ -50,31 +99,79 @@ class LoansMigrator(MigrationTaskBase):
50
99
  self,
51
100
  task_configuration: TaskConfiguration,
52
101
  library_config: LibraryConfiguration,
102
+ folio_client,
53
103
  ):
54
104
  csv.register_dialect("tsv", delimiter="\t")
105
+ self.patron_item_combos: set = set()
106
+ self.t0 = time.time()
107
+ self.num_duplicate_loans = 0
108
+ self.skipped_since_already_added = 0
109
+ self.processed_items: set = set()
110
+ self.failed: dict = {}
111
+ self.failed_and_not_dupe: dict = {}
55
112
  self.migration_report = MigrationReport()
56
- self.valid_legacy_loans = []
57
- super().__init__(library_config, task_configuration)
113
+ self.valid_legacy_loans: List[LegacyLoan] = []
114
+ super().__init__(library_config, task_configuration, folio_client)
58
115
  self.circulation_helper = CirculationHelper(
59
116
  self.folio_client,
60
117
  task_configuration.fallback_service_point_id,
61
118
  self.migration_report,
62
119
  )
63
- with open(
64
- self.folder_structure.legacy_records_folder
65
- / task_configuration.open_loans_file.file_name,
66
- "r",
67
- encoding="utf-8",
68
- ) as loans_file:
69
- self.semi_valid_legacy_loans = list(
70
- self.load_and_validate_legacy_loans(
71
- InsensitiveDictReader(loans_file, dialect="tsv")
72
- )
73
- )
74
- logging.info(
75
- "Loaded and validated %s loans in file",
76
- len(self.semi_valid_legacy_loans),
120
+ logging.info("Check that SMTP is disabled before migrating loans")
121
+ self.check_smtp_config()
122
+ logging.info("Proceeding with loans migration")
123
+ logging.info("Attempting to retrieve tenant timezone configuration...")
124
+ my_path = "/configurations/entries?query=(module==ORG%20and%20configName==localeSettings)"
125
+ try:
126
+ self.tenant_timezone_str = json.loads(
127
+ self.folio_client.folio_get_single_object(my_path)["configs"][0][
128
+ "value"
129
+ ]
130
+ )["timezone"]
131
+ logging.info("Tenant timezone is: %s", self.tenant_timezone_str)
132
+ except Exception:
133
+ logging.info('Tenant locale settings not available. Using "UTC".')
134
+ self.tenant_timezone_str = "UTC"
135
+ self.tenant_timezone = ZoneInfo(self.tenant_timezone_str)
136
+ self.semi_valid_legacy_loans = []
137
+ for file_def in task_configuration.open_loans_files:
138
+ loans_file_path = (
139
+ self.folder_structure.legacy_records_folder / file_def.file_name
77
140
  )
141
+ with open(loans_file_path, "r", encoding="utf-8") as loans_file:
142
+ total_rows, empty_rows, reader = (
143
+ MappingFileMapperBase._get_delimited_file_reader(
144
+ loans_file, loans_file_path
145
+ )
146
+ )
147
+ logging.info("Source data file contains %d rows", total_rows)
148
+ logging.info("Source data file contains %d empty rows", empty_rows)
149
+ self.migration_report.set(
150
+ "GeneralStatistics",
151
+ f"Total rows in {loans_file_path.name}",
152
+ total_rows,
153
+ )
154
+ self.migration_report.set(
155
+ "GeneralStatistics",
156
+ f"Empty rows in {loans_file_path.name}",
157
+ empty_rows,
158
+ )
159
+ self.semi_valid_legacy_loans.extend(
160
+ self.load_and_validate_legacy_loans(
161
+ reader,
162
+ file_def.service_point_id
163
+ or task_configuration.fallback_service_point_id,
164
+ )
165
+ )
166
+
167
+ logging.info(
168
+ "Loaded and validated %s loans in file from %s",
169
+ len(self.semi_valid_legacy_loans),
170
+ file_def.file_name,
171
+ )
172
+ logging.info(
173
+ "Loaded and validated %s loans in total", len(self.semi_valid_legacy_loans)
174
+ )
78
175
  if any(self.task_configuration.item_files) or any(
79
176
  self.task_configuration.patron_files
80
177
  ):
@@ -89,34 +186,62 @@ class LoansMigrator(MigrationTaskBase):
89
186
  "previously migrated objects"
90
187
  )
91
188
  self.valid_legacy_loans = self.semi_valid_legacy_loans
92
- self.patron_item_combos = set()
93
- self.t0 = time.time()
94
- self.num_duplicate_loans = 0
95
- self.skipped_since_already_added = 0
96
- self.processed_items = set()
97
- self.failed = {}
98
- self.num_legacy_loans_processed = 0
99
- self.failed_and_not_dupe = {}
100
- logging.info("Starting row is %s", task_configuration.starting_row)
189
+ logging.info("Starting row number is %s", task_configuration.starting_row)
101
190
  logging.info("Init completed")
102
191
 
192
+ def check_smtp_config(self):
193
+ try:
194
+ smtp_config = self.folio_client.folio_get_single_object(
195
+ "/smtp-configuration"
196
+ )["smtpConfigurations"][0]
197
+ smtp_config_disabled = "disabled" in smtp_config["host"].lower()
198
+ except IndexError:
199
+ smtp_config_disabled = True
200
+ print_smtp_warning()
201
+ if not smtp_config_disabled:
202
+ logging.warn("SMTP connection not disabled...")
203
+ for i in range(10, 0, -1):
204
+ sys.stdout.write(
205
+ "Pausing for {:02d} seconds. Press Ctrl+C to exit...\r".format(i)
206
+ )
207
+ time.sleep(1)
208
+ else:
209
+ logging.info("SMTP connection is disabled...")
210
+
103
211
  def do_work(self):
104
- logging.info("Starting")
105
- if self.task_configuration.starting_row > 1:
106
- logging.info(f"Skipping {(self.task_configuration.starting_row-1)} records")
107
- for num_loans, legacy_loan in enumerate(
108
- self.valid_legacy_loans[self.task_configuration.starting_row :], start=1
109
- ):
110
- t0_migration = time.time()
111
- self.migration_report.add_general_statistics("Processed loans")
112
- try:
113
- self.checkout_single_loan(legacy_loan)
114
- except Exception as ee:
115
- logging.exception(
116
- f"Error in row {num_loans} Item barcode: {legacy_loan.item_barcode} Patron barcode: {legacy_loan.patron_barcode} {ee}"
212
+ with self.folio_client.get_folio_http_client() as self.http_client:
213
+ logging.info("Starting")
214
+ starting_index = (
215
+ self.task_configuration.starting_row - 1
216
+ if self.task_configuration.starting_row > 0
217
+ else 0
218
+ )
219
+ if self.task_configuration.starting_row > 1:
220
+ logging.info(f"Skipping {(starting_index)} records")
221
+ for num_loans, legacy_loan in enumerate(
222
+ self.valid_legacy_loans[starting_index:], start=1
223
+ ):
224
+ t0_migration = time.time()
225
+ self.migration_report.add_general_statistics(
226
+ i18n.t("Processed pre-validated loans")
117
227
  )
118
- if num_loans % 25 == 0:
119
- logging.info(f"{timings(self.t0, t0_migration, num_loans)} {num_loans}")
228
+ try:
229
+ self.checkout_single_loan(legacy_loan)
230
+ except TransformationRecordFailedError as ee:
231
+ logging.error(
232
+ f"Transformation failed in row {num_loans} Item barcode: {legacy_loan.item_barcode} "
233
+ f"Patron barcode: {legacy_loan.patron_barcode}"
234
+ )
235
+ ee.log_it()
236
+ except Exception as ee:
237
+ logging.exception(
238
+ f"Error in row {num_loans} Item barcode: {legacy_loan.item_barcode} "
239
+ f"Patron barcode: {legacy_loan.patron_barcode} {ee}"
240
+ )
241
+ if num_loans % 25 == 0:
242
+ logging.info(
243
+ f"{timings(self.t0, t0_migration, num_loans)} {num_loans}"
244
+ )
120
245
 
121
246
  def checkout_single_loan(self, legacy_loan: LegacyLoan):
122
247
  """Checks a legacy loan out. Retries once if it fails.
@@ -127,30 +252,65 @@ class LoansMigrator(MigrationTaskBase):
127
252
  res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
128
253
 
129
254
  if res_checkout.was_successful:
130
- self.migration_report.add(Blurbs.Details, "Checked out on first try")
255
+ self.migration_report.add("Details", i18n.t("Checked out on first try"))
256
+ self.migration_report.add_general_statistics(
257
+ i18n.t("Successfully checked out")
258
+ )
131
259
  self.set_renewal_count(legacy_loan, res_checkout)
132
260
  self.set_new_status(legacy_loan, res_checkout)
133
261
  elif res_checkout.should_be_retried:
134
262
  res_checkout2 = self.handle_checkout_failure(legacy_loan, res_checkout)
135
263
  if res_checkout2.was_successful and res_checkout2.folio_loan:
136
- self.migration_report.add(Blurbs.Details, "Checked out on second try")
264
+ self.migration_report.add(
265
+ "Details", i18n.t("Checked out on second try")
266
+ )
267
+ self.migration_report.add_general_statistics(
268
+ i18n.t("Successfully checked out")
269
+ )
137
270
  logging.info("Checked out on second try")
138
271
  self.set_renewal_count(legacy_loan, res_checkout2)
139
272
  self.set_new_status(legacy_loan, res_checkout2)
140
273
  elif legacy_loan.item_barcode not in self.failed:
141
- self.failed[legacy_loan.item_barcode] = legacy_loan
142
- logging.error("Failed on second try: %s", res_checkout2.error_message)
143
- self.migration_report.add(
144
- Blurbs.Details,
145
- f"Second failure: {res_checkout2.migration_report_message}",
146
- )
274
+ if res_checkout2.error_message == "Aged to lost and checked out":
275
+ self.migration_report.add(
276
+ "Details",
277
+ i18n.t("Second failure")
278
+ + f": {res_checkout2.migration_report_message}",
279
+ )
280
+ logging.error(
281
+ f"{res_checkout2.error_message}. Item barcode: {legacy_loan.item_barcode}"
282
+ )
283
+ else:
284
+ self.failed[legacy_loan.item_barcode] = legacy_loan
285
+ self.migration_report.add_general_statistics(i18n.t("Failed loans"))
286
+ logging.error(
287
+ "Failed on second try: %s", res_checkout2.error_message
288
+ )
289
+ self.migration_report.add(
290
+ "Details",
291
+ i18n.t("Second failure")
292
+ + f": {res_checkout2.migration_report_message}",
293
+ )
294
+ raise TransformationRecordFailedError(
295
+ f"Row {legacy_loan.row}",
296
+ i18n.t("Loans failing during checkout, second try"),
297
+ json.dumps(legacy_loan.to_dict()),
298
+ )
147
299
  elif not res_checkout.should_be_retried:
148
300
  logging.error(
149
301
  "Failed first time. No retries: %s", res_checkout.error_message
150
302
  )
303
+ self.migration_report.add_general_statistics(i18n.t("Failed loans"))
151
304
  self.migration_report.add(
152
- Blurbs.Details,
153
- f"Failed 1st time. No retries: {res_checkout.migration_report_message}",
305
+ "Details",
306
+ i18n.t("Failed 1st time. No retries")
307
+ + f": {res_checkout.migration_report_message}",
308
+ )
309
+ self.failed[legacy_loan.item_barcode] = legacy_loan
310
+ raise TransformationRecordFailedError(
311
+ f"Row {legacy_loan.row}",
312
+ i18n.t("Loans failing during checkout"),
313
+ json.dumps(legacy_loan.to_dict()),
154
314
  )
155
315
 
156
316
  def set_new_status(self, legacy_loan: LegacyLoan, res_checkout: TransactionResult):
@@ -174,45 +334,39 @@ class LoansMigrator(MigrationTaskBase):
174
334
  if legacy_loan.renewal_count > 0:
175
335
  self.update_open_loan(res_checkout.folio_loan, legacy_loan)
176
336
  self.migration_report.add_general_statistics(
177
- "Updated renewal count for loan"
337
+ i18n.t("Updated renewal count for loan")
178
338
  )
179
339
 
180
340
  def wrap_up(self):
181
341
  for k, v in self.failed.items():
182
- self.failed_and_not_dupe[k] = [v.to_dict()]
183
- self.migration_report.set(
184
- Blurbs.GeneralStatistics, "Failed loans", len(self.failed_and_not_dupe)
185
- )
186
- self.migration_report.set(
187
- Blurbs.GeneralStatistics,
188
- "Total Rows in file",
189
- self.num_legacy_loans_processed,
190
- )
342
+ self.failed_and_not_dupe[k] = [v if isinstance(v, dict) else v.to_dict()]
343
+ print(f"Wrapping up. Unique loans in failed:{len(self.failed_and_not_dupe)}")
191
344
 
192
345
  self.write_failed_loans_to_file()
193
346
 
194
347
  with open(self.folder_structure.migration_reports_file, "w+") as report_file:
195
- report_file.write("# Loans migration results \n")
196
- report_file.write(
197
- f"Time Finished: {datetime.isoformat(datetime.now(timezone.utc))}\n"
348
+ self.migration_report.write_migration_report(
349
+ i18n.t("Loans migration report"), report_file, self.start_datetime
198
350
  )
199
- self.migration_report.write_migration_report(report_file)
351
+ self.clean_out_empty_logs()
200
352
 
201
353
  def write_failed_loans_to_file(self):
202
354
  csv_columns = [
203
- "due_date",
355
+ "patron_barcode",
356
+ "proxy_patron_barcode",
204
357
  "item_barcode",
205
- "next_item_status",
358
+ "due_date",
206
359
  "out_date",
207
- "patron_barcode",
360
+ "next_item_status",
208
361
  "renewal_count",
362
+ "service_point_id",
209
363
  ]
210
364
  with open(self.folder_structure.failed_recs_path, "w+") as failed_loans_file:
211
365
  writer = csv.DictWriter(
212
366
  failed_loans_file, fieldnames=csv_columns, dialect="tsv"
213
367
  )
214
368
  writer.writeheader()
215
- for k, failed_loan in self.failed_and_not_dupe.items():
369
+ for _k, failed_loan in self.failed_and_not_dupe.items():
216
370
  writer.writerow(failed_loan[0])
217
371
 
218
372
  def check_barcodes(self):
@@ -225,21 +379,36 @@ class LoansMigrator(MigrationTaskBase):
225
379
  user_barcodes, self.task_configuration.patron_files, self.folder_structure
226
380
  )
227
381
  for loan in self.semi_valid_legacy_loans:
228
- has_item_barcode = loan.item_barcode in item_barcodes
229
- has_patron_barcode = loan.patron_barcode in user_barcodes
230
- if has_item_barcode and has_patron_barcode:
382
+ has_item_barcode = loan.item_barcode in item_barcodes or not any(
383
+ item_barcodes
384
+ )
385
+ has_patron_barcode = loan.patron_barcode in user_barcodes or not any(
386
+ user_barcodes
387
+ )
388
+ has_proxy_barcode = True
389
+ if loan.proxy_patron_barcode:
390
+ has_proxy_barcode = (
391
+ loan.proxy_patron_barcode in user_barcodes or not any(user_barcodes)
392
+ )
393
+ if has_item_barcode and has_patron_barcode and has_proxy_barcode:
231
394
  self.migration_report.add_general_statistics(
232
- "Loans verified against migrated user and item"
395
+ i18n.t("Loans verified against migrated user and item")
233
396
  )
234
397
  yield loan
235
398
  else:
399
+ # Add this loan to failed loans for later correction and re-run.
400
+ self.failed[loan.item_barcode] = loan
401
+ self.migration_report.add_general_statistics(i18n.t("Failed loans"))
236
402
  self.migration_report.add(
237
- Blurbs.DiscardedLoans,
238
- f"Loans discarded. Had migrated item barcode: {has_item_barcode}. "
239
- f"Had migrated user barcode: {has_patron_barcode}",
403
+ "DiscardedLoans",
404
+ i18n.t("Loans discarded. Had migrated item barcode")
405
+ + f": {has_item_barcode}. "
406
+ + i18n.t("Had migrated user barcode")
407
+ + f": {has_patron_barcode}"
408
+ + f": {has_proxy_barcode}",
240
409
  )
241
410
  if not has_item_barcode:
242
- Helper.log_data_issue(
411
+ Helper.log_data_issue_failed(
243
412
  "", "Loan without matched item barcode", json.dumps(loan.to_dict())
244
413
  )
245
414
  if not has_patron_barcode:
@@ -248,38 +417,72 @@ class LoansMigrator(MigrationTaskBase):
248
417
  "Loan without matched patron barcode",
249
418
  json.dumps(loan.to_dict()),
250
419
  )
420
+ if not has_proxy_barcode:
421
+ Helper.log_data_issue_failed(
422
+ "",
423
+ "Loan without matched proxy patron barcode",
424
+ json.dumps(loan.to_dict()),
425
+ )
251
426
 
252
- def load_and_validate_legacy_loans(self, loans_reader):
427
+ def load_and_validate_legacy_loans(
428
+ self, loans_reader, service_point_id: str
429
+ ) -> list:
430
+ results = []
253
431
  num_bad = 0
254
432
  logging.info("Validating legacy loans in file...")
255
433
  for legacy_loan_count, legacy_loan_dict in enumerate(loans_reader):
256
434
  try:
257
435
  legacy_loan = LegacyLoan(
258
436
  legacy_loan_dict,
259
- self.task_configuration.utc_difference,
437
+ service_point_id,
438
+ self.migration_report,
439
+ self.tenant_timezone,
260
440
  legacy_loan_count,
261
441
  )
262
442
  if any(legacy_loan.errors):
263
443
  num_bad += 1
264
- self.migration_report.add_general_statistics("Discarded Loans")
444
+ self.migration_report.add_general_statistics(
445
+ i18n.t("Loans failed pre-validation")
446
+ )
447
+ self.migration_report.add_general_statistics(i18n.t("Failed loans"))
265
448
  for error in legacy_loan.errors:
266
449
  self.migration_report.add(
267
- Blurbs.DiscardedLoans, f"{error[0]} - {error[1]}"
450
+ "DiscardedLoans", f"{error[0]} - {error[1]}"
268
451
  )
452
+ # Add this loan to failed loans for later correction and re-run.
453
+ self.failed[
454
+ legacy_loan.item_barcode or f"no_barcode_{legacy_loan_count}"
455
+ ] = legacy_loan
269
456
  else:
270
- yield legacy_loan
457
+ results.append(legacy_loan)
458
+ except TransformationRecordFailedError as trfe:
459
+ num_bad += 1
460
+ self.migration_report.add_general_statistics(
461
+ i18n.t("Loans failed pre-validation")
462
+ )
463
+ self.migration_report.add(
464
+ "DiscardedLoans",
465
+ f"{trfe.message} - see data issues log",
466
+ )
467
+ trfe.log_it()
468
+ self.failed[
469
+ legacy_loan_dict.get(
470
+ "item_barcode", f"no_barcode_{legacy_loan_count}"
471
+ )
472
+ ] = legacy_loan_dict
271
473
  except ValueError as ve:
272
474
  logging.exception(ve)
273
475
  logging.info(
274
- f"Done validating {legacy_loan_count} "
275
- f"legacy loans with {num_bad} rotten apples"
476
+ f"Done validating {legacy_loan_count + 1} legacy loans out of which "
477
+ f"{num_bad} where discarded."
276
478
  )
277
- if num_bad / legacy_loan_count > 0.5:
278
- q = num_bad / legacy_loan_count
479
+ if num_bad / (legacy_loan_count + 1) > 0.5:
480
+ q = num_bad / (legacy_loan_count + 1)
279
481
  logging.error("%s percent of loans failed to validate.", (q * 100))
280
482
  self.migration_report.log_me()
281
483
  logging.critical("Halting...")
282
484
  sys.exit(1)
485
+ return results
283
486
 
284
487
  def handle_checkout_failure(
285
488
  self, legacy_loan, folio_checkout: TransactionResult
@@ -306,27 +509,38 @@ class LoansMigrator(MigrationTaskBase):
306
509
  elif folio_checkout.error_message.startswith(
307
510
  "Cannot check out item that already has an open loan"
308
511
  ):
309
- return folio_checkout
310
- elif folio_checkout.error_message.startswith("Aged to lost for item"):
311
- return self.handle_aged_to_lost_item(legacy_loan)
512
+ return self.handle_checked_out_item(legacy_loan)
513
+ elif "Item is already checked out" in folio_checkout.error_message:
514
+ return self.handle_checked_out_item(legacy_loan)
515
+ elif "Aged to lost" in folio_checkout.error_message:
516
+ return self.handle_lost_item(legacy_loan, "Aged to lost")
312
517
  elif folio_checkout.error_message == "Declared lost":
313
- return folio_checkout
518
+ return self.handle_lost_item(legacy_loan, "Declared lost")
314
519
  elif folio_checkout.error_message.startswith(
315
520
  "Cannot check out to inactive user"
316
521
  ):
317
- return self.checkout_to_inactice_user(legacy_loan)
522
+ return self.checkout_to_inactive_user(legacy_loan)
523
+ elif (
524
+ "has the item status Claimed returned and cannot be checked out"
525
+ in folio_checkout.error_message
526
+ ):
527
+ return self.handle_claimed_returned_item(legacy_loan)
318
528
  else:
319
529
  self.migration_report.add(
320
- Blurbs.Details,
321
- f"Other checkout failure: {folio_checkout.error_message}",
530
+ "Details",
531
+ i18n.t("Other checkout failure") + f": {folio_checkout.error_message}",
322
532
  )
323
533
  # First failure. Add to list of failed loans
324
534
  if legacy_loan.item_barcode not in self.failed:
325
535
  self.failed[legacy_loan.item_barcode] = legacy_loan
326
536
  else:
327
- logging.debug(
328
- f"Loan already in failed. item barcode {legacy_loan.item_barcode} "
329
- f"Patron barcode: {legacy_loan.patron_barcode}"
537
+ logging.info(
538
+ i18n.t("Loan already in failed.")
539
+ + " "
540
+ + i18n.t("item barcode")
541
+ + f": {legacy_loan.item_barcode}"
542
+ + i18n.t("Patron barcode")
543
+ + f": {legacy_loan.patron_barcode}",
330
544
  )
331
545
  self.failed_and_not_dupe[legacy_loan.item_barcode] = [
332
546
  legacy_loan,
@@ -337,12 +551,12 @@ class LoansMigrator(MigrationTaskBase):
337
551
  f"{legacy_loan.item_barcode} Patron barcode: {legacy_loan.patron_barcode}"
338
552
  )
339
553
  self.migration_report.add(
340
- Blurbs.Details, "Duplicate loans (or failed twice)"
554
+ "Details", i18n.t("Duplicate loans (or failed twice)")
341
555
  )
342
556
  del self.failed[legacy_loan.item_barcode]
343
557
  return TransactionResult(False, False, "", "", "")
344
558
 
345
- def checkout_to_inactice_user(self, legacy_loan) -> TransactionResult:
559
+ def checkout_to_inactive_user(self, legacy_loan) -> TransactionResult:
346
560
  logging.info("Cannot check out to inactive user. Activating and trying again")
347
561
  user = self.get_user_by_barcode(legacy_loan.patron_barcode)
348
562
  expiration_date = user.get("expirationDate", datetime.isoformat(datetime.now()))
@@ -352,57 +566,135 @@ class LoansMigrator(MigrationTaskBase):
352
566
  res = self.circulation_helper.check_out_by_barcode(
353
567
  legacy_loan
354
568
  ) # checkout_and_update
355
- self.migration_report.add(Blurbs.Details, res.migration_report_message)
569
+ if res.should_be_retried:
570
+ res = self.handle_checkout_failure(legacy_loan, res)
571
+ self.migration_report.add("Details", res.migration_report_message)
356
572
  self.deactivate_user(user, expiration_date)
357
573
  logging.debug("Successfully Deactivated user again")
358
- self.migration_report.add(Blurbs.Details, "Handled inactive users")
574
+ self.migration_report.add("Details", i18n.t("Handled inactive users"))
359
575
  return res
360
576
 
361
- def handle_aged_to_lost_item(self, legacy_loan) -> TransactionResult:
362
- logging.debug("Setting Available")
363
- legacy_loan.next_item_status = "Available"
364
- self.set_item_status(legacy_loan)
365
- res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
366
- legacy_loan.next_item_status = "Aged to lost"
367
- self.set_item_status(legacy_loan)
368
- s = "Successfully Checked out Aged to lost item and put the status back"
369
- logging.info(s)
370
- self.migration_report.add(Blurbs.Details, s)
371
- return res_checkout
577
+ def handle_checked_out_item(self, legacy_loan: LegacyLoan) -> TransactionResult:
578
+ if self.circulation_helper.is_checked_out(legacy_loan):
579
+ return TransactionResult(
580
+ False,
581
+ False,
582
+ legacy_loan,
583
+ i18n.t(
584
+ "Loan already exists for %{item_barcode}",
585
+ item_barcode=legacy_loan.item_barcode,
586
+ ),
587
+ i18n.t(
588
+ "Loan already exists for %{item_barcode}",
589
+ item_barcode=legacy_loan.item_barcode,
590
+ ),
591
+ )
592
+ else:
593
+ logging.debug(
594
+ i18n.t(
595
+ 'Setting item %{item_barcode} to status "Available"',
596
+ item_barcode=legacy_loan.item_barcode,
597
+ )
598
+ )
599
+ legacy_loan.next_item_status = "Available"
600
+ self.set_item_status(legacy_loan)
601
+ res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
602
+ legacy_loan.next_item_status = "Checked out"
603
+ return res_checkout
604
+
605
+ def handle_lost_item(
606
+ self,
607
+ legacy_loan: LegacyLoan,
608
+ lost_type: Literal["Aged to lost", "Declared lost"],
609
+ ) -> TransactionResult:
610
+ if self.circulation_helper.is_checked_out(legacy_loan):
611
+ return TransactionResult(
612
+ False,
613
+ False,
614
+ legacy_loan,
615
+ i18n.t("%{lost_type} and checked out", lost_type=lost_type),
616
+ i18n.t("%{lost_type} and checked out", lost_type=lost_type),
617
+ )
618
+
619
+ else:
620
+ logging.debug(
621
+ 'Setting item %{item_barcode} to status "Available"',
622
+ item_barcode=legacy_loan.item_barcode,
623
+ )
624
+ legacy_loan.next_item_status = "Available"
625
+ self.set_item_status(legacy_loan)
626
+ res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
627
+ legacy_loan.next_item_status = lost_type
628
+ if lost_type == "Aged to lost":
629
+ self.set_item_status(legacy_loan)
630
+ s = i18n.t(
631
+ "Successfully Checked out %{lost_type} item and put the status back",
632
+ lost_type=lost_type,
633
+ )
634
+ else:
635
+ s = i18n.t(
636
+ "Successfully Checked out %{lost_type} item. Item will be declared lost.",
637
+ lost_type=lost_type,
638
+ )
639
+ logging.info(s)
640
+ self.migration_report.add("Details", s)
641
+ return res_checkout
642
+
643
+ def handle_claimed_returned_item(self, legacy_loan: LegacyLoan):
644
+ if self.circulation_helper.is_checked_out(legacy_loan):
645
+ return TransactionResult(
646
+ False,
647
+ False,
648
+ legacy_loan,
649
+ i18n.t("Claimed returned and checked out"),
650
+ i18n.t("Claimed returned and checked out"),
651
+ )
652
+ else:
653
+ logging.debug(
654
+ 'Setting item %{item_barcode} to status "Available"',
655
+ item_barcode=legacy_loan.item_barcode,
656
+ )
657
+ legacy_loan.next_item_status = "Available"
658
+ self.set_item_status(legacy_loan)
659
+ res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
660
+ legacy_loan.next_item_status = "Claimed returned"
661
+ return res_checkout
372
662
 
373
663
  def update_open_loan(self, folio_loan: dict, legacy_loan: LegacyLoan):
374
664
  due_date = du_parser.isoparse(str(legacy_loan.due_date))
375
665
  out_date = du_parser.isoparse(str(legacy_loan.out_date))
376
666
  renewal_count = legacy_loan.renewal_count
377
- # TODO: add logging instead of print out
378
667
  try:
379
668
  loan_to_put = copy.deepcopy(folio_loan)
380
669
  del loan_to_put["metadata"]
381
670
  loan_to_put["dueDate"] = due_date.isoformat()
382
671
  loan_to_put["loanDate"] = out_date.isoformat()
383
672
  loan_to_put["renewalCount"] = renewal_count
384
- url = f"{self.folio_client.okapi_url}/circulation/loans/{loan_to_put['id']}"
385
- req = requests.put(
673
+ url = (
674
+ f"{self.folio_client.gateway_url}/circulation/loans/{loan_to_put['id']}"
675
+ )
676
+ req = self.http_client.put(
386
677
  url,
387
678
  headers=self.folio_client.okapi_headers,
388
- data=json.dumps(loan_to_put),
679
+ json=loan_to_put,
389
680
  )
390
681
  if req.status_code == 422:
391
682
  error_message = json.loads(req.text)["errors"][0]["message"]
392
683
  s = f"Update open loan error: {error_message} {req.status_code}"
393
- self.migration_report.add(Blurbs.Details, s)
684
+ self.migration_report.add("Details", s)
394
685
  logging.error(s)
395
686
  return False
396
687
  elif req.status_code in [201, 204]:
397
688
  self.migration_report.add(
398
- Blurbs.Details,
399
- f"Successfully updated open loan ({req.status_code})",
689
+ "Details",
690
+ i18n.t("Successfully updated open loan") + f" ({req.status_code})",
400
691
  )
401
692
  return True
402
693
  else:
403
694
  self.migration_report.add(
404
- Blurbs.Details,
405
- f"Update open loan error http status: {req.status_code}",
695
+ "Details",
696
+ i18n.t("Update open loan error http status")
697
+ + f": {req.status_code}",
406
698
  )
407
699
  req.raise_for_status()
408
700
  logging.debug("Updating open loan was successful")
@@ -432,16 +724,17 @@ class LoansMigrator(MigrationTaskBase):
432
724
  "servicePointId": str(self.task_configuration.fallback_service_point_id),
433
725
  }
434
726
  logging.debug(f"Declare lost data: {json.dumps(data, indent=4)}")
435
- if self.folio_put_post(declare_lost_url, data, "POST", "Declare item as lost"):
727
+ if self.folio_put_post(
728
+ declare_lost_url, data, "POST", i18n.t("Declare item as lost")
729
+ ):
436
730
  self.migration_report.add(
437
- Blurbs.Details, "Successfully declared loan as lost"
731
+ "Details", i18n.t("Successfully declared loan as lost")
438
732
  )
439
733
  else:
440
734
  logging.error(f"Unsuccessfully declared loan {folio_loan} as lost")
441
735
  self.migration_report.add(
442
- Blurbs.Details, "Unsuccessfully declared loan as lost"
736
+ "Details", i18n.t("Unsuccessfully declared loan as lost")
443
737
  )
444
- # TODO: Exception handling
445
738
 
446
739
  def claim_returned(self, folio_loan):
447
740
  claim_returned_url = (
@@ -457,34 +750,44 @@ class LoansMigrator(MigrationTaskBase):
457
750
  }
458
751
  logging.debug(f"Claim returned data:\t{json.dumps(data)}")
459
752
  if self.folio_put_post(
460
- claim_returned_url, data, "POST", "Declare item as lost"
753
+ claim_returned_url, data, "POST", i18n.t("Claim item returned")
461
754
  ):
462
755
  self.migration_report.add(
463
- Blurbs.Details, "Successfully declared loan as Claimed returned"
756
+ "Details", i18n.t("Successfully declared loan as Claimed returned")
464
757
  )
465
758
  else:
466
759
  logging.error(
467
760
  f"Unsuccessfully declared loan {folio_loan} as Claimed returned"
468
761
  )
469
762
  self.migration_report.add(
470
- Blurbs.Details,
471
- f"Unsuccessfully declared loan {folio_loan} as Claimed returned",
763
+ "Details",
764
+ i18n.t(
765
+ "Unsuccessfully declared loan %{loan} as Claimed returned",
766
+ loan=folio_loan,
767
+ ),
472
768
  )
473
- # TODO: Exception handling
474
769
 
475
770
  def set_item_status(self, legacy_loan: LegacyLoan):
476
771
  try:
477
772
  # Get Item by barcode, update status.
478
- item_url = f'{self.folio_client.okapi_url}/item-storage/items?query=(barcode=="{legacy_loan.item_barcode}")'
479
- resp = requests.get(item_url, headers=self.folio_client.okapi_headers)
773
+ item_path = (
774
+ f'item-storage/items?query=(barcode=="{legacy_loan.item_barcode}")'
775
+ )
776
+ item_url = f"{self.folio_client.gateway_url}/{item_path}"
777
+ resp = self.http_client.get(
778
+ item_url, headers=self.folio_client.okapi_headers
779
+ )
480
780
  resp.raise_for_status()
481
781
  data = resp.json()
482
782
  folio_item = data["items"][0]
483
783
  folio_item["status"]["name"] = legacy_loan.next_item_status
484
784
  if self.update_item(folio_item):
485
785
  self.migration_report.add(
486
- Blurbs.Details,
487
- f"Successfully set item status to {legacy_loan.next_item_status}",
786
+ "Details",
787
+ i18n.t(
788
+ "Successfully set item status to %{status}",
789
+ status=legacy_loan.next_item_status,
790
+ ),
488
791
  )
489
792
  logging.debug(
490
793
  f"Successfully set item with barcode "
@@ -498,8 +801,11 @@ class LoansMigrator(MigrationTaskBase):
498
801
  f"{legacy_loan.item_barcode} to {legacy_loan.next_item_status}"
499
802
  )
500
803
  self.migration_report.add(
501
- Blurbs.Details,
502
- f"Error setting item status to {legacy_loan.next_item_status}",
804
+ "Details",
805
+ i18n.t(
806
+ "Error setting item status to %{status}",
807
+ status=legacy_loan.next_item_status,
808
+ ),
503
809
  )
504
810
  except Exception as ee:
505
811
  logging.error(
@@ -511,43 +817,43 @@ class LoansMigrator(MigrationTaskBase):
511
817
  def activate_user(self, user):
512
818
  user["active"] = True
513
819
  self.update_user(user)
514
- self.migration_report.add(Blurbs.Details, "Successfully activated user")
820
+ self.migration_report.add("Details", i18n.t("Successfully activated user"))
515
821
 
516
822
  def deactivate_user(self, user, expiration_date):
517
823
  user["expirationDate"] = expiration_date
518
824
  user["active"] = False
519
825
  self.update_user(user)
520
- self.migration_report.add(Blurbs.Details, "Successfully deactivated user")
826
+ self.migration_report.add("Details", i18n.t("Successfully deactivated user"))
521
827
 
522
828
  def update_item(self, item):
523
- url = f'/item-storage/items/{item["id"]}'
524
- return self.folio_put_post(url, item, "PUT", "Update item")
829
+ url = f"/item-storage/items/{item['id']}"
830
+ return self.folio_put_post(url, item, "PUT", i18n.t("Update item"))
525
831
 
526
832
  def update_user(self, user):
527
- url = f'/users/{user["id"]}'
528
- self.folio_put_post(url, user, "PUT", "Update user")
833
+ url = f"/users/{user['id']}"
834
+ self.folio_put_post(url, user, "PUT", i18n.t("Update user"))
529
835
 
530
836
  def get_user_by_barcode(self, barcode):
531
- url = f'{self.folio_client.okapi_url}/users?query=(barcode=="{barcode}")'
532
- resp = requests.get(url, headers=self.folio_client.okapi_headers)
837
+ url = f'{self.folio_client.gateway_url}/users?query=(barcode=="{barcode}")'
838
+ resp = self.http_client.get(url, headers=self.folio_client.okapi_headers)
533
839
  resp.raise_for_status()
534
840
  data = resp.json()
535
841
  return data["users"][0]
536
842
 
537
843
  def folio_put_post(self, url, data_dict, verb, action_description=""):
538
- full_url = f"{self.folio_client.okapi_url}{url}"
844
+ full_url = f"{self.folio_client.gateway_url}{url}"
539
845
  try:
540
846
  if verb == "PUT":
541
- resp = requests.put(
847
+ resp = self.http_client.put(
542
848
  full_url,
543
849
  headers=self.folio_client.okapi_headers,
544
- data=json.dumps(data_dict),
850
+ json=data_dict,
545
851
  )
546
852
  elif verb == "POST":
547
- resp = requests.post(
853
+ resp = self.http_client.post(
548
854
  full_url,
549
855
  headers=self.folio_client.okapi_headers,
550
- data=json.dumps(data_dict),
856
+ json=data_dict,
551
857
  )
552
858
  else:
553
859
  raise Exception("Bad verb")
@@ -555,18 +861,28 @@ class LoansMigrator(MigrationTaskBase):
555
861
  error_message = json.loads(resp.text)["errors"][0]["message"]
556
862
  logging.error(error_message)
557
863
  self.migration_report.add(
558
- Blurbs.Details, f"{action_description} error: {error_message}"
864
+ "Details",
865
+ i18n.t(
866
+ "%{action} error: %{message}",
867
+ action=action_description,
868
+ message=error_message,
869
+ ),
559
870
  )
560
871
  resp.raise_for_status()
561
872
  elif resp.status_code in [201, 204]:
562
873
  self.migration_report.add(
563
- Blurbs.Details,
564
- f"Successfully {action_description} ({resp.status_code})",
874
+ "Details",
875
+ i18n.t("Successfully %{action}", action=action_description)
876
+ + f" ({resp.status_code})",
565
877
  )
566
878
  else:
567
879
  self.migration_report.add(
568
- Blurbs.Details,
569
- f"{action_description} error. http status: {resp.status_code}",
880
+ "Details",
881
+ i18n.t(
882
+ "%{action} error. http status: %{status}",
883
+ action=action_description,
884
+ status=resp.status_code,
885
+ ),
570
886
  )
571
887
 
572
888
  resp.raise_for_status()
@@ -579,38 +895,41 @@ class LoansMigrator(MigrationTaskBase):
579
895
 
580
896
  def change_due_date(self, folio_loan, legacy_loan):
581
897
  try:
582
- t0_function = time.time()
583
- api_url = f"{self.folio_client.okapi_url}/circulation/loans/{folio_loan['id']}/change-due-date"
898
+ api_path = f"{folio_loan['id']}/change-due-date"
899
+ api_url = f"{self.folio_client.gateway_url}/circulation/loans/{api_path}"
584
900
  body = {
585
901
  "dueDate": du_parser.isoparse(str(legacy_loan.due_date)).isoformat()
586
902
  }
587
- req = requests.post(
588
- api_url, headers=self.folio_client.okapi_headers, data=json.dumps(body)
903
+ req = self.http_client.post(
904
+ api_url, headers=self.folio_client.okapi_headers, json=body
589
905
  )
590
906
  if req.status_code == 422:
591
907
  error_message = json.loads(req.text)["errors"][0]["message"]
592
908
  self.migration_report.add(
593
- Blurbs.Details, f"Change due date error: {error_message}"
909
+ "Details", i18n.t("Change due date error") + f": {error_message}"
594
910
  )
595
911
  logging.info(
596
912
  f"{error_message}\t",
597
913
  )
598
- self.migration_report.add(Blurbs.Details, error_message)
914
+ self.migration_report.add("Details", error_message)
599
915
  return False
600
916
  elif req.status_code == 201:
601
917
  self.migration_report.add(
602
- Blurbs.Details, f"Successfully changed due date ({req.status_code})"
918
+ "Details",
919
+ i18n.t("Successfully changed due date") + f" ({req.status_code})",
603
920
  )
604
921
  return True, json.loads(req.text), None
605
922
  elif req.status_code == 204:
606
923
  self.migration_report.add(
607
- Blurbs.Details, f"Successfully changed due date ({req.status_code})"
924
+ "Details",
925
+ i18n.t("Successfully changed due date") + f" ({req.status_code})",
608
926
  )
609
927
  return True, None, None
610
928
  else:
611
929
  self.migration_report.add(
612
- Blurbs.Details,
613
- f"Update open loan error http status: {req.status_code}",
930
+ "Details",
931
+ i18n.t("Update open loan error http status"),
932
+ f": {req.status_code}",
614
933
  )
615
934
  req.raise_for_status()
616
935
  except HTTPError as exception:
@@ -621,18 +940,6 @@ class LoansMigrator(MigrationTaskBase):
621
940
  logging.info(exception)
622
941
  return False, None, None
623
942
 
624
- def make_loan_utc(self, legacy_loan: LegacyLoan):
625
- if self.task_configuration.utc_difference != 0:
626
- legacy_loan.due_date = legacy_loan.due_date + timedelta(
627
- hours=self.task_configuration.utc_difference
628
- )
629
- legacy_loan.out_date = legacy_loan.out_date + timedelta(
630
- hours=self.task_configuration.utc_difference
631
- )
632
- self.migration_report.add_general_statistics(
633
- "Adjusted out and due dates to UTC"
634
- )
635
-
636
943
 
637
944
  def timings(t0, t0func, num_objects):
638
945
  avg = num_objects / (time.time() - t0)
@@ -642,3 +949,7 @@ def timings(t0, t0func, num_objects):
642
949
  f"Total objects: {num_objects}\tTotal elapsed: {elapsed:.2f}\t"
643
950
  f"Average per object: {avg:.2f}\tElapsed this time: {elapsed_func:.2f}"
644
951
  )
952
+
953
+
954
+ def print_smtp_warning():
955
+ tprint("\nSMTP?\n", space=2)