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,12 +1,13 @@
1
- import csv
2
1
  import json
3
2
  import logging
4
- from abc import abstractmethod
5
3
  import sys
6
- from typing import Dict, List, Optional
4
+ from typing import Optional, Annotated
5
+ from pydantic import Field
7
6
 
7
+ import i18n
8
8
  from folio_uuid.folio_namespaces import FOLIONamespaces
9
- from folio_migration_tools import migration_report
9
+ from art import tprint
10
+
10
11
  from folio_migration_tools.custom_exceptions import (
11
12
  TransformationProcessError,
12
13
  TransformationRecordFailedError,
@@ -21,18 +22,89 @@ from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base
21
22
  )
22
23
  from folio_migration_tools.mapping_file_transformation.user_mapper import UserMapper
23
24
  from folio_migration_tools.migration_tasks.migration_task_base import MigrationTaskBase
24
- from pydantic import BaseModel
25
+ from folio_migration_tools.task_configuration import AbstractTaskConfiguration
25
26
 
26
27
 
27
28
  class UserTransformer(MigrationTaskBase):
28
- class TaskConfiguration(BaseModel):
29
- name: str
30
- migration_task_type: str
31
- group_map_path: str
32
- departments_map_path: Optional[str] = ""
33
- use_group_map: Optional[bool] = True
34
- user_mapping_file_name: str
35
- user_file: FileDefinition
29
+ class TaskConfiguration(AbstractTaskConfiguration):
30
+ name: Annotated[
31
+ str,
32
+ Field(
33
+ title="Migration task name",
34
+ description=(
35
+ "Name of this migration task. The name is being used to call "
36
+ "the specific task, and to distinguish tasks of similar types"
37
+ ),
38
+ ),
39
+ ]
40
+ migration_task_type: Annotated[
41
+ str,
42
+ Field(
43
+ title="Migration task type",
44
+ description="The type of migration task you want to perform",
45
+ ),
46
+ ]
47
+ group_map_path: Annotated[
48
+ str,
49
+ Field(
50
+ title="Group map path",
51
+ description="Define the path for group mapping",
52
+ )
53
+ ]
54
+ departments_map_path: Annotated[
55
+ Optional[str],
56
+ Field(
57
+ title="Departments map path",
58
+ description=(
59
+ "Define the path for departments mapping. "
60
+ "Optional, by dfault is empty string"
61
+ ),
62
+ )
63
+ ] = ""
64
+ use_group_map: Annotated[
65
+ Optional[bool],
66
+ Field(
67
+ title="Use group map",
68
+ description=(
69
+ "Specify whether to use group mapping. "
70
+ "Optional, by default is True"
71
+ ),
72
+ )
73
+ ] = True
74
+ user_mapping_file_name: Annotated[
75
+ str,
76
+ Field(
77
+ title="User mapping file name",
78
+ description="Specify the user mapping file name",
79
+ )
80
+ ]
81
+ user_file: Annotated[
82
+ FileDefinition,
83
+ Field(
84
+ title="User file",
85
+ description="Select the user data file",
86
+ )
87
+ ]
88
+ remove_id_and_request_preferences: Annotated[
89
+ Optional[bool],
90
+ Field(
91
+ title="Remove ID and request preferences",
92
+ description=(
93
+ "Specify whether to remove user ID and request preferences. "
94
+ "Optional, by default is False"
95
+ ),
96
+ )
97
+ ] = False
98
+ remove_request_preferences: Annotated[
99
+ Optional[bool],
100
+ Field(
101
+ title="Remove request preferences",
102
+ description=(
103
+ "Specify whether to remove user request preferences. "
104
+ "Optional, by default is False"
105
+ ),
106
+ )
107
+ ] = False
36
108
 
37
109
  @staticmethod
38
110
  def get_object_type() -> FOLIONamespaces:
@@ -42,20 +114,19 @@ class UserTransformer(MigrationTaskBase):
42
114
  self,
43
115
  task_config: TaskConfiguration,
44
116
  library_config: LibraryConfiguration,
117
+ folio_client,
45
118
  use_logging: bool = True,
46
119
  ):
47
- super().__init__(library_config, task_config, use_logging)
120
+ super().__init__(library_config, task_config, folio_client, use_logging)
48
121
  self.task_config = task_config
122
+ self.task_configuration = self.task_config
49
123
  self.total_records = 0
50
124
 
51
125
  self.user_map = self.setup_records_map(
52
- self.folder_structure.mapping_files_folder
53
- / self.task_config.user_mapping_file_name
126
+ self.folder_structure.mapping_files_folder / self.task_config.user_mapping_file_name
54
127
  )
55
128
  self.folio_keys = []
