patch.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. from __future__ import annotations
  2. import warnings
  3. from typing import IO, Any, Optional
  4. from uuid import uuid4
  5. from rdflib import Dataset
  6. from rdflib.plugins.serializers.nquads import _nq_row
  7. from rdflib.plugins.serializers.nt import _nt_row
  8. from rdflib.serializer import Serializer
  9. add_remove_methods = {"add": "A", "remove": "D"}
  10. class PatchSerializer(Serializer):
  11. """
  12. Creates an RDF patch file to add and remove triples/quads.
  13. Can either:
  14. - Create an add or delete patch for a single Dataset.
  15. - Create a patch to represent the difference between two Datasets.
  16. """
  17. def __init__(
  18. self,
  19. store: Dataset,
  20. ):
  21. self.store: Dataset = store
  22. super().__init__(store)
  23. def serialize(
  24. self,
  25. stream: IO[bytes],
  26. base: Optional[str] = None,
  27. encoding: Optional[str] = None,
  28. **kwargs: Any,
  29. ) -> None:
  30. """Serialize the store to the given stream.
  31. Args:
  32. stream: The stream to serialize to.
  33. base: The base URI to use for the serialization.
  34. encoding: The encoding to use for the serialization.
  35. kwargs: Additional keyword arguments.
  36. Supported keyword arguments:
  37. - operation: The operation to perform. Either 'add' or 'remove'.
  38. - target: The target Dataset to compare against.
  39. NB: Only one of 'operation' or 'target' should be provided.
  40. - header_id: The header ID to use.
  41. - header_prev: The previous header ID to use.
  42. """
  43. operation = kwargs.get("operation")
  44. target = kwargs.get("target")
  45. header_id = kwargs.get("header_id")
  46. header_prev = kwargs.get("header_prev")
  47. if not header_id:
  48. header_id = f"uuid:{uuid4()}"
  49. encoding = self.encoding
  50. if base is not None:
  51. warnings.warn("PatchSerializer does not support base.")
  52. if encoding is not None and encoding.lower() != self.encoding.lower():
  53. warnings.warn(
  54. "PatchSerializer does not use custom encoding. "
  55. f"Given encoding was: {encoding}"
  56. )
  57. def write_header():
  58. stream.write(f"H id <{header_id}> .\n".encode(encoding, "replace"))
  59. if header_prev:
  60. stream.write(f"H prev <{header_prev}>\n".encode(encoding, "replace"))
  61. stream.write("TX .\n".encode(encoding, "replace"))
  62. def write_triples(contexts, op_code, use_passed_contexts=False):
  63. for context in contexts:
  64. if not use_passed_contexts:
  65. context = self.store.get_context(context.identifier)
  66. for triple in context:
  67. stream.write(
  68. self._patch_row(triple, context.identifier, op_code).encode(
  69. encoding, "replace"
  70. )
  71. )
  72. if operation:
  73. assert operation in add_remove_methods, f"Invalid operation: {operation}"
  74. elif not target:
  75. # No operation specified and no target specified
  76. # Fall back to default operation of "add" to prevent a no-op
  77. operation = "add"
  78. write_header()
  79. if operation:
  80. operation_code = add_remove_methods.get(operation)
  81. write_triples(self.store.contexts(), operation_code)
  82. elif target:
  83. to_add, to_remove = self._diff(target)
  84. write_triples(to_add.contexts(), "A", use_passed_contexts=True)
  85. write_triples(to_remove.contexts(), "D", use_passed_contexts=True)
  86. stream.write("TC .\n".encode(encoding, "replace"))
  87. def _diff(self, target):
  88. rows_to_add = target - self.store
  89. rows_to_remove = self.store - target
  90. return rows_to_add, rows_to_remove
  91. def _patch_row(self, triple, context_id, operation):
  92. if context_id == self.store.default_context.identifier:
  93. return f"{operation} {_nt_row(triple)}"
  94. else:
  95. return f"{operation} {_nq_row(triple, context_id)}"