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,11 @@
1
1
  import json
2
- import sys
3
2
  import logging
3
+ import i18n
4
4
  from uuid import uuid4
5
5
 
6
6
  from folio_migration_tools import custom_exceptions
7
7
  from folio_migration_tools import helper
8
8
  from folio_migration_tools.migration_report import MigrationReport
9
- from folio_migration_tools.report_blurbs import Blurbs
10
9
 
11
10
 
12
11
  class HoldingsHelper:
@@ -20,20 +19,20 @@ class HoldingsHelper:
20
19
  """Creates a key from values determined by the fields_crieteria in a holding
21
20
  record to determine uniquenes
22
21
 
23
- fields_criterias are limited to the strings and UUID properties on the first level of the object.
24
- If the property is not found, or empty, it will be ignored
22
+ fields_criterias are limited to the strings and UUID properties on the first level of
23
+ the object. If the property is not found, or empty, it will be ignored
25
24
 
26
25
  IF the holdings type id is matched to holdings_type_id_to_exclude_from_merging,
27
26
  the key will be added with a uuid to prevent merging of this holding
28
27
 
29
28
  Args:
30
29
  holdings_record (dict): The Holdingsrecord
31
- fields_criteria (list[str]): names of the properties of the holdingsrecord.
30
+ fields_criterias (list[str]): names of the properties of the holdingsrecord.
32
31
  migration_report (MigrationReport): Report to help reporting merge
33
32
  holdings_type_id_to_exclude_from_merging: (str): the holdings type UUID to exclude
34
33
 
35
34
  Raises:
36
- exception: _description_
35
+ Exception: _description_
37
36
 
38
37
  Returns:
39
38
  str: _description_
@@ -44,18 +43,18 @@ class HoldingsHelper:
44
43
  v = holdings_record.get(fields_criteria, "")
45
44
  if not v:
46
45
  migration_report.add(
47
- Blurbs.HoldingsMerging, f"{fields_criteria} empty or not set"
46
+ "HoldingsMerging",
47
+ i18n.t(
48
+ "%{fields_criteria} empty or not set", fields_criteria=fields_criteria
49
+ ),
48
50
  )
49
51
  values.append(v)
50
52
 
51
- if (
52
- holdings_record.get("holdingsTypeId")
53
- == holdings_type_id_to_exclude_from_merging
54
- ):
53
+ if holdings_record.get("holdingsTypeId") == holdings_type_id_to_exclude_from_merging:
55
54
  values.append(str(uuid4()))
56
55
  migration_report.add(
57
- Blurbs.HoldingsMerging,
58
- "Holding prevented from merging by holdingsTypeId",
56
+ "HoldingsMerging",
57
+ i18n.t("Holding prevented from merging by holdingsTypeId"),
59
58
  )
60
59
  return "-".join(values)
61
60
  except Exception as exception:
@@ -89,43 +88,55 @@ class HoldingsHelper:
89
88
  )
90
89
  if stored_key in prev_holdings:
91
90
  message = (
92
- f"Previously stored holdings key already exists in the "
93
- f"list of previously stored Holdings. You have likely not used the same "
94
- f"matching criterias ({fields_criteria}) as you did in the previous process"
95
- )
96
- helper.Helper.log_data_issue(
97
- stored_holding["formerIds"], message, stored_key
91
+ f"Previously stored holdings key already exists in the list of previously"
92
+ f" stored Holdings. You have likely not used the same matching criterias"
93
+ f" ({fields_criteria}) as you did in the previous process"
98
94
  )
95
+ helper.Helper.log_data_issue(stored_holding["formerIds"], message, stored_key)
99
96
  logging.warn(message)
100
97
  prev_holdings[stored_key] = HoldingsHelper.merge_holding(
101
98
  prev_holdings[stored_key], stored_holding
102
99
  )
103
100
  migration_report.add(
104
- Blurbs.HoldingsMerging,
105
- "Duplicate key based on current merge criteria. Records merged",
101
+ "HoldingsMerging",
102
+ i18n.t("Duplicate key based on current merge criteria. Records merged"),
106
103
  )
107
104
  else:
108
105
  migration_report.add(
109
- Blurbs.HoldingsMerging,
110
- "Previously transformed holdings record loaded",
106
+ "HoldingsMerging",
107
+ i18n.t("Previously transformed holdings record loaded"),
111
108
  )
112
109
  prev_holdings[stored_key] = stored_holding
113
110
  return prev_holdings
114
111
 
115
112
  @staticmethod
116
113
  def merge_holding(holdings_record: dict, incoming_holdings: dict) -> dict:
117
- extend_list("holdingsStatementsForIndexes", holdings_record, incoming_holdings)
118
- extend_list("holdingsStatements", holdings_record, incoming_holdings)
119
- extend_list(
120
- "holdingsStatementsForSupplements", holdings_record, incoming_holdings
121
- )
114
+ extend_list("holdingsStatementsForIndexes", holdings_record, incoming_holdings, True)
115
+ extend_list("holdingsStatements", holdings_record, incoming_holdings, True)
116
+ extend_list("holdingsStatementsForSupplements", holdings_record, incoming_holdings, True)
122
117
  extend_list("notes", holdings_record, incoming_holdings)
123
118
  holdings_record["notes"] = dedupe(holdings_record.get("notes", []))
124
119
  extend_list("formerIds", holdings_record, incoming_holdings)
125
- holdings_record["formerIds"] = list(set(holdings_record["formerIds"]))
126
120
  extend_list("electronicAccess", holdings_record, incoming_holdings)
121
+ HoldingsHelper.remove_empty_holdings_statements(holdings_record)
122
+ merge_boolean("discoverySuppress", holdings_record, incoming_holdings)
127
123
  return holdings_record
128
124
 
125
+ @staticmethod
126
+ def remove_empty_holdings_statements(holdings_record: dict):
127
+ keys = [
128
+ "holdingsStatements",
129
+ "holdingsStatementsForIndexes",
130
+ "holdingsStatementsForSupplements",
131
+ ]
132
+
133
+ for key in keys:
134
+ if key in holdings_record:
135
+ temp_l = [stmt for stmt in holdings_record[key] if any(stmt.values())]
136
+ holdings_record[key] = temp_l
137
+ if key in holdings_record and not holdings_record.get(key, []):
138
+ del holdings_record[key]
139
+
129
140
  @staticmethod
130
141
  def handle_notes(folio_object):
131
142
  if folio_object.get("notes", []):
@@ -145,15 +156,27 @@ class HoldingsHelper:
145
156
  del folio_object["notes"]
146
157
 
147
158
 
148
- def extend_list(prop_name: str, holdings_record: dict, incoming_holdings: dict):
149
-
159
+ def extend_list(
160
+ prop_name: str, holdings_record: dict, incoming_holdings: dict, accept_dupe_items: bool = False
161
+ ):
150
162
  temp = holdings_record.get(prop_name, [])
163
+ all_already_in = all(i in temp for i in incoming_holdings.get(prop_name, []))
151
164
  for f in incoming_holdings.get(prop_name, []):
152
- if f not in temp:
165
+ if not all_already_in and (accept_dupe_items or f not in temp):
153
166
  temp.append(f)
154
- holdings_record[prop_name] = temp
167
+ if temp:
168
+ holdings_record[prop_name] = temp
155
169
 
156
170
 
157
171
  def dedupe(list_of_dicts):
158
- # TODO: Move to interface or parent class
159
172
  return [dict(t) for t in {tuple(d.items()) for d in list_of_dicts}]
173
+
174
+
175
+ def merge_boolean(prop_name: str, holdings_record: dict, incoming_holdings: dict):
176
+ if (
177
+ holdings_record.get(prop_name, False) is True
178
+ and incoming_holdings.get(prop_name, False) is True
179
+ ):
180
+ holdings_record[prop_name] = True
181
+ else:
182
+ holdings_record[prop_name] = False
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+ settings = {
4
+ "file_format": "json",
5
+ "skip_locale_root_data": True,
6
+ "fallback": "en",
7
+ "filename_format": "{locale}.{format}",
8
+ "load_path": [Path(__file__).parent / "translations"],
9
+ }
@@ -1,17 +1,69 @@
1
- from typing import Optional
1
+ from enum import Enum
2
+ from typing import Annotated
3
+
2
4
  from pydantic import BaseModel, Field
3
5
  from pydantic.types import DirectoryPath
4
- from enum import Enum
5
6
 
6
7
 
7
8
  class HridHandling(str, Enum):
9
+ """Enum determining how the HRID generation should be handled.
10
+ - default: Enumerates the HRID, building on the current value in the HRID settings
11
+ - preserve001: Takes the 001 and uses this as the HRID.
12
+
13
+ Args:
14
+ str (_type_): _description_
15
+ Enum (_type_): _description_
16
+ """
17
+
8
18
  default = "default"
9
19
  preserve001 = "preserve001"
10
20
 
11
21
 
12
22
  class FileDefinition(BaseModel):
13
- file_name: str
14
- suppressed: Optional[bool] = False
23
+ file_name: Annotated[
24
+ str,
25
+ Field(
26
+ title="File name",
27
+ description=(
28
+ "Name of the file to be processed. "
29
+ "The location of the file depends on the context"
30
+ ),
31
+ ),
32
+ ] = ""
33
+ discovery_suppressed: Annotated[bool, Field(title="Discovery suppressed")] = False
34
+ staff_suppressed: Annotated[bool, Field(title="Staff suppressed")] = False
35
+ service_point_id: Annotated[
36
+ str,
37
+ Field(
38
+ title="Service point ID",
39
+ description=(
40
+ "Service point to be used for "
41
+ "transactions created from this file (Loans-only)."
42
+ ),
43
+ )
44
+ ] = ""
45
+ statistical_code: Annotated[
46
+ str,
47
+ Field(
48
+ title="Statistical code",
49
+ description=(
50
+ "Statistical code (code) to be used inventory records created from "
51
+ "this file (Instances, Holdings, Items). Specify multiple codes using "
52
+ "multi_field_delimiter."
53
+ ),
54
+ )
55
+ ] = ""
56
+ create_source_records: Annotated[
57
+ bool,
58
+ Field(
59
+ title="Create source records",
60
+ description=(
61
+ "If set to true, the source records will be created in FOLIO. "
62
+ "If set to false, the source records will not be created in FOLIO. "
63
+ "Only applied for MARC-based transformations."
64
+ ),
65
+ ),
66
+ ] = True
15
67
 
16
68
 
17
69
  class IlsFlavour(str, Enum):
@@ -25,27 +77,102 @@ class IlsFlavour(str, Enum):
25
77
  tag907y = "tag907y"
26
78
  tag001 = "tag001"
27
79
  tagf990a = "tagf990a"
80
+ custom = "custom"
28
81
  none = "none"
29
82
 
30
83
 
31
84
  class FolioRelease(str, Enum):
32
- kiwi = "kiwi"
33
- juniper = "juniper"
85
+ ramsons = "ramsons"
86
+ sunflower = "sunflower"
87
+ trillium = "trillium"
34
88
 
35
89
 
36
90
  class LibraryConfiguration(BaseModel):
37
- okapi_url: str
38
- tenant_id: str
39
- okapi_username: str
40
- okapi_password: str
91
+ gateway_url: Annotated[
92
+ str,
93
+ Field(
94
+ title="FOLIO API Gateway URL",
95
+ description=(
96
+ "The URL of the FOLIO API gateway instance. "
97
+ "You can find this in Settings > Software versions > API gateway services."
98
+ ),
99
+ alias="okapi_url"
100
+ ),
101
+ ]
102
+ tenant_id: Annotated[
103
+ str,
104
+ Field(
105
+ title="FOLIO tenant ID",
106
+ description=(
107
+ "The ID of the FOLIO tenant instance. "
108
+ "You can find this in Settings > Software versions > API gateway services. "
109
+ "In an ECS environment, this is the ID of the central tenant, for all configurations."
110
+ ),
111
+ ),
112
+ ]
113
+ ecs_tenant_id: Annotated[
114
+ str,
115
+ Field(
116
+ title="ECS tenant ID",
117
+ description=(
118
+ "For use in ECS environments when the configuration file is meant to target a ",
119
+ "data tenant. Set to the tenant ID of the data tenant.",
120
+ ),
121
+ ),
122
+ ] = ""
123
+ folio_username: Annotated[
124
+ str,
125
+ Field(
126
+ title="FOLIO API Gateway username",
127
+ description=(
128
+ "The username for the FOLIO user account performing the migration. "
129
+ "User should have a full admin permissions/roles in FOLIO. "
130
+ ),
131
+ alias="okapi_username"
132
+ ),
133
+ ]
134
+ folio_password: Annotated[
135
+ str,
136
+ Field(
137
+ title="FOLIO API Gateway password",
138
+ description=(
139
+ "The password for the FOLIO user account performing the migration. "
140
+ ),
141
+ alias="okapi_password"
142
+ )
143
+ ]
41
144
  base_folder: DirectoryPath = Field(
42
145
  description=(
43
146
  "The base folder for migration. "
44
147
  "Should ideally be a github clone of the migration_repo_template"
45
148
  )
46
149
  )
47
- failed_records_threshold: Optional[int] = 5000
48
- failed_percentage_threshold: Optional[int] = 20
150
+ multi_field_delimiter: Annotated[
151
+ str,
152
+ Field(
153
+ title="Multi field delimiter",
154
+ description=(
155
+ "The delimiter used to separate multiple values in a single field. "
156
+ "This is used for delimited text (CSV/TSV) fields with multiple sub-delimited values."
157
+ ),
158
+ ),
159
+ ] = "<delimiter>"
160
+ failed_records_threshold: Annotated[
161
+ int,
162
+ Field(description=("Number of failed records until the process shuts down")),
163
+ ] = 5000
164
+ failed_percentage_threshold: Annotated[
165
+ int,
166
+ Field(
167
+ description=("Percentage of failed records until the process shuts down")
168
+ ),
169
+ ] = 20
170
+ generic_exception_threshold: Annotated[
171
+ int,
172
+ Field(
173
+ description=("Number of generic exceptions until the process shuts down")
174
+ ),
175
+ ] = 50
49
176
  library_name: str
50
177
  log_level_debug: bool
51
178
  folio_release: FolioRelease = Field(
@@ -56,4 +183,37 @@ class LibraryConfiguration(BaseModel):
56
183
  )
57
184
  )
58
185
  iteration_identifier: str
59
- add_time_stamp_to_file_names: Optional[bool] = False
186
+ add_time_stamp_to_file_names: Annotated[
187
+ bool, Field(title="Add time stamp to file names")
188
+ ] = False
189
+ use_gateway_url_for_uuids: Annotated[
190
+ bool,
191
+ Field(
192
+ title="Use gateway URL for UUIDs",
193
+ description=(
194
+ "If set to true, folio_uuid will use the gateway URL when generating deterministic UUIDs for FOLIO records. "
195
+ "If set to false (default), the UUIDs will be generated using the tenant_id (or ecs_tenant_id)."
196
+ ),
197
+ ),
198
+ ] = False
199
+ is_ecs: Annotated[
200
+ bool,
201
+ Field(
202
+ title="Library is running ECS FOLIO",
203
+ description=(
204
+ "If set to true, the migration is running in an ECS environment. "
205
+ "If set to false (default), the migration is running in a non-ECS environment. "
206
+ "If ecs_tenant_id is set, this will be set to true, regardless of the value here."
207
+ ),
208
+ ),
209
+ ] = False
210
+ ecs_central_iteration_identifier: Annotated[
211
+ str,
212
+ Field(
213
+ title="ECS central iteration identifier",
214
+ description=(
215
+ "The iteration_identifier value from the central tenant configuration that corresponds "
216
+ "to this configuration's iteration_identifier. Used to access the central instances_id_map."
217
+ ),
218
+ ),
219
+ ] = ""