folio-migration-tools 1.9.9__py3-none-any.whl → 1.10.0__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.
- folio_migration_tools/__init__.py +3 -4
- folio_migration_tools/__main__.py +53 -31
- folio_migration_tools/circulation_helper.py +118 -108
- folio_migration_tools/custom_dict.py +2 -2
- folio_migration_tools/custom_exceptions.py +4 -5
- folio_migration_tools/folder_structure.py +17 -7
- folio_migration_tools/helper.py +8 -7
- folio_migration_tools/holdings_helper.py +4 -3
- folio_migration_tools/i18n_cache.py +79 -0
- folio_migration_tools/library_configuration.py +77 -37
- folio_migration_tools/mapper_base.py +45 -31
- folio_migration_tools/mapping_file_transformation/courses_mapper.py +1 -1
- folio_migration_tools/mapping_file_transformation/holdings_mapper.py +7 -3
- folio_migration_tools/mapping_file_transformation/item_mapper.py +13 -26
- folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +1 -2
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +13 -11
- folio_migration_tools/mapping_file_transformation/order_mapper.py +23 -5
- folio_migration_tools/mapping_file_transformation/organization_mapper.py +3 -3
- folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +3 -0
- folio_migration_tools/mapping_file_transformation/user_mapper.py +47 -28
- folio_migration_tools/marc_rules_transformation/conditions.py +82 -97
- folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +13 -5
- folio_migration_tools/marc_rules_transformation/hrid_handler.py +3 -2
- folio_migration_tools/marc_rules_transformation/marc_file_processor.py +26 -24
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +56 -51
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +28 -17
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +68 -37
- folio_migration_tools/migration_report.py +18 -7
- folio_migration_tools/migration_tasks/batch_poster.py +285 -354
- folio_migration_tools/migration_tasks/bibs_transformer.py +14 -9
- folio_migration_tools/migration_tasks/courses_migrator.py +2 -3
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py +23 -24
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py +14 -24
- folio_migration_tools/migration_tasks/items_transformer.py +23 -34
- folio_migration_tools/migration_tasks/loans_migrator.py +67 -144
- folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +3 -3
- folio_migration_tools/migration_tasks/migration_task_base.py +43 -52
- folio_migration_tools/migration_tasks/orders_transformer.py +25 -41
- folio_migration_tools/migration_tasks/organization_transformer.py +9 -18
- folio_migration_tools/migration_tasks/requests_migrator.py +21 -24
- folio_migration_tools/migration_tasks/reserves_migrator.py +6 -5
- folio_migration_tools/migration_tasks/user_transformer.py +25 -20
- folio_migration_tools/task_configuration.py +6 -7
- folio_migration_tools/transaction_migration/legacy_loan.py +15 -27
- folio_migration_tools/transaction_migration/legacy_request.py +1 -1
- folio_migration_tools/translations/en.json +3 -8
- {folio_migration_tools-1.9.9.dist-info → folio_migration_tools-1.10.0.dist-info}/METADATA +19 -28
- folio_migration_tools-1.10.0.dist-info/RECORD +63 -0
- folio_migration_tools-1.10.0.dist-info/WHEEL +4 -0
- folio_migration_tools-1.10.0.dist-info/entry_points.txt +3 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +0 -241
- folio_migration_tools/migration_tasks/authority_transformer.py +0 -119
- folio_migration_tools/test_infrastructure/__init__.py +0 -0
- folio_migration_tools/test_infrastructure/mocked_classes.py +0 -406
- folio_migration_tools-1.9.9.dist-info/RECORD +0 -67
- folio_migration_tools-1.9.9.dist-info/WHEEL +0 -4
- folio_migration_tools-1.9.9.dist-info/entry_points.txt +0 -3
- folio_migration_tools-1.9.9.dist-info/licenses/LICENSE +0 -21
|
@@ -12,6 +12,7 @@ from pymarc import Optional
|
|
|
12
12
|
from pymarc.field import Field
|
|
13
13
|
from pymarc.record import Record
|
|
14
14
|
|
|
15
|
+
from folio_migration_tools.i18n_cache import i18n_t
|
|
15
16
|
from folio_migration_tools.custom_exceptions import (
|
|
16
17
|
TransformationFieldMappingError,
|
|
17
18
|
TransformationProcessError,
|
|
@@ -95,25 +96,21 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
95
96
|
)
|
|
96
97
|
self.mappings["852"] = new_852_mapping
|
|
97
98
|
|
|
98
|
-
def integrate_supplemental_mfhd_mappings(self, new_rules=
|
|
99
|
+
def integrate_supplemental_mfhd_mappings(self, new_rules=None):
|
|
99
100
|
try:
|
|
100
|
-
self.mappings.update(new_rules)
|
|
101
|
+
self.mappings.update(new_rules or {})
|
|
101
102
|
self.fix_853_bug_in_rules()
|
|
102
103
|
except Exception as e:
|
|
103
104
|
raise TransformationProcessError(
|
|
104
105
|
"",
|
|
105
106
|
"Failed to integrate supplemental mfhd mappings",
|
|
106
107
|
str(e),
|
|
107
|
-
)
|
|
108
|
+
) from e
|
|
108
109
|
|
|
109
110
|
def prep_852_notes(self, marc_record: Record):
|
|
110
111
|
for field in marc_record.get_fields("852"):
|
|
111
112
|
field.subfields.sort(key=lambda x: x[0])
|
|
112
|
-
new_952 = Field(
|
|
113
|
-
tag="952",
|
|
114
|
-
indicators=["f", "f"],
|
|
115
|
-
subfields=field.subfields
|
|
116
|
-
)
|
|
113
|
+
new_952 = Field(tag="952", indicators=["f", "f"], subfields=field.subfields)
|
|
117
114
|
marc_record.add_ordered_field(new_952)
|
|
118
115
|
|
|
119
116
|
def parse_record(
|
|
@@ -257,7 +254,7 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
257
254
|
ignored_subsequent_fields (_type_): _description_
|
|
258
255
|
index_or_legacy_ids (_type_): _description_
|
|
259
256
|
"""
|
|
260
|
-
self.migration_report.add("Trivia",
|
|
257
|
+
self.migration_report.add("Trivia", i18n_t("Total number of Tags processed"))
|
|
261
258
|
if marc_field.tag not in self.mappings:
|
|
262
259
|
self.report_legacy_mapping(marc_field.tag, True, False)
|
|
263
260
|
elif marc_field.tag not in ignored_subsequent_fields:
|
|
@@ -270,7 +267,11 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
270
267
|
ignored_subsequent_fields.add(marc_field.tag)
|
|
271
268
|
|
|
272
269
|
def perform_additional_mapping(
|
|
273
|
-
self,
|
|
270
|
+
self,
|
|
271
|
+
marc_record: Record,
|
|
272
|
+
folio_holding: Dict,
|
|
273
|
+
legacy_ids: List[str],
|
|
274
|
+
file_def: FileDefinition,
|
|
274
275
|
):
|
|
275
276
|
"""_summary_
|
|
276
277
|
|
|
@@ -306,11 +307,13 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
306
307
|
"",
|
|
307
308
|
)
|
|
308
309
|
self.handle_suppression(folio_holding, file_def, True)
|
|
309
|
-
# First, map statistical codes from MARC fields and FileDefinitions to FOLIO statistical
|
|
310
|
-
# Then, convert the mapped statistical codes to their corresponding code IDs.
|
|
310
|
+
# First, map statistical codes from MARC fields and FileDefinitions to FOLIO statistical
|
|
311
|
+
# codes. Then, convert the mapped statistical codes to their corresponding code IDs.
|
|
311
312
|
self.map_statistical_codes(folio_holding, file_def, marc_record)
|
|
312
313
|
self.map_statistical_code_ids(legacy_ids, folio_holding)
|
|
313
|
-
self.set_source_id(
|
|
314
|
+
self.set_source_id(
|
|
315
|
+
self.create_source_records, folio_holding, self.holdingssources, file_def
|
|
316
|
+
)
|
|
314
317
|
|
|
315
318
|
def pick_first_location_if_many(self, folio_holding: Dict, legacy_ids: List[str]):
|
|
316
319
|
if " " in folio_holding.get("permanentLocationId", ""):
|
|
@@ -324,7 +327,12 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
324
327
|
]
|
|
325
328
|
|
|
326
329
|
@staticmethod
|
|
327
|
-
def set_source_id(
|
|
330
|
+
def set_source_id(
|
|
331
|
+
create_source_records: bool,
|
|
332
|
+
folio_rec: Dict,
|
|
333
|
+
holdingssources: Dict,
|
|
334
|
+
file_def: FileDefinition,
|
|
335
|
+
):
|
|
328
336
|
if file_def.create_source_records and create_source_records:
|
|
329
337
|
folio_rec["sourceId"] = holdingssources.get("MARC")
|
|
330
338
|
else:
|
|
@@ -371,10 +379,14 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
371
379
|
"""
|
|
372
380
|
if self.task_configuration.include_mrk_statements:
|
|
373
381
|
mrk_statement_notes = []
|
|
374
|
-
for field in marc_record.get_fields(
|
|
382
|
+
for field in marc_record.get_fields(
|
|
383
|
+
"853", "854", "855", "863", "864", "865", "866", "867", "868"
|
|
384
|
+
):
|
|
375
385
|
mrk_statement_notes.append(str(field))
|
|
376
386
|
if mrk_statement_notes:
|
|
377
|
-
folio_holding["notes"] = folio_holding.get(
|
|
387
|
+
folio_holding["notes"] = folio_holding.get(
|
|
388
|
+
"notes", []
|
|
389
|
+
) + self.add_mrk_statements_note(mrk_statement_notes, legacy_ids)
|
|
378
390
|
|
|
379
391
|
def add_mrk_statements_note(self, mrk_statement_notes: List[str], legacy_ids) -> List[Dict]:
|
|
380
392
|
"""Creates a note from the MRK statements
|
|
@@ -386,7 +398,9 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
386
398
|
List: A list containing the FOLIO holdings note object (Dict)
|
|
387
399
|
"""
|
|
388
400
|
holdings_note_type_tuple = self.conditions.get_ref_data_tuple_by_name(
|
|
389
|
-
self.folio.holding_note_types,
|
|
401
|
+
self.folio.holding_note_types,
|
|
402
|
+
"holding_note_types",
|
|
403
|
+
self.task_configuration.mrk_holdings_note_type,
|
|
390
404
|
)
|
|
391
405
|
try:
|
|
392
406
|
holdings_note_type_id = holdings_note_type_tuple[0]
|
|
@@ -394,7 +408,8 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
394
408
|
logging.error(ee)
|
|
395
409
|
raise TransformationRecordFailedError(
|
|
396
410
|
legacy_ids,
|
|
397
|
-
f
|
|
411
|
+
f"Holdings note type mapping error.\tNote type name: "
|
|
412
|
+
f"{self.task_configuration.mrk_holdings_note_type}\t"
|
|
398
413
|
f"MFHD holdings statement note type not found in FOLIO.",
|
|
399
414
|
self.task_configuration.mrk_holdings_note_type,
|
|
400
415
|
) from ee
|
|
@@ -403,7 +418,8 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
403
418
|
"note": chunk,
|
|
404
419
|
"holdingsNoteTypeId": holdings_note_type_id,
|
|
405
420
|
"staffOnly": True,
|
|
406
|
-
}
|
|
421
|
+
}
|
|
422
|
+
for chunk in self.split_mrk_by_max_note_size("\n".join(mrk_statement_notes))
|
|
407
423
|
]
|
|
408
424
|
|
|
409
425
|
@staticmethod
|
|
@@ -423,7 +439,9 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
423
439
|
chunks.append(current_chunk)
|
|
424
440
|
return chunks
|
|
425
441
|
|
|
426
|
-
def add_mfhd_as_mrk_note(
|
|
442
|
+
def add_mfhd_as_mrk_note(
|
|
443
|
+
self, marc_record: Record, folio_holding: Dict, legacy_ids: List[str]
|
|
444
|
+
):
|
|
427
445
|
"""Adds the MFHD as a note to the holdings record
|
|
428
446
|
|
|
429
447
|
This is done to preserve the information in the MARC record for future reference.
|
|
@@ -434,7 +452,9 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
434
452
|
"""
|
|
435
453
|
if self.task_configuration.include_mfhd_mrk_as_note:
|
|
436
454
|
holdings_note_type_tuple = self.conditions.get_ref_data_tuple_by_name(
|
|
437
|
-
self.folio.holding_note_types,
|
|
455
|
+
self.folio.holding_note_types,
|
|
456
|
+
"holding_note_types",
|
|
457
|
+
self.task_configuration.mfhd_mrk_note_type,
|
|
438
458
|
)
|
|
439
459
|
try:
|
|
440
460
|
holdings_note_type_id = holdings_note_type_tuple[0]
|
|
@@ -442,7 +462,8 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
442
462
|
logging.error(ee)
|
|
443
463
|
raise TransformationRecordFailedError(
|
|
444
464
|
legacy_ids,
|
|
445
|
-
f
|
|
465
|
+
f"Holdings note type mapping error.\tNote type name: "
|
|
466
|
+
f"{self.task_configuration.mfhd_mrk_note_type}\t"
|
|
446
467
|
f"Note type not found in FOLIO.",
|
|
447
468
|
self.task_configuration.mfhd_mrk_note_type,
|
|
448
469
|
) from ee
|
|
@@ -451,13 +472,16 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
451
472
|
"note": chunk,
|
|
452
473
|
"holdingsNoteTypeId": holdings_note_type_id,
|
|
453
474
|
"staffOnly": True,
|
|
454
|
-
}
|
|
475
|
+
}
|
|
476
|
+
for chunk in self.split_mrk_by_max_note_size(str(marc_record))
|
|
455
477
|
]
|
|
456
478
|
|
|
457
479
|
@staticmethod
|
|
458
|
-
def split_mrc_by_max_note_size(
|
|
480
|
+
def split_mrc_by_max_note_size(
|
|
481
|
+
data: bytes, sep: bytes = b"\x1e", max_chunk_size: int = 32000
|
|
482
|
+
) -> List[bytes]:
|
|
459
483
|
# Split data into segments, each ending with the separator (except possibly the last)
|
|
460
|
-
pattern = re.compile(b
|
|
484
|
+
pattern = re.compile(b"(.*?" + re.escape(sep) + b"|.+?$)", re.DOTALL)
|
|
461
485
|
parts = [m.group(0) for m in pattern.finditer(data) if m.group(0)]
|
|
462
486
|
chunks = []
|
|
463
487
|
current_chunk = b""
|
|
@@ -471,7 +495,9 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
471
495
|
chunks.append(current_chunk)
|
|
472
496
|
return chunks
|
|
473
497
|
|
|
474
|
-
def add_mfhd_as_mrc_note(
|
|
498
|
+
def add_mfhd_as_mrc_note(
|
|
499
|
+
self, marc_record: Record, folio_holding: Dict, legacy_ids: List[str]
|
|
500
|
+
):
|
|
475
501
|
"""Adds the MFHD as a note to the holdings record
|
|
476
502
|
|
|
477
503
|
This is done to preserve the information in the MARC record for future reference.
|
|
@@ -482,7 +508,9 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
482
508
|
"""
|
|
483
509
|
if self.task_configuration.include_mfhd_mrc_as_note:
|
|
484
510
|
holdings_note_type_tuple = self.conditions.get_ref_data_tuple_by_name(
|
|
485
|
-
self.folio.holding_note_types,
|
|
511
|
+
self.folio.holding_note_types,
|
|
512
|
+
"holding_note_types",
|
|
513
|
+
self.task_configuration.mfhd_mrc_note_type,
|
|
486
514
|
)
|
|
487
515
|
try:
|
|
488
516
|
holdings_note_type_id = holdings_note_type_tuple[0]
|
|
@@ -490,7 +518,8 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
490
518
|
logging.error(ee)
|
|
491
519
|
raise TransformationRecordFailedError(
|
|
492
520
|
legacy_ids,
|
|
493
|
-
f
|
|
521
|
+
f"Holdings note type mapping error.\tNote type name: "
|
|
522
|
+
f"{self.task_configuration.mfhd_mrc_note_type}\t"
|
|
494
523
|
f"Note type not found in FOLIO.",
|
|
495
524
|
self.task_configuration.mfhd_mrc_note_type,
|
|
496
525
|
) from ee
|
|
@@ -499,7 +528,8 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
499
528
|
"note": chunk.decode("utf-8"),
|
|
500
529
|
"holdingsNoteTypeId": holdings_note_type_id,
|
|
501
530
|
"staffOnly": True,
|
|
502
|
-
}
|
|
531
|
+
}
|
|
532
|
+
for chunk in self.split_mrc_by_max_note_size(marc_record.as_marc())
|
|
503
533
|
]
|
|
504
534
|
|
|
505
535
|
def wrap_up(self):
|
|
@@ -555,7 +585,7 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
555
585
|
Helper.log_data_issue(
|
|
556
586
|
legacy_ids,
|
|
557
587
|
(
|
|
558
|
-
|
|
588
|
+
i18n_t("blurbs.HoldingsTypeMapping.title") + " is 'unknown'. "
|
|
559
589
|
"(leader 06 is set to 'u') Check if this is correct"
|
|
560
590
|
),
|
|
561
591
|
ldr06,
|
|
@@ -569,14 +599,14 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
569
599
|
folio_holding["holdingsTypeId"] = self.fallback_holdings_type_id
|
|
570
600
|
self.migration_report.add(
|
|
571
601
|
"HoldingsTypeMapping",
|
|
572
|
-
|
|
602
|
+
i18n_t("An Unmapped")
|
|
573
603
|
+ f" {ldr06} -> {holdings_type} -> "
|
|
574
|
-
+
|
|
604
|
+
+ i18n_t("Unmapped"),
|
|
575
605
|
)
|
|
576
606
|
Helper.log_data_issue(
|
|
577
607
|
legacy_ids,
|
|
578
608
|
(
|
|
579
|
-
|
|
609
|
+
i18n_t("blurbs.HoldingsTypeMapping.title", locale="en")
|
|
580
610
|
+ ". leader 06 was unmapped."
|
|
581
611
|
),
|
|
582
612
|
ldr06,
|
|
@@ -642,7 +672,7 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
642
672
|
Raises:
|
|
643
673
|
TransformationProcessError: If MFHD_ID or BIB_ID is missing from the entry or if the instance_uuid is not in the parent_id_map.
|
|
644
674
|
TransformationRecordFailedError: If BIB_ID is not in the instance id map.
|
|
645
|
-
"""
|
|
675
|
+
""" # noqa: E501
|
|
646
676
|
new_map = {}
|
|
647
677
|
for idx, entry in enumerate(boundwith_relationship_map_list):
|
|
648
678
|
self.verity_boundwith_map_entry(entry)
|
|
@@ -663,9 +693,10 @@ class RulesMapperHoldings(RulesMapperBase):
|
|
|
663
693
|
def get_bw_instance_id_map_tuple(self, entry: Dict):
|
|
664
694
|
try:
|
|
665
695
|
return self.parent_id_map[entry["BIB_ID"]]
|
|
666
|
-
except KeyError:
|
|
696
|
+
except KeyError as e:
|
|
667
697
|
raise TransformationRecordFailedError(
|
|
668
698
|
entry["MFHD_ID"],
|
|
669
|
-
"Boundwith relationship map contains a BIB_ID id not in the instance id map.
|
|
699
|
+
"Boundwith relationship map contains a BIB_ID id not in the instance id map. "
|
|
700
|
+
"No boundwith holdings created for this BIB_ID.",
|
|
670
701
|
entry["BIB_ID"],
|
|
671
|
-
)
|
|
702
|
+
) from e
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import json
|
|
2
3
|
import i18n
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from datetime import timezone
|
|
5
6
|
|
|
7
|
+
from folio_migration_tools.i18n_cache import i18n_t
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
class MigrationReport:
|
|
8
11
|
"""Class responsible for handling the migration report"""
|
|
@@ -47,6 +50,14 @@ class MigrationReport:
|
|
|
47
50
|
"""
|
|
48
51
|
self.add("GeneralStatistics", measure_to_add)
|
|
49
52
|
|
|
53
|
+
def _write_json_report(self, report_file):
|
|
54
|
+
"""Writes the raw migration report data to a JSON file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
report_file: An open file object to write the JSON data to
|
|
58
|
+
"""
|
|
59
|
+
json.dump(self.report, report_file, indent=2)
|
|
60
|
+
|
|
50
61
|
def write_migration_report(
|
|
51
62
|
self,
|
|
52
63
|
report_title,
|
|
@@ -66,17 +77,17 @@ class MigrationReport:
|
|
|
66
77
|
[
|
|
67
78
|
"# " + report_title,
|
|
68
79
|
i18n.t("blurbs.Introduction.description"),
|
|
69
|
-
"## " +
|
|
80
|
+
"## " + i18n_t("Timings"),
|
|
70
81
|
"",
|
|
71
|
-
|
|
82
|
+
i18n_t("Measure") + " | " + i18n_t("Value"),
|
|
72
83
|
"--- | ---:",
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
i18n_t("Time Started:") + " | " + datetime.isoformat(time_started),
|
|
85
|
+
i18n_t("Time Finished:") + " | " + datetime.isoformat(time_finished),
|
|
86
|
+
i18n_t("Elapsed time:") + " | " + str(time_finished - time_started),
|
|
76
87
|
]
|
|
77
88
|
)
|
|
78
89
|
)
|
|
79
|
-
logging.info(f"Elapsed time: {time_finished-time_started}")
|
|
90
|
+
logging.info(f"Elapsed time: {time_finished - time_started}")
|
|
80
91
|
for a in self.report:
|
|
81
92
|
blurb_id = self.report[a].get("blurb_id") or ""
|
|
82
93
|
report_file.write(
|
|
@@ -89,7 +100,7 @@ class MigrationReport:
|
|
|
89
100
|
+ i18n.t("Click to expand all %{count} things", count=len(self.report[a]))
|
|
90
101
|
+ "</summary>",
|
|
91
102
|
"",
|
|
92
|
-
|
|
103
|
+
i18n_t("Measure") + " | " + i18n_t("Count"),
|
|
93
104
|
"--- | ---:",
|
|
94
105
|
]
|
|
95
106
|
+ [
|