amulet-core 1.9.19__py3-none-any.whl → 1.9.20__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.

Potentially problematic release.


This version of amulet-core might be problematic. Click here for more details.

Files changed (198) hide show
  1. amulet/__init__.py +27 -27
  2. amulet/__pyinstaller/__init__.py +2 -2
  3. amulet/__pyinstaller/hook-amulet.py +4 -4
  4. amulet/_version.py +21 -21
  5. amulet/api/__init__.py +2 -2
  6. amulet/api/abstract_base_entity.py +128 -128
  7. amulet/api/block.py +630 -630
  8. amulet/api/block_entity.py +71 -71
  9. amulet/api/cache.py +107 -107
  10. amulet/api/chunk/__init__.py +6 -6
  11. amulet/api/chunk/biomes.py +207 -207
  12. amulet/api/chunk/block_entity_dict.py +175 -175
  13. amulet/api/chunk/blocks.py +46 -46
  14. amulet/api/chunk/chunk.py +389 -389
  15. amulet/api/chunk/entity_list.py +75 -75
  16. amulet/api/chunk/status.py +167 -167
  17. amulet/api/data_types/__init__.py +4 -4
  18. amulet/api/data_types/generic_types.py +4 -4
  19. amulet/api/data_types/operation_types.py +16 -16
  20. amulet/api/data_types/world_types.py +49 -49
  21. amulet/api/data_types/wrapper_types.py +71 -71
  22. amulet/api/entity.py +74 -74
  23. amulet/api/errors.py +119 -119
  24. amulet/api/history/__init__.py +36 -36
  25. amulet/api/history/base/__init__.py +3 -3
  26. amulet/api/history/base/base_history.py +26 -26
  27. amulet/api/history/base/history_manager.py +63 -63
  28. amulet/api/history/base/revision_manager.py +73 -73
  29. amulet/api/history/changeable.py +15 -15
  30. amulet/api/history/data_types.py +7 -7
  31. amulet/api/history/history_manager/__init__.py +3 -3
  32. amulet/api/history/history_manager/container.py +102 -102
  33. amulet/api/history/history_manager/database.py +279 -279
  34. amulet/api/history/history_manager/meta.py +93 -93
  35. amulet/api/history/history_manager/object.py +116 -116
  36. amulet/api/history/revision_manager/__init__.py +2 -2
  37. amulet/api/history/revision_manager/disk.py +33 -33
  38. amulet/api/history/revision_manager/ram.py +12 -12
  39. amulet/api/item.py +75 -75
  40. amulet/api/level/__init__.py +4 -4
  41. amulet/api/level/base_level/__init__.py +1 -1
  42. amulet/api/level/base_level/base_level.py +1035 -1026
  43. amulet/api/level/base_level/chunk_manager.py +227 -227
  44. amulet/api/level/base_level/clone.py +389 -389
  45. amulet/api/level/base_level/player_manager.py +101 -101
  46. amulet/api/level/immutable_structure/__init__.py +1 -1
  47. amulet/api/level/immutable_structure/immutable_structure.py +94 -94
  48. amulet/api/level/immutable_structure/void_format_wrapper.py +117 -117
  49. amulet/api/level/structure.py +22 -22
  50. amulet/api/level/world.py +19 -19
  51. amulet/api/partial_3d_array/__init__.py +2 -2
  52. amulet/api/partial_3d_array/base_partial_3d_array.py +263 -263
  53. amulet/api/partial_3d_array/bounded_partial_3d_array.py +528 -528
  54. amulet/api/partial_3d_array/data_types.py +15 -15
  55. amulet/api/partial_3d_array/unbounded_partial_3d_array.py +229 -229
  56. amulet/api/partial_3d_array/util.py +152 -152
  57. amulet/api/player.py +65 -65
  58. amulet/api/registry/__init__.py +2 -2
  59. amulet/api/registry/base_registry.py +34 -34
  60. amulet/api/registry/biome_manager.py +153 -153
  61. amulet/api/registry/block_manager.py +156 -156
  62. amulet/api/selection/__init__.py +2 -2
  63. amulet/api/selection/abstract_selection.py +315 -315
  64. amulet/api/selection/box.py +805 -805
  65. amulet/api/selection/group.py +488 -488
  66. amulet/api/structure.py +37 -37
  67. amulet/api/wrapper/__init__.py +8 -8
  68. amulet/api/wrapper/chunk/interface.py +441 -441
  69. amulet/api/wrapper/chunk/translator.py +567 -567
  70. amulet/api/wrapper/format_wrapper.py +772 -772
  71. amulet/api/wrapper/structure_format_wrapper.py +116 -116
  72. amulet/api/wrapper/world_format_wrapper.py +63 -63
  73. amulet/level/__init__.py +1 -1
  74. amulet/level/formats/anvil_forge_world.py +40 -40
  75. amulet/level/formats/anvil_world/__init__.py +3 -3
  76. amulet/level/formats/anvil_world/_sector_manager.py +291 -384
  77. amulet/level/formats/anvil_world/data_pack/__init__.py +2 -2
  78. amulet/level/formats/anvil_world/data_pack/data_pack.py +224 -224
  79. amulet/level/formats/anvil_world/data_pack/data_pack_manager.py +77 -77
  80. amulet/level/formats/anvil_world/dimension.py +177 -177
  81. amulet/level/formats/anvil_world/format.py +769 -769
  82. amulet/level/formats/anvil_world/region.py +384 -384
  83. amulet/level/formats/construction/__init__.py +3 -3
  84. amulet/level/formats/construction/format_wrapper.py +515 -515
  85. amulet/level/formats/construction/interface.py +134 -134
  86. amulet/level/formats/construction/section.py +60 -60
  87. amulet/level/formats/construction/util.py +165 -165
  88. amulet/level/formats/leveldb_world/__init__.py +3 -3
  89. amulet/level/formats/leveldb_world/chunk.py +33 -33
  90. amulet/level/formats/leveldb_world/dimension.py +385 -419
  91. amulet/level/formats/leveldb_world/format.py +659 -641
  92. amulet/level/formats/leveldb_world/interface/chunk/__init__.py +36 -36
  93. amulet/level/formats/leveldb_world/interface/chunk/base_leveldb_interface.py +836 -836
  94. amulet/level/formats/leveldb_world/interface/chunk/generate_interface.py +31 -31
  95. amulet/level/formats/leveldb_world/interface/chunk/leveldb_0.py +30 -30
  96. amulet/level/formats/leveldb_world/interface/chunk/leveldb_1.py +12 -12
  97. amulet/level/formats/leveldb_world/interface/chunk/leveldb_10.py +12 -12
  98. amulet/level/formats/leveldb_world/interface/chunk/leveldb_11.py +12 -12
  99. amulet/level/formats/leveldb_world/interface/chunk/leveldb_12.py +12 -12
  100. amulet/level/formats/leveldb_world/interface/chunk/leveldb_13.py +12 -12
  101. amulet/level/formats/leveldb_world/interface/chunk/leveldb_14.py +12 -12
  102. amulet/level/formats/leveldb_world/interface/chunk/leveldb_15.py +12 -12
  103. amulet/level/formats/leveldb_world/interface/chunk/leveldb_16.py +12 -12
  104. amulet/level/formats/leveldb_world/interface/chunk/leveldb_17.py +12 -12
  105. amulet/level/formats/leveldb_world/interface/chunk/leveldb_18.py +12 -12
  106. amulet/level/formats/leveldb_world/interface/chunk/leveldb_19.py +12 -12
  107. amulet/level/formats/leveldb_world/interface/chunk/leveldb_2.py +12 -12
  108. amulet/level/formats/leveldb_world/interface/chunk/leveldb_20.py +12 -12
  109. amulet/level/formats/leveldb_world/interface/chunk/leveldb_21.py +12 -12
  110. amulet/level/formats/leveldb_world/interface/chunk/leveldb_22.py +12 -12
  111. amulet/level/formats/leveldb_world/interface/chunk/leveldb_23.py +10 -10
  112. amulet/level/formats/leveldb_world/interface/chunk/leveldb_24.py +10 -10
  113. amulet/level/formats/leveldb_world/interface/chunk/leveldb_25.py +24 -24
  114. amulet/level/formats/leveldb_world/interface/chunk/leveldb_26.py +10 -10
  115. amulet/level/formats/leveldb_world/interface/chunk/leveldb_27.py +10 -10
  116. amulet/level/formats/leveldb_world/interface/chunk/leveldb_28.py +10 -10
  117. amulet/level/formats/leveldb_world/interface/chunk/leveldb_29.py +33 -33
  118. amulet/level/formats/leveldb_world/interface/chunk/leveldb_3.py +57 -57
  119. amulet/level/formats/leveldb_world/interface/chunk/leveldb_30.py +10 -10
  120. amulet/level/formats/leveldb_world/interface/chunk/leveldb_31.py +10 -10
  121. amulet/level/formats/leveldb_world/interface/chunk/leveldb_32.py +10 -10
  122. amulet/level/formats/leveldb_world/interface/chunk/leveldb_33.py +10 -10
  123. amulet/level/formats/leveldb_world/interface/chunk/leveldb_34.py +10 -10
  124. amulet/level/formats/leveldb_world/interface/chunk/leveldb_35.py +10 -10
  125. amulet/level/formats/leveldb_world/interface/chunk/leveldb_36.py +10 -10
  126. amulet/level/formats/leveldb_world/interface/chunk/leveldb_37.py +10 -10
  127. amulet/level/formats/leveldb_world/interface/chunk/leveldb_38.py +10 -10
  128. amulet/level/formats/leveldb_world/interface/chunk/leveldb_39.py +12 -12
  129. amulet/level/formats/leveldb_world/interface/chunk/leveldb_4.py +12 -12
  130. amulet/level/formats/leveldb_world/interface/chunk/leveldb_40.py +16 -16
  131. amulet/level/formats/leveldb_world/interface/chunk/leveldb_5.py +12 -12
  132. amulet/level/formats/leveldb_world/interface/chunk/leveldb_6.py +12 -12
  133. amulet/level/formats/leveldb_world/interface/chunk/leveldb_7.py +12 -12
  134. amulet/level/formats/leveldb_world/interface/chunk/leveldb_8.py +180 -180
  135. amulet/level/formats/leveldb_world/interface/chunk/leveldb_9.py +18 -18
  136. amulet/level/formats/leveldb_world/interface/chunk/leveldb_chunk_versions.py +79 -79
  137. amulet/level/formats/mcstructure/__init__.py +3 -3
  138. amulet/level/formats/mcstructure/chunk.py +50 -50
  139. amulet/level/formats/mcstructure/format_wrapper.py +408 -408
  140. amulet/level/formats/mcstructure/interface.py +175 -175
  141. amulet/level/formats/schematic/__init__.py +3 -3
  142. amulet/level/formats/schematic/chunk.py +55 -55
  143. amulet/level/formats/schematic/data_types.py +4 -4
  144. amulet/level/formats/schematic/format_wrapper.py +373 -373
  145. amulet/level/formats/schematic/interface.py +142 -142
  146. amulet/level/formats/sponge_schem/__init__.py +4 -4
  147. amulet/level/formats/sponge_schem/chunk.py +62 -62
  148. amulet/level/formats/sponge_schem/format_wrapper.py +463 -463
  149. amulet/level/formats/sponge_schem/interface.py +118 -118
  150. amulet/level/formats/sponge_schem/varint/__init__.py +1 -1
  151. amulet/level/formats/sponge_schem/varint/varint.py +87 -87
  152. amulet/level/interfaces/chunk/anvil/anvil_0.py +72 -72
  153. amulet/level/interfaces/chunk/anvil/anvil_1444.py +336 -336
  154. amulet/level/interfaces/chunk/anvil/anvil_1466.py +94 -94
  155. amulet/level/interfaces/chunk/anvil/anvil_1467.py +37 -37
  156. amulet/level/interfaces/chunk/anvil/anvil_1484.py +20 -20
  157. amulet/level/interfaces/chunk/anvil/anvil_1503.py +20 -20
  158. amulet/level/interfaces/chunk/anvil/anvil_1519.py +34 -34
  159. amulet/level/interfaces/chunk/anvil/anvil_1901.py +20 -20
  160. amulet/level/interfaces/chunk/anvil/anvil_1908.py +20 -20
  161. amulet/level/interfaces/chunk/anvil/anvil_1912.py +21 -21
  162. amulet/level/interfaces/chunk/anvil/anvil_1934.py +20 -20
  163. amulet/level/interfaces/chunk/anvil/anvil_2203.py +69 -69
  164. amulet/level/interfaces/chunk/anvil/anvil_2529.py +19 -19
  165. amulet/level/interfaces/chunk/anvil/anvil_2681.py +76 -76
  166. amulet/level/interfaces/chunk/anvil/anvil_2709.py +19 -19
  167. amulet/level/interfaces/chunk/anvil/anvil_2844.py +267 -267
  168. amulet/level/interfaces/chunk/anvil/anvil_3463.py +19 -19
  169. amulet/level/interfaces/chunk/anvil/anvil_na.py +607 -607
  170. amulet/level/interfaces/chunk/anvil/base_anvil_interface.py +326 -326
  171. amulet/level/load.py +59 -59
  172. amulet/level/loader.py +95 -95
  173. amulet/level/translators/chunk/bedrock/__init__.py +267 -267
  174. amulet/level/translators/chunk/bedrock/bedrock_nbt_blockstate_translator.py +46 -46
  175. amulet/level/translators/chunk/bedrock/bedrock_numerical_translator.py +39 -39
  176. amulet/level/translators/chunk/bedrock/bedrock_psudo_numerical_translator.py +37 -37
  177. amulet/level/translators/chunk/java/java_1_18_translator.py +40 -40
  178. amulet/level/translators/chunk/java/java_blockstate_translator.py +94 -94
  179. amulet/level/translators/chunk/java/java_numerical_translator.py +62 -62
  180. amulet/libs/leveldb/__init__.py +7 -7
  181. amulet/operations/__init__.py +5 -5
  182. amulet/operations/clone.py +18 -18
  183. amulet/operations/delete_chunk.py +32 -32
  184. amulet/operations/fill.py +30 -30
  185. amulet/operations/paste.py +65 -65
  186. amulet/operations/replace.py +58 -58
  187. amulet/utils/__init__.py +14 -14
  188. amulet/utils/format_utils.py +41 -41
  189. amulet/utils/generator.py +15 -15
  190. amulet/utils/matrix.py +243 -243
  191. amulet/utils/numpy_helpers.py +46 -46
  192. amulet/utils/world_utils.py +349 -349
  193. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/METADATA +97 -97
  194. amulet_core-1.9.20.dist-info/RECORD +208 -0
  195. amulet_core-1.9.19.dist-info/RECORD +0 -208
  196. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/WHEEL +0 -0
  197. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/entry_points.txt +0 -0
  198. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/top_level.txt +0 -0
