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,47 +1,218 @@
1
- '''Main "script."'''
2
1
  import csv
3
2
  import json
4
3
  import logging
5
- import sys
6
- from os import listdir
7
- from os.path import isfile
8
- from typing import List, Optional
9
- from pydantic import BaseModel
4
+ from typing import Annotated, List
10
5
 
6
+ import i18n
11
7
  from folio_uuid.folio_namespaces import FOLIONamespaces
12
- from folio_migration_tools.custom_exceptions import (
13
- TransformationProcessError,
14
- TransformationRecordFailedError,
15
- )
8
+ from pydantic import Field
9
+
10
+ from folio_migration_tools.custom_exceptions import TransformationProcessError
11
+ from folio_migration_tools.helper import Helper
16
12
  from folio_migration_tools.library_configuration import (
17
13
  FileDefinition,
18
14
  HridHandling,
19
15
  LibraryConfiguration,
20
16
  )
21
- from folio_migration_tools.marc_rules_transformation.holdings_processor import (
22
- HoldingsProcessor,
23
- )
24
17
  from folio_migration_tools.marc_rules_transformation.rules_mapper_holdings import (
25
18
  RulesMapperHoldings,
26
19
  )
27
- from pymarc import MARCReader
28
-
29
- from folio_migration_tools.migration_tasks.migration_task_base import MigrationTaskBase
20
+ from folio_migration_tools.migration_tasks.migration_task_base import (
21
+ MarcTaskConfigurationBase,
22
+ MigrationTaskBase
23
+ )
30
24
 
31
25
 
32
26
  class HoldingsMarcTransformer(MigrationTaskBase):
