folio-migration-tools 1.9.0rc13__tar.gz → 1.9.1__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.
Files changed (66) hide show
  1. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/PKG-INFO +1 -1
  2. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/pyproject.toml +1 -1
  3. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/item_mapper.py +3 -3
  4. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +14 -2
  5. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +41 -6
  6. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/batch_poster.py +65 -2
  7. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/items_transformer.py +1 -1
  8. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/LICENSE +0 -0
  9. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/README.md +0 -0
  10. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/__init__.py +0 -0
  11. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/__main__.py +0 -0
  12. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/circulation_helper.py +0 -0
  13. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/colors.py +0 -0
  14. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/config_file_load.py +0 -0
  15. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/custom_dict.py +0 -0
  16. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/custom_exceptions.py +0 -0
  17. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/extradata_writer.py +0 -0
  18. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/folder_structure.py +0 -0
  19. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/helper.py +0 -0
  20. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/holdings_helper.py +0 -0
  21. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/i18n_config.py +0 -0
  22. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/library_configuration.py +0 -0
  23. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapper_base.py +0 -0
  24. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/__init__.py +0 -0
  25. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/courses_mapper.py +0 -0
  26. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/holdings_mapper.py +0 -0
  27. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +0 -0
  28. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/notes_mapper.py +0 -0
  29. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/order_mapper.py +0 -0
  30. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/organization_mapper.py +0 -0
  31. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +0 -0
  32. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/mapping_file_transformation/user_mapper.py +0 -0
  33. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/__init__.py +0 -0
  34. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/conditions.py +0 -0
  35. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +0 -0
  36. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/hrid_handler.py +0 -0
  37. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +0 -0
  38. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/marc_file_processor.py +0 -0
  39. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +0 -0
  40. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +0 -0
  41. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +0 -0
  42. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +0 -0
  43. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_report.py +0 -0
  44. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/__init__.py +0 -0
  45. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/authority_transformer.py +0 -0
  46. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/bibs_transformer.py +0 -0
  47. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/courses_migrator.py +0 -0
  48. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/holdings_csv_transformer.py +0 -0
  49. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/holdings_marc_transformer.py +0 -0
  50. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/loans_migrator.py +0 -0
  51. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +0 -0
  52. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/migration_task_base.py +0 -0
  53. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/orders_transformer.py +0 -0
  54. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/organization_transformer.py +0 -0
  55. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/requests_migrator.py +0 -0
  56. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/reserves_migrator.py +0 -0
  57. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/migration_tasks/user_transformer.py +0 -0
  58. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/task_configuration.py +0 -0
  59. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/test_infrastructure/__init__.py +0 -0
  60. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/test_infrastructure/mocked_classes.py +0 -0
  61. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/transaction_migration/__init__.py +0 -0
  62. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/transaction_migration/legacy_loan.py +0 -0
  63. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/transaction_migration/legacy_request.py +0 -0
  64. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/transaction_migration/legacy_reserve.py +0 -0
  65. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/transaction_migration/transaction_result.py +0 -0
  66. {folio_migration_tools-1.9.0rc13 → folio_migration_tools-1.9.1}/src/folio_migration_tools/translations/en.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: folio_migration_tools
3
- Version: 1.9.0rc13
3
+ Version: 1.9.1
4
4
  Summary: A tool allowing you to migrate data from legacy ILS:s (Library systems) into FOLIO LSP
5
5
  License: MIT
6
6
  Keywords: FOLIO,ILS,LSP,Library Systems,MARC21,Library data
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "folio_migration_tools"
3
- version = "1.9.0rc13"
3
+ version = "1.9.1"
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"},
@@ -2,7 +2,7 @@ import json
2
2
  import logging
3
3
  import sys
4
4
  from datetime import datetime, timezone
5
- from typing import Set
5
+ from typing import Dict, List, Set, Union
6
6
  from uuid import uuid4
7
7
 
8
8
  import i18n
@@ -117,12 +117,12 @@ class ItemMapper(MappingFileMapperBase):
117
117
  "LocationMapping",
