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 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
+ ```
@@ -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
+ filelock==3.15.4
@@ -0,0 +1 @@
1
+ pys
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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}"}}'