mappableobject.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. from abc import (
  2. ABCMeta,
  3. abstractmethod
  4. )
  5. from typing import (
  6. Iterable,
  7. Optional
  8. )
  9. from dataladmetadatamodel.log import logger
  10. from dataladmetadatamodel.modifiableobject import ModifiableObject
  11. from dataladmetadatamodel.mapper.reference import Reference
  12. class MappableObject(ModifiableObject, metaclass=ABCMeta):
  13. """
  14. Base class for objects that can be mapped onto a
  15. storage backend.
  16. """
  17. def __init__(self,
  18. reference: Optional[Reference] = None):
  19. """
  20. Create a mappable object with a reference or None.
  21. If the reference is given, we assume that the object
  22. is saved on the respective realm, and that it is
  23. not modified.
  24. We also assume that an object that is created with
  25. a reference is not mapped. That means it has to be
  26. read in before operating on its elements.
  27. Generally unmapped objects are expected to be
  28. unmodified.
  29. """
  30. assert isinstance(reference, (type(None), Reference))
  31. # We assume that objects that carry a reference
  32. # have been saved on the realm in the reference.
  33. # That also means that they are not modified
  34. super().__init__(
  35. reference.realm
  36. if reference is not None and not reference.is_none_reference()
  37. else None
  38. )
  39. self.reference = reference
  40. self.mapper_private_data = dict()
  41. self.mapped = reference is None
  42. assert isinstance(reference, (type(None), Reference)), \
  43. f"object {self} initialized with invalid reference: {reference}"
  44. def get_modifiable_sub_objects(self) -> Iterable["MappableObject"]:
  45. """
  46. Mappable objects might be mapped (in memory) or not mapped
  47. (stored on secondary storage and purged in order to consume
  48. as little memory as possible).
  49. If on abject is not mapped, we assume that it is not modified
  50. because modifying means: "read-in", "modify", "write-out", and
  51. "purge". Since "purge" will only succeed if the object was
  52. written out.
  53. """
  54. if not self.mapped:
  55. return []
  56. # delegate to our subclasses
  57. return self.get_modifiable_sub_objects_impl()
  58. def read_in(self,
  59. backend_type="git") -> "MappableObject":
  60. from dataladmetadatamodel.mapper import get_mapper
  61. if self.mapped is False:
  62. assert self.reference is not None
  63. # if the reference is a None-reference,
  64. # we can handle this here
  65. if self.reference.is_none_reference():
  66. assert self.reference.class_name == type(self).__name__
  67. logger.warning(f"read_in({self}): None-reference in {self}")
  68. self.purge_impl()
  69. return self
  70. # ensure that the object is save on the given realm
  71. if not self.is_saved_on(self.reference.realm):
  72. logger.error(
  73. f"read_in({self}): trying to overwrite a modified object")
  74. raise RuntimeError(
  75. "read_in({self}): tried to read over a modified object")
  76. # the object is not mapped, but saved on reference.realm,
  77. # use the mappable object-specific mapper to read
  78. # the object in.
  79. get_mapper(
  80. type(self).__name__,
  81. backend_type).map_in(
  82. self,
  83. self.reference)
  84. # Mark the object as mapped.
  85. self.mapped = True
  86. else:
  87. logger.debug(
  88. f"read_in({self}): not needed, object is already mapped")
  89. return self
  90. def write_out(self,
  91. destination: Optional[str] = None,
  92. backend_type: str = "git",
  93. force_write: bool = False) -> Reference:
  94. from dataladmetadatamodel.mapper import get_mapper
  95. # if the object is not mapped and not modified,
  96. # we do not have to do anything.
  97. if not self.mapped:
  98. assert isinstance(self.reference, Reference), \
  99. f"write_out: object {self} has no valid " \
  100. f"reference: {self.reference}"
  101. if not self.is_saved_on(destination):
  102. if self.reference.is_none_reference():
  103. logger.debug(
  104. f"write_out({self}): not possible, because object "
  105. f" has a None-reference: {self.reference}")
  106. return self.reference
  107. raise RuntimeError(
  108. f"write_out({self}): modified object got lost "
  109. f"on {destination}")
  110. logger.debug(
  111. f"write_out({self}): not needed, object already"
  112. f" saved on {destination}")
  113. return self.reference
  114. if self.reference and not self.reference.is_none_reference():
  115. destination = destination or self.reference.realm
  116. assert destination is not None, \
  117. f"write_out({self}): no destination available for {self}"
  118. if self.is_saved_on(destination):
  119. if not force_write:
  120. logger.debug(
  121. f"write_out({self}): skipping map_out because {self} "
  122. f"is already stored on {destination}")
  123. return self.reference
  124. logger.debug(
  125. f"write_out({self}): forcing map_out, although {self} "
  126. f"is already stored on {destination}")
  127. logger.debug(
  128. f"write_out({self}): calling map_out to save {self} "
  129. f"to {destination}")
  130. self.reference = get_mapper(
  131. type(self).__name__,
  132. backend_type).map_out(
  133. self,
  134. destination,
  135. force_write)
  136. self.set_saved_on(destination)
  137. assert isinstance(self.reference, Reference), \
  138. f"write_out({self}): object {self} has no valid " \
  139. f"reference: {self.reference}"
  140. return self.reference
  141. def purge(self):
  142. if self.mapped:
  143. if len(self.saved_on) == 0:
  144. raise ValueError(
  145. f"purge({self}): called with unsaved object: {self}")
  146. self.purge_impl()
  147. self.mapped = False
  148. def ensure_mapped(self,
  149. backend_type="git") -> bool:
  150. if not self.mapped:
  151. self.read_in(backend_type)
  152. return True
  153. return False
  154. def deepcopy(self,
  155. new_mapper_family: Optional[str] = None,
  156. new_destination: Optional[str] = None,
  157. **kwargs) -> "MappableObject":
  158. needs_purge = self.ensure_mapped()
  159. result = self.deepcopy_impl(new_mapper_family,
  160. new_destination,
  161. **kwargs)
  162. if needs_purge is True:
  163. self.purge()
  164. return result
  165. @abstractmethod
  166. def get_modifiable_sub_objects_impl(self) -> Iterable["MappableObject"]:
  167. raise NotImplementedError
  168. @abstractmethod
  169. def deepcopy_impl(self,
  170. new_mapper_family: Optional[str] = None,
  171. new_destination: Optional[str] = None,
  172. **kwargs) -> "MappableObject":
  173. raise NotImplementedError
  174. @abstractmethod
  175. def purge_impl(self):
  176. raise NotImplementedError