pysdato 0.0.1__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.
- pysdato-0.0.1/LICENSE +21 -0
- pysdato-0.0.1/PKG-INFO +157 -0
- pysdato-0.0.1/README.md +136 -0
- pysdato-0.0.1/pys/__init__.py +192 -0
- pysdato-0.0.1/pys/pydantic.py +26 -0
- pysdato-0.0.1/pysdato.egg-info/PKG-INFO +157 -0
- pysdato-0.0.1/pysdato.egg-info/SOURCES.txt +15 -0
- pysdato-0.0.1/pysdato.egg-info/dependency_links.txt +1 -0
- pysdato-0.0.1/pysdato.egg-info/requires.txt +1 -0
- pysdato-0.0.1/pysdato.egg-info/top_level.txt +1 -0
- pysdato-0.0.1/setup.cfg +4 -0
- pysdato-0.0.1/setup.py +35 -0
- pysdato-0.0.1/tests/test_file_storage.py +93 -0
- pysdato-0.0.1/tests/test_sample1.py +39 -0
- pysdato-0.0.1/tests/test_sample2.py +37 -0
- pysdato-0.0.1/tests/test_sample3.py +33 -0
- pysdato-0.0.1/tests/test_with_id.py +42 -0
pysdato-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Stas Davydov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pysdato-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pysdato
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Simple JSON file storage for Python dataclasses and pydantic models, thread and multiprocess safe
|
|
5
|
+
Home-page: https://github.com/stasdavydov/pys
|
|
6
|
+
Author: Stas Davydov
|
|
7
|
+
Author-email: davidovsv@yandex.ru
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: filelock==3.15.4
|
|
21
|
+
|
|
22
|
+
# Python Object Storage
|
|
23
|
+
|
|
24
|
+
Simple fast JSON file storage for Python dataclasses and Pydantic models, thread and multiprocess safe.
|
|
25
|
+
|
|
26
|
+
----
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
```shell
|
|
30
|
+
pip install pysdato
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
The library is intended to store Python dataclasses or Pydantic models as JSON-files referenced by ID
|
|
35
|
+
and supports object hierarchy.
|
|
36
|
+
|
|
37
|
+
Let's say we have `Author` model. Object's ID is key point for persistence -- it will be used as name of
|
|
38
|
+
file to store and load. We can have ID as object's field, but we may also keep it outside.
|
|
39
|
+
The default expected name of ID field is `id`, but it can be changed with `id_field`
|
|
40
|
+
parameter of `@saveable` decorator: `@saveable(id_field='email')`.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
import pys
|
|
45
|
+
|
|
46
|
+
# Initialize storage with path where files will be saved
|
|
47
|
+
storage = pys.storage('.storage')
|
|
48
|
+
|
|
49
|
+
@pys.saveable
|
|
50
|
+
@dataclass
|
|
51
|
+
class Author:
|
|
52
|
+
name: str
|
|
53
|
+
|
|
54
|
+
# Persist model Author
|
|
55
|
+
leo = Author(name='Leo Tolstoy')
|
|
56
|
+
leo_id = storage.save(leo) # At this point the file `.storage/Author/<random uuid id>.json` will be saved
|
|
57
|
+
# with content {"name":"Leo Tolstoy"}
|
|
58
|
+
|
|
59
|
+
# Load model Author by its ID and check it's the same
|
|
60
|
+
another_leo = storage.load(Author, leo_id)
|
|
61
|
+
assert another_leo.name == leo.name
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Work with dependant data
|
|
65
|
+
We may have a class that relates to other classes (like Authors and their Books). We can persist
|
|
66
|
+
that dependant class separately (as we did before with `Author`), but we can also persist
|
|
67
|
+
in context of their "primary" class.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import pys
|
|
71
|
+
from pydantic import BaseModel
|
|
72
|
+
|
|
73
|
+
# An author
|
|
74
|
+
@pys.saveable
|
|
75
|
+
class Author(BaseModel):
|
|
76
|
+
name: str
|
|
77
|
+
|
|
78
|
+
# And a book
|
|
79
|
+
@pys.saveable
|
|
80
|
+
class Book(BaseModel):
|
|
81
|
+
title: str
|
|
82
|
+
|
|
83
|
+
storage = pys.storage('.storage')
|
|
84
|
+
|
|
85
|
+
# A few books of Leo Tolstoy
|
|
86
|
+
leo = Author(name='Leo Tolstoy')
|
|
87
|
+
war_and_peace = Book(title='War and peace')
|
|
88
|
+
|
|
89
|
+
# Save Leo's book
|
|
90
|
+
leo_id = storage.save(leo)
|
|
91
|
+
wnp_id = storage.save(war_and_peace, leo)
|
|
92
|
+
|
|
93
|
+
# One more author :)
|
|
94
|
+
gpt = Author(name='Chat GPT')
|
|
95
|
+
|
|
96
|
+
# Do we have the same book by GPT?
|
|
97
|
+
gpt_war_and_peace = storage.load(Book, wnp_id, gpt)
|
|
98
|
+
assert gpt_war_and_peace is None
|
|
99
|
+
|
|
100
|
+
# Now it has :)
|
|
101
|
+
storage.save(war_and_peace, gpt)
|
|
102
|
+
gpt_war_and_peace = storage.load(Book, wnp_id, gpt)
|
|
103
|
+
assert gpt_war_and_peace is not None
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
We may have as many dependant models as we need. Actually, it's the way to have model dependent indexes
|
|
107
|
+
that let us easily faster get (dependent) model list by another model.
|
|
108
|
+
```python
|
|
109
|
+
from pydantic import BaseModel
|
|
110
|
+
import pys
|
|
111
|
+
|
|
112
|
+
# An author
|
|
113
|
+
@pys.saveable
|
|
114
|
+
class Author(BaseModel):
|
|
115
|
+
name: str
|
|
116
|
+
|
|
117
|
+
# And a book
|
|
118
|
+
@pys.saveable
|
|
119
|
+
class Book(BaseModel):
|
|
120
|
+
title: str
|
|
121
|
+
|
|
122
|
+
storage = pys.storage('.storage')
|
|
123
|
+
|
|
124
|
+
# A few books of Leo Tolstoy
|
|
125
|
+
leo = Author(name='Leo Tolstoy')
|
|
126
|
+
war_and_peace = Book(title='War and peace')
|
|
127
|
+
for_kids = Book(title='For Kids')
|
|
128
|
+
|
|
129
|
+
storage.save(leo)
|
|
130
|
+
storage.save(war_and_peace, leo)
|
|
131
|
+
storage.save(for_kids, leo)
|
|
132
|
+
|
|
133
|
+
leo_books = list(storage.list(Book, leo))
|
|
134
|
+
assert len(leo_books) == 2
|
|
135
|
+
assert war_and_peace in leo_books
|
|
136
|
+
assert for_kids in leo_books
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Reference
|
|
140
|
+
```python
|
|
141
|
+
import pys
|
|
142
|
+
|
|
143
|
+
# Initialize storage
|
|
144
|
+
storage = pys.storage('.path-to-storage')
|
|
145
|
+
|
|
146
|
+
# Save a model with optional relation to other models
|
|
147
|
+
storage.save(model, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
148
|
+
|
|
149
|
+
# Load a model by ModelClass and model_id with optional relation to other models
|
|
150
|
+
storage.load(ModelClass, model_id, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
151
|
+
|
|
152
|
+
# Delete a model by ModelClass and model_id with optional relation to other models
|
|
153
|
+
storage.delete(ModelClass, model_id, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
154
|
+
|
|
155
|
+
# List models by specified ModelClass with optional relation to other models
|
|
156
|
+
storage.list(ModelClass, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
157
|
+
```
|
pysdato-0.0.1/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Python Object Storage
|
|
2
|
+
|
|
3
|
+
Simple fast JSON file storage for Python dataclasses and Pydantic models, thread and multiprocess safe.
|
|
4
|
+
|
|
5
|
+
----
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
```shell
|
|
9
|
+
pip install pysdato
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
The library is intended to store Python dataclasses or Pydantic models as JSON-files referenced by ID
|
|
14
|
+
and supports object hierarchy.
|
|
15
|
+
|
|
16
|
+
Let's say we have `Author` model. Object's ID is key point for persistence -- it will be used as name of
|
|
17
|
+
file to store and load. We can have ID as object's field, but we may also keep it outside.
|
|
18
|
+
The default expected name of ID field is `id`, but it can be changed with `id_field`
|
|
19
|
+
parameter of `@saveable` decorator: `@saveable(id_field='email')`.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
import pys
|
|
24
|
+
|
|
25
|
+
# Initialize storage with path where files will be saved
|
|
26
|
+
storage = pys.storage('.storage')
|
|
27
|
+
|
|
28
|
+
@pys.saveable
|
|
29
|
+
@dataclass
|
|
30
|
+
class Author:
|
|
31
|
+
name: str
|
|
32
|
+
|
|
33
|
+
# Persist model Author
|
|
34
|
+
leo = Author(name='Leo Tolstoy')
|
|
35
|
+
leo_id = storage.save(leo) # At this point the file `.storage/Author/<random uuid id>.json` will be saved
|
|
36
|
+
# with content {"name":"Leo Tolstoy"}
|
|
37
|
+
|
|
38
|
+
# Load model Author by its ID and check it's the same
|
|
39
|
+
another_leo = storage.load(Author, leo_id)
|
|
40
|
+
assert another_leo.name == leo.name
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Work with dependant data
|
|
44
|
+
We may have a class that relates to other classes (like Authors and their Books). We can persist
|
|
45
|
+
that dependant class separately (as we did before with `Author`), but we can also persist
|
|
46
|
+
in context of their "primary" class.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import pys
|
|
50
|
+
from pydantic import BaseModel
|
|
51
|
+
|
|
52
|
+
# An author
|
|
53
|
+
@pys.saveable
|
|
54
|
+
class Author(BaseModel):
|
|
55
|
+
name: str
|
|
56
|
+
|
|
57
|
+
# And a book
|
|
58
|
+
@pys.saveable
|
|
59
|
+
class Book(BaseModel):
|
|
60
|
+
title: str
|
|
61
|
+
|
|
62
|
+
storage = pys.storage('.storage')
|
|
63
|
+
|
|
64
|
+
# A few books of Leo Tolstoy
|
|
65
|
+
leo = Author(name='Leo Tolstoy')
|
|
66
|
+
war_and_peace = Book(title='War and peace')
|
|
67
|
+
|
|
68
|
+
# Save Leo's book
|
|
69
|
+
leo_id = storage.save(leo)
|
|
70
|
+
wnp_id = storage.save(war_and_peace, leo)
|
|
71
|
+
|
|
72
|
+
# One more author :)
|
|
73
|
+
gpt = Author(name='Chat GPT')
|
|
74
|
+
|
|
75
|
+
# Do we have the same book by GPT?
|
|
76
|
+
gpt_war_and_peace = storage.load(Book, wnp_id, gpt)
|
|
77
|
+
assert gpt_war_and_peace is None
|
|
78
|
+
|
|
79
|
+
# Now it has :)
|
|
80
|
+
storage.save(war_and_peace, gpt)
|
|
81
|
+
gpt_war_and_peace = storage.load(Book, wnp_id, gpt)
|
|
82
|
+
assert gpt_war_and_peace is not None
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
We may have as many dependant models as we need. Actually, it's the way to have model dependent indexes
|
|
86
|
+
that let us easily faster get (dependent) model list by another model.
|
|
87
|
+
```python
|
|
88
|
+
from pydantic import BaseModel
|
|
89
|
+
import pys
|
|
90
|
+
|
|
91
|
+
# An author
|
|
92
|
+
@pys.saveable
|
|
93
|
+
class Author(BaseModel):
|
|
94
|
+
name: str
|
|
95
|
+
|
|
96
|
+
# And a book
|
|
97
|
+
@pys.saveable
|
|
98
|
+
class Book(BaseModel):
|
|
99
|
+
title: str
|
|
100
|
+
|
|
101
|
+
storage = pys.storage('.storage')
|
|
102
|
+
|
|
103
|
+
# A few books of Leo Tolstoy
|
|
104
|
+
leo = Author(name='Leo Tolstoy')
|
|
105
|
+
war_and_peace = Book(title='War and peace')
|
|
106
|
+
for_kids = Book(title='For Kids')
|
|
107
|
+
|
|
108
|
+
storage.save(leo)
|
|
109
|
+
storage.save(war_and_peace, leo)
|
|
110
|
+
storage.save(for_kids, leo)
|
|
111
|
+
|
|
112
|
+
leo_books = list(storage.list(Book, leo))
|
|
113
|
+
assert len(leo_books) == 2
|
|
114
|
+
assert war_and_peace in leo_books
|
|
115
|
+
assert for_kids in leo_books
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Reference
|
|
119
|
+
```python
|
|
120
|
+
import pys
|
|
121
|
+
|
|
122
|
+
# Initialize storage
|
|
123
|
+
storage = pys.storage('.path-to-storage')
|
|
124
|
+
|
|
125
|
+
# Save a model with optional relation to other models
|
|
126
|
+
storage.save(model, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
127
|
+
|
|
128
|
+
# Load a model by ModelClass and model_id with optional relation to other models
|
|
129
|
+
storage.load(ModelClass, model_id, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
130
|
+
|
|
131
|
+
# Delete a model by ModelClass and model_id with optional relation to other models
|
|
132
|
+
storage.delete(ModelClass, model_id, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
133
|
+
|
|
134
|
+
# List models by specified ModelClass with optional relation to other models
|
|
135
|
+
storage.list(ModelClass, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
136
|
+
```
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import json
|
|
3
|
+
import shutil
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import is_dataclass, asdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Type, Optional, Tuple, Iterable, Any
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
from filelock import FileLock
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def saveable(cls=None, /, field_as_id: str = 'id'):
|
|
13
|
+
def wrapper(cls):
|
|
14
|
+
def my_id_or_default(self):
|
|
15
|
+
__default_id__ = '__default_id__'
|
|
16
|
+
if hasattr(self, field_as_id):
|
|
17
|
+
if getattr(self, field_as_id, None) is None:
|
|
18
|
+
setattr(self, field_as_id, str(uuid.uuid4()))
|
|
19
|
+
return getattr(self, field_as_id)
|
|
20
|
+
elif hasattr(self, '__loaded_id__'):
|
|
21
|
+
setattr(self, __default_id__, getattr(self, '__loaded_id__'))
|
|
22
|
+
elif getattr(self, __default_id__, None) is None:
|
|
23
|
+
setattr(self, __default_id__, str(uuid.uuid4()))
|
|
24
|
+
return getattr(self, __default_id__)
|
|
25
|
+
|
|
26
|
+
setattr(cls, '__my_id__', my_id_or_default)
|
|
27
|
+
if not hasattr(cls, '__json__'):
|
|
28
|
+
if is_dataclass(cls):
|
|
29
|
+
setattr(cls, '__json__',
|
|
30
|
+
lambda self:
|
|
31
|
+
json.dumps(
|
|
32
|
+
asdict(self),
|
|
33
|
+
separators=(',', ':')
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
elif is_pydantic(cls):
|
|
37
|
+
setattr(cls, '__json__', lambda self: self.model_dump_json())
|
|
38
|
+
else:
|
|
39
|
+
raise NotImplementedError(
|
|
40
|
+
f'The class {cls} is not @dataclass nor Pydantic Model and does not have __json__() method.'
|
|
41
|
+
f'Please implement __json__() method by yourself.')
|
|
42
|
+
return cls
|
|
43
|
+
|
|
44
|
+
if cls is None:
|
|
45
|
+
return wrapper
|
|
46
|
+
else:
|
|
47
|
+
return wrapper(cls)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_pydantic(cls):
|
|
51
|
+
return any(filter(lambda c: c.__name__ == 'BaseModel', cls.__mro__))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
StoredModel = TypeVar('StoredModel')
|
|
55
|
+
RelatedModel = TypeVar('RelatedModel')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BaseStorage(abc.ABC):
|
|
59
|
+
"""
|
|
60
|
+
Abstract base storage
|
|
61
|
+
"""
|
|
62
|
+
def load(self, model_class: Type[StoredModel], model_id: str,
|
|
63
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]) \
|
|
64
|
+
-> Optional[StoredModel]:
|
|
65
|
+
"""
|
|
66
|
+
Load model.
|
|
67
|
+
|
|
68
|
+
:param model_class: Class of the model.
|
|
69
|
+
:param model_id: Model ID.
|
|
70
|
+
:param related_model: Related model(s) -- model that the loaded model is belong to.
|
|
71
|
+
:return: Loaded model or None in case if model is not found.
|
|
72
|
+
"""
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
def save(self, model: StoredModel,
|
|
76
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]) -> Any:
|
|
77
|
+
"""
|
|
78
|
+
Save model.
|
|
79
|
+
|
|
80
|
+
:param model: A model.
|
|
81
|
+
:param related_model: Related model(s) -- model that the stored model is belong to.
|
|
82
|
+
:return Returns saved model id
|
|
83
|
+
"""
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
def delete(self, model_class: Type[StoredModel], model_id: str,
|
|
87
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Delete model.
|
|
90
|
+
|
|
91
|
+
:param model_class: Model class.
|
|
92
|
+
:param model_id: Model ID.
|
|
93
|
+
:param related_model: Related model(s) -- model that the deleted model is belong to.
|
|
94
|
+
"""
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
|
|
97
|
+
def list(self, model_class: Type[StoredModel],
|
|
98
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]) -> Iterable[StoredModel]:
|
|
99
|
+
"""
|
|
100
|
+
List models.
|
|
101
|
+
:param model_class: Model class
|
|
102
|
+
:param related_model: Related model(s) -- model that the listed models are belong to.
|
|
103
|
+
:return: List of found models.
|
|
104
|
+
"""
|
|
105
|
+
raise NotImplementedError
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class FileStorage(BaseStorage):
|
|
109
|
+
"""
|
|
110
|
+
File based storage implementation. Thread and interprocess safe.
|
|
111
|
+
"""
|
|
112
|
+
base_path: Path
|
|
113
|
+
|
|
114
|
+
def __init__(self, base_path: str | Path) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Base path for the storage files
|
|
117
|
+
:param base_path: base path.
|
|
118
|
+
"""
|
|
119
|
+
self.base_path = base_path if isinstance(base_path, Path) else Path(base_path)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _get_model_path(model_class: Type[StoredModel], model_id: str,
|
|
123
|
+
*related_model: RelatedModel | Tuple[Type[RelatedModel], str]) -> Path:
|
|
124
|
+
path = Path()
|
|
125
|
+
if not related_model:
|
|
126
|
+
path /= ''
|
|
127
|
+
else:
|
|
128
|
+
for model in related_model:
|
|
129
|
+
if isinstance(model, tuple):
|
|
130
|
+
path /= FileStorage._get_model_path(*model)
|
|
131
|
+
else:
|
|
132
|
+
path /= FileStorage._get_model_path(model.__class__, model.__my_id__())
|
|
133
|
+
|
|
134
|
+
return path / model_class.__name__ / model_id
|
|
135
|
+
|
|
136
|
+
def _prepare_file(self, model_class: Type[StoredModel], model_id: str,
|
|
137
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]):
|
|
138
|
+
path = self.base_path / FileStorage._get_model_path(model_class, model_id, *related_model).with_suffix('.json')
|
|
139
|
+
lock = path.with_suffix('.lock')
|
|
140
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
return path, FileLock(lock)
|
|
142
|
+
|
|
143
|
+
def save(self, model: StoredModel,
|
|
144
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]) -> Any:
|
|
145
|
+
model_id = model.__my_id__()
|
|
146
|
+
path, lock = self._prepare_file(model.__class__, model_id, *related_model)
|
|
147
|
+
with lock:
|
|
148
|
+
with path.open('w', encoding='UTF-8') as f:
|
|
149
|
+
f.write(model.__json__())
|
|
150
|
+
return model_id
|
|
151
|
+
|
|
152
|
+
def load(self, model_class: Type[StoredModel], model_id: str,
|
|
153
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]) -> Optional[StoredModel]:
|
|
154
|
+
path, lock = self._prepare_file(model_class, model_id, *related_model)
|
|
155
|
+
with lock:
|
|
156
|
+
if not path.exists():
|
|
157
|
+
return None
|
|
158
|
+
with open(path, 'r') as f:
|
|
159
|
+
content = json.load(f)
|
|
160
|
+
if is_pydantic(model_class):
|
|
161
|
+
model = model_class.model_validate(content)
|
|
162
|
+
else:
|
|
163
|
+
model = model_class(**content)
|
|
164
|
+
setattr(model, '__loaded_id__', model_id)
|
|
165
|
+
return model
|
|
166
|
+
|
|
167
|
+
def delete(self, model_class: Type[StoredModel], model_id: str,
|
|
168
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]) -> None:
|
|
169
|
+
path, lock = self._prepare_file(model_class, model_id, *related_model)
|
|
170
|
+
with lock:
|
|
171
|
+
path.unlink(missing_ok=True)
|
|
172
|
+
sub_path = path.with_suffix('')
|
|
173
|
+
if sub_path.exists():
|
|
174
|
+
shutil.rmtree(sub_path)
|
|
175
|
+
|
|
176
|
+
def list(self, model_class: Type[StoredModel],
|
|
177
|
+
*related_model: RelatedModel | Tuple[RelatedModel, str]) -> Iterable[StoredModel]:
|
|
178
|
+
|
|
179
|
+
path, lock = self._prepare_file(model_class, '__list__', *related_model)
|
|
180
|
+
with lock:
|
|
181
|
+
for p in path.parent.glob('*.json'):
|
|
182
|
+
yield self.load(model_class, p.with_suffix('').name, *related_model)
|
|
183
|
+
|
|
184
|
+
def __str__(self) -> str:
|
|
185
|
+
return f'file.Storage(base_path={self.base_path})'
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def storage(base_path: str | Path):
|
|
189
|
+
return FileStorage(base_path)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
__all__ = ('saveable', 'storage', )
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ModelWithID(BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Base class for models with `id` field prefilled with random UUID if not initialized.
|
|
10
|
+
"""
|
|
11
|
+
id: str = None
|
|
12
|
+
|
|
13
|
+
def __init__(self, **data: Any) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Initialize a model
|
|
16
|
+
:param data: model fields
|
|
17
|
+
"""
|
|
18
|
+
super().__init__(**data)
|
|
19
|
+
if self.id is None:
|
|
20
|
+
self.id = str(uuid.uuid4())
|
|
21
|
+
|
|
22
|
+
def __my_id__(self):
|
|
23
|
+
return self.id
|
|
24
|
+
|
|
25
|
+
def __json__(self):
|
|
26
|
+
return self.model_dump_json()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pysdato
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Simple JSON file storage for Python dataclasses and pydantic models, thread and multiprocess safe
|
|
5
|
+
Home-page: https://github.com/stasdavydov/pys
|
|
6
|
+
Author: Stas Davydov
|
|
7
|
+
Author-email: davidovsv@yandex.ru
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: filelock==3.15.4
|
|
21
|
+
|
|
22
|
+
# Python Object Storage
|
|
23
|
+
|
|
24
|
+
Simple fast JSON file storage for Python dataclasses and Pydantic models, thread and multiprocess safe.
|
|
25
|
+
|
|
26
|
+
----
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
```shell
|
|
30
|
+
pip install pysdato
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
The library is intended to store Python dataclasses or Pydantic models as JSON-files referenced by ID
|
|
35
|
+
and supports object hierarchy.
|
|
36
|
+
|
|
37
|
+
Let's say we have `Author` model. Object's ID is key point for persistence -- it will be used as name of
|
|
38
|
+
file to store and load. We can have ID as object's field, but we may also keep it outside.
|
|
39
|
+
The default expected name of ID field is `id`, but it can be changed with `id_field`
|
|
40
|
+
parameter of `@saveable` decorator: `@saveable(id_field='email')`.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
import pys
|
|
45
|
+
|
|
46
|
+
# Initialize storage with path where files will be saved
|
|
47
|
+
storage = pys.storage('.storage')
|
|
48
|
+
|
|
49
|
+
@pys.saveable
|
|
50
|
+
@dataclass
|
|
51
|
+
class Author:
|
|
52
|
+
name: str
|
|
53
|
+
|
|
54
|
+
# Persist model Author
|
|
55
|
+
leo = Author(name='Leo Tolstoy')
|
|
56
|
+
leo_id = storage.save(leo) # At this point the file `.storage/Author/<random uuid id>.json` will be saved
|
|
57
|
+
# with content {"name":"Leo Tolstoy"}
|
|
58
|
+
|
|
59
|
+
# Load model Author by its ID and check it's the same
|
|
60
|
+
another_leo = storage.load(Author, leo_id)
|
|
61
|
+
assert another_leo.name == leo.name
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Work with dependant data
|
|
65
|
+
We may have a class that relates to other classes (like Authors and their Books). We can persist
|
|
66
|
+
that dependant class separately (as we did before with `Author`), but we can also persist
|
|
67
|
+
in context of their "primary" class.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import pys
|
|
71
|
+
from pydantic import BaseModel
|
|
72
|
+
|
|
73
|
+
# An author
|
|
74
|
+
@pys.saveable
|
|
75
|
+
class Author(BaseModel):
|
|
76
|
+
name: str
|
|
77
|
+
|
|
78
|
+
# And a book
|
|
79
|
+
@pys.saveable
|
|
80
|
+
class Book(BaseModel):
|
|
81
|
+
title: str
|
|
82
|
+
|
|
83
|
+
storage = pys.storage('.storage')
|
|
84
|
+
|
|
85
|
+
# A few books of Leo Tolstoy
|
|
86
|
+
leo = Author(name='Leo Tolstoy')
|
|
87
|
+
war_and_peace = Book(title='War and peace')
|
|
88
|
+
|
|
89
|
+
# Save Leo's book
|
|
90
|
+
leo_id = storage.save(leo)
|
|
91
|
+
wnp_id = storage.save(war_and_peace, leo)
|
|
92
|
+
|
|
93
|
+
# One more author :)
|
|
94
|
+
gpt = Author(name='Chat GPT')
|
|
95
|
+
|
|
96
|
+
# Do we have the same book by GPT?
|
|
97
|
+
gpt_war_and_peace = storage.load(Book, wnp_id, gpt)
|
|
98
|
+
assert gpt_war_and_peace is None
|
|
99
|
+
|
|
100
|
+
# Now it has :)
|
|
101
|
+
storage.save(war_and_peace, gpt)
|
|
102
|
+
gpt_war_and_peace = storage.load(Book, wnp_id, gpt)
|
|
103
|
+
assert gpt_war_and_peace is not None
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
We may have as many dependant models as we need. Actually, it's the way to have model dependent indexes
|
|
107
|
+
that let us easily faster get (dependent) model list by another model.
|
|
108
|
+
```python
|
|
109
|
+
from pydantic import BaseModel
|
|
110
|
+
import pys
|
|
111
|
+
|
|
112
|
+
# An author
|
|
113
|
+
@pys.saveable
|
|
114
|
+
class Author(BaseModel):
|
|
115
|
+
name: str
|
|
116
|
+
|
|
117
|
+
# And a book
|
|
118
|
+
@pys.saveable
|
|
119
|
+
class Book(BaseModel):
|
|
120
|
+
title: str
|
|
121
|
+
|
|
122
|
+
storage = pys.storage('.storage')
|
|
123
|
+
|
|
124
|
+
# A few books of Leo Tolstoy
|
|
125
|
+
leo = Author(name='Leo Tolstoy')
|
|
126
|
+
war_and_peace = Book(title='War and peace')
|
|
127
|
+
for_kids = Book(title='For Kids')
|
|
128
|
+
|
|
129
|
+
storage.save(leo)
|
|
130
|
+
storage.save(war_and_peace, leo)
|
|
131
|
+
storage.save(for_kids, leo)
|
|
132
|
+
|
|
133
|
+
leo_books = list(storage.list(Book, leo))
|
|
134
|
+
assert len(leo_books) == 2
|
|
135
|
+
assert war_and_peace in leo_books
|
|
136
|
+
assert for_kids in leo_books
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Reference
|
|
140
|
+
```python
|
|
141
|
+
import pys
|
|
142
|
+
|
|
143
|
+
# Initialize storage
|
|
144
|
+
storage = pys.storage('.path-to-storage')
|
|
145
|
+
|
|
146
|
+
# Save a model with optional relation to other models
|
|
147
|
+
storage.save(model, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
148
|
+
|
|
149
|
+
# Load a model by ModelClass and model_id with optional relation to other models
|
|
150
|
+
storage.load(ModelClass, model_id, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
151
|
+
|
|
152
|
+
# Delete a model by ModelClass and model_id with optional relation to other models
|
|
153
|
+
storage.delete(ModelClass, model_id, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
154
|
+
|
|
155
|
+
# List models by specified ModelClass with optional relation to other models
|
|
156
|
+
storage.list(ModelClass, [related_model | (RelatedModelClass, related_model_id), ...])
|
|
157
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
pys/__init__.py
|
|
5
|
+
pys/pydantic.py
|
|
6
|
+
pysdato.egg-info/PKG-INFO
|
|
7
|
+
pysdato.egg-info/SOURCES.txt
|
|
8
|
+
pysdato.egg-info/dependency_links.txt
|
|
9
|
+
pysdato.egg-info/requires.txt
|
|
10
|
+
pysdato.egg-info/top_level.txt
|
|
11
|
+
tests/test_file_storage.py
|
|
12
|
+
tests/test_sample1.py
|
|
13
|
+
tests/test_sample2.py
|
|
14
|
+
tests/test_sample3.py
|
|
15
|
+
tests/test_with_id.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
filelock==3.15.4
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pys
|
pysdato-0.0.1/setup.cfg
ADDED
pysdato-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from distutils.core import setup
|
|
3
|
+
|
|
4
|
+
# The directory containing this file
|
|
5
|
+
HERE = pathlib.Path(__file__).parent
|
|
6
|
+
|
|
7
|
+
# The text of the README file
|
|
8
|
+
README = (HERE / "README.md").read_text()
|
|
9
|
+
|
|
10
|
+
setup(name='pysdato',
|
|
11
|
+
version='0.0.1',
|
|
12
|
+
description='Simple JSON file storage for Python dataclasses and pydantic models, thread and multiprocess safe',
|
|
13
|
+
long_description=README,
|
|
14
|
+
long_description_content_type="text/markdown",
|
|
15
|
+
url="https://github.com/stasdavydov/pys",
|
|
16
|
+
author="Stas Davydov",
|
|
17
|
+
author_email="davidovsv@yandex.ru",
|
|
18
|
+
license="MIT",
|
|
19
|
+
classifiers=[
|
|
20
|
+
"Development Status :: 5 - Production/Stable",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Topic :: Database :: Database Engines/Servers",
|
|
27
|
+
"Topic :: Software Development :: Libraries",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
],
|
|
30
|
+
packages=['pys'],
|
|
31
|
+
include_package_data=False,
|
|
32
|
+
install_requires=[
|
|
33
|
+
'filelock==3.15.4',
|
|
34
|
+
],
|
|
35
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from .model import BotOwner, Bot, BotResponse
|
|
6
|
+
import pys
|
|
7
|
+
|
|
8
|
+
base_path = Path(__file__).parent / '.storage'
|
|
9
|
+
shutil.rmtree(base_path, ignore_errors=True)
|
|
10
|
+
base_path.mkdir(parents=True, exist_ok=False)
|
|
11
|
+
storage = pys.storage(base_path)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_file_storage():
|
|
15
|
+
bot_owner = BotOwner(username='itisme', first_name='Test', last_name='Tester')
|
|
16
|
+
assert storage.base_path.absolute() == base_path.absolute()
|
|
17
|
+
path, _ = storage._prepare_file(BotOwner, bot_owner.id)
|
|
18
|
+
assert path.absolute() == (base_path / 'BotOwner' / bot_owner.id).with_suffix('.json').absolute()
|
|
19
|
+
storage.delete(bot_owner.__class__, bot_owner.id)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_load():
|
|
23
|
+
owner = BotOwner(username='itisme', first_name='Test', last_name='Tester')
|
|
24
|
+
storage.save(owner)
|
|
25
|
+
bot_owner = storage.load(BotOwner, owner.id)
|
|
26
|
+
assert bot_owner is not None
|
|
27
|
+
assert bot_owner.id == owner.id
|
|
28
|
+
assert bot_owner.username == owner.username
|
|
29
|
+
assert bot_owner.first_name == owner.first_name
|
|
30
|
+
assert bot_owner.last_name == owner.last_name
|
|
31
|
+
storage.delete(bot_owner.__class__, bot_owner.id)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_save():
|
|
35
|
+
bot_owner2a = BotOwner(first_name='Second', last_name='', username='')
|
|
36
|
+
storage.save(bot_owner2a)
|
|
37
|
+
assert (storage.base_path / 'BotOwner' / bot_owner2a.id).with_suffix('.json').exists()
|
|
38
|
+
bot_owner2b = storage.load(BotOwner, bot_owner2a.id)
|
|
39
|
+
assert bot_owner2b == bot_owner2a
|
|
40
|
+
|
|
41
|
+
storage.delete(BotOwner, bot_owner2a.id)
|
|
42
|
+
assert not (storage.base_path / 'BotOwner' / bot_owner2a.id).with_suffix('.json').exists()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_related_model():
|
|
46
|
+
bot_owner2 = BotOwner(first_name='Second', last_name='Owner', username='')
|
|
47
|
+
bot_owner3 = BotOwner(first_name='Third', last_name='Owner', username='')
|
|
48
|
+
storage.save(Bot(id='3', name='A bot'), bot_owner2)
|
|
49
|
+
storage.save(Bot(id='3', name='B bot'), bot_owner3)
|
|
50
|
+
|
|
51
|
+
bot_a = storage.load(Bot, '3', bot_owner2)
|
|
52
|
+
assert bot_a.id == '3'
|
|
53
|
+
assert bot_a.name == 'A bot'
|
|
54
|
+
|
|
55
|
+
bot_b = storage.load(Bot, '3', bot_owner3)
|
|
56
|
+
assert bot_b.id == '3'
|
|
57
|
+
assert bot_b.name == 'B bot'
|
|
58
|
+
|
|
59
|
+
storage.delete(bot_owner2.__class__, bot_owner2.id)
|
|
60
|
+
storage.delete(bot_owner3.__class__, bot_owner3.id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_related_models():
|
|
64
|
+
bot_owner = BotOwner(first_name='Second', last_name='Owner', username='')
|
|
65
|
+
bot = Bot(id='1', name='A bot')
|
|
66
|
+
storage.save(bot, bot_owner)
|
|
67
|
+
response = BotResponse(author=bot_owner, content='123')
|
|
68
|
+
storage.save(response, bot_owner, bot)
|
|
69
|
+
|
|
70
|
+
bot_response = storage.load(BotResponse, response.id, bot_owner, bot)
|
|
71
|
+
assert bot_response.id == response.id
|
|
72
|
+
assert bot_response.content == response.content
|
|
73
|
+
|
|
74
|
+
bot_response = storage.load(BotResponse, response.id, bot_owner, (Bot, bot.id))
|
|
75
|
+
assert bot_response.id == response.id
|
|
76
|
+
assert bot_response.content == response.content
|
|
77
|
+
|
|
78
|
+
storage.delete(bot_owner.__class__, bot_owner.id)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_list():
|
|
82
|
+
bot_owner = BotOwner(first_name='Second', last_name='Owner', username='')
|
|
83
|
+
storage.save(Bot(id='1', name='Bot #1'), bot_owner)
|
|
84
|
+
storage.save(Bot(id='2', name='Bot #2'), bot_owner)
|
|
85
|
+
storage.save(Bot(id='3', name='Bot #3'), bot_owner)
|
|
86
|
+
|
|
87
|
+
bot_list = list(storage.list(Bot, bot_owner))
|
|
88
|
+
assert len(bot_list) == 3
|
|
89
|
+
bot_ids = ('1', '2', '3')
|
|
90
|
+
for bot in bot_list:
|
|
91
|
+
assert bot.id in bot_ids
|
|
92
|
+
|
|
93
|
+
storage.delete(bot_owner.__class__, bot_owner.id)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import pys
|
|
4
|
+
from pys.pydantic import ModelWithID
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_sample_pydantic():
|
|
8
|
+
# Initialize storage with path where files will be saved
|
|
9
|
+
storage = pys.storage('.storage')
|
|
10
|
+
|
|
11
|
+
class Author(ModelWithID):
|
|
12
|
+
name: str
|
|
13
|
+
|
|
14
|
+
# Persist model Author
|
|
15
|
+
leo = Author(name='Leo Tolstoy')
|
|
16
|
+
storage.save(leo)
|
|
17
|
+
|
|
18
|
+
# Load model Author by its ID and check it's the same
|
|
19
|
+
another_leo = storage.load(Author, leo.id)
|
|
20
|
+
assert another_leo.id == leo.id
|
|
21
|
+
assert another_leo.name == leo.name
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_sample_dataclass():
|
|
25
|
+
# Initialize storage with path where files will be saved
|
|
26
|
+
storage = pys.storage('.storage')
|
|
27
|
+
|
|
28
|
+
@pys.saveable
|
|
29
|
+
@dataclass
|
|
30
|
+
class Author:
|
|
31
|
+
name: str
|
|
32
|
+
|
|
33
|
+
# Persist model Author
|
|
34
|
+
leo = Author(name='Leo Tolstoy')
|
|
35
|
+
leo_id = storage.save(leo)
|
|
36
|
+
|
|
37
|
+
# Load model Author by its ID and check it's the same
|
|
38
|
+
another_leo = storage.load(Author, leo_id)
|
|
39
|
+
assert another_leo.name == leo.name
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
import pys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_sample_pydantic2():
|
|
7
|
+
# An author
|
|
8
|
+
@pys.saveable
|
|
9
|
+
class Author(BaseModel):
|
|
10
|
+
name: str
|
|
11
|
+
|
|
12
|
+
# And a book
|
|
13
|
+
@pys.saveable
|
|
14
|
+
class Book(BaseModel):
|
|
15
|
+
title: str
|
|
16
|
+
|
|
17
|
+
storage = pys.storage('.storage')
|
|
18
|
+
|
|
19
|
+
# A few books of Leo Tolstoy
|
|
20
|
+
leo = Author(name='Leo Tolstoy')
|
|
21
|
+
war_and_peace = Book(title='War and peace')
|
|
22
|
+
|
|
23
|
+
# Save Leo's book
|
|
24
|
+
storage.save(leo)
|
|
25
|
+
wnp_id = storage.save(war_and_peace, leo)
|
|
26
|
+
|
|
27
|
+
# One more author :)
|
|
28
|
+
gpt = Author(name='Chat GPT')
|
|
29
|
+
|
|
30
|
+
# Do we have the same book by GPT?
|
|
31
|
+
gpt_war_and_peace = storage.load(Book, wnp_id, gpt)
|
|
32
|
+
assert gpt_war_and_peace is None
|
|
33
|
+
|
|
34
|
+
# Now it has :)
|
|
35
|
+
storage.save(war_and_peace, gpt)
|
|
36
|
+
gpt_war_and_peace = storage.load(Book, wnp_id, gpt)
|
|
37
|
+
assert gpt_war_and_peace is not None
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
import pys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# An author
|
|
7
|
+
@pys.saveable
|
|
8
|
+
class Author(BaseModel):
|
|
9
|
+
name: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# And a book
|
|
13
|
+
@pys.saveable
|
|
14
|
+
class Book(BaseModel):
|
|
15
|
+
title: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_sample():
|
|
19
|
+
storage = pys.storage('.storage')
|
|
20
|
+
|
|
21
|
+
# A few books of Leo Tolstoy
|
|
22
|
+
leo = Author(name='Leo Tolstoy')
|
|
23
|
+
war_and_peace = Book(title='War and peace')
|
|
24
|
+
for_kids = Book(title='For Kids')
|
|
25
|
+
|
|
26
|
+
storage.save(leo)
|
|
27
|
+
storage.save(war_and_peace, leo)
|
|
28
|
+
storage.save(for_kids, leo)
|
|
29
|
+
|
|
30
|
+
leo_books = list(storage.list(Book, leo))
|
|
31
|
+
assert len(leo_books) == 2
|
|
32
|
+
assert war_and_peace in leo_books
|
|
33
|
+
assert for_kids in leo_books
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import pys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pys.saveable(field_as_id='some_other')
|
|
7
|
+
@dataclass
|
|
8
|
+
class A:
|
|
9
|
+
some_other: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pys.saveable
|
|
13
|
+
@dataclass
|
|
14
|
+
class B:
|
|
15
|
+
id: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pys.saveable
|
|
19
|
+
@dataclass
|
|
20
|
+
class C:
|
|
21
|
+
name: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_with_id():
|
|
25
|
+
a = A(some_other='xyz')
|
|
26
|
+
assert a.__my_id__() is not None
|
|
27
|
+
assert a.some_other is not None
|
|
28
|
+
assert a.some_other == a.__my_id__()
|
|
29
|
+
assert a.__json__() is not None
|
|
30
|
+
assert a.__json__() == '{"some_other":"' f'{a.__my_id__()}' '"}'
|
|
31
|
+
|
|
32
|
+
b = B(id='123')
|
|
33
|
+
assert b.__my_id__() is not None
|
|
34
|
+
assert b.id == b.__my_id__()
|
|
35
|
+
assert b.id == '123'
|
|
36
|
+
assert b.__json__() is not None
|
|
37
|
+
assert b.__json__() == '{"id":"' f'{b.id}' '"}'
|
|
38
|
+
|
|
39
|
+
c = C(name='C class')
|
|
40
|
+
assert c.__my_id__() is not None
|
|
41
|
+
assert c.__json__() is not None
|
|
42
|
+
assert c.__json__() == f'{{"name":"{c.name}"}}'
|