mappableobject.py 8.0 KB

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