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.
- amulet/__init__.py +27 -27
- amulet/__pyinstaller/__init__.py +2 -2
- amulet/__pyinstaller/hook-amulet.py +4 -4
- amulet/_version.py +21 -21
- amulet/api/__init__.py +2 -2
- amulet/api/abstract_base_entity.py +128 -128
- amulet/api/block.py +630 -630
- amulet/api/block_entity.py +71 -71
- amulet/api/cache.py +107 -107
- amulet/api/chunk/__init__.py +6 -6
- amulet/api/chunk/biomes.py +207 -207
- amulet/api/chunk/block_entity_dict.py +175 -175
- amulet/api/chunk/blocks.py +46 -46
- amulet/api/chunk/chunk.py +389 -389
- amulet/api/chunk/entity_list.py +75 -75
- amulet/api/chunk/status.py +167 -167
- amulet/api/data_types/__init__.py +4 -4
- amulet/api/data_types/generic_types.py +4 -4
- amulet/api/data_types/operation_types.py +16 -16
- amulet/api/data_types/world_types.py +49 -49
- amulet/api/data_types/wrapper_types.py +71 -71
- amulet/api/entity.py +74 -74
- amulet/api/errors.py +119 -119
- amulet/api/history/__init__.py +36 -36
- amulet/api/history/base/__init__.py +3 -3
- amulet/api/history/base/base_history.py +26 -26
- amulet/api/history/base/history_manager.py +63 -63
- amulet/api/history/base/revision_manager.py +73 -73
- amulet/api/history/changeable.py +15 -15
- amulet/api/history/data_types.py +7 -7
- amulet/api/history/history_manager/__init__.py +3 -3
- amulet/api/history/history_manager/container.py +102 -102
- amulet/api/history/history_manager/database.py +279 -279
- amulet/api/history/history_manager/meta.py +93 -93
- amulet/api/history/history_manager/object.py +116 -116
- amulet/api/history/revision_manager/__init__.py +2 -2
- amulet/api/history/revision_manager/disk.py +33 -33
- amulet/api/history/revision_manager/ram.py +12 -12
- amulet/api/item.py +75 -75
- amulet/api/level/__init__.py +4 -4
- amulet/api/level/base_level/__init__.py +1 -1
- amulet/api/level/base_level/base_level.py +1035 -1026
- amulet/api/level/base_level/chunk_manager.py +227 -227
- amulet/api/level/base_level/clone.py +389 -389
- amulet/api/level/base_level/player_manager.py +101 -101
- amulet/api/level/immutable_structure/__init__.py +1 -1
- amulet/api/level/immutable_structure/immutable_structure.py +94 -94
- amulet/api/level/immutable_structure/void_format_wrapper.py +117 -117
- amulet/api/level/structure.py +22 -22
- amulet/api/level/world.py +19 -19
- amulet/api/partial_3d_array/__init__.py +2 -2
- amulet/api/partial_3d_array/base_partial_3d_array.py +263 -263
- amulet/api/partial_3d_array/bounded_partial_3d_array.py +528 -528
- amulet/api/partial_3d_array/data_types.py +15 -15
- amulet/api/partial_3d_array/unbounded_partial_3d_array.py +229 -229
- amulet/api/partial_3d_array/util.py +152 -152
- amulet/api/player.py +65 -65
- amulet/api/registry/__init__.py +2 -2
- amulet/api/registry/base_registry.py +34 -34
- amulet/api/registry/biome_manager.py +153 -153
- amulet/api/registry/block_manager.py +156 -156
- amulet/api/selection/__init__.py +2 -2
- amulet/api/selection/abstract_selection.py +315 -315
- amulet/api/selection/box.py +805 -805
- amulet/api/selection/group.py +488 -488
- amulet/api/structure.py +37 -37
- amulet/api/wrapper/__init__.py +8 -8
- amulet/api/wrapper/chunk/interface.py +441 -441
- amulet/api/wrapper/chunk/translator.py +567 -567
- amulet/api/wrapper/format_wrapper.py +772 -772
- amulet/api/wrapper/structure_format_wrapper.py +116 -116
- amulet/api/wrapper/world_format_wrapper.py +63 -63
- amulet/level/__init__.py +1 -1
- amulet/level/formats/anvil_forge_world.py +40 -40
- amulet/level/formats/anvil_world/__init__.py +3 -3
- amulet/level/formats/anvil_world/_sector_manager.py +291 -384
- amulet/level/formats/anvil_world/data_pack/__init__.py +2 -2
- amulet/level/formats/anvil_world/data_pack/data_pack.py +224 -224
- amulet/level/formats/anvil_world/data_pack/data_pack_manager.py +77 -77
- amulet/level/formats/anvil_world/dimension.py +177 -177
- amulet/level/formats/anvil_world/format.py +769 -769
- amulet/level/formats/anvil_world/region.py +384 -384
- amulet/level/formats/construction/__init__.py +3 -3
- amulet/level/formats/construction/format_wrapper.py +515 -515
- amulet/level/formats/construction/interface.py +134 -134
- amulet/level/formats/construction/section.py +60 -60
- amulet/level/formats/construction/util.py +165 -165
- amulet/level/formats/leveldb_world/__init__.py +3 -3
- amulet/level/formats/leveldb_world/chunk.py +33 -33
- amulet/level/formats/leveldb_world/dimension.py +385 -419
- amulet/level/formats/leveldb_world/format.py +659 -641
- amulet/level/formats/leveldb_world/interface/chunk/__init__.py +36 -36
- amulet/level/formats/leveldb_world/interface/chunk/base_leveldb_interface.py +836 -836
- amulet/level/formats/leveldb_world/interface/chunk/generate_interface.py +31 -31
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_0.py +30 -30
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_1.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_10.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_11.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_12.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_13.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_14.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_15.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_16.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_17.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_18.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_19.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_2.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_20.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_21.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_22.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_23.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_24.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_25.py +24 -24
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_26.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_27.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_28.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_29.py +33 -33
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_3.py +57 -57
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_30.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_31.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_32.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_33.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_34.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_35.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_36.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_37.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_38.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_39.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_4.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_40.py +16 -16
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_5.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_6.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_7.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_8.py +180 -180
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_9.py +18 -18
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_chunk_versions.py +79 -79
- amulet/level/formats/mcstructure/__init__.py +3 -3
- amulet/level/formats/mcstructure/chunk.py +50 -50
- amulet/level/formats/mcstructure/format_wrapper.py +408 -408
- amulet/level/formats/mcstructure/interface.py +175 -175
- amulet/level/formats/schematic/__init__.py +3 -3
- amulet/level/formats/schematic/chunk.py +55 -55
- amulet/level/formats/schematic/data_types.py +4 -4
- amulet/level/formats/schematic/format_wrapper.py +373 -373
- amulet/level/formats/schematic/interface.py +142 -142
- amulet/level/formats/sponge_schem/__init__.py +4 -4
- amulet/level/formats/sponge_schem/chunk.py +62 -62
- amulet/level/formats/sponge_schem/format_wrapper.py +463 -463
- amulet/level/formats/sponge_schem/interface.py +118 -118
- amulet/level/formats/sponge_schem/varint/__init__.py +1 -1
- amulet/level/formats/sponge_schem/varint/varint.py +87 -87
- amulet/level/interfaces/chunk/anvil/anvil_0.py +72 -72
- amulet/level/interfaces/chunk/anvil/anvil_1444.py +336 -336
- amulet/level/interfaces/chunk/anvil/anvil_1466.py +94 -94
- amulet/level/interfaces/chunk/anvil/anvil_1467.py +37 -37
- amulet/level/interfaces/chunk/anvil/anvil_1484.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1503.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1519.py +34 -34
- amulet/level/interfaces/chunk/anvil/anvil_1901.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1908.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1912.py +21 -21
- amulet/level/interfaces/chunk/anvil/anvil_1934.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_2203.py +69 -69
- amulet/level/interfaces/chunk/anvil/anvil_2529.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_2681.py +76 -76
- amulet/level/interfaces/chunk/anvil/anvil_2709.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_2844.py +267 -267
- amulet/level/interfaces/chunk/anvil/anvil_3463.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_na.py +607 -607
- amulet/level/interfaces/chunk/anvil/base_anvil_interface.py +326 -326
- amulet/level/load.py +59 -59
- amulet/level/loader.py +95 -95
- amulet/level/translators/chunk/bedrock/__init__.py +267 -267
- amulet/level/translators/chunk/bedrock/bedrock_nbt_blockstate_translator.py +46 -46
- amulet/level/translators/chunk/bedrock/bedrock_numerical_translator.py +39 -39
- amulet/level/translators/chunk/bedrock/bedrock_psudo_numerical_translator.py +37 -37
- amulet/level/translators/chunk/java/java_1_18_translator.py +40 -40
- amulet/level/translators/chunk/java/java_blockstate_translator.py +94 -94
- amulet/level/translators/chunk/java/java_numerical_translator.py +62 -62
- amulet/libs/leveldb/__init__.py +7 -7
- amulet/operations/__init__.py +5 -5
- amulet/operations/clone.py +18 -18
- amulet/operations/delete_chunk.py +32 -32
- amulet/operations/fill.py +30 -30
- amulet/operations/paste.py +65 -65
- amulet/operations/replace.py +58 -58
- amulet/utils/__init__.py +14 -14
- amulet/utils/format_utils.py +41 -41
- amulet/utils/generator.py +15 -15
- amulet/utils/matrix.py +243 -243
- amulet/utils/numpy_helpers.py +46 -46
- amulet/utils/world_utils.py +349 -349
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/METADATA +97 -97
- amulet_core-1.9.20.dist-info/RECORD +208 -0
- amulet_core-1.9.19.dist-info/RECORD +0 -208
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/WHEEL +0 -0
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/entry_points.txt +0 -0
- {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()
|