slowstore 0.1.0__tar.gz
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.
- slowstore-0.1.0/LICENSE +9 -0
- slowstore-0.1.0/PKG-INFO +114 -0
- slowstore-0.1.0/README.md +99 -0
- slowstore-0.1.0/pyproject.toml +16 -0
- slowstore-0.1.0/slowstore/__init__.py +312 -0
slowstore-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) <year> <copyright holders>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
slowstore-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: slowstore
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple filesystem backed key value storage for easy testing
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Costa Halicea
|
|
7
|
+
Author-email: costa@codechem.com
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: pydantic (>=2.8.2,<3.0.0)
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Slowstore
|
|
16
|
+
|
|
17
|
+
This is a very simple single file single class key-value store written in Python. It is designed to be slow and inefficient, and is intended to be used for testing purposes only.
|
|
18
|
+
The rules are simple:
|
|
19
|
+
Every store has it's own directory, and every item is stored in a separate JSON file.
|
|
20
|
+
|
|
21
|
+
That's it.
|
|
22
|
+
|
|
23
|
+
It is easy to use and inspect the data being manipulated.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
You can install Slowstore using pip:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install slowstore
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
You can create a new Slowstore instance by calling the constructor:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from slowstore import Slowstore
|
|
38
|
+
|
|
39
|
+
class SampleModel(BaseModel):
|
|
40
|
+
name: str
|
|
41
|
+
age: int = 0
|
|
42
|
+
|
|
43
|
+
# Create a new store and load the data
|
|
44
|
+
store = Slowstore[SampleModel](SampleModel, "mydata")).load()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**One feature that I use a lot is to commit on property change, so I can inspect exactly what is going on**
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
store.save_on_change = True
|
|
51
|
+
```
|
|
52
|
+
you can also set this from the constructor
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
store = Slowstore[SampleModel](SampleModel, "mydata", save_on_change=True).load()
|
|
56
|
+
```
|
|
57
|
+
After this code is executed, every time a property is changed,
|
|
58
|
+
the changes will be saved to disk immediately.
|
|
59
|
+
|
|
60
|
+
This is slow, but it is useful for debugging and testing.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Adding items to the store
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
s1 = store.upsert(SampleModel(name="Alice", age=30))
|
|
67
|
+
s2 = store.upsert(SampleModel(name="Bob", age=25))
|
|
68
|
+
s3 = store.upsert(SampleModel(name="Charlie", age=35))
|
|
69
|
+
store.commit_all()
|
|
70
|
+
|
|
71
|
+
s1.age = 32
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Commit the changes to the store
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
store.commit(s1)
|
|
78
|
+
store.commit(s2)
|
|
79
|
+
# or just commit all changes
|
|
80
|
+
store.commit_all()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Undo/Redo
|
|
84
|
+
every slowstore item is a proxy object around what you've created. This allows you to access the object's attributes and adds undo/redo/dirty functionality
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
s1.undo()
|
|
88
|
+
s1.redo()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Querying
|
|
92
|
+
```python
|
|
93
|
+
# get all items, returns an iterator
|
|
94
|
+
all_items = store.all()
|
|
95
|
+
|
|
96
|
+
# yield all items that match a condition
|
|
97
|
+
filtered_items = store.filter(lambda x: x.age > 30)
|
|
98
|
+
|
|
99
|
+
# get the first item that matches a condition
|
|
100
|
+
first_item = store.first(lambda x: x.age > 30)
|
|
101
|
+
|
|
102
|
+
# Check if some object or it's key is in the store
|
|
103
|
+
if s2 in store:
|
|
104
|
+
print("s2 is in store")
|
|
105
|
+
else:
|
|
106
|
+
print("s2 is not in store")
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Slowstore
|
|
2
|
+
|
|
3
|
+
This is a very simple single file single class key-value store written in Python. It is designed to be slow and inefficient, and is intended to be used for testing purposes only.
|
|
4
|
+
The rules are simple:
|
|
5
|
+
Every store has it's own directory, and every item is stored in a separate JSON file.
|
|
6
|
+
|
|
7
|
+
That's it.
|
|
8
|
+
|
|
9
|
+
It is easy to use and inspect the data being manipulated.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
You can install Slowstore using pip:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install slowstore
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
You can create a new Slowstore instance by calling the constructor:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from slowstore import Slowstore
|
|
24
|
+
|
|
25
|
+
class SampleModel(BaseModel):
|
|
26
|
+
name: str
|
|
27
|
+
age: int = 0
|
|
28
|
+
|
|
29
|
+
# Create a new store and load the data
|
|
30
|
+
store = Slowstore[SampleModel](SampleModel, "mydata")).load()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**One feature that I use a lot is to commit on property change, so I can inspect exactly what is going on**
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
store.save_on_change = True
|
|
37
|
+
```
|
|
38
|
+
you can also set this from the constructor
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
store = Slowstore[SampleModel](SampleModel, "mydata", save_on_change=True).load()
|
|
42
|
+
```
|
|
43
|
+
After this code is executed, every time a property is changed,
|
|
44
|
+
the changes will be saved to disk immediately.
|
|
45
|
+
|
|
46
|
+
This is slow, but it is useful for debugging and testing.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Adding items to the store
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
s1 = store.upsert(SampleModel(name="Alice", age=30))
|
|
53
|
+
s2 = store.upsert(SampleModel(name="Bob", age=25))
|
|
54
|
+
s3 = store.upsert(SampleModel(name="Charlie", age=35))
|
|
55
|
+
store.commit_all()
|
|
56
|
+
|
|
57
|
+
s1.age = 32
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Commit the changes to the store
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
store.commit(s1)
|
|
64
|
+
store.commit(s2)
|
|
65
|
+
# or just commit all changes
|
|
66
|
+
store.commit_all()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Undo/Redo
|
|
70
|
+
every slowstore item is a proxy object around what you've created. This allows you to access the object's attributes and adds undo/redo/dirty functionality
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
s1.undo()
|
|
74
|
+
s1.redo()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Querying
|
|
78
|
+
```python
|
|
79
|
+
# get all items, returns an iterator
|
|
80
|
+
all_items = store.all()
|
|
81
|
+
|
|
82
|
+
# yield all items that match a condition
|
|
83
|
+
filtered_items = store.filter(lambda x: x.age > 30)
|
|
84
|
+
|
|
85
|
+
# get the first item that matches a condition
|
|
86
|
+
first_item = store.first(lambda x: x.age > 30)
|
|
87
|
+
|
|
88
|
+
# Check if some object or it's key is in the store
|
|
89
|
+
if s2 in store:
|
|
90
|
+
print("s2 is in store")
|
|
91
|
+
else:
|
|
92
|
+
print("s2 is not in store")
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
98
|
+
|
|
99
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "slowstore"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Simple filesystem backed key value storage for easy testing"
|
|
5
|
+
authors = ["Costa Halicea <costa@codechem.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.12"
|
|
11
|
+
pydantic = "^2.8.2"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["poetry-core"]
|
|
16
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from logging import getLogger as get_logger
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
import datetime
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Generic, List, Literal, TypeVar, cast
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound=BaseModel)
|
|
12
|
+
|
|
13
|
+
from logging import getLogger as get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def json_default(o):
|
|
19
|
+
if isinstance(o, (datetime.date, datetime.datetime)):
|
|
20
|
+
return o.isoformat()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Change(BaseModel):
|
|
24
|
+
"""A property change that can be undone or redone on a model"""
|
|
25
|
+
|
|
26
|
+
key: str
|
|
27
|
+
prop_name: str
|
|
28
|
+
prev_val: Any
|
|
29
|
+
new_val: Any
|
|
30
|
+
date: datetime.datetime = datetime.datetime.now()
|
|
31
|
+
|
|
32
|
+
# Not supporting this yet, since i dont need it
|
|
33
|
+
transaction_id: str = ""
|
|
34
|
+
|
|
35
|
+
def __init__(self, key, prop_name, prev_val, new_val):
|
|
36
|
+
super().__init__(
|
|
37
|
+
key=key, prop_name=prop_name, prev_val=prev_val, new_val=new_val
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def undo(self, model):
|
|
41
|
+
setattr(model, self.prop_name, self.prev_val)
|
|
42
|
+
|
|
43
|
+
def redo(self, model):
|
|
44
|
+
setattr(model, self.prop_name, self.new_val)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__special_fields__ = [
|
|
48
|
+
"store",
|
|
49
|
+
"model",
|
|
50
|
+
"is_dirty",
|
|
51
|
+
"commit",
|
|
52
|
+
"__key__",
|
|
53
|
+
"__changes__",
|
|
54
|
+
"__add_change__",
|
|
55
|
+
"__reset__",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ModelProxy(BaseModel, Generic[T]):
|
|
60
|
+
store: "Slowstore[T]"
|
|
61
|
+
model: T
|
|
62
|
+
is_dirty: bool = False
|
|
63
|
+
__changes__: List[Change] = []
|
|
64
|
+
__key__: str
|
|
65
|
+
|
|
66
|
+
def __init__(self, store: "Slowstore[T]", key: str, model: T):
|
|
67
|
+
self.model = model
|
|
68
|
+
self.store = store
|
|
69
|
+
self.is_dirty = False
|
|
70
|
+
self.__key__ = key
|
|
71
|
+
self.__changes__ = []
|
|
72
|
+
|
|
73
|
+
def __getattr__(self, name):
|
|
74
|
+
if name in __special_fields__:
|
|
75
|
+
return super().__getattribute__(name)
|
|
76
|
+
else:
|
|
77
|
+
return getattr(self.model, name)
|
|
78
|
+
|
|
79
|
+
def __setattr__(self, name, value):
|
|
80
|
+
if name == "model":
|
|
81
|
+
super().__setattr__(name, value)
|
|
82
|
+
self.is_dirty = False
|
|
83
|
+
self.__changes__ = []
|
|
84
|
+
elif name in __special_fields__:
|
|
85
|
+
super().__setattr__(name, value)
|
|
86
|
+
else:
|
|
87
|
+
prev = self.model.__getattribute__(name)
|
|
88
|
+
if prev == value:
|
|
89
|
+
return
|
|
90
|
+
setattr(self.model, name, value)
|
|
91
|
+
self.__add_change__(name, prev, value)
|
|
92
|
+
self.__changes__.insert(0, Change(self.__key__, name, prev, value))
|
|
93
|
+
self.is_dirty = True
|
|
94
|
+
if self.store.save_on_change:
|
|
95
|
+
self.commit()
|
|
96
|
+
|
|
97
|
+
def commit(self):
|
|
98
|
+
self.store.commit(self)
|
|
99
|
+
|
|
100
|
+
def __add_change__(self, prop_name: str, prev_val: Any, new_val: Any):
|
|
101
|
+
change = Change(self.__key__, prop_name, prev_val, new_val)
|
|
102
|
+
self.store.__changes__.append(change)
|
|
103
|
+
self.__changes__.append(change)
|
|
104
|
+
|
|
105
|
+
def __reset__(self, count: int = sys.maxsize):
|
|
106
|
+
size = len(self.__changes__)
|
|
107
|
+
while size > 0 and count > 0:
|
|
108
|
+
change = self.__changes__.pop(0)
|
|
109
|
+
count -= 1
|
|
110
|
+
size -= 1
|
|
111
|
+
change.undo(self.model)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Slowstore(Generic[T]):
|
|
115
|
+
"""A simple key-value store that persists data to disk,
|
|
116
|
+
it uses a pydantic model as the value type
|
|
117
|
+
:param cls: The class
|
|
118
|
+
:type cls: type
|
|
119
|
+
|
|
120
|
+
:param directory: The directory where the store will be stored
|
|
121
|
+
:type directory: str
|
|
122
|
+
|
|
123
|
+
:param load_on_start: If the store should load all data on start
|
|
124
|
+
:type load_on_start: bool, Defaults to True
|
|
125
|
+
|
|
126
|
+
:param save_on_change: If the store should save data on change
|
|
127
|
+
:type save_on_change: bool, Defaults to False
|
|
128
|
+
|
|
129
|
+
:param save_on_exit: If the store should save data on exit
|
|
130
|
+
:type save_on_exit: bool, Defaults to True
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
directory: str
|
|
134
|
+
cls: type
|
|
135
|
+
save_on_change: bool = False
|
|
136
|
+
save_on_exit: bool = False
|
|
137
|
+
loaded: bool = False
|
|
138
|
+
__data__: dict[str, ModelProxy[T]]
|
|
139
|
+
__changes__: List[Change]
|
|
140
|
+
|
|
141
|
+
def __init__(self, cls: type, directory: str, **kwargs):
|
|
142
|
+
"""Creates a new Slowstore instance"""
|
|
143
|
+
super().__init__()
|
|
144
|
+
self.__data__ = {}
|
|
145
|
+
self.directory = directory
|
|
146
|
+
self.cls = cls
|
|
147
|
+
# get the model type from the generic
|
|
148
|
+
if kwargs.get("load_on_start", True):
|
|
149
|
+
self.load()
|
|
150
|
+
self.save_on_change = kwargs.get("save_on_change", False)
|
|
151
|
+
self.save_on_exit = kwargs.get("save_on_exit", True)
|
|
152
|
+
|
|
153
|
+
def get(self, key) -> T:
|
|
154
|
+
"""gets an object from the story"""
|
|
155
|
+
self.__ensure_loaded__()
|
|
156
|
+
return cast(T, self.__data__[key])
|
|
157
|
+
|
|
158
|
+
def upsert(self, key: str, value: T) -> T:
|
|
159
|
+
"""sets a new object in the store and returns it's proxy
|
|
160
|
+
if we overwrite the key it will completely change the underlying model and this can cause data inconsistencies
|
|
161
|
+
"""
|
|
162
|
+
self.__ensure_loaded__()
|
|
163
|
+
if key in self.__data__:
|
|
164
|
+
proxy = self.__data__[key]
|
|
165
|
+
if proxy.model != value:
|
|
166
|
+
self.__data__[key].model = value
|
|
167
|
+
proxy.is_dirty = True
|
|
168
|
+
|
|
169
|
+
if self.save_on_change:
|
|
170
|
+
self.commit(proxy)
|
|
171
|
+
|
|
172
|
+
return cast(T, proxy)
|
|
173
|
+
|
|
174
|
+
proxy = ModelProxy[T](self, key, value)
|
|
175
|
+
proxy.is_dirty = True
|
|
176
|
+
|
|
177
|
+
if self.save_on_change:
|
|
178
|
+
self.commit(proxy)
|
|
179
|
+
|
|
180
|
+
self.__data__[key] = proxy
|
|
181
|
+
return cast(T, proxy)
|
|
182
|
+
|
|
183
|
+
def delete(self, key: str) -> bool:
|
|
184
|
+
self.__ensure_loaded__()
|
|
185
|
+
if key in self.__data__:
|
|
186
|
+
del self.__data__[key]
|
|
187
|
+
os.remove(f"{self.directory}/{self.__sanitize_file_name__(key)}.json")
|
|
188
|
+
return True
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def __contains__(self, key: str | ModelProxy[T]) -> bool:
|
|
192
|
+
self.__ensure_loaded__()
|
|
193
|
+
|
|
194
|
+
if isinstance(key, ModelProxy):
|
|
195
|
+
return key.__key__ in self.__data__
|
|
196
|
+
|
|
197
|
+
return key in self.__data__
|
|
198
|
+
|
|
199
|
+
def __getitem__(self, key: str):
|
|
200
|
+
return cast(T, self.get(key))
|
|
201
|
+
|
|
202
|
+
def __setitem__(self, key: str, value: T):
|
|
203
|
+
return cast(T, self.upsert(key, value))
|
|
204
|
+
|
|
205
|
+
def __delitem__(self, key: str):
|
|
206
|
+
return self.delete(key)
|
|
207
|
+
|
|
208
|
+
def filter(self, filter: Callable[[str, ModelProxy], bool]):
|
|
209
|
+
"""yield all models that satisfy the filter function"""
|
|
210
|
+
for key, model in self.__data__.items():
|
|
211
|
+
if filter(key, model):
|
|
212
|
+
yield cast(T, model)
|
|
213
|
+
|
|
214
|
+
def first(
|
|
215
|
+
self,
|
|
216
|
+
filter: Callable[[str, ModelProxy[T]], bool | Literal[True] | Literal[False]],
|
|
217
|
+
) -> T | None:
|
|
218
|
+
"""return the model that satisfy the filter function otherwise return None"""
|
|
219
|
+
self.__ensure_loaded__()
|
|
220
|
+
for key, model in self.__data__.items():
|
|
221
|
+
if filter(key, model):
|
|
222
|
+
return cast(T, model)
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def update(
|
|
226
|
+
self,
|
|
227
|
+
filter: Callable[[str, ModelProxy], bool],
|
|
228
|
+
updater: Callable[[str, ModelProxy], None],
|
|
229
|
+
):
|
|
230
|
+
for key, model in self.__data__.items():
|
|
231
|
+
if filter(key, model):
|
|
232
|
+
updater(key, model)
|
|
233
|
+
|
|
234
|
+
def all(self):
|
|
235
|
+
self.__ensure_loaded__()
|
|
236
|
+
for x in self.__data__:
|
|
237
|
+
yield self.get(x)
|
|
238
|
+
|
|
239
|
+
def load(self):
|
|
240
|
+
if not os.path.exists(self.directory):
|
|
241
|
+
os.makedirs(self.directory, exist_ok=True)
|
|
242
|
+
|
|
243
|
+
for filename in os.listdir(self.directory):
|
|
244
|
+
with open(f"{self.directory}/{filename}", "r") as f:
|
|
245
|
+
try:
|
|
246
|
+
d = json.load(f)
|
|
247
|
+
key: str = d["__key__"]
|
|
248
|
+
del d["__key__"]
|
|
249
|
+
proxy = ModelProxy[T](store=self, key=key, model=self.cls(**d))
|
|
250
|
+
self.__data__[key] = proxy
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.error(f"Error loading {filename}: {e}")
|
|
253
|
+
self.loaded = True
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
def clear(self):
|
|
257
|
+
shutil.rmtree(self.directory)
|
|
258
|
+
self.__data__ = {}
|
|
259
|
+
self.loaded = False
|
|
260
|
+
self.__ensure_loaded__()
|
|
261
|
+
|
|
262
|
+
def commit_all(self):
|
|
263
|
+
self.__ensure_loaded__()
|
|
264
|
+
for proxy in self.__data__.values():
|
|
265
|
+
self.commit(proxy)
|
|
266
|
+
|
|
267
|
+
def commit(self, item: ModelProxy[T]):
|
|
268
|
+
if item.store != self:
|
|
269
|
+
raise ValueError("Item does not belong to this store")
|
|
270
|
+
if item.is_dirty:
|
|
271
|
+
with open(
|
|
272
|
+
f"{self.directory}/{self.__sanitize_file_name__(item.__key__)}.json",
|
|
273
|
+
"w",
|
|
274
|
+
) as f:
|
|
275
|
+
d = item.model.model_dump()
|
|
276
|
+
d["__key__"] = item.__key__
|
|
277
|
+
f.write(json.dumps(d, indent=2, default=json_default))
|
|
278
|
+
item.is_dirty = False
|
|
279
|
+
item.__changes__ = []
|
|
280
|
+
|
|
281
|
+
def __ensure_loaded__(self):
|
|
282
|
+
if not self.loaded:
|
|
283
|
+
self.load()
|
|
284
|
+
|
|
285
|
+
def __sanitize_file_name__(self, name: str):
|
|
286
|
+
return (
|
|
287
|
+
name.replace("/", "_")
|
|
288
|
+
.replace("\\", "_")
|
|
289
|
+
.replace(":", "_")
|
|
290
|
+
.replace(" ", "_")
|
|
291
|
+
.replace(".", "_")
|
|
292
|
+
.replace("!", "_")
|
|
293
|
+
.replace("?", "_")
|
|
294
|
+
.lower()
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def __iter__(self):
|
|
298
|
+
self.__ensure_loaded__()
|
|
299
|
+
return iter(self.__data__.keys())
|
|
300
|
+
|
|
301
|
+
def __enter__(self):
|
|
302
|
+
return self
|
|
303
|
+
|
|
304
|
+
def __exit__(self, exc_type, exc_val, exc_tb): # pyright:ignore
|
|
305
|
+
if exc_type is None and self.save_on_exit:
|
|
306
|
+
self.commit_all()
|
|
307
|
+
self.__data__ = {}
|
|
308
|
+
self.loaded = False
|
|
309
|
+
else:
|
|
310
|
+
self.__data__ = {}
|
|
311
|
+
self.loaded = False
|
|
312
|
+
return False
|