33
- class TaskConfiguration(BaseModel):
34
- name: str
35
- legacy_id_marc_path: str
36
- migration_task_type: str
37
- use_tenant_mapping_rules: bool
38
- hrid_handling: HridHandling
39
- files: List[FileDefinition]
40
- mfhd_mapping_file_name: str
41
- location_map_file_name: str
42
- default_call_number_type_name: str
43
- fallback_holdings_type_id: str
44
- create_source_records: Optional[bool] = False
27
+ class TaskConfiguration(MarcTaskConfigurationBase):
28
+ name: Annotated[
29
+ str,
30
+ Field(
31
+ description=(
32
+ "Name of this migration task. The name is being used to call the specific "
33
+ "task, and to distinguish tasks of similar types"
34
+ )
35
+ ),
36
+ ]
37
+ migration_task_type: Annotated[
38
+ str,
39
+ Field(
40
+ title="Migration task type",
41
+ description=(
42
+ "The type of migration task you want to perform"
43
+ ),
44
+ ),
45
+ ]
46
+ files: Annotated[
47
+ List[FileDefinition],
48
+ Field(
49
+ title="Source files",
50
+ description=(
51
+ "List of MARC21 files with holdings records"
52
+ ),
53
+ ),
54
+ ]
55
+ hrid_handling: Annotated[
56
+ HridHandling,
57
+ Field(
58
+ title="HRID Handling",
59
+ description=(
60
+ "Setting to default will make FOLIO generate HRIDs and move the existing "
61
+ "001:s into a 035, concatenated with the 003. Choosing preserve001 means "
62
+ "the 001:s will remain in place, and that they will also become the HRIDs"
63
+ ),
64
+ ),
65
+ ] = HridHandling.default
66
+ holdings_type_uuid_for_boundwiths: Annotated[
67
+ str,
68
+ Field(
69
+ title="Holdings Type for Boundwith Holdings",
70
+ description=(
71
+ "UUID for a Holdings type (set in Settings->Inventory) "
72
+ "for Bound-with Holdings)"
73
+ ),
74
+ ),
75
+ ] = ""
76
+ boundwith_relationship_file_path: Annotated[
77
+ str,
78
+ Field(
79
+ title="Boundwith relationship file path",
80
+ description=(
81
+ "Path to a file outlining Boundwith relationships, in the style of Voyager."
82
+ " A TSV file with MFHD_ID and BIB_ID headers and values"
83
+ ),
84
+ ),
85
+ ] = ""
86
+ update_hrid_settings: Annotated[
87
+ bool,
88
+ Field(
89
+ title="Update HRID settings",
90
+ description="At the end of the run, update FOLIO with the HRID settings",
91
+ ),
92
+ ] = True
93
+ reset_hrid_settings: Annotated[
94
+ bool,
95
+ Field(
96
+ title="Reset HRID settings",
97
+ description=(
98
+ "Setting to true means the task will "
99
+ "reset the HRID counters for this particular record type"
100
+ ),
101
+ ),
102
+ ] = False
103
+ legacy_id_marc_path: Annotated[
104
+ str,
105
+ Field(
106
+ title="Path to legacy id in the records",
107
+ description=(
108
+ "The path to the field where the legacy id is located. "
109
+ "Example syntax: '001' or '951$c'"
110
+ ),
111
+ ),
112
+ ]
113
+ deduplicate_holdings_statements: Annotated[
114
+ bool,
115
+ Field(
116
+ title="Deduplicate holdings statements",
117
+ description=(
118
+ "If set to False, duplicate holding statements within the same record will "
119
+ "remain in place"
120
+ ),
121
+ ),
122
+ ] = True
123
+ location_map_file_name: Annotated[
124
+ str,
125
+ Field(
126
+ title="Path to location map file",
127
+ description="Must be a TSV file located in the mapping_files folder",
128
+ ),
129
+ ]
130
+ default_call_number_type_name: Annotated[
131
+ str,
132
+ Field(
133
+ title="Default call_number type name",
134
+ description="The name of the call_number type that will be used as fallback",
135
+ ),
136
+ ]
137
+ fallback_holdings_type_id: Annotated[
138
+ str,
139
+ Field(
140
+ title="Fallback holdings type id",
141
+ description="The UUID of the Holdings type that will be used for unmapped values",
142
+ ),
143
+ ]
144
+ supplemental_mfhd_mapping_rules_file: Annotated[
145
+ str,
146
+ Field(
147
+ title="Supplemental MFHD mapping rules file",
148
+ description=(
149
+ "The name of the file in the mapping_files directory "
150
+ "containing supplemental MFHD mapping rules"
151
+ ),
152
+ ),
153
+ ] = ""
154
+ include_mrk_statements: Annotated[
155
+ bool,
156
+ Field(
157
+ title="Include MARC statements (MRK-format) as staff-only Holdings notes",
158
+ description=(
159
+ "If set to true, the MARC statements "
160
+ "will be included in the output as MARC Maker format fields. "
161
+ "If set to false (default), the MARC statements "
162
+ "will not be included in the output."
163
+ ),
164
+ ),
165
+ ] = False
166
+ mrk_holdings_note_type: Annotated[
167
+ str,
168
+ Field(
169
+ title="MARC Holdings Note type",
170
+ description=(
171
+ "The name of the note type to use for MARC (MRK) statements. "
172
+ ),
173
+ ),
174
+ ] = "Original MARC holdings statements"
175
+ include_mfhd_mrk_as_note: Annotated[
176
+ bool,
177
+ Field(
178
+ title="Include MARC Record (as MARC Maker Representation) as note",
179
+ description=(
180
+ "If set to true, the MARC statements will be included in the output as a "
181
+ "(MRK) note. If set to false (default), the MARC statements will not be "
182
+ "included in the output."
183
+ ),
184
+ ),
185
+ ] = False
186
+ mfhd_mrk_note_type: Annotated[
187
+ str,
188
+ Field(
189
+ title="MARC Record (as MARC Maker Representation) note type",
190
+ description=(
191
+ "The name of the note type to use for MFHD (MRK) note. "
192
+ ),
193
+ ),
194
+ ] = "Original MFHD Record"
195
+ include_mfhd_mrc_as_note: Annotated[
196
+ bool,
197
+ Field(
198
+ title="Include MARC Record (as MARC21 decoded string) as note",
199
+ description=(
200
+ "If set to true, the MARC record will be included in the output as a "
201
+ "decoded binary MARC21 record. If set to false (default), "
202
+ "the MARC record will not be "
203
+ "included in the output."
204
+ ),
205
+ ),
206
+ ] = False
207
+ mfhd_mrc_note_type: Annotated[
208
+ str,
209
+ Field(
210
+ title="MARC Record (as MARC21 decoded string) note type",
211
+ description=(
212
+ "The name of the note type to use for MFHD (MRC) note. "
213
+ ),
214
+ ),
215
+ ] = "Original MFHD (MARC21)"
45
216
 
46
217
  @staticmethod
47
218
  def get_object_type() -> FOLIONamespaces:
@@ -51,11 +222,21 @@ class HoldingsMarcTransformer(MigrationTaskBase):
51
222
  self,
52
223
  task_config: TaskConfiguration,
53
224
  library_config: LibraryConfiguration,
225
+ folio_client,
54
226
  use_logging: bool = True,
55
227
  ):
56
228
  csv.register_dialect("tsv", delimiter="\t")
57
- super().__init__(library_config, task_config, use_logging)
58
- self.task_config = task_config
229
+ super().__init__(library_config, task_config, folio_client, use_logging)
230
+ if self.task_configuration.statistical_codes_map_file_name:
231
+ statcode_mapping = self.load_ref_data_mapping_file(
232
+ "statisticalCodeIds",
233
+ self.folder_structure.mapping_files_folder
234
+ / self.task_configuration.statistical_codes_map_file_name,
235
+ [],
236
+ False,
237
+ )
238
+ else:
239
+ statcode_mapping = None
59
240
  self.holdings_types = list(
60
241
  self.folio_client.folio_get_all("/holdings-types", "holdingsTypes")
61
242
  )
