syncwave 0.1.2__tar.gz → 0.1.3__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.
- {syncwave-0.1.2 → syncwave-0.1.3}/PKG-INFO +1 -1
- {syncwave-0.1.2 → syncwave-0.1.3}/pyproject.toml +1 -1
- {syncwave-0.1.2 → syncwave-0.1.3}/src/syncwave/__init__.py +1 -1
- {syncwave-0.1.2 → syncwave-0.1.3}/src/syncwave/io.py +1 -0
- {syncwave-0.1.2 → syncwave-0.1.3}/src/syncwave/syncwave.py +81 -46
- {syncwave-0.1.2 → syncwave-0.1.3}/LICENSE +0 -0
- {syncwave-0.1.2 → syncwave-0.1.3}/README.md +0 -0
- {syncwave-0.1.2 → syncwave-0.1.3}/src/syncwave/py.typed +0 -0
- {syncwave-0.1.2 → syncwave-0.1.3}/src/syncwave/watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "syncwave"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Lucas Iannitello", email = "lucas.iannitello@protonmail.com" }]
|
|
@@ -3,9 +3,9 @@ from inspect import isclass
|
|
|
3
3
|
from json import JSONDecodeError
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from threading import RLock
|
|
6
|
-
from typing import Any, Callable, Iterator, Optional, Type, Union, get_args
|
|
6
|
+
from typing import Any, Callable, Iterator, Optional, Type, Union, get_args, overload
|
|
7
7
|
|
|
8
|
-
from pydantic import BaseModel, ValidationError
|
|
8
|
+
from pydantic import BaseModel, ValidationError, create_model, model_validator
|
|
9
9
|
|
|
10
10
|
from .io import io
|
|
11
11
|
from .watcher import watcher
|
|
@@ -33,37 +33,44 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
|
|
|
33
33
|
self,
|
|
34
34
|
syncwave: "Syncwave",
|
|
35
35
|
cls: Type[BaseModel],
|
|
36
|
-
|
|
36
|
+
/,
|
|
37
37
|
name: Optional[str] = None,
|
|
38
|
-
key: str,
|
|
38
|
+
key: Optional[str] = None,
|
|
39
|
+
*,
|
|
39
40
|
skip_key_validation: bool = False,
|
|
40
41
|
file_name: Optional[str] = None,
|
|
41
42
|
sub_dir: Optional[Union[Path, str]] = None,
|
|
42
43
|
file_path: Optional[Union[Path, str]] = None,
|
|
44
|
+
new_cls_name: Optional[str] = None,
|
|
43
45
|
# in_memory: bool = True, # TODO: implement in_memory
|
|
44
46
|
) -> None:
|
|
45
47
|
if not isinstance(syncwave, Syncwave):
|
|
46
|
-
raise
|
|
48
|
+
raise TypeError("The first argument must be an instance of Syncwave.")
|
|
47
49
|
if not (isclass(cls) and issubclass(cls, BaseModel)):
|
|
48
|
-
raise
|
|
50
|
+
raise TypeError("Only subclasses of Pydantic's BaseModel can be used.")
|
|
51
|
+
# `store.cls` is created with `create_model`, with `cls` as its only base class
|
|
52
|
+
# in other words, this checks that `cls` has not been registered already
|
|
53
|
+
if any(store.cls.__bases__[0] is cls for store in syncwave.values()):
|
|
54
|
+
raise ValueError(f"The class '{cls.__name__}' has already been registered.")
|
|
55
|
+
# `name` defaults to the lowercased class name if not provided
|
|
49
56
|
if (name_ := name or cls.__name__.lower()) in syncwave:
|
|
50
|
-
raise ValueError(f"The name '{name_}'
|
|
51
|
-
if
|
|
52
|
-
raise ValueError(f"
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
raise ValueError(f"The name '{name_}' has already been registered.")
|
|
58
|
+
if not cls.model_fields:
|
|
59
|
+
raise ValueError(f"Class '{cls.__name__}' has no fields defined.")
|
|
60
|
+
# `key` defaults to the first field in the model if not provided
|
|
61
|
+
if (key_ := key or next(iter(cls.model_fields))) not in cls.model_fields:
|
|
62
|
+
raise ValueError(f"'{cls.__name__}' does not have a field named '{key_}'.")
|
|
55
63
|
if not skip_key_validation:
|
|
56
|
-
_validate_key_type(cls,
|
|
64
|
+
_validate_key_type(cls, key_)
|
|
57
65
|
|
|
58
66
|
self.syncwave = syncwave
|
|
59
|
-
self.cls = cls
|
|
67
|
+
self.cls = self._patch_cls(cls, new_cls_name or f"Syncwave{cls.__name__}")
|
|
60
68
|
self.name = name_
|
|
61
|
-
self.key =
|
|
69
|
+
self.key = key_
|
|
62
70
|
self.path = self._get_path(file_name or name_, sub_dir, file_path)
|
|
63
71
|
|
|
64
72
|
self._ssd: dict[JSONKey, BaseModel] = {} # actual mapping, ssd = SyncStoreDict
|
|
65
73
|
self._lock = RLock()
|
|
66
|
-
self._patch_cls()
|
|
67
74
|
io.init_json_file(self.path)
|
|
68
75
|
self.load()
|
|
69
76
|
watcher.watch(self.path, self.load)
|
|
@@ -75,7 +82,7 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
|
|
|
75
82
|
|
|
76
83
|
def __iter__(self) -> Iterator[JSONKey]:
|
|
77
84
|
with self._lock:
|
|
78
|
-
# first
|
|
85
|
+
# first convert to a list so the iterator is over a frozen object
|
|
79
86
|
return iter(list(self._ssd))
|
|
80
87
|
|
|
81
88
|
def __len__(self) -> int:
|
|
@@ -91,10 +98,7 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
|
|
|
91
98
|
|
|
92
99
|
def to_dict(self) -> dict[JSONKey, dict[str, Any]]:
|
|
93
100
|
with self._lock:
|
|
94
|
-
return {
|
|
95
|
-
key: model_instance.model_dump()
|
|
96
|
-
for key, model_instance in self._ssd.items()
|
|
97
|
-
}
|
|
101
|
+
return {key: instance.model_dump() for key, instance in self._ssd.items()}
|
|
98
102
|
|
|
99
103
|
def delete(self, key: JSONKey) -> None:
|
|
100
104
|
with self._lock:
|
|
@@ -129,41 +133,53 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
|
|
|
129
133
|
return (self.syncwave.data_dir / sub_dir / file_name).resolve()
|
|
130
134
|
return (self.syncwave.data_dir / file_name).resolve()
|
|
131
135
|
|
|
132
|
-
def _patch_cls(self) ->
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
original_init(model_instance, *args, **kwargs)
|
|
140
|
-
key_value = getattr(model_instance, self.key)
|
|
136
|
+
def _patch_cls(self, cls: Type[BaseModel], new_cls_name: str) -> Type[BaseModel]:
|
|
137
|
+
# This is a validator that acts as a post-initialization hook.
|
|
138
|
+
# Using a validator instead of `__init__` ensures every instance is tracked.
|
|
139
|
+
# Libraries such as FastAPI may create instances indirectly with methods like
|
|
140
|
+
# `validate_python` or `model_validate` and `__init__` may never be called.
|
|
141
|
+
def init(self_model: BaseModel) -> BaseModel:
|
|
142
|
+
key_value = getattr(self_model, self.key)
|
|
141
143
|
with self._lock:
|
|
142
|
-
self._ssd[key_value]
|
|
144
|
+
if key_value in self._ssd and self._ssd[key_value] == self_model:
|
|
145
|
+
return self_model # no changes, no need to write
|
|
146
|
+
self._ssd[key_value] = self_model
|
|
143
147
|
io.write_json(self.path, self.to_dict)
|
|
148
|
+
return self_model
|
|
149
|
+
|
|
150
|
+
new_cls = create_model(
|
|
151
|
+
new_cls_name,
|
|
152
|
+
__base__=cls,
|
|
153
|
+
__validators__={"__syncwave_init__": model_validator(mode="after")(init)},
|
|
154
|
+
)
|
|
144
155
|
|
|
145
|
-
|
|
156
|
+
# preserve original methods
|
|
157
|
+
original_setattr = cls.__setattr__
|
|
158
|
+
original_delattr = cls.__delattr__
|
|
159
|
+
|
|
160
|
+
def __setattr__(self_model: BaseModel, attr: str, value: Any) -> None:
|
|
146
161
|
# TODO check for edge cases
|
|
147
|
-
old_value = getattr(
|
|
148
|
-
original_setattr(
|
|
162
|
+
old_value = getattr(self_model, attr)
|
|
163
|
+
original_setattr(self_model, attr, value)
|
|
149
164
|
with self._lock:
|
|
150
165
|
if attr == self.key:
|
|
151
166
|
del self._ssd[old_value]
|
|
152
|
-
self._ssd[value] =
|
|
167
|
+
self._ssd[value] = self_model
|
|
153
168
|
io.write_json(self.path, self.to_dict)
|
|
154
169
|
|
|
155
|
-
def __delattr__(
|
|
156
|
-
old_value = getattr(
|
|
157
|
-
original_delattr(
|
|
170
|
+
def __delattr__(self_model: BaseModel, attr: str) -> None:
|
|
171
|
+
old_value = getattr(self_model, attr)
|
|
172
|
+
original_delattr(self_model, attr)
|
|
158
173
|
with self._lock:
|
|
159
174
|
if attr == self.key:
|
|
160
175
|
del self._ssd[old_value]
|
|
161
176
|
io.write_json(self.path, self.to_dict)
|
|
162
177
|
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
# assign the new methods
|
|
179
|
+
new_cls.__setattr__ = __setattr__
|
|
180
|
+
new_cls.__delattr__ = __delattr__
|
|
181
|
+
|
|
182
|
+
return new_cls
|
|
167
183
|
|
|
168
184
|
|
|
169
185
|
class Syncwave(Mapping[str, SyncStore]):
|
|
@@ -196,19 +212,33 @@ class Syncwave(Mapping[str, SyncStore]):
|
|
|
196
212
|
def stop(self) -> None:
|
|
197
213
|
watcher.stop()
|
|
198
214
|
|
|
215
|
+
@overload
|
|
216
|
+
def register(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
name: Optional[str] = None,
|
|
220
|
+
key: Optional[str] = None,
|
|
221
|
+
skip_key_validation: bool = False,
|
|
222
|
+
file_name: Optional[str] = None,
|
|
223
|
+
sub_dir: Optional[Union[Path, str]] = None,
|
|
224
|
+
file_path: Optional[Union[Path, str]] = None,
|
|
225
|
+
) -> Callable[[Type[BaseModel]], Type[BaseModel]]: ...
|
|
226
|
+
|
|
199
227
|
def register(
|
|
200
228
|
self,
|
|
229
|
+
_cls: Optional[Type[BaseModel]] = None,
|
|
230
|
+
/,
|
|
201
231
|
*,
|
|
202
232
|
name: Optional[str] = None,
|
|
203
|
-
key: str,
|
|
233
|
+
key: Optional[str] = None,
|
|
204
234
|
skip_key_validation: bool = False,
|
|
205
235
|
file_name: Optional[str] = None,
|
|
206
236
|
sub_dir: Optional[Union[Path, str]] = None,
|
|
207
237
|
file_path: Optional[Union[Path, str]] = None,
|
|
208
238
|
# in_memory: bool = True, # TODO: implement in_memory
|
|
209
|
-
) -> Callable[[Type[BaseModel]], Type[BaseModel]]:
|
|
239
|
+
) -> Union[Type[BaseModel], Callable[[Type[BaseModel]], Type[BaseModel]]]:
|
|
210
240
|
def decorator(cls: Type[BaseModel]) -> Type[BaseModel]:
|
|
211
|
-
SyncStore(
|
|
241
|
+
store = SyncStore(
|
|
212
242
|
self,
|
|
213
243
|
cls,
|
|
214
244
|
name=name,
|
|
@@ -217,7 +247,12 @@ class Syncwave(Mapping[str, SyncStore]):
|
|
|
217
247
|
file_name=file_name,
|
|
218
248
|
sub_dir=sub_dir,
|
|
219
249
|
file_path=file_path,
|
|
250
|
+
new_cls_name=cls.__name__,
|
|
220
251
|
)
|
|
221
|
-
return cls
|
|
252
|
+
return store.cls
|
|
222
253
|
|
|
223
|
-
|
|
254
|
+
if _cls is None:
|
|
255
|
+
# called with parenthesis: @syncwave.register(...)
|
|
256
|
+
return decorator
|
|
257
|
+
# called without parenthesis: @syncwave.register
|
|
258
|
+
return decorator(_cls)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|