folio-migration-tools 1.9.0rc2__tar.gz → 1.9.0rc4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/PKG-INFO +1 -1
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/pyproject.toml +5 -1
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/holdings_mapper.py +1 -1
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/order_mapper.py +8 -4
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +22 -2
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/batch_poster.py +143 -26
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/courses_migrator.py +54 -8
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/holdings_csv_transformer.py +102 -14
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/holdings_marc_transformer.py +46 -4
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/items_transformer.py +133 -20
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/loans_migrator.py +61 -9
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/migration_task_base.py +104 -11
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/orders_transformer.py +107 -14
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/organization_transformer.py +79 -14
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/requests_migrator.py +56 -7
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/reserves_migrator.py +26 -4
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/user_transformer.py +88 -18
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/task_configuration.py +2 -2
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/transaction_migration/legacy_loan.py +13 -1
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/transaction_migration/legacy_reserve.py +3 -5
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/LICENSE +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/README.md +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/__init__.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/__main__.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/circulation_helper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/colors.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/config_file_load.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/custom_dict.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/custom_exceptions.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/extradata_writer.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/folder_structure.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/helper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/holdings_helper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/i18n_config.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/library_configuration.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapper_base.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/__init__.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/courses_mapper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/item_mapper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/notes_mapper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/organization_mapper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/mapping_file_transformation/user_mapper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/__init__.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/conditions.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/hrid_handler.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/marc_file_processor.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_report.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/__init__.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/authority_transformer.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/bibs_transformer.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/test_infrastructure/__init__.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/test_infrastructure/mocked_classes.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/transaction_migration/__init__.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/transaction_migration/legacy_request.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/transaction_migration/transaction_result.py +0 -0
- {folio_migration_tools-1.9.0rc2 → folio_migration_tools-1.9.0rc4}/src/folio_migration_tools/translations/en.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "folio_migration_tools"
|
|
3
|
-
version = "1.9.
|
|
3
|
+
version = "1.9.0rc4"
|
|
4
4
|
description = "A tool allowing you to migrate data from legacy ILS:s (Library systems) into FOLIO LSP"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Theodor Tolstoy", email = "github.teddes@tolstoy.se"},
|
|
@@ -77,6 +77,10 @@ pandas = "^1.5.3"
|
|
|
77
77
|
types-requests = "^2.28.11.17"
|
|
78
78
|
types-python-dateutil = "^2.8.19.11"
|
|
79
79
|
ipykernel = "^6.29.5"
|
|
80
|
+
pytest-asyncio = "^0.23.0"
|
|
80
81
|
|
|
81
82
|
[tool.poetry.extras]
|
|
82
83
|
docs = ["m2r", "sphinx", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "toml"]
|
|
84
|
+
|
|
85
|
+
[tool.poetry.requires-plugins]
|
|
86
|
+
poetry-plugin-export = ">=1.8"
|
|
@@ -101,7 +101,7 @@ class HoldingsMapper(MappingFileMapperBase):
|
|
|
101
101
|
if legacy_value.startswith("[") and len(legacy_value.split(",")) == 1:
|
|
102
102
|
try:
|
|
103
103
|
legacy_value = ast.literal_eval(str(legacy_value))[0]
|
|
104
|
-
except SyntaxError:
|
|
104
|
+
except (SyntaxError, ValueError):
|
|
105
105
|
return legacy_value
|
|
106
106
|
return legacy_value
|
|
107
107
|
|
|
@@ -25,7 +25,6 @@ from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class CompositeOrderMapper(MappingFileMapperBase):
|
|
28
|
-
VALID_PO_NUMBER_CHARACTERS = r"[A-Za-z0-9]"
|
|
29
28
|
|
|
30
29
|
def __init__(
|
|
31
30
|
self,
|
|
@@ -270,9 +269,9 @@ class CompositeOrderMapper(MappingFileMapperBase):
|
|
|
270
269
|
and "$ref" in object_schema
|
|
271
270
|
and object_schema["type"] == "object"
|
|
272
271
|
):
|
|
273
|
-
|
|
272
|
+
object_schema["properties"] = CompositeOrderMapper.inject_schema_by_ref(
|
|
274
273
|
submodule_path, github_headers, object_schema
|
|
275
|
-
).get("properties")
|
|
274
|
+
).get("properties", {})#TODO: Investigate new CustomFields schema and figure out how to actually handle it
|
|
276
275
|
|
|
277
276
|
for property_name_level1, property_level1 in object_schema.get(
|
|
278
277
|
"properties", {}
|
|
@@ -385,7 +384,7 @@ class CompositeOrderMapper(MappingFileMapperBase):
|
|
|
385
384
|
index_or_id: str,
|
|
386
385
|
po_number: str,
|
|
387
386
|
):
|
|
388
|
-
if
|
|
387
|
+
if not self.is_valid_po_number(po_number):
|
|
389
388
|
self.migration_report.add(
|
|
390
389
|
"PurchaseOrderVendorLinking",
|
|
391
390
|
i18n.t("RECORD FAILED: PO number has invalid character(s)"),
|
|
@@ -396,6 +395,11 @@ class CompositeOrderMapper(MappingFileMapperBase):
|
|
|
396
395
|
po_number,
|
|
397
396
|
)
|
|
398
397
|
|
|
398
|
+
@staticmethod
|
|
399
|
+
def is_valid_po_number(po_number: str) -> bool:
|
|
400
|
+
valid_po_number_characters = r"[A-Za-z0-9]"
|
|
401
|
+
return re.sub(valid_po_number_characters, "", po_number) == ""
|
|
402
|
+
|
|
399
403
|
def get_matching_record_from_folio(
|
|
400
404
|
self,
|
|
401
405
|
index_or_id,
|
|
@@ -69,7 +69,6 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
69
69
|
logging.info("Fetching mapping rules from the tenant")
|
|
70
70
|
rules_endpoint = "/mapping-rules/marc-holdings"
|
|
71
71
|
self.mappings = self.folio_client.folio_get_single_object(rules_endpoint)
|
|
72
|
-
self.fix_853_bug_in_rules()
|
|
73
72
|
|
|
74
73
|
def fix_853_bug_in_rules(self):
|
|
75
74
|
f852_mappings = self.mappings["852"]
|
|
@@ -90,6 +89,27 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
90
89
|
)
|
|
91
90
|
self.mappings["852"] = new_852_mapping
|
|
92
91
|
|
|
92
|
+
def integrate_supplemental_mfhd_mappings(self, new_rules={}):
|
|
93
|
+
try:
|
|
94
|
+
self.mappings.update(new_rules)
|
|
95
|
+
self.fix_853_bug_in_rules()
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise TransformationProcessError(
|
|
98
|
+
"",
|
|
99
|
+
"Failed to integrate supplemental mfhd mappings",
|
|
100
|
+
str(e),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def prep_852_notes(self, marc_record: Record):
|
|
104
|
+
for field in marc_record.get_fields("852"):
|
|
105
|
+
field.subfields.sort(key=lambda x: x[0])
|
|
106
|
+
new_952 = Field(
|
|
107
|
+
tag="952",
|
|
108
|
+
indicators=["f", "f"],
|
|
109
|
+
subfields=field.subfields
|
|
110
|
+
)
|
|
111
|
+
marc_record.add_ordered_field(new_952)
|
|
112
|
+
|
|
93
113
|
def parse_record(
|
|
94
114
|
self, marc_record: Record, file_def: FileDefinition, legacy_ids: List[str]
|
|
95
115
|
) -> list[dict]:
|
|
@@ -111,7 +131,7 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
111
131
|
|
|
112
132
|
self.print_progress()
|
|
113
133
|
folio_holding = self.perform_initial_preparation(marc_record, legacy_ids)
|
|
114
|
-
|
|
134
|
+
self.prep_852_notes(marc_record)
|
|
115
135
|
self.migration_report.add("RecordStatus", marc_record.leader[5])
|
|
116
136
|
ignored_subsequent_fields: set = set()
|
|
117
137
|
num_852s = 0
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import copy
|
|
2
3
|
import json
|
|
3
4
|
import logging
|
|
@@ -50,46 +51,84 @@ class BatchPoster(MigrationTaskBase):
|
|
|
50
51
|
"""
|
|
51
52
|
|
|
52
53
|
class TaskConfiguration(AbstractTaskConfiguration):
|
|
53
|
-
name:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
name: Annotated[
|
|
55
|
+
str,
|
|
56
|
+
Field(
|
|
57
|
+
title="Task name",
|
|
58
|
+
description="The name of the task",
|
|
59
|
+
),
|
|
60
|
+
]
|
|
61
|
+
migration_task_type: Annotated[
|
|
62
|
+
str,
|
|
63
|
+
Field(
|
|
64
|
+
title="Migration task type",
|
|
65
|
+
description="The type of migration task",
|
|
66
|
+
),
|
|
67
|
+
]
|
|
68
|
+
object_type: Annotated[
|
|
69
|
+
str,
|
|
70
|
+
Field(
|
|
71
|
+
title="Object type",
|
|
72
|
+
description=(
|
|
73
|
+
"The type of object being migrated"
|
|
74
|
+
"Examples of possible values: "
|
|
75
|
+
"'Extradata', 'SRS', Instances', 'Holdings', 'Items'"
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
]
|
|
79
|
+
files: Annotated[
|
|
80
|
+
List[FileDefinition],
|
|
81
|
+
Field(
|
|
82
|
+
title="List of files",
|
|
83
|
+
description="List of files to be processed",
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
batch_size: Annotated[
|
|
87
|
+
int,
|
|
88
|
+
Field(
|
|
89
|
+
title="Batch size",
|
|
90
|
+
description="The batch size for processing files",
|
|
91
|
+
),
|
|
92
|
+
]
|
|
58
93
|
rerun_failed_records: Annotated[
|
|
59
94
|
bool,
|
|
60
95
|
Field(
|
|
96
|
+
title="Rerun failed records",
|
|
61
97
|
description=(
|
|
62
|
-
"Toggles whether or not BatchPoster should try to rerun
|
|
63
|
-
"just leave the failing records on disk."
|
|
64
|
-
)
|
|
98
|
+
"Toggles whether or not BatchPoster should try to rerun "
|
|
99
|
+
"failed batches or just leave the failing records on disk."
|
|
100
|
+
),
|
|
65
101
|
),
|
|
66
102
|
] = True
|
|
67
103
|
use_safe_inventory_endpoints: Annotated[
|
|
68
104
|
bool,
|
|
69
105
|
Field(
|
|
106
|
+
title="Use safe inventory endpoints",
|
|
70
107
|
description=(
|
|
71
|
-
"Toggles the use of the safe/unsafe Inventory storage
|
|
72
|
-
"Unsafe circumvents the Optimistic locking
|
|
73
|
-
"True (using the 'safe' options)"
|
|
74
|
-
)
|
|
108
|
+
"Toggles the use of the safe/unsafe Inventory storage "
|
|
109
|
+
"endpoints. Unsafe circumvents the Optimistic locking "
|
|
110
|
+
"in FOLIO. Defaults to True (using the 'safe' options)"
|
|
111
|
+
),
|
|
75
112
|
),
|
|
76
113
|
] = True
|
|
77
114
|
extradata_endpoints: Annotated[
|
|
78
115
|
dict,
|
|
79
116
|
Field(
|
|
117
|
+
title="Extradata endpoints",
|
|
80
118
|
description=(
|
|
81
119
|
"A dictionary of extradata endpoints. "
|
|
82
120
|
"The key is the object type and the value is the endpoint"
|
|
83
|
-
)
|
|
121
|
+
),
|
|
84
122
|
),
|
|
85
123
|
] = {}
|
|
86
124
|
upsert: Annotated[
|
|
87
125
|
bool,
|
|
88
126
|
Field(
|
|
127
|
+
title="Upsert",
|
|
89
128
|
description=(
|
|
90
|
-
"Toggles whether or not to use the upsert feature
|
|
91
|
-
"endpoints. Defaults to False"
|
|
92
|
-
)
|
|
129
|
+
"Toggles whether or not to use the upsert feature "
|
|
130
|
+
"of the Inventory storage endpoints. Defaults to False"
|
|
131
|
+
),
|
|
93
132
|
),
|
|
94
133
|
] = False
|
|
95
134
|
|
|
@@ -188,9 +227,10 @@ class BatchPoster(MigrationTaskBase):
|
|
|
188
227
|
|
|
189
228
|
except Exception as ee:
|
|
190
229
|
if "idx" in locals() and self.task_configuration.files[idx:]:
|
|
191
|
-
for
|
|
230
|
+
for file_def in self.task_configuration.files[idx:]:
|
|
231
|
+
path = self.folder_structure.results_folder / file_def.file_name
|
|
192
232
|
try:
|
|
193
|
-
with open(
|
|
233
|
+
with open(path, "r") as failed_file:
|
|
194
234
|
failed_file.seek(self.processed)
|
|
195
235
|
failed_recs_file.write(failed_file.read())
|
|
196
236
|
self.processed = 0
|
|
@@ -209,22 +249,84 @@ class BatchPoster(MigrationTaskBase):
|
|
|
209
249
|
if self.task_configuration.object_type == "SRS":
|
|
210
250
|
self.commit_snapshot()
|
|
211
251
|
|
|
252
|
+
@staticmethod
|
|
253
|
+
def set_consortium_source(json_rec):
|
|
254
|
+
if json_rec['source'] == 'MARC':
|
|
255
|
+
json_rec['source'] = 'CONSORTIUM-MARC'
|
|
256
|
+
elif json_rec['source'] == 'FOLIO':
|
|
257
|
+
json_rec['source'] = 'CONSORTIUM-FOLIO'
|
|
258
|
+
|
|
259
|
+
def set_version(self, batch, query_api, object_type) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Synchronous wrapper for set_version_async
|
|
262
|
+
"""
|
|
263
|
+
loop = asyncio.get_event_loop()
|
|
264
|
+
if loop.is_running():
|
|
265
|
+
loop.run_until_complete(self.set_version_async(batch, query_api, object_type))
|
|
266
|
+
else:
|
|
267
|
+
asyncio.run(self.set_version_async(batch, query_api, object_type))
|
|
268
|
+
|
|
269
|
+
async def set_version_async(self, batch, query_api, object_type) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Fetches the current version of the records in the batch, if the record exists in FOLIO
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
batch (list): List of records to fetch versions for
|
|
275
|
+
query_api (str): The query API endpoint to use
|
|
276
|
+
object_type (str): The key in the API response that contains the records
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
None
|
|
280
|
+
"""
|
|
281
|
+
fetch_batch_size = 90
|
|
282
|
+
fetch_tasks = []
|
|
283
|
+
updates = {}
|
|
284
|
+
async with httpx.AsyncClient(base_url=self.folio_client.okapi_url) as client:
|
|
285
|
+
for i in range(0, len(batch), fetch_batch_size):
|
|
286
|
+
batch_slice = batch[i:i + fetch_batch_size]
|
|
287
|
+
fetch_tasks.append(
|
|
288
|
+
client.get(
|
|
289
|
+
query_api,
|
|
290
|
+
params={
|
|
291
|
+
"query": f"id==({' OR '.join([record['id'] for record in batch_slice if 'id' in record])})",
|
|
292
|
+
"limit": fetch_batch_size
|
|
293
|
+
},
|
|
294
|
+
headers=self.folio_client.okapi_headers
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
responses = await asyncio.gather(*fetch_tasks)
|
|
298
|
+
|
|
299
|
+
for response in responses:
|
|
300
|
+
if response.status_code == 200:
|
|
301
|
+
response_json = await response.json()
|
|
302
|
+
for record in response_json[object_type]:
|
|
303
|
+
updates[record["id"]] = {
|
|
304
|
+
"_version": record["_version"],
|
|
305
|
+
}
|
|
306
|
+
if "status" in record:
|
|
307
|
+
updates[record["id"]]["status"] = record["status"]
|
|
308
|
+
else:
|
|
309
|
+
logging.error(
|
|
310
|
+
"Failed to fetch current records. HTTP %s\t%s",
|
|
311
|
+
response.status_code,
|
|
312
|
+
response.text,
|
|
313
|
+
)
|
|
314
|
+
for record in batch:
|
|
315
|
+
if record["id"] in updates:
|
|
316
|
+
record.update(updates[record["id"]])
|
|
317
|
+
|
|
212
318
|
def post_record_batch(self, batch, failed_recs_file, row):
|
|
213
319
|
json_rec = json.loads(row.split("\t")[-1])
|
|
214
|
-
if
|
|
215
|
-
self.
|
|
216
|
-
and not self.task_configuration.use_safe_inventory_endpoints
|
|
217
|
-
):
|
|
218
|
-
self.migration_report.add_general_statistics(
|
|
219
|
-
i18n.t("Set _version to -1 to enable upsert")
|
|
220
|
-
)
|
|
221
|
-
json_rec["_version"] = -1
|
|
320
|
+
if self.task_configuration.object_type == "ShadowInstances":
|
|
321
|
+
self.set_consortium_source(json_rec)
|
|
222
322
|
if self.task_configuration.object_type == "SRS":
|
|
223
323
|
json_rec["snapshotId"] = self.snapshot_id
|
|
224
324
|
if self.processed == 1:
|
|
225
325
|
logging.info(json.dumps(json_rec, indent=True))
|
|
226
326
|
batch.append(json_rec)
|
|
227
327
|
if len(batch) == int(self.batch_size):
|
|
328
|
+
if self.query_params.get("upsert", False) and self.api_info.get("query_endpoint", ""):
|
|
329
|
+
self.set_version(batch, self.api_info['query_endpoint'], self.api_info['object_name'])
|
|
228
330
|
self.post_batch(batch, failed_recs_file, self.processed)
|
|
229
331
|
batch = []
|
|
230
332
|
return batch
|
|
@@ -609,6 +711,7 @@ def get_api_info(object_type: str, use_safe: bool = True):
|
|
|
609
711
|
if use_safe
|
|
610
712
|
else "/item-storage/batch/synchronous-unsafe"
|
|
611
713
|
),
|
|
714
|
+
"query_endpoint": "/item-storage/items",
|
|
612
715
|
"is_batch": True,
|
|
613
716
|
"total_records": False,
|
|
614
717
|
"addSnapshotId": False,
|
|
@@ -621,12 +724,26 @@ def get_api_info(object_type: str, use_safe: bool = True):
|
|
|
621
724
|
if use_safe
|
|
622
725
|
else "/holdings-storage/batch/synchronous-unsafe"
|
|
623
726
|
),
|
|
727
|
+
"query_endpoint": "/holdings-storage/holdings",
|
|
624
728
|
"is_batch": True,
|
|
625
729
|
"total_records": False,
|
|
626
730
|
"addSnapshotId": False,
|
|
627
731
|
"supports_upsert": True,
|
|
628
732
|
},
|
|
629
733
|
"Instances": {
|
|
734
|
+
"object_name": "instances",
|
|
735
|
+
"api_endpoint": (
|
|
736
|
+
"/instance-storage/batch/synchronous"
|
|
737
|
+
if use_safe
|
|
738
|
+
else "/instance-storage/batch/synchronous-unsafe"
|
|
739
|
+
),
|
|
740
|
+
"query_endpoint": "/instance-storage/instances",
|
|
741
|
+
"is_batch": True,
|
|
742
|
+
"total_records": False,
|
|
743
|
+
"addSnapshotId": False,
|
|
744
|
+
"supports_upsert": True,
|
|
745
|
+
},
|
|
746
|
+
"ShadowInstances": {
|
|
630
747
|
"object_name": "instances",
|
|
631
748
|
"api_endpoint": (
|
|
632
749
|
"/instance-storage/batch/synchronous"
|
|
@@ -4,7 +4,8 @@ import logging
|
|
|
4
4
|
import sys
|
|
5
5
|
import time
|
|
6
6
|
import traceback
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Optional, Annotated
|
|
8
|
+
from pydantic import Field
|
|
8
9
|
|
|
9
10
|
import i18n
|
|
10
11
|
from folio_uuid.folio_namespaces import FOLIONamespaces
|
|
@@ -29,13 +30,58 @@ from folio_migration_tools.task_configuration import AbstractTaskConfiguration
|
|
|
29
30
|
|
|
30
31
|
class CoursesMigrator(MigrationTaskBase):
|
|
31
32
|
class TaskConfiguration(AbstractTaskConfiguration):
|
|
32
|
-
name:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
name: Annotated[
|
|
34
|
+
str,
|
|
35
|
+
Field(
|
|
36
|
+
title="Task name",
|
|
37
|
+
description="The name of the task",
|
|
38
|
+
),
|
|
39
|
+
]
|
|
40
|
+
composite_course_map_path: Annotated[
|
|
41
|
+
str,
|
|
42
|
+
Field(
|
|
43
|
+
title="Composite course map path",
|
|
44
|
+
description="Path to the composite course map file",
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
migration_task_type: Annotated[
|
|
48
|
+
str,
|
|
49
|
+
Field(
|
|
50
|
+
title="Migration task type",
|
|
51
|
+
description="Type of migration task",
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
courses_file: Annotated[
|
|
55
|
+
FileDefinition,
|
|
56
|
+
Field(
|
|
57
|
+
title="Courses file",
|
|
58
|
+
description="File containing course data",
|
|
59
|
+
),
|
|
60
|
+
]
|
|
61
|
+
terms_map_path: Annotated[
|
|
62
|
+
str,
|
|
63
|
+
Field(
|
|
64
|
+
title="Terms map path",
|
|
65
|
+
description="Path to the terms map file",
|
|
66
|
+
),
|
|
67
|
+
]
|
|
68
|
+
departments_map_path: Annotated[
|
|
69
|
+
str,
|
|
70
|
+
Field(
|
|
71
|
+
title="Departments map path",
|
|
72
|
+
description="Path to the departments map file",
|
|
73
|
+
),
|
|
74
|
+
]
|
|
75
|
+
look_up_instructor: Annotated[
|
|
76
|
+
Optional[bool],
|
|
77
|
+
Field(
|
|
78
|
+
title="Look up instructor",
|
|
79
|
+
description=(
|
|
80
|
+
"Flag to indicate whether to look up instructors. "
|
|
81
|
+
"By default is False."
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
] = False
|
|
39
85
|
|
|
40
86
|
@staticmethod
|
|
41
87
|
def get_object_type() -> FOLIONamespaces:
|
|
@@ -39,37 +39,125 @@ csv.register_dialect("tsv", delimiter="\t")
|
|
|
39
39
|
|
|
40
40
|
class HoldingsCsvTransformer(MigrationTaskBase):
|
|
41
41
|
class TaskConfiguration(AbstractTaskConfiguration):
|
|
42
|
-
name:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
name: Annotated[
|
|
43
|
+
str,
|
|
44
|
+
Field(
|
|
45
|
+
title="Task name",
|
|
46
|
+
description="Name of the task",
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
migration_task_type: Annotated[
|
|
50
|
+
str,
|
|
51
|
+
Field(
|
|
52
|
+
title="Migration task type",
|
|
53
|
+
description="Type of migration task",
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
hrid_handling: Annotated[
|
|
57
|
+
HridHandling,
|
|
58
|
+
Field(
|
|
59
|
+
title="HRID handling",
|
|
60
|
+
description=(
|
|
61
|
+
"Determining how the HRID generation "
|
|
62
|
+
"should be handled."
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
files: Annotated[
|
|
67
|
+
List[FileDefinition],
|
|
68
|
+
Field(
|
|
69
|
+
title="Files",
|
|
70
|
+
description="List of files",
|
|
71
|
+
),
|
|
72
|
+
]
|
|
73
|
+
holdings_map_file_name: Annotated[
|
|
74
|
+
str,
|
|
75
|
+
Field(
|
|
76
|
+
title="Holdings map file name",
|
|
77
|
+
description="File name for holdings map",
|
|
78
|
+
),
|
|
79
|
+
]
|
|
80
|
+
location_map_file_name: Annotated[
|
|
81
|
+
str,
|
|
82
|
+
Field(
|
|
83
|
+
title="Location map file name",
|
|
84
|
+
description="File name for location map",
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
default_call_number_type_name: Annotated[
|
|
88
|
+
str,
|
|
89
|
+
Field(
|
|
90
|
+
title="Default call number type name",
|
|
91
|
+
description="Default name for call number type",
|
|
92
|
+
),
|
|
93
|
+
]
|
|
94
|
+
previously_generated_holdings_files: Annotated[
|
|
95
|
+
Optional[list[str]],
|
|
96
|
+
Field(
|
|
97
|
+
title="Previously generated holdings files",
|
|
98
|
+
description=(
|
|
99
|
+
"List of previously generated holdings files. "
|
|
100
|
+
"By default is empty list."
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
] = []
|
|
104
|
+
fallback_holdings_type_id: Annotated[
|
|
105
|
+
str,
|
|
106
|
+
Field(
|
|
107
|
+
title="Fallback holdings type ID",
|
|
108
|
+
description="ID for fallback holdings type",
|
|
109
|
+
),
|
|
110
|
+
]
|
|
51
111
|
holdings_type_uuid_for_boundwiths: Annotated[
|
|
52
112
|
str,
|
|
53
113
|
Field(
|
|
54
114
|
title="Holdings Type for Boundwith Holdings",
|
|
55
115
|
description=(
|
|
56
116
|
"UUID for a Holdings type (set in Settings->Inventory) "
|
|
57
|
-
"for Bound-with Holdings
|
|
117
|
+
"for Bound-with Holdings. Default is empty string."
|
|
58
118
|
),
|
|
59
119
|
),
|
|
60
120
|
] = ""
|
|
61
|
-
call_number_type_map_file_name:
|
|
62
|
-
|
|
121
|
+
call_number_type_map_file_name: Annotated[
|
|
122
|
+
Optional[str],
|
|
123
|
+
Field(
|
|
124
|
+
title="Call number type map file name",
|
|
125
|
+
description="File name for call number type map",
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
holdings_merge_criteria: Annotated[
|
|
129
|
+
Optional[list[str]],
|
|
130
|
+
Field(
|
|
131
|
+
title="Holdings merge criteria",
|
|
132
|
+
description=(
|
|
133
|
+
"List of holdings merge criteria. "
|
|
134
|
+
"Default value is "
|
|
135
|
+
"['instanceId', 'permanentLocationId', 'callNumber']."
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
] = [
|
|
63
139
|
"instanceId",
|
|
64
140
|
"permanentLocationId",
|
|
65
141
|
"callNumber",
|
|
66
142
|
]
|
|
67
|
-
reset_hrid_settings:
|
|
143
|
+
reset_hrid_settings: Annotated[
|
|
144
|
+
Optional[bool],
|
|
145
|
+
Field(
|
|
146
|
+
title="Reset HRID settings",
|
|
147
|
+
description=(
|
|
148
|
+
"At the end of the run reset "
|
|
149
|
+
"FOLIO with the HRID settings. Default is FALSE."
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
] = False
|
|
68
153
|
update_hrid_settings: Annotated[
|
|
69
154
|
bool,
|
|
70
155
|
Field(
|
|
71
156
|
title="Update HRID settings",
|
|
72
|
-
description=
|
|
157
|
+
description=(
|
|
158
|
+
"At the end of the run update "
|
|
159
|
+
"FOLIO with the HRID settings. Default is TRUE."
|
|
160
|
+
),
|
|
73
161
|
),
|
|
74
162
|
] = True
|
|
75
163
|
|