56
- self.folio_keys = MappingFileMapperBase.get_mapped_folio_properties_from_map(
57
- self.user_map
58
- )
129
+ self.folio_keys = MappingFileMapperBase.get_mapped_folio_properties_from_map(self.user_map)
59
130
  # Properties
60
131
  self.failed_ids = []
61
132
  self.failed_objects = []
@@ -64,67 +135,60 @@ class UserTransformer(MigrationTaskBase):
64
135
  ).is_file():
65
136
  group_mapping = self.load_ref_data_mapping_file(
66
137
  "patronGroup",
67
- self.folder_structure.mapping_files_folder
68
- / self.task_config.group_map_path,
138
+ self.folder_structure.mapping_files_folder / self.task_config.group_map_path,
69
139
  self.folio_keys,
70
140
  )
71
141
  else:
72
142
  logging.info(
73
143
  "%s not found. No patronGroup mapping will be performed",
74
- self.folder_structure.mapping_files_folder
75
- / self.task_config.group_map_path,
144
+ self.folder_structure.mapping_files_folder / self.task_config.group_map_path,
76
145
  )
77
146
  group_mapping = []
78
147
 
79
148
  if (
80
- self.folder_structure.mapping_files_folder
81
- / self.task_config.departments_map_path
149
+ self.folder_structure.mapping_files_folder / self.task_config.departments_map_path
82
150
  ).is_file():
83
151
  departments_mapping = self.load_ref_data_mapping_file(
84
152
  "departments",
85
- self.folder_structure.mapping_files_folder
86
- / self.task_config.departments_map_path,
153
+ self.folder_structure.mapping_files_folder / self.task_config.departments_map_path,
87
154
  self.folio_keys,
88
155
  )
89
156
  else:
90
157
  logging.info(
91
158
  "%s not found. No departments mapping will be performed",
92
- self.folder_structure.mapping_files_folder
93
- / self.task_config.departments_map_path,
159
+ self.folder_structure.mapping_files_folder / self.task_config.departments_map_path,
94
160
  )
95
161
  departments_mapping = []