118
118
  )
119
119
 
120
- def perform_additional_mappings(self, legacy_ids, folio_rec, file_def):
120
+ def perform_additional_mappings(self, legacy_ids: Union[str, List[str]], folio_rec: Dict, file_def: FileDefinition):
121
121
  self.handle_suppression(folio_rec, file_def)
122
122
  self.map_statistical_codes(folio_rec, file_def)
123
123
  self.map_statistical_code_ids(legacy_ids, folio_rec)
124
124
 
125
- def handle_suppression(self, folio_record, file_def: FileDefinition):
125
+ def handle_suppression(self, folio_record: Dict, file_def: FileDefinition):
126
126
  folio_record["discoverySuppress"] = file_def.discovery_suppressed
127
127
  self.migration_report.add(
128
128
  "Suppression",
@@ -531,9 +531,21 @@ class MappingFileMapperBase(MapperBase):
531
531
  isinstance(res, str)
532
532
  and self.library_configuration.multi_field_delimiter in res
533
533
  ):
534
+ for delim_value in res.split(
535
+ self.library_configuration.multi_field_delimiter
536
+ ):
537
+ if delim_value not in empty_vals:
538
+ self.validate_enums(
539
+ delim_value,
540
+ sub_prop,
541
+ sub_prop_name,
542
+ index_or_id,
543
+ required,
544
+ )
534
545
  multi_field_props.append(sub_prop_name)
546
+ else:
547
+ self.validate_enums(res, sub_prop, sub_prop_name, index_or_id, required)
535
548
 
536
- self.validate_enums(res, sub_prop, sub_prop_name, index_or_id, required)
537
549
  if res or isinstance(res, bool):
538
550
  temp_object[sub_prop_name] = res
539
551
 
@@ -809,7 +821,7 @@ class MappingFileMapperBase(MapperBase):
809
821
  ):
810
822
  raise TransformationRecordFailedError(
811
823
  index_or_id,
812
- f"Allowed values for {mapped_schema_property_name}"
824
+ f"Allowed values for {mapped_schema_property_name} "
813
825
  f"are {mapped_schema_property['enum']} "
814
826
  f"Forbidden enum value found: ",
815
827
  mapped_value,
@@ -1,6 +1,7 @@
1
1
  import copy
2
2
  import json
3
3
  import logging
4
+ import re
4
5
  from typing import Dict, List, Set
5
6
 
6
7
  import i18n
@@ -393,12 +394,29 @@ class RulesMapperHoldings(RulesMapperBase):
393
394
  ) from ee
394
395
  return [
395
396
  {
396
- "note": "\n".join(mrk_statement_notes),
397
+ "note": chunk,
397
398
  "holdingsNoteTypeId": holdings_note_type_id,
398
399
  "staffOnly": True,
399
- }
400
+ } for chunk in self.split_mrk_by_max_note_size("\n".join(mrk_statement_notes))
400
401
  ]
401
402
 
403
+ @staticmethod
404
+ def split_mrk_by_max_note_size(s: str, max_chunk_size: int = 32000) -> List[str]:
405
+ lines = s.splitlines(keepends=True)
406
+ chunks = []
407
+ current_chunk = ""
408
+ for line in lines:
409
+ # If adding this line would exceed the limit, start a new chunk
410
+ if len(current_chunk) + len(line) > max_chunk_size:
411
+ if current_chunk:
412
+ chunks.append(current_chunk)
413
+ current_chunk = line
414
+ else:
415
+ current_chunk += line
416
+ if current_chunk:
417
+ chunks.append(current_chunk)
418
+ return chunks
419
+
402
420
  def add_mfhd_as_mrk_note(self, marc_record: Record, folio_holding: Dict, legacy_ids: List[str]):