@@ -1,279 +1,279 @@
1
- """
2
- The DatabaseHistoryManager is like a dictionary that can cache historical versions of the values.
3
- The class consists of the temporary database in RAM, the cache on disk and a way to pull from the original data source.
4
- The temporary database:
5
- This is a normal dictionary in RAM.
6
- When accessing and modifying data this is the data you are modifying.
7
- The cache on disk:
8
- This is a database on disk of the data serialised using pickle.
9
- This is populated with the original version of the data and each revision of the data.
10
- The temporary database can be cleared using the unload methods and successive get calls will re-populate the temporary database from this cache.
11
- As previously stated, the cache also stores historical versions of the data which enables undoing and redoing changes.
12
- The original data source (raw form)
13
- This is the original data from the world/structure.
14
- If the data does not exist in the temporary or cache databases it will be loaded from here.
15
- """
16
-
17
- from abc import abstractmethod
18
- from typing import Tuple, Any, Dict, Generator, Iterable, Set
19
- import threading
20
-
21
- from amulet.api.history.data_types import EntryKeyType, EntryType
22
- from amulet.api.history.base import RevisionManager
23
- from amulet.api.history import Changeable
24
- from .container import ContainerHistoryManager
25
- from amulet.api.errors import EntryDoesNotExist, EntryLoadError
26
- from ..revision_manager import RAMRevisionManager
27
-
28
- SnapshotType = Tuple[Any, ...]
29
-
30
-
31
- class DatabaseHistoryManager(ContainerHistoryManager):
32
- """Manage the history of a number of items in a database."""
33
-
34
- _temporary_database: Dict[EntryKeyType, EntryType]
35
- _history_database: Dict[EntryKeyType, RevisionManager]
36
-
37
- DoesNotExistError = EntryDoesNotExist
38
- LoadError = EntryLoadError
39
-
40
- def __init__(self):
41
- super().__init__()
42
- self._lock = threading.RLock()
43
- # this is the database that entries will be directly edited in
44
- self._temporary_database: Dict[EntryKeyType, EntryType] = {}
45
-
46
- # this is the database where revisions will be cached
47
- self._history_database: Dict[EntryKeyType, RevisionManager] = {}
48
-
49
- def _check_snapshot(self, snapshot: SnapshotType):
50
- assert isinstance(snapshot, tuple)
51
-
52
- @property
53
- def changed(self) -> bool:
54
- """Have there been modifications since the last save."""
55
- if super().changed:
56
- return True
57
- try:
58
- next(self.changed_entries())
59
- except StopIteration:
60
- return False
61
- else:
62
- return True
63
-
64
- def changed_entries(self) -> Generator[EntryKeyType, None, None]:
65
- """A generator of all the entry keys that have changed since the last save."""
66
- changed = set()
67
- with self._lock:
68
- for key, entry in self._temporary_database.items():
69
- if entry is None:
70
- # If the temporary entry is deleted but there was no historical
71
- # record or the historical record was not deleted
72
- if (
73
- key not in self._history_database
74
- or not self._history_database[key].is_deleted
75
- ):
76
- changed.add(key)
77
- yield key
78
-
79
- elif entry.changed:
80
- changed.add(key)
81
- yield key
82
- for key, history_entry in self._history_database.items():
83
- if history_entry.changed and key not in changed:
84
- yield key
85
-
86
- def _all_entries(self, *args, **kwargs) -> Set[EntryKeyType]:
87
- with self._lock:
88
- keys = set()
89
- deleted_keys = set()
90
- for key in self._temporary_database.keys():
91
- if self._temporary_database[key] is None:
92
- deleted_keys.add(key)
93
- else:
94
- keys.add(key)
95
-
96
- for key in self._history_database.keys():
97
- if key not in self._temporary_database:
98
- if self._history_database[key].is_deleted:
99
- deleted_keys.add(key)
100
- else:
101
- keys.add(key)
102
-
103
- for key in self._raw_all_entries(*args, **kwargs):
104
- if key not in keys and key not in deleted_keys:
105
- keys.add(key)
106
-
107
- return keys
108
-
109
- @abstractmethod
110
- def _raw_all_entries(self, *args, **kwargs) -> Iterable[EntryKeyType]:
111
- """
112
- The keys for all entries in the raw database.
113
- """
114
- raise NotImplementedError
115
-
116
- def __contains__(self, item: EntryKeyType) -> bool:
117
- return self._has_entry(item)
118
-
119
- def _has_entry(self, key: EntryKeyType):
120
- """
121
- Does the entry exist in one of the databases.
122
- Subclasses should implement a proper method calling this.
123
- """
124
- with self._lock:
125
- if key in self._temporary_database:
126
- return self._temporary_database[key] is not None
127
- elif key in self._history_database:
128
- return not self._history_database[key].is_deleted
129
- else:
130
- return self._raw_has_entry(key)
131
-
132
- @abstractmethod
133
- def _raw_has_entry(self, key: EntryKeyType) -> bool:
134
- """
135
- Does the raw database have this entry.
136
- Will be called if the key is not present in the loaded database.
137
- """
138
- raise NotImplementedError
139
-
140
- def _get_entry(self, key: EntryKeyType) -> Changeable:
141
- """
142
- Get a key from the database.
143
- Subclasses should implement a proper method calling this.
144
- """
145
- with self._lock:
146
- if key in self._temporary_database:
147
- # if the entry is loaded in RAM, just return it.
148
- entry = self._temporary_database[key]
149
- elif key in self._history_database:
150
- # if it is present in the cache, load it and return it.
151
- entry = self._temporary_database[key] = self._history_database[
152
- key
153
- ].get_current_entry()
154
- else:
155
- # If it has not been loaded request it from the raw database.
156
- entry = self._temporary_database[
157
- key
158
- ] = self._get_register_original_entry(key)
159
- if entry is None:
160
- raise self.DoesNotExistError
161
- return entry
162
-
163
- def _get_register_original_entry(self, key: EntryKeyType) -> EntryType:
164
- """Get and register the original entry."""
165
- try:
166
- entry = self._raw_get_entry(key)
167
- except EntryDoesNotExist:
168
- entry = None
169
- self._register_original_entry(key, entry)
170
- return entry
171
-
172
- def _register_original_entry(self, key: EntryKeyType, entry: EntryType):
173
- if key in self._history_database:
174
- raise Exception(f"The entry for {key} has already been registered.")
175
- self._history_database[key] = self._create_new_revision_manager(key, entry)
176
-
177
- @abstractmethod
178
- def _raw_get_entry(self, key: EntryKeyType) -> EntryType:
179
- """
180
- Get the entry from the raw database.
181
- Will be called if the key is not present in the loaded database.
182
- """
183
- raise NotImplementedError
184
-
185
- @staticmethod
186
- def _create_new_revision_manager(
187
- key: EntryKeyType, original_entry: EntryType
188
- ) -> RevisionManager:
189
- """Create an RevisionManager as desired and populate it with the original entry."""
190
- return RAMRevisionManager(original_entry)
191
-
192
- def _put_entry(self, key: EntryKeyType, entry: EntryType):
193
- with self._lock:
194
- entry.changed = True
195
- self._temporary_database[key] = entry
196
-
197
- def _delete_entry(self, key: EntryKeyType):
198
- with self._lock:
199
- self._temporary_database[key] = None
200
-
201
- def create_undo_point_iter(self) -> Generator[float, None, bool]:
202
- """
203
- Find all entries in the temporary database that have changed since the last undo point and create a new undo point.
204
-
205
- :return: Was an undo point created. If there were no changes no snapshot will be created.
206
- """
207
- with self._lock:
208
- snapshot = []
209
- count = len(self._temporary_database)
210
- for index, (key, entry) in enumerate(
211
- tuple(self._temporary_database.items())
212
- ):
213
- if entry is None or entry.changed:
214
- if key not in self._history_database:
215
- # The entry was added without populating from the world
216
- # populate the history with the original entry
217
- self._get_register_original_entry(key)
218
- history_entry = self._history_database[key]
219
- if (entry is None and not history_entry.is_deleted) or (
220
- entry is not None and entry.changed
221
- ):
222
- # if the entry has been modified since the last history version
223
- history_entry.put_new_entry(entry)
224
- snapshot.append(key)
225
- yield index / count
226
-
227
- self._temporary_database.clear() # unload all the data from the temporary database
228
- # so that it is repopulated from the history database. This fixes the issue of entries
229
- # being modified without the `changed` flag being set to True.
230
-
231
- return self._register_snapshot(tuple(snapshot))
232
-
233
- def _mark_saved(self):
234
- """Let the class know that the current state has been saved."""
235
- for entry in self._history_database.values():
236
- entry.mark_saved()
237
-
238
- def _undo(self, snapshot: SnapshotType):
239
- """Undoes the last set of changes to the database"""
240
- with self._lock:
241
- for key in snapshot:
242
- self._history_database[key].undo()
243
- if key in self._temporary_database:
244
- del self._temporary_database[key]
245
-
246
- def _redo(self, snapshot: SnapshotType):
247
- """Redoes the last set of changes to the database"""
248
- with self._lock:
249
- for key in snapshot:
250
- self._history_database[key].redo()
251
- if key in self._temporary_database:
252
- del self._temporary_database[key]
253
-
254
- def restore_last_undo_point(self):
255
- """Restore the state of the database to what it was when :meth:`create_undo_point_iter` was last called."""
256
- with self._lock:
257
- self._temporary_database.clear()
258
-
259
- def unload(self, *args, **kwargs):
260
- """Unload the entries loaded in RAM."""
261
- with self._lock:
262
- self._temporary_database.clear()
263
-
264
- def unload_unchanged(self, *args, **kwargs):
265
- """Unload all entries from RAM that have not been marked as changed."""
266
- with self._lock:
267
- unchanged = []
268
- for key, chunk in self._temporary_database.items():
269
- if not chunk.changed:
270
- unchanged.append(key)
271
- for key in unchanged:
272
- del self._temporary_database[key]
273
-
274
- def purge(self):
275
- """Unload all cached data. Effectively returns the class to its starting state."""
276
- with self._lock:
277
- super().purge()
278
- self._temporary_database.clear()
279
- self._history_database.clear()
1
+ """
2
+ The DatabaseHistoryManager is like a dictionary that can cache historical versions of the values.
3
+ The class consists of the temporary database in RAM, the cache on disk and a way to pull from the original data source.
4
+ The temporary database:
5
+ This is a normal dictionary in RAM.
6
+ When accessing and modifying data this is the data you are modifying.
7
+ The cache on disk:
8
+ This is a database on disk of the data serialised using pickle.
9
+ This is populated with the original version of the data and each revision of the data.
10
+ The temporary database can be cleared using the unload methods and successive get calls will re-populate the temporary database from this cache.
11
+ As previously stated, the cache also stores historical versions of the data which enables undoing and redoing changes.
12
+ The original data source (raw form)
13
+ This is the original data from the world/structure.
14
+ If the data does not exist in the temporary or cache databases it will be loaded from here.
15
+ """
16
+
17
+ from abc import abstractmethod
18
+ from typing import Tuple, Any, Dict, Generator, Iterable, Set
19
+ import threading
20
+
21
+ from amulet.api.history.data_types import EntryKeyType, EntryType
22
+ from amulet.api.history.base import RevisionManager
23
+ from amulet.api.history import Changeable
24
+ from .container import ContainerHistoryManager
25
+ from amulet.api.errors import EntryDoesNotExist, EntryLoadError
26
+ from ..revision_manager import RAMRevisionManager
27
+
28
+ SnapshotType = Tuple[Any, ...]
29
+
30
+
31
+ class DatabaseHistoryManager(ContainerHistoryManager):
32
+ """Manage the history of a number of items in a database."""
33
+
34
+ _temporary_database: Dict[EntryKeyType, EntryType]
35
+ _history_database: Dict[EntryKeyType, RevisionManager]
36
+
37
+ DoesNotExistError = EntryDoesNotExist
38
+ LoadError = EntryLoadError
39
+
40
+ def __init__(self):
41
+ super().__init__()
42
+ self._lock = threading.RLock()
43
+ # this is the database that entries will be directly edited in
44
+ self._temporary_database: Dict[EntryKeyType, EntryType] = {}
45
+
46
+ # this is the database where revisions will be cached
47
+ self._history_database: Dict[EntryKeyType, RevisionManager] = {}
48
+
49
+ def _check_snapshot(self, snapshot: SnapshotType):
50
+ assert isinstance(snapshot, tuple)
51
+
52
+ @property
53
+ def changed(self) -> bool:
54
+ """Have there been modifications since the last save."""
55
+ if super().changed:
56
+ return True
57
+ try:
58
+ next(self.changed_entries())
59
+ except StopIteration:
60
+ return False
61
+ else:
62
+ return True
63
+
64
+ def changed_entries(self) -> Generator[EntryKeyType, None, None]:
65
+ """A generator of all the entry keys that have changed since the last save."""
66
+ changed = set()
67
+ with self._lock:
68
+ for key, entry in self._temporary_database.items():
69
+ if entry is None:
70
+ # If the temporary entry is deleted but there was no historical
71
+ # record or the historical record was not deleted
72
+ if (
73
+ key not in self._history_database
74
+ or not self._history_database[key].is_deleted
75
+ ):
76
+ changed.add(key)
77
+ yield key
78
+
79
+ elif entry.changed:
80
+ changed.add(key)
81
+ yield key
82
+ for key, history_entry in self._history_database.items():
83
+ if history_entry.changed and key not in changed:
84
+ yield key
85
+
86
+ def _all_entries(self, *args, **kwargs) -> Set[EntryKeyType]:
87
+ with self._lock:
88
+ keys = set()
89
+ deleted_keys = set()
90
+ for key in self._temporary_database.keys():
91
+ if self._temporary_database[key] is None:
92
+ deleted_keys.add(key)
93
+ else:
94
+ keys.add(key)
95
+
96
+ for key in self._history_database.keys():
97
+ if key not in self._temporary_database:
98
+ if self._history_database[key].is_deleted:
99
+ deleted_keys.add(key)
100
+ else:
101
+ keys.add(key)
102
+
103
+ for key in self._raw_all_entries(*args, **kwargs):
104
+ if key not in keys and key not in deleted_keys:
105
+ keys.add(key)
106
+
107
+ return keys
108
+
109
+ @abstractmethod
110
+ def _raw_all_entries(self, *args, **kwargs) -> Iterable[EntryKeyType]:
111
+ """
112
+ The keys for all entries in the raw database.
113
+ """
114
+ raise NotImplementedError
115
+
116
+ def __contains__(self, item: EntryKeyType) -> bool:
117
+ return self._has_entry(item)
118
+
119
+ def _has_entry(self, key: EntryKeyType):
120
+ """
121
+ Does the entry exist in one of the databases.
122
+ Subclasses should implement a proper method calling this.
123
+ """
124
+ with self._lock:
125
+ if key in self._temporary_database:
126
+ return self._temporary_database[key] is not None
127
+ elif key in self._history_database:
128
+ return not self._history_database[key].is_deleted
129
+ else:
130
+ return self._raw_has_entry(key)
131
+
132
+ @abstractmethod
133
+ def _raw_has_entry(self, key: EntryKeyType) -> bool:
134
+ """
135
+ Does the raw database have this entry.
136
+ Will be called if the key is not present in the loaded database.
137
+ """
138
+ raise NotImplementedError
139
+
140
+ def _get_entry(self, key: EntryKeyType) -> Changeable:
141
+ """
142
+ Get a key from the database.
143
+ Subclasses should implement a proper method calling this.
144
+ """
145
+ with self._lock:
146
+ if key in self._temporary_database:
147
+ # if the entry is loaded in RAM, just return it.
148
+ entry = self._temporary_database[key]
149
+ elif key in self._history_database:
150
+ # if it is present in the cache, load it and return it.
151
+ entry = self._temporary_database[key] = self._history_database[
152
+ key
153
+ ].get_current_entry()
154
+ else:
155
+ # If it has not been loaded request it from the raw database.
156
+ entry = self._temporary_database[
157
+ key
158
+ ] = self._get_register_original_entry(key)
159
+ if entry is None:
160
+ raise self.DoesNotExistError
161
+ return entry
162
+
163
+ def _get_register_original_entry(self, key: EntryKeyType) -> EntryType:
164
+ """Get and register the original entry."""
165
+ try:
166
+ entry = self._raw_get_entry(key)
167
+ except EntryDoesNotExist:
168
+ entry = None
169
+ self._register_original_entry(key, entry)
170
+ return entry
171
+
172
+ def _register_original_entry(self, key: EntryKeyType, entry: EntryType):
173
+ if key in self._history_database:
174
+ raise Exception(f"The entry for {key} has already been registered.")
175
+ self._history_database[key] = self._create_new_revision_manager(key, entry)
176
+
177
+ @abstractmethod
178
+ def _raw_get_entry(self, key: EntryKeyType) -> EntryType:
179
+ """
180
+ Get the entry from the raw database.
181
+ Will be called if the key is not present in the loaded database.
182
+ """
183
+ raise NotImplementedError
184
+
185
+ @staticmethod
186
+ def _create_new_revision_manager(
187
+ key: EntryKeyType, original_entry: EntryType
188
+ ) -> RevisionManager:
189
+ """Create an RevisionManager as desired and populate it with the original entry."""
190
+ return RAMRevisionManager(original_entry)
191
+
192
+ def _put_entry(self, key: EntryKeyType, entry: EntryType):
193
+ with self._lock:
194
+ entry.changed = True
195
+ self._temporary_database[key] = entry
196
+
197
+ def _delete_entry(self, key: EntryKeyType):
198
+ with self._lock:
199
+ self._temporary_database[key] = None
200
+
201
+ def create_undo_point_iter(self) -> Generator[float, None, bool]:
202
+ """
203
+ Find all entries in the temporary database that have changed since the last undo point and create a new undo point.
204
+
205
+ :return: Was an undo point created. If there were no changes no snapshot will be created.
206
+ """
207
+ with self._lock:
208
+ snapshot = []
209
+ count = len(self._temporary_database)
210
+ for index, (key, entry) in enumerate(
211
+ tuple(self._temporary_database.items())
212
+ ):
213
+ if entry is None or entry.changed:
214
+ if key not in self._history_database:
215
+ # The entry was added without populating from the world
216
+ # populate the history with the original entry
217
+ self._get_register_original_entry(key)
218
+ history_entry = self._history_database[key]
219
+ if (entry is None and not history_entry.is_deleted) or (
220
+ entry is not None and entry.changed
221
+ ):
222
+ # if the entry has been modified since the last history version
223
+ history_entry.put_new_entry(entry)
224
+ snapshot.append(key)
225
+ yield index / count
226
+
227
+ self._temporary_database.clear() # unload all the data from the temporary database
228
+ # so that it is repopulated from the history database. This fixes the issue of entries
229
+ # being modified without the `changed` flag being set to True.
230
+
231
+ return self._register_snapshot(tuple(snapshot))
232
+
233
+ def _mark_saved(self):
234
+ """Let the class know that the current state has been saved."""
235
+ for entry in self._history_database.values():
236
+ entry.mark_saved()
237
+
238
+ def _undo(self, snapshot: SnapshotType):
239
+ """Undoes the last set of changes to the database"""
240
+ with self._lock:
241
+ for key in snapshot:
242
+ self._history_database[key].undo()
243
+ if key in self._temporary_database:
244
+ del self._temporary_database[key]
245
+
246
+ def _redo(self, snapshot: SnapshotType):
247
+ """Redoes the last set of changes to the database"""
248
+ with self._lock:
249
+ for key in snapshot:
250
+ self._history_database[key].redo()
251
+ if key in self._temporary_database:
252
+ del self._temporary_database[key]
253
+
254
+ def restore_last_undo_point(self):
255
+ """Restore the state of the database to what it was when :meth:`create_undo_point_iter` was last called."""
256
+ with self._lock:
257
+ self._temporary_database.clear()
258
+
259
+ def unload(self, *args, **kwargs):
260
+ """Unload the entries loaded in RAM."""
261
+ with self._lock:
262
+ self._temporary_database.clear()
263
+
264
+ def unload_unchanged(self, *args, **kwargs):
265
+ """Unload all entries from RAM that have not been marked as changed."""
266
+ with self._lock:
267
+ unchanged = []
268
+ for key, chunk in self._temporary_database.items():
269
+ if not chunk.changed:
270
+ unchanged.append(key)
271
+ for key in unchanged:
272
+ del self._temporary_database[key]
273
+
274
+ def purge(self):
275
+ """Unload all cached data. Effectively returns the class to its starting state."""
276
+ with self._lock:
277
+ super().purge()
278
+ self._temporary_database.clear()
279
+ self._history_database.clear()