96
- self.mapper = UserMapper(
97
- self.folio_client,
98
- task_config,
99
- library_config,
100
- departments_mapping,
101
- group_mapping,
162
+ map_path = (
163
+ self.folder_structure.mapping_files_folder / self.task_config.user_mapping_file_name
102
164
  )
165
+ with open(map_path, encoding="utf8") as mapping_file:
166
+ user_map = json.load(mapping_file)
167
+ self.mapper = UserMapper(
168
+ self.folio_client,
169
+ task_config,
170
+ library_config,
171
+ user_map,
172
+ departments_mapping,
173
+ group_mapping,
174
+ )
175
+
103
176
  logging.info("UserTransformer init done")
104
177
 
105
178
  def do_work(self):
106
179
  logging.info("Starting....")
107
180
  source_path = (
108
- self.folder_structure.legacy_records_folder
109
- / self.task_config.user_file.file_name
110
- )
111
- map_path = (
112
- self.folder_structure.mapping_files_folder
113
- / self.task_config.user_mapping_file_name
181
+ self.folder_structure.legacy_records_folder / self.task_config.user_file.file_name
114
182
  )
183
+
115
184
  try:
116
185
  with open(
117
186
  self.folder_structure.created_objects_path,
118
187
  "w+",
119
188
  encoding="utf-8",
120
189
  ) as results_file:
121
- with open(source_path, encoding="utf8") as object_file, open(
122
- map_path, encoding="utf8"
123
- ) as mapping_file:
190
+ with open(source_path, encoding="utf8") as object_file:
124
191
  logging.info(f"processing {source_path}")
125
- user_map = json.load(mapping_file)
126
- legacy_property_name = self.get_legacy_id_prop(user_map)
127
-
128
192
  file_format = "tsv" if str(source_path).endswith(".tsv") else "csv"
129
193
  for num_users, legacy_user in enumerate(
130
194
  self.mapper.get_users(object_file, file_format), start=1
@@ -134,33 +198,32 @@ class UserTransformer(MigrationTaskBase):
134
198
  logging.info("First Legacy user")
135
199
  logging.info(json.dumps(legacy_user, indent=4))
136
200
  print_email_warning()
137
- folio_user = self.mapper.do_map(
201
+ folio_user, index_or_id = self.mapper.do_map(
138
202
  legacy_user,
139
- user_map,
140
- legacy_user.get(legacy_property_name),
203
+ str(num_users),
204
+ FOLIONamespaces.users,
205
+ )
206
+ folio_user = self.mapper.perform_additional_mapping(
207
+ legacy_user, folio_user, index_or_id
141
208
  )
142
- self.clean_user(folio_user)
209
+ self.clean_user(folio_user, index_or_id)
143
210
  results_file.write(f"{json.dumps(folio_user)}\n")
144
211
  if num_users == 1:
145
212
  logging.info("## First FOLIO user")
146
- logging.info(
147
- json.dumps(folio_user, indent=4, sort_keys=True)
148
- )
213
+ logging.info(json.dumps(folio_user, indent=4, sort_keys=True))
149
214
  self.mapper.migration_report.add_general_statistics(
150
- "Successful user transformations"
215
+ i18n.t("Successful user transformations")
151
216
  )
152
217
  if num_users % 1000 == 0:
153
218
  logging.info(f"{num_users} users processed.")
154
219
  except TransformationRecordFailedError as tre:
155
220
  self.mapper.migration_report.add_general_statistics(
156
- "Records failed"
157
- )
158
- Helper.log_data_issue(
159
- tre.index_or_id, tre.message, tre.data_value
221
+ i18n.t("Records failed")
160
222
  )
223
+ Helper.log_data_issue(tre.index_or_id, tre.message, tre.data_value)
161
224
  logging.error(tre)
162
225
  except TransformationProcessError as tpe:
163
- logging.error(tpe)
226
+ logging.critical(tpe)
164
227
  print(f"\n{tpe.message}: {tpe.data_value}")
165
228
  print("\nHalting")
166
229
  sys.exit(1)
@@ -172,63 +235,24 @@ class UserTransformer(MigrationTaskBase):
172
235
  logging.error(num_users)
173
236
  logging.error(json.dumps(legacy_user))
174
237
  self.mapper.migration_report.add_general_statistics(
175
- "Failed user transformations"
238
+ i18n.t("Failed user transformations")
176
239
  )
177
240
  logging.error(ee, exc_info=True)
178
241
 
179
242
  self.total_records = num_users
180
- except FileNotFoundError as fnfe:
243
+ except FileNotFoundError as fn:
181
244
  logging.exception("File not found")
182
- print(f"\n{fnfe}")
245
+ print(f"\n{fn}")
183
246
  sys.exit(1)
184
247
 
185
- @staticmethod
186
- def get_legacy_id_prop(record_map):
187
- field_map = {} # Map of folio_fields and source fields as an array
188
- for k in record_map["data"]:
189
- if not field_map.get(k["folio_field"]):
190
- field_map[k["folio_field"]] = [k["legacy_field"]]
191
- else:
192
- field_map[k["folio_field"]].append(k["legacy_field"])
193
- if "legacyIdentifier" not in field_map:
194
- raise TransformationProcessError(
195
- "",
196
- (
197
- "property legacyIdentifier is not in map. Add this property "
198
- "to the mapping file as if it was a FOLIO property"
199
- ),
200
- )
201
- try:
202
- legacy_id_property_name = field_map["legacyIdentifier"][0]
203
- logging.info(
204
- "Legacy identifier will be mapped from %s", legacy_id_property_name
205
- )
206
- return legacy_id_property_name
207
- except Exception as exception:
208
- raise TransformationProcessError(
209
- "",
210
- (
211
- f"property legacyIdentifier not setup in map: "
212
- f"{field_map.get('legacyIdentifier', '') ({exception})}"
213
- ),
214
- ) from exception
215
-
216
248
  def wrap_up(self):
217
- path = self.folder_structure.results_folder / "user_id_map.json"
218
- logging.info(
219
- f"Saving map of {len(self.mapper.legacy_id_map)} old and new IDs to {path}"
220
- )
221
-
222
- with open(path, "w+") as id_map_file:
223
- json.dump(self.mapper.legacy_id_map, id_map_file, indent=4)
224
- with open(
225
- self.folder_structure.migration_reports_file, "w"
226
- ) as migration_report_file:
227
- logging.info(
228
- "Writing migration- and mapping report to %s",
229
- self.folder_structure.migration_reports_file,
249
+ self.extradata_writer.flush()
250
+ with open(self.folder_structure.migration_reports_file, "w") as migration_report_file:
251
+ self.mapper.migration_report.write_migration_report(
252
+ i18n.t("Users transformation report"),
253
+ migration_report_file,
254
+ self.mapper.start_datetime,
230
255
  )
231
- self.mapper.migration_report.write_migration_report(migration_report_file)
232
256
  Helper.print_mapping_report(
233
257
  migration_report_file,
234
258
  self.total_records,
@@ -236,42 +260,59 @@ class UserTransformer(MigrationTaskBase):
236
260
  self.mapper.mapped_legacy_fields,
237
261
  )
238
262
  logging.info("All done!")
263
+ self.clean_out_empty_logs()
239
264
 
240
265
  @staticmethod
241
- def clean_user(folio_user):
242
- if addresses := folio_user.get("personal", {}).get("addresses", []):
243
- primary_address_exists = False
266
+ def clean_user(folio_user, index_or_id):
267
+ valid_addresses = remove_empty_addresses(folio_user)
268
+ # Make sure the user has exactly one primary address
269
+ if valid_addresses:
270
+ primary_true = find_primary_addresses(valid_addresses)
271
+ if len(primary_true) < 1:
272
+ valid_addresses[0]["primaryAddress"] = True
273
+ elif len(primary_true) > 1:
274
+ logging.log(
275
+ 26,
276
+ "DATA ISSUE\t%s\t%s\t%s",
277
+ index_or_id,
278
+ "Too many addresses mapped as primary. Setting first one to primary.",
279
+ primary_true,
280
+ )
281
+ for pt in primary_true[1:]:
282
+ pt["primaryAddress"] = False
283
+ folio_user["personal"]["addresses"] = valid_addresses
244
284
 
245
- for address in addresses:
246
- if "id" in address:
247
- del address["id"]
248
285
 
249
- if address["primaryAddress"] is True:
250
- primary_address_exists = True
251
-
252
- if not primary_address_exists:
253
- addresses[0]["primaryAddress"] = True
286
+ def print_email_warning():
287
+ tprint("\nEMAILS?\n", space=2)
254
288
 
255
289
 
256
- def print_email_warning():
257
- s = (
258
- " ______ __ __ _____ _ _____ ___ \n" # pylint: disable=anomalous-backslash-in-string
259
- " | ____| | \/ | /\ |_ _| | | / ____| |__ \ \n" # pylint: disable=anomalous-backslash-in-string
260
- " | |__ | \ / | / \ | | | | | (___ ) |\n" # pylint: disable=anomalous-backslash-in-string
261
- " | __| | |\/| | / /\ \ | | | | \___ \ / / \n" # pylint: disable=anomalous-backslash-in-string
262
- " | |____ | | | | / ____ \ _| |_ | |____ ____) | |_| \n" # pylint: disable=anomalous-backslash-in-string
263
- " |______| |_| |_| /_/ \_\ |_____| |______| |_____/ (_) \n" # pylint: disable=anomalous-backslash-in-string
264
- " \n" # pylint: disable=anomalous-backslash-in-string
265
- " \n"
266
- )
267
- print(s)
290
+ def remove_empty_addresses(folio_user):
291
+ valid_addresses = []
292
+ # Remove empty addresses
293
+ if addresses := folio_user.get("personal", {}).pop("addresses", []):
294
+ for address in addresses:
295
+ address_fields = [
296
+ x for x in address.keys() if x not in ["primaryAddress", "addressTypeId", "id"]
297
+ ]
298
+ if address_fields:
299
+ valid_addresses.append(address)
300
+ return valid_addresses
268
301
 
269
302
 
270
- def get_import_struct(batch) -> Dict:
271
- return {
272
- "source_type": "",
273
- "deactivateMissingUsers": False,
274
- "users": list(batch),
275
- "updateOnlyPresentFields": False,
276
- "totalRecords": len(batch),
277
- }
303
+ def find_primary_addresses(addresses):
304
+ primary_true = []
305
+ for address in addresses:
306
+ if "primaryAddress" not in address:
307
+ address["primaryAddress"] = False
308
+ elif (
309
+ isinstance(address["primaryAddress"], bool)
310
+ and address["primaryAddress"] is True
311
+ ) or (
312
+ isinstance(address["primaryAddress"], str)
313
+ and address["primaryAddress"].lower() == "true"
314
+ ):
315
+ primary_true.append(address)
316
+ else:
317
+ address["primaryAddress"] = False
318
+ return primary_true
@@ -0,0 +1,46 @@
1
+ from typing import Annotated
2
+
3
+ from humps import camelize
4
+ from pydantic import BaseModel
5
+ from pydantic import Field
6
+
7
+
8
+ def to_camel(string):
9
+ return camelize(string)
10
+
11
+
12
+ class AbstractTaskConfiguration(BaseModel):
13
+ """Abstract class for task configuration."""
14
+
15
+ name: Annotated[
16
+ str,
17
+ Field(
18
+ description=(
19
+ "Name of this migration task. The name is being used to call the specific "
20
+ "task, and to distinguish tasks of similar types"
21
+ )
22
+ ),
23
+ ]
24
+ migration_task_type: Annotated[
25
+ str,
26
+ Field(
27
+ title="Migration task type",
28
+ description=(
29
+ "The type of migration task you want to perform."
30
+ ),
31
+ ),
32
+ ]
33
+ ecs_tenant_id: Annotated[
34
+ str,
35
+ Field(
36
+ title="ECS tenant ID",
37
+ description=(
38
+ "The tenant ID to use when making requests to FOLIO APIs "
39
+ "for this task, if different from library configuration",
40
+ ),
41
+ ),
42
+ ] = ""
43
+
44
+ class Config:
45
+ alias_generator = to_camel
46
+ allow_population_by_field_name = True
File without changes