@@ -63,111 +244,136 @@ class HoldingsMarcTransformer(MigrationTaskBase):
63
244
  (
64
245
  h
65
246
  for h in self.holdings_types
66
- if h["id"] == self.task_config.fallback_holdings_type_id
247
+ if h["id"] == self.task_configuration.fallback_holdings_type_id
67
248
  ),
68
- "",
249
+ {"name": ""},
69
250
  )
70
251
  if not self.default_holdings_type:
71
252
  raise TransformationProcessError(
72
253
  "",
73
254
  (
74
- f"Holdings type with ID {self.task_config.fallback_holdings_type_id}"
255
+ f"Holdings type with ID {self.task_configuration.fallback_holdings_type_id}"
75
256
  " not found in FOLIO."
76
257
  ),
77
258
  )
78
259
  logging.info(
79
260
  "%s will be used as default holdings type",
80
- self.default_holdings_type["name"],
81
- )
82
- self.instance_id_map = self.load_id_map(
83
- self.folder_structure.instance_id_map_path, True
261
+ self.default_holdings_type.get("name", ""),
84
262
  )
85
- logging.info("%s Instance ids in map", len(self.instance_id_map))
86
- logging.info("Init done")
87
263
 
88
- def do_work(self):
89
- files = self.list_source_files()
90
- loc_map_path = (
264
+ # Load Boundwith relationship map
265
+ self.boundwith_relationship_map_rows = []
266
+ if self.task_configuration.boundwith_relationship_file_path:
267
+ try:
268
+ with open(
269
+ self.folder_structure.legacy_records_folder
270
+ / self.task_configuration.boundwith_relationship_file_path
271
+ ) as boundwith_relationship_file:
272
+ self.boundwith_relationship_map_rows = list(
273
+ csv.DictReader(boundwith_relationship_file, dialect="tsv")
274
+ )
275
+ logging.info(
276
+ "Rows in Bound with relationship map: %s",
277
+ len(self.boundwith_relationship_map_rows),
278
+ )
279
+ except FileNotFoundError:
280
+ raise TransformationProcessError(
281
+ "",
282
+ i18n.t("Provided boundwith relationship file not found"),
283
+ self.task_configuration.boundwith_relationship_file_path,
284
+ )
285
+
286
+ location_map_path = (
91
287
  self.folder_structure.mapping_files_folder
92
- / self.task_config.location_map_file_name
288
+ / self.task_configuration.location_map_file_name
93
289
  )
94
- map_path = (
95
- self.folder_structure.mapping_files_folder
96
- / self.task_config.mfhd_mapping_file_name
290
+ with open(location_map_path) as location_map_file:
291
+ self.location_map = list(csv.DictReader(location_map_file, dialect="tsv"))
292
+ logging.info("Locations in map: %s", len(self.location_map))
293
+
294
+ self.check_source_files(
295
+ self.folder_structure.legacy_records_folder, self.task_configuration.files
97
296
  )
98
- with open(loc_map_path) as loc_map_f, open(map_path) as map_f:
99
- location_map = list(csv.DictReader(loc_map_f, dialect="tsv"))
100
- logging.info("Locations in map: %s", len(location_map))
101
- rules_file = json.load(map_f)
102
- logging.info("Default location code %s", rules_file["defaultLocationCode"])
103
- mapper = RulesMapperHoldings(
104
- self.folio_client,
105
- self.instance_id_map,
106
- location_map,
107
- self.task_config,
108
- self.library_configuration,
109
- )
110
- mapper.mappings = rules_file["rules"]
111
- processor = HoldingsProcessor(mapper, self.folder_structure)
112
- for file_def in files:
113
- self.process_single_file(file_def, processor)
114
- processor.wrap_up()
297
+ self.instance_id_map = self.load_instance_id_map(True)
298
+ self.mapper = RulesMapperHoldings(
299
+ self.folio_client,
300
+ self.location_map,
301
+ self.task_configuration,
302
+ self.library_configuration,
303
+ self.instance_id_map,
304
+ self.boundwith_relationship_map_rows,
305
+ statcode_mapping
306
+ )
307
+ self.add_supplemental_mfhd_mappings()
308
+ if (
309
+ self.task_configuration.reset_hrid_settings
310
+ and self.task_configuration.update_hrid_settings
311
+ ):
312
+ self.mapper.hrid_handler.reset_holdings_hrid_counter()
313
+ logging.info("%s Instance ids in map", len(self.instance_id_map))
314
+ logging.info("Init done")
115
315
 
116
- def list_source_files(self):
117
- files = [
118
- f
119
- for f in self.task_config.files
120
- if isfile(self.folder_structure.legacy_records_folder / f.file_name)
121
- ]
122
- if not any(files):
123
- ret_str = ",".join(f.file_name for f in self.task_config.files)
124
- raise TransformationProcessError(
125
- "",
126
- f"Files {ret_str} not found in {self.folder_structure.data_folder / 'holdings'}",
127
- )
316
+ def add_supplemental_mfhd_mappings(self):
317
+ if self.task_configuration.supplemental_mfhd_mapping_rules_file:
318
+ try:
319
+ with open(
320
+ (
321
+ self.folder_structure.mapping_files_folder
322
+ / self.task_configuration.supplemental_mfhd_mapping_rules_file
323
+ ),
324
+ "r",
325
+ ) as new_rules_file:
326
+ new_rules = json.load(new_rules_file)
327
+ if not isinstance(new_rules, dict):
328
+ raise TransformationProcessError(
329
+ "",
330
+ "Supplemental MFHD mapping rules file must contain a dictionary",
331
+ json.dumps(new_rules),
332
+ )
333
+ except FileNotFoundError:
334
+ raise TransformationProcessError(
335
+ "",
336
+ "Provided supplemental MFHD mapping rules file not found",
337
+ self.task_configuration.supplemental_mfhd_mapping_rules_file,
338
+ )
339
+ else:
340
+ new_rules = {}
341
+ self.mapper.integrate_supplemental_mfhd_mappings(new_rules)
128
342
 
129
- return files
343
+ def do_work(self):
344
+ self.do_work_marc_transformer()
130
345
 
131
- def process_single_file(
132
- self, file_def: FileDefinition, processor: HoldingsProcessor
133
- ):
134
- try:
346
+ def wrap_up(self):
347
+ logging.info("Done. Transformer Wrapping up...")
348
+ self.extradata_writer.flush()
349
+ self.processor.wrap_up()
350
+ if self.mapper.boundwith_relationship_map:
135
351
  with open(
136
- self.folder_structure.legacy_records_folder / file_def.file_name,
137
- "rb",
138
- ) as marc_file:
139
- reader = MARCReader(marc_file, to_unicode=True, permissive=True)
140
- reader.hide_utf8_warnings = True
141
- reader.force_utf8 = True
142
- logging.info("Running %s", file_def.file_name)
143
- read_records(reader, processor, file_def)
144
- except TransformationProcessError as tpe:
145
- logging.critical(tpe)
146
- sys.exit(1)
147
- except Exception:
148
- logging.exception(
149
- "Failure in Main: %s", file_def.file_name, stack_info=True
150
- )
352
+ self.folder_structure.boundwith_relationships_map_path, "w+"
353
+ ) as boundwith_relationship_file:
354
+ logging.info(
355
+ "Writing boundwiths relationship map to %s",
356
+ boundwith_relationship_file.name,
357
+ )
358
+ for key, val in self.mapper.boundwith_relationship_map.items():
359
+ boundwith_relationship_file.write(json.dumps((key, val)) + "\n")
151
360
 
152
- def wrap_up(self):
153
- logging.info("wapping up")
361
+ with open(self.folder_structure.migration_reports_file, "w+") as report_file:
362
+ self.mapper.migration_report.write_migration_report(
363
+ i18n.t("Bibliographic records transformation report"),
364
+ report_file,
365
+ self.start_datetime,
366
+ )
367
+ Helper.print_mapping_report(
368
+ report_file,
369
+ self.mapper.parsed_records,
370
+ self.mapper.mapped_folio_fields,
371
+ self.mapper.mapped_legacy_fields,
372
+ )
154
373
 
374
+ logging.info(
375
+ "Done. Transformation report written to %s",
376
+ self.folder_structure.migration_reports_file.name,
377
+ )
155
378
 
156
- def read_records(reader, processor: HoldingsProcessor, file_def: FileDefinition):
157
- for idx, record in enumerate(reader):
158
- try:
159
- if record is None:
160
- processor.mapper.migration_report.add_general_statistics(
161
- "Records with encoding errors. See data issues log for details"
162
- )
163
- raise TransformationRecordFailedError(
164
- f"Index in file:{idx}",
165
- f"MARC parsing error: {reader.current_exception}",
166
- f"{reader.current_chunk}",
167
- )
168
- else:
169
- processor.process_record(record, file_def)
170
- except TransformationRecordFailedError as error:
171
- error.log_it()
172
- except ValueError as error:
173
- logging.error(error)
379
+ self.clean_out_empty_logs()