403
421
  """Adds the MFHD as a note to the holdings record
404
422
 
@@ -424,12 +442,29 @@ class RulesMapperHoldings(RulesMapperBase):
424
442
  ) from ee
425
443
  folio_holding["notes"] = folio_holding.get("notes", []) + [
426
444
  {
427
- "note": str(marc_record),
445
+ "note": chunk,
428
446
  "holdingsNoteTypeId": holdings_note_type_id,
429
447
  "staffOnly": True,
430
- }
448
+ } for chunk in self.split_mrk_by_max_note_size(str(marc_record))
431
449
  ]
432
450
 
451
+ @staticmethod
452
+ def split_mrc_by_max_note_size(data: bytes, sep: bytes = b"\x1e", max_chunk_size: int = 32000) -> List[bytes]:
453
+ # Split data into segments, each ending with the separator (except possibly the last)
454
+ pattern = re.compile(b'(.*?' + re.escape(sep) + b'|.+?$)', re.DOTALL)
455
+ parts = [m.group(0) for m in pattern.finditer(data) if m.group(0)]
456
+ chunks = []
457
+ current_chunk = b""
458
+ for part in parts:
459
+ if len(current_chunk) + len(part) > max_chunk_size and current_chunk:
460
+ chunks.append(current_chunk)
461
+ current_chunk = part
462
+ else:
463
+ current_chunk += part
464
+ if current_chunk:
465
+ chunks.append(current_chunk)
466
+ return chunks
467
+
433
468
  def add_mfhd_as_mrc_note(self, marc_record: Record, folio_holding: Dict, legacy_ids: List[str]):
434
469
  """Adds the MFHD as a note to the holdings record
435
470
 
@@ -455,10 +490,10 @@ class RulesMapperHoldings(RulesMapperBase):
455
490
  ) from ee
456
491
  folio_holding["notes"] = folio_holding.get("notes", []) + [
457
492
  {
458
- "note": marc_record.as_marc().decode("utf-8"),
493
+ "note": chunk.decode("utf-8"),
459
494
  "holdingsNoteTypeId": holdings_note_type_id,
460
495
  "staffOnly": True,
461
- }
496
+ } for chunk in self.split_mrc_by_max_note_size(marc_record.as_marc())
462
497
  ]
463
498
 
464
499
  def wrap_up(self):
@@ -6,7 +6,7 @@ import sys
6
6
  import time
7
7
  import traceback
8
8
  from datetime import datetime
9
- from typing import Annotated, List
9
+ from typing import Annotated, List, Optional
10
10
  from uuid import uuid4
11
11
 
12
12
  import httpx
@@ -173,11 +173,13 @@ class BatchPoster(MigrationTaskBase):
173
173
  self.num_posted = 0
174
174
  self.okapi_headers = self.folio_client.okapi_headers
175
175
  self.http_client = None
176
+ self.starting_record_count_in_folio: Optional[int] = None
176
177
 
177
178
  def do_work(self):
178
179
  with self.folio_client.get_folio_http_client() as httpx_client:
179
180
  self.http_client = httpx_client
180
181
  with open(self.folder_structure.failed_recs_path, "w", encoding='utf-8') as failed_recs_file:
182
+ self.get_starting_record_count()
181
183
  try:
182
184
  batch = []
183
185
  if self.task_configuration.object_type == "SRS":
@@ -317,6 +319,8 @@ class BatchPoster(MigrationTaskBase):
317
319
  updates[record["id"]] = {
318
320
  "_version": record["_version"],
319
321
  }
322
+ if "hrid" in record:
323
+ updates[record["id"]]["hrid"] = record["hrid"]
320
324
  if "status" in record:
321
325
  updates[record["id"]]["status"] = record["status"]
322
326
  if "lastCheckIn" in record:
@@ -604,6 +608,42 @@ class BatchPoster(MigrationTaskBase):
604
608
  else:
605
609
  return httpx.post(url, headers=self.okapi_headers, json=payload, params=self.query_params, timeout=None)
606
610
 
611
+ def get_current_record_count_in_folio(self):
612
+ if "query_endpoint" in self.api_info:
613
+ url = f"{self.folio_client.gateway_url}{self.api_info['query_endpoint']}"
614
+ query_params = {"query": "cql.allRecords=1", "limit": 0}
615
+ if self.http_client and not self.http_client.is_closed:
616
+ res = self.http_client.get(url, headers=self.folio_client.okapi_headers, params=query_params)
617
+ else:
618
+ res = httpx.get(url, headers=self.okapi_headers, params=query_params, timeout=None)
619
+ try:
620
+ res.raise_for_status()
621
+ return res.json()["totalRecords"]
622
+ except httpx.HTTPStatusError:
623
+ logging.error("Failed to get current record count. HTTP %s", res.status_code)
624
+ return 0
625
+ except KeyError:
626
+ logging.error(f"Failed to get current record count. No 'totalRecords' in response: {res.json()}")
627
+ return 0
628
+ else:
629
+ raise ValueError(
630
+ "No 'query_endpoint' available for %s. Cannot get current record count.", self.task_configuration.object_type
631
+ )
632
+
633
+ def get_starting_record_count(self):
634
+ if "query_endpoint" in self.api_info and not self.starting_record_count_in_folio:
635
+ logging.info("Getting starting record count in FOLIO")
636
+ self.starting_record_count_in_folio = self.get_current_record_count_in_folio()
637
+ else:
638
+ logging.info("No query_endpoint available for %s. Cannot get starting record count.", self.task_configuration.object_type)
639
+
640
+ def get_finished_record_count(self):
641
+ if "query_endpoint" in self.api_info:
642
+ logging.info("Getting finished record count in FOLIO")
643
+ self.finished_record_count_in_folio = self.get_current_record_count_in_folio()
644
+ else:
645
+ logging.info("No query_endpoint available for %s. Cannot get ending record count.", self.task_configuration.object_type)
646
+
607
647
  def wrap_up(self):
608
648
  logging.info("Done. Wrapping up")
609
649
  self.extradata_writer.flush()
@@ -621,11 +661,34 @@ class BatchPoster(MigrationTaskBase):
621
661
  )
622
662
  else:
623
663
  logging.info("Done posting %s records. %s failed", self.num_posted, self.num_failures)
624
-
664
+ if self.starting_record_count_in_folio:
665
+ self.get_finished_record_count()
666
+ total_on_server = self.finished_record_count_in_folio - self.starting_record_count_in_folio
667
+ discrepancy = self.processed - self.num_failures - total_on_server
668
+ if discrepancy != 0:
669
+ logging.error(
670
+ (
671
+ "Discrepancy in record count. "
672
+ "Starting record count: %s. Finished record count: %s. "
673
+ "Records posted: %s. Discrepancy: %s"
674
+ ),
675
+ self.starting_record_count_in_folio,
676
+ self.finished_record_count_in_folio,
677
+ self.num_posted - self.num_failures,
678
+ discrepancy,
679
+ )
680
+ else:
681
+ discrepancy = 0
625
682
  run = "second time" if self.performing_rerun else "first time"
626
683
  self.migration_report.set("GeneralStatistics", f"Records processed {run}", self.processed)
627
684
  self.migration_report.set("GeneralStatistics", f"Records posted {run}", self.num_posted)
628
685
  self.migration_report.set("GeneralStatistics", f"Failed to post {run}", self.num_failures)
686
+ if discrepancy:
687
+ self.migration_report.set(
688
+ "GeneralStatistics",
689
+ f"Discrepancy in record count {run}",
690
+ discrepancy,
691
+ )
629
692
  self.rerun_run()
630
693
  with open(self.folder_structure.migration_reports_file, "w+") as report_file:
631
694
  self.migration_report.write_migration_report(
@@ -354,7 +354,7 @@ class ItemsTransformer(MigrationTaskBase):
354
354
  record, f"row {idx}", FOLIONamespaces.items
355
355
  )
356
356
 
357
- self.mapper.perform_additional_mappings(folio_rec, file_def)
357
+ self.mapper.perform_additional_mappings(legacy_id, folio_rec, file_def)
358
358
  self.handle_circiulation_notes(folio_rec, self.folio_client.current_user)
359
359
  self.handle_notes(folio_rec)
360
360
  if folio_rec["holdingsRecordId"] in self.boundwith_relationship_map: