scruby 0.24.4__py3-none-any.whl → 0.26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of scruby might be problematic. Click here for more details.
- scruby/__init__.py +14 -0
- scruby/db.py +25 -15
- scruby/mixins/__init__.py +2 -2
- scruby/mixins/collection.py +1 -1
- scruby/mixins/custom_task.py +1 -1
- scruby/mixins/{keys.py → docs.py} +45 -45
- {scruby-0.24.4.dist-info → scruby-0.26.0.dist-info}/METADATA +51 -35
- scruby-0.26.0.dist-info/RECORD +18 -0
- scruby-0.24.4.dist-info/RECORD +0 -18
- {scruby-0.24.4.dist-info → scruby-0.26.0.dist-info}/WHEEL +0 -0
- {scruby-0.24.4.dist-info → scruby-0.26.0.dist-info}/licenses/LICENSE +0 -0
scruby/__init__.py
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
#
|
|
2
|
+
# .|'''| '||
|
|
3
|
+
# || ||
|
|
4
|
+
# `|'''|, .|'', '||''| '|| ||` ||''|, '|| ||`
|
|
5
|
+
# . || || || || || || || `|..||
|
|
6
|
+
# |...|' `|..' .||. `|..'|. .||..|' ||
|
|
7
|
+
# , |'
|
|
8
|
+
# ''
|
|
9
|
+
#
|
|
10
|
+
#
|
|
11
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
12
|
+
# Scruby is free software under terms of the MIT License.
|
|
13
|
+
# Repository https://github.com/kebasyaty/scruby
|
|
14
|
+
#
|
|
1
15
|
"""Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
16
|
|
|
3
17
|
The library uses fractal-tree addressing and
|
scruby/db.py
CHANGED
|
@@ -6,9 +6,10 @@ __all__ = ("Scruby",)
|
|
|
6
6
|
|
|
7
7
|
import contextlib
|
|
8
8
|
import logging
|
|
9
|
+
import re
|
|
9
10
|
import zlib
|
|
10
11
|
from shutil import rmtree
|
|
11
|
-
from typing import Any, Literal, Never,
|
|
12
|
+
from typing import Any, Literal, Never, assert_never
|
|
12
13
|
|
|
13
14
|
from anyio import Path
|
|
14
15
|
from pydantic import BaseModel
|
|
@@ -17,8 +18,6 @@ from scruby import constants, mixins
|
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
19
20
|
|
|
20
|
-
T = TypeVar("T")
|
|
21
|
-
|
|
22
21
|
|
|
23
22
|
class _Meta(BaseModel):
|
|
24
23
|
"""Metadata of Collection."""
|
|
@@ -30,8 +29,8 @@ class _Meta(BaseModel):
|
|
|
30
29
|
counter_documents: int
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
class Scruby
|
|
34
|
-
mixins.
|
|
32
|
+
class Scruby(
|
|
33
|
+
mixins.Docs,
|
|
35
34
|
mixins.Find,
|
|
36
35
|
mixins.CustomTask,
|
|
37
36
|
mixins.Collection,
|
|
@@ -61,19 +60,20 @@ class Scruby[T](
|
|
|
61
60
|
case _ as unreachable:
|
|
62
61
|
msg: str = f"{unreachable} - Unacceptable value for HASH_REDUCE_LEFT."
|
|
63
62
|
logger.critical(msg)
|
|
64
|
-
assert_never(Never(unreachable))
|
|
63
|
+
assert_never(Never(unreachable)) # pyrefly: ignore[not-callable]
|
|
65
64
|
|
|
66
65
|
@classmethod
|
|
67
|
-
async def
|
|
66
|
+
async def collection(cls, class_model: Any) -> Any:
|
|
68
67
|
"""Get an object to access a collection.
|
|
69
68
|
|
|
70
69
|
Args:
|
|
71
|
-
class_model: Class of Model (
|
|
70
|
+
class_model: Class of Model (pydantic.BaseModel).
|
|
72
71
|
|
|
73
72
|
Returns:
|
|
74
73
|
Instance of Scruby for access a collection.
|
|
75
74
|
"""
|
|
76
75
|
assert BaseModel in class_model.__bases__, "`class_model` does not contain the base class `pydantic.BaseModel`!"
|
|
76
|
+
|
|
77
77
|
instance = cls()
|
|
78
78
|
instance.__dict__["_class_model"] = class_model
|
|
79
79
|
# Caching a pati for metadata.
|
|
@@ -144,22 +144,32 @@ class Scruby[T](
|
|
|
144
144
|
meta_json = meta.model_dump_json()
|
|
145
145
|
await meta_path.write_text(meta_json, "utf-8")
|
|
146
146
|
|
|
147
|
-
async def _get_leaf_path(self, key: str) -> Path:
|
|
147
|
+
async def _get_leaf_path(self, key: str) -> tuple[Path, str]:
|
|
148
148
|
"""Asynchronous method for getting path to collection cell by key.
|
|
149
149
|
|
|
150
150
|
This method is for internal use.
|
|
151
151
|
|
|
152
152
|
Args:
|
|
153
|
-
key: Key name.
|
|
153
|
+
key (str): Key name.
|
|
154
154
|
|
|
155
155
|
Returns:
|
|
156
156
|
Path to cell of collection.
|
|
157
157
|
"""
|
|
158
|
-
if
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
if not isinstance(key, str):
|
|
159
|
+
msg = "The key is not a string."
|
|
160
|
+
logger.error(msg)
|
|
161
|
+
raise KeyError(msg)
|
|
162
|
+
# Prepare key.
|
|
163
|
+
# Removes spaces at the beginning and end of a string.
|
|
164
|
+
# Replaces all whitespace characters with a single space.
|
|
165
|
+
prepared_key = re.sub(r"\s+", " ", key).strip().lower()
|
|
166
|
+
# Check the key for an empty string.
|
|
167
|
+
if len(prepared_key) == 0:
|
|
168
|
+
msg = "The key should not be empty."
|
|
169
|
+
logger.error(msg)
|
|
170
|
+
raise KeyError(msg)
|
|
161
171
|
# Key to crc32 sum.
|
|
162
|
-
key_as_hash: str = f"{zlib.crc32(
|
|
172
|
+
key_as_hash: str = f"{zlib.crc32(prepared_key.encode('utf-8')):08x}"[self._hash_reduce_left :]
|
|
163
173
|
# Convert crc32 sum in the segment of path.
|
|
164
174
|
separated_hash: str = "/".join(list(key_as_hash))
|
|
165
175
|
# The path of the branch to the database.
|
|
@@ -175,7 +185,7 @@ class Scruby[T](
|
|
|
175
185
|
await branch_path.mkdir(parents=True)
|
|
176
186
|
# The path to the database cell.
|
|
177
187
|
leaf_path: Path = Path(*(branch_path, "leaf.json"))
|
|
178
|
-
return leaf_path
|
|
188
|
+
return (leaf_path, prepared_key)
|
|
179
189
|
|
|
180
190
|
@staticmethod
|
|
181
191
|
def napalm() -> None:
|
scruby/mixins/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ __all__ = (
|
|
|
8
8
|
"CustomTask",
|
|
9
9
|
"Delete",
|
|
10
10
|
"Find",
|
|
11
|
-
"
|
|
11
|
+
"Docs",
|
|
12
12
|
"Update",
|
|
13
13
|
)
|
|
14
14
|
|
|
@@ -16,6 +16,6 @@ from scruby.mixins.collection import Collection
|
|
|
16
16
|
from scruby.mixins.count import Count
|
|
17
17
|
from scruby.mixins.custom_task import CustomTask
|
|
18
18
|
from scruby.mixins.delete import Delete
|
|
19
|
+
from scruby.mixins.docs import Docs
|
|
19
20
|
from scruby.mixins.find import Find
|
|
20
|
-
from scruby.mixins.keys import Keys
|
|
21
21
|
from scruby.mixins.update import Update
|
scruby/mixins/collection.py
CHANGED
scruby/mixins/custom_task.py
CHANGED
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__all__ = ("
|
|
5
|
+
__all__ = ("Docs",)
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
import
|
|
9
|
-
from typing import TypeVar
|
|
8
|
+
from typing import Any
|
|
10
9
|
|
|
11
10
|
import orjson
|
|
12
|
-
from anyio import Path
|
|
13
11
|
|
|
14
12
|
from scruby.errors import (
|
|
15
13
|
KeyAlreadyExistsError,
|
|
@@ -18,75 +16,80 @@ from scruby.errors import (
|
|
|
18
16
|
|
|
19
17
|
logger = logging.getLogger(__name__)
|
|
20
18
|
|
|
21
|
-
T = TypeVar("T")
|
|
22
19
|
|
|
20
|
+
class Docs:
|
|
21
|
+
"""Methods for working with document."""
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
async def add_key(
|
|
28
|
-
self,
|
|
29
|
-
key: str,
|
|
30
|
-
value: T,
|
|
31
|
-
) -> None:
|
|
32
|
-
"""Asynchronous method for adding key to collection.
|
|
23
|
+
async def add_doc(self, doc: Any) -> None:
|
|
24
|
+
"""Asynchronous method for adding document to collection.
|
|
33
25
|
|
|
34
26
|
Args:
|
|
35
|
-
|
|
36
|
-
value: Value of key. Type `BaseModel`.
|
|
27
|
+
doc: Value of key. Type, derived from `BaseModel`.
|
|
37
28
|
|
|
38
29
|
Returns:
|
|
39
30
|
None.
|
|
40
31
|
"""
|
|
41
|
-
|
|
32
|
+
# Check if the Model matches the collection
|
|
33
|
+
if not isinstance(doc, self._class_model):
|
|
34
|
+
doc_class_name = doc.__class__.__name__
|
|
35
|
+
collection_name = self._class_model.__name__
|
|
36
|
+
msg = (
|
|
37
|
+
f"(add_doc) Parameter `doc` => Model `{doc_class_name}` does not match collection `{collection_name}`!"
|
|
38
|
+
)
|
|
39
|
+
logger.error(msg)
|
|
40
|
+
raise TypeError(msg)
|
|
42
41
|
# The path to cell of collection.
|
|
43
|
-
leaf_path
|
|
44
|
-
|
|
42
|
+
leaf_path, prepared_key = await self._get_leaf_path(doc.key)
|
|
43
|
+
doc_json: str = doc.model_dump_json()
|
|
45
44
|
# Write key-value to collection.
|
|
46
45
|
if await leaf_path.exists():
|
|
47
46
|
# Add new key.
|
|
48
47
|
data_json: bytes = await leaf_path.read_bytes()
|
|
49
48
|
data: dict = orjson.loads(data_json) or {}
|
|
50
49
|
try:
|
|
51
|
-
data[
|
|
50
|
+
data[prepared_key]
|
|
52
51
|
except KeyError:
|
|
53
|
-
data[
|
|
52
|
+
data[prepared_key] = doc_json
|
|
54
53
|
await leaf_path.write_bytes(orjson.dumps(data))
|
|
55
54
|
else:
|
|
56
55
|
err = KeyAlreadyExistsError()
|
|
57
56
|
logger.error(err.message)
|
|
58
57
|
raise err
|
|
59
58
|
else:
|
|
60
|
-
# Add new
|
|
61
|
-
await leaf_path.write_bytes(orjson.dumps({
|
|
59
|
+
# Add new document to a blank leaf.
|
|
60
|
+
await leaf_path.write_bytes(orjson.dumps({prepared_key: doc_json}))
|
|
62
61
|
await self._counter_documents(1)
|
|
63
62
|
|
|
64
|
-
async def
|
|
65
|
-
self,
|
|
66
|
-
key: str,
|
|
67
|
-
value: T,
|
|
68
|
-
) -> None:
|
|
63
|
+
async def update_doc(self, doc: Any) -> None:
|
|
69
64
|
"""Asynchronous method for updating key to collection.
|
|
70
65
|
|
|
71
66
|
Args:
|
|
72
|
-
|
|
73
|
-
value: Value of key. Type `BaseModel`.
|
|
67
|
+
doc: Value of key. Type `BaseModel`.
|
|
74
68
|
|
|
75
69
|
Returns:
|
|
76
70
|
None.
|
|
77
71
|
"""
|
|
78
|
-
|
|
72
|
+
# Check if the Model matches the collection
|
|
73
|
+
if not isinstance(doc, self._class_model):
|
|
74
|
+
doc_class_name = doc.__class__.__name__
|
|
75
|
+
collection_name = self._class_model.__name__
|
|
76
|
+
msg = (
|
|
77
|
+
f"(update_doc) Parameter `doc` => Model `{doc_class_name}` "
|
|
78
|
+
f"does not match collection `{collection_name}`!"
|
|
79
|
+
)
|
|
80
|
+
logger.error(msg)
|
|
81
|
+
raise TypeError(msg)
|
|
79
82
|
# The path to cell of collection.
|
|
80
|
-
leaf_path
|
|
81
|
-
|
|
83
|
+
leaf_path, prepared_key = await self._get_leaf_path(doc.key)
|
|
84
|
+
doc_json: str = doc.model_dump_json()
|
|
82
85
|
# Update the existing key.
|
|
83
86
|
if await leaf_path.exists():
|
|
84
87
|
# Update the existing key.
|
|
85
88
|
data_json: bytes = await leaf_path.read_bytes()
|
|
86
89
|
data: dict = orjson.loads(data_json) or {}
|
|
87
90
|
try:
|
|
88
|
-
data[
|
|
89
|
-
data[
|
|
91
|
+
data[prepared_key]
|
|
92
|
+
data[prepared_key] = doc_json
|
|
90
93
|
await leaf_path.write_bytes(orjson.dumps(data))
|
|
91
94
|
except KeyError:
|
|
92
95
|
err = KeyNotExistsError()
|
|
@@ -96,7 +99,7 @@ class Keys[T]:
|
|
|
96
99
|
logger.error("The key not exists.")
|
|
97
100
|
raise KeyError()
|
|
98
101
|
|
|
99
|
-
async def get_key(self, key: str) ->
|
|
102
|
+
async def get_key(self, key: str) -> Any:
|
|
100
103
|
"""Asynchronous method for getting value of key from collection.
|
|
101
104
|
|
|
102
105
|
Args:
|
|
@@ -105,14 +108,13 @@ class Keys[T]:
|
|
|
105
108
|
Returns:
|
|
106
109
|
Value of key or KeyError.
|
|
107
110
|
"""
|
|
108
|
-
key = re.sub(r"\s+", " ", key.strip())
|
|
109
111
|
# The path to the database cell.
|
|
110
|
-
leaf_path
|
|
112
|
+
leaf_path, prepared_key = await self._get_leaf_path(key)
|
|
111
113
|
# Get value of key.
|
|
112
114
|
if await leaf_path.exists():
|
|
113
115
|
data_json: bytes = await leaf_path.read_bytes()
|
|
114
116
|
data: dict = orjson.loads(data_json) or {}
|
|
115
|
-
obj:
|
|
117
|
+
obj: Any = self._class_model.model_validate_json(data[prepared_key])
|
|
116
118
|
return obj
|
|
117
119
|
msg: str = "`get_key` - The unacceptable key value."
|
|
118
120
|
logger.error(msg)
|
|
@@ -127,15 +129,14 @@ class Keys[T]:
|
|
|
127
129
|
Returns:
|
|
128
130
|
True, if the key is present.
|
|
129
131
|
"""
|
|
130
|
-
key = re.sub(r"\s+", " ", key.strip())
|
|
131
132
|
# Get path to cell of collection.
|
|
132
|
-
leaf_path
|
|
133
|
+
leaf_path, prepared_key = await self._get_leaf_path(key)
|
|
133
134
|
# Checking whether there is a key.
|
|
134
135
|
if await leaf_path.exists():
|
|
135
136
|
data_json: bytes = await leaf_path.read_bytes()
|
|
136
137
|
data: dict = orjson.loads(data_json) or {}
|
|
137
138
|
try:
|
|
138
|
-
data[
|
|
139
|
+
data[prepared_key]
|
|
139
140
|
return True
|
|
140
141
|
except KeyError:
|
|
141
142
|
return False
|
|
@@ -150,14 +151,13 @@ class Keys[T]:
|
|
|
150
151
|
Returns:
|
|
151
152
|
None.
|
|
152
153
|
"""
|
|
153
|
-
key = re.sub(r"\s+", " ", key.strip())
|
|
154
154
|
# The path to the database cell.
|
|
155
|
-
leaf_path
|
|
155
|
+
leaf_path, prepared_key = await self._get_leaf_path(key)
|
|
156
156
|
# Deleting key.
|
|
157
157
|
if await leaf_path.exists():
|
|
158
158
|
data_json: bytes = await leaf_path.read_bytes()
|
|
159
159
|
data: dict = orjson.loads(data_json) or {}
|
|
160
|
-
del data[
|
|
160
|
+
del data[prepared_key]
|
|
161
161
|
await leaf_path.write_bytes(orjson.dumps(data))
|
|
162
162
|
await self._counter_documents(-1)
|
|
163
163
|
return
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scruby
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
5
|
-
Project-URL: Homepage, https://github.
|
|
3
|
+
Version: 0.26.0
|
|
4
|
+
Summary: Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
5
|
+
Project-URL: Homepage, https://kebasyaty.github.io/scruby/
|
|
6
6
|
Project-URL: Repository, https://github.com/kebasyaty/scruby
|
|
7
7
|
Project-URL: Source, https://github.com/kebasyaty/scruby
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/kebasyaty/scruby/issues
|
|
@@ -30,7 +30,7 @@ Requires-Dist: anyio>=4.10.0
|
|
|
30
30
|
Requires-Dist: orjson>=3.11.3
|
|
31
31
|
Requires-Dist: phonenumbers>=9.0.13
|
|
32
32
|
Requires-Dist: pydantic-extra-types>=2.10.5
|
|
33
|
-
Requires-Dist: pydantic[email]>=2.11.7
|
|
33
|
+
Requires-Dist: pydantic[email,timezone]>=2.11.7
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
|
|
36
36
|
<div align="center">
|
|
@@ -52,7 +52,7 @@ Description-Content-Type: text/markdown
|
|
|
52
52
|
<a href="https://pypi.python.org/pypi/scruby/" alt="PyPI status"><img src="https://img.shields.io/pypi/status/scruby.svg" alt="PyPI status"></a>
|
|
53
53
|
<a href="https://pypi.python.org/pypi/scruby/" alt="PyPI version fury.io"><img src="https://badge.fury.io/py/scruby.svg" alt="PyPI version fury.io"></a>
|
|
54
54
|
<br>
|
|
55
|
-
<a href="https://
|
|
55
|
+
<a href="https://pyrefly.org/" alt="Types: Pyrefly"><img src="https://img.shields.io/badge/types-Pyrefly-FFB74D.svg" alt="Types: Pyrefly"></a>
|
|
56
56
|
<a href="https://docs.astral.sh/ruff/" alt="Code style: Ruff"><img src="https://img.shields.io/badge/code%20style-Ruff-FDD835.svg" alt="Code style: Ruff"></a>
|
|
57
57
|
<a href="https://pypi.org/project/scruby"><img src="https://img.shields.io/pypi/format/scruby" alt="Format"></a>
|
|
58
58
|
<a href="https://pepy.tech/projects/scruby"><img src="https://static.pepy.tech/badge/scruby" alt="PyPI Downloads"></a>
|
|
@@ -65,11 +65,11 @@ Description-Content-Type: text/markdown
|
|
|
65
65
|
<br>
|
|
66
66
|
The database consists of collections.
|
|
67
67
|
<br>
|
|
68
|
-
The maximum size of the one collection is 16
|
|
68
|
+
The maximum size of the one collection is <b>16**8=4294967296</b> branches,
|
|
69
69
|
<br>
|
|
70
70
|
each branch can store one or more keys.
|
|
71
71
|
<br>
|
|
72
|
-
The value of any key in collection can be obtained in 8 steps,
|
|
72
|
+
The value of any key in collection can be obtained maximum in <b>8</b> steps,
|
|
73
73
|
<br>
|
|
74
74
|
thereby achieving high performance.
|
|
75
75
|
<br>
|
|
@@ -108,7 +108,7 @@ See more examples here [https://kebasyaty.github.io/scruby/latest/pages/usage/](
|
|
|
108
108
|
import anyio
|
|
109
109
|
import datetime
|
|
110
110
|
from typing import Annotated
|
|
111
|
-
from pydantic import BaseModel, EmailStr
|
|
111
|
+
from pydantic import BaseModel, EmailStr, Field
|
|
112
112
|
from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator
|
|
113
113
|
from scruby import Scruby, constants
|
|
114
114
|
|
|
@@ -116,18 +116,24 @@ constants.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
|
|
|
116
116
|
constants.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
117
117
|
|
|
118
118
|
class User(BaseModel):
|
|
119
|
-
"""
|
|
120
|
-
first_name: str
|
|
121
|
-
last_name: str
|
|
122
|
-
birthday: datetime.datetime
|
|
123
|
-
email: EmailStr
|
|
124
|
-
phone: Annotated[PhoneNumber, PhoneNumberValidator(number_format="E164")]
|
|
119
|
+
"""User model."""
|
|
120
|
+
first_name: str = Field(strict=True)
|
|
121
|
+
last_name: str = Field(strict=True)
|
|
122
|
+
birthday: datetime.datetime = Field(strict=True)
|
|
123
|
+
email: EmailStr = Field(strict=True)
|
|
124
|
+
phone: Annotated[PhoneNumber, PhoneNumberValidator(number_format="E164")] = Field(frozen=True)
|
|
125
|
+
# The key is always at the bottom
|
|
126
|
+
key: str = Field(
|
|
127
|
+
strict=True,
|
|
128
|
+
frozen=True,
|
|
129
|
+
default_factory=lambda data: data["phone"],
|
|
130
|
+
)
|
|
125
131
|
|
|
126
132
|
|
|
127
133
|
async def main() -> None:
|
|
128
134
|
"""Example."""
|
|
129
135
|
# Get collection of `User`.
|
|
130
|
-
user_coll = await Scruby.
|
|
136
|
+
user_coll = await Scruby.collection(User)
|
|
131
137
|
|
|
132
138
|
user = User(
|
|
133
139
|
first_name="John",
|
|
@@ -137,9 +143,9 @@ async def main() -> None:
|
|
|
137
143
|
phone="+447986123456",
|
|
138
144
|
)
|
|
139
145
|
|
|
140
|
-
await user_coll.
|
|
146
|
+
await user_coll.add_doc(user)
|
|
141
147
|
|
|
142
|
-
await user_coll.
|
|
148
|
+
await user_coll.update_doc(user)
|
|
143
149
|
|
|
144
150
|
await user_coll.get_key("+447986123456") # => user
|
|
145
151
|
await user_coll.get_key("key missing") # => KeyError
|
|
@@ -171,7 +177,7 @@ Ideally, hundreds and even thousands of threads are required.
|
|
|
171
177
|
import anyio
|
|
172
178
|
import datetime
|
|
173
179
|
from typing import Annotated
|
|
174
|
-
from pydantic import BaseModel
|
|
180
|
+
from pydantic import BaseModel, Field
|
|
175
181
|
from scruby import Scruby, constants
|
|
176
182
|
from pprint import pprint as pp
|
|
177
183
|
|
|
@@ -181,16 +187,22 @@ constants.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
|
181
187
|
|
|
182
188
|
class Phone(BaseModel):
|
|
183
189
|
"""Phone model."""
|
|
184
|
-
brand: str
|
|
185
|
-
model: str
|
|
186
|
-
screen_diagonal: float
|
|
187
|
-
matrix_type: str
|
|
190
|
+
brand: str = Field(strict=True, frozen=True)
|
|
191
|
+
model: str = Field(strict=True, frozen=True)
|
|
192
|
+
screen_diagonal: float = Field(strict=True)
|
|
193
|
+
matrix_type: str = Field(strict=True)
|
|
194
|
+
# The key is always at the bottom
|
|
195
|
+
key: str = Field(
|
|
196
|
+
strict=True,
|
|
197
|
+
frozen=True,
|
|
198
|
+
default_factory=lambda data: f"{data['brand']}:{data['model']}",
|
|
199
|
+
)
|
|
188
200
|
|
|
189
201
|
|
|
190
202
|
async def main() -> None:
|
|
191
203
|
"""Example."""
|
|
192
204
|
# Get collection of `Phone`.
|
|
193
|
-
phone_coll = await Scruby.
|
|
205
|
+
phone_coll = await Scruby.collection(Phone)
|
|
194
206
|
|
|
195
207
|
# Create phone.
|
|
196
208
|
phone = Phone(
|
|
@@ -201,8 +213,7 @@ async def main() -> None:
|
|
|
201
213
|
)
|
|
202
214
|
|
|
203
215
|
# Add phone to collection.
|
|
204
|
-
|
|
205
|
-
await phone_coll.add_key(key, phone)
|
|
216
|
+
await phone_coll.add_doc(phone)
|
|
206
217
|
|
|
207
218
|
# Find phone by brand.
|
|
208
219
|
phone_details: Phone | None = await phone_coll.find_one(
|
|
@@ -242,7 +253,7 @@ Ideally, hundreds and even thousands of threads are required.
|
|
|
242
253
|
import anyio
|
|
243
254
|
import datetime
|
|
244
255
|
from typing import Annotated
|
|
245
|
-
from pydantic import BaseModel
|
|
256
|
+
from pydantic import BaseModel, Field
|
|
246
257
|
from scruby import Scruby, constants
|
|
247
258
|
from pprint import pprint as pp
|
|
248
259
|
|
|
@@ -252,16 +263,22 @@ constants.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
|
252
263
|
|
|
253
264
|
class Car(BaseModel):
|
|
254
265
|
"""Car model."""
|
|
255
|
-
brand: str
|
|
256
|
-
model: str
|
|
257
|
-
year: int
|
|
258
|
-
power_reserve: int
|
|
266
|
+
brand: str = Field(strict=True, frozen=True)
|
|
267
|
+
model: str = Field(strict=True, frozen=True)
|
|
268
|
+
year: int = Field(strict=True)
|
|
269
|
+
power_reserve: int = Field(strict=True)
|
|
270
|
+
# The key is always at the bottom
|
|
271
|
+
key: str = Field(
|
|
272
|
+
strict=True,
|
|
273
|
+
frozen=True,
|
|
274
|
+
default_factory=lambda data: f"{data['brand']}:{data['model']}",
|
|
275
|
+
)
|
|
259
276
|
|
|
260
277
|
|
|
261
278
|
async def main() -> None:
|
|
262
279
|
"""Example."""
|
|
263
280
|
# Get collection of `Car`.
|
|
264
|
-
car_coll = await Scruby.
|
|
281
|
+
car_coll = await Scruby.collection(Car)
|
|
265
282
|
|
|
266
283
|
# Create cars.
|
|
267
284
|
for name in range(1, 10):
|
|
@@ -271,12 +288,11 @@ async def main() -> None:
|
|
|
271
288
|
year=2025,
|
|
272
289
|
power_reserve=600,
|
|
273
290
|
)
|
|
274
|
-
|
|
275
|
-
await car_coll.add_key(key, car)
|
|
291
|
+
await car_coll.add_doc(car)
|
|
276
292
|
|
|
277
293
|
# Find cars by brand and year.
|
|
278
294
|
car_list: list[Car] | None = await car_coll.find_many(
|
|
279
|
-
filter_fn=lambda doc: doc.brand == "Mazda"
|
|
295
|
+
filter_fn=lambda doc: doc.brand == "Mazda" and doc.year == 2025,
|
|
280
296
|
)
|
|
281
297
|
if car_list is not None:
|
|
282
298
|
pp(car_list)
|
|
@@ -285,7 +301,7 @@ async def main() -> None:
|
|
|
285
301
|
|
|
286
302
|
# Get collection list.
|
|
287
303
|
collection_list = await Scruby.collection_list()
|
|
288
|
-
print(
|
|
304
|
+
print(collection_list) # ["Car"]
|
|
289
305
|
|
|
290
306
|
# Full database deletion.
|
|
291
307
|
# Hint: The main purpose is tests.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
scruby/__init__.py,sha256=iYjvi002DeRh-U_ND2cCOHlbX2xwxN8IIhsposeotNw,1504
|
|
2
|
+
scruby/aggregation.py,sha256=SYGcnMy2eq9vJb-pW3xR9LLAQIQ55TK-LGW_yKQ-7sU,3318
|
|
3
|
+
scruby/constants.py,sha256=KInSZ_4dsQNXilrs7DvtQXevKEYibnNzl69a7XiWG4k,1099
|
|
4
|
+
scruby/db.py,sha256=06GjnhN9lKvZo585nxKFd4z8Ox858Ep08c7eCbMA99k,6462
|
|
5
|
+
scruby/errors.py,sha256=aj1zQlfxGwZC-bZZ07DRX2vHx31SpyWPqXHMpQ9kRVY,1124
|
|
6
|
+
scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
scruby/mixins/__init__.py,sha256=w1Be13FHAGSkdRfXcmoZ-eDn5Q8cFsPRAV7k1tXkIwY,454
|
|
8
|
+
scruby/mixins/collection.py,sha256=kqUgzJbgG9pTZhlP7OD5DsOaArRzu0fl6fVibLAdNtk,1260
|
|
9
|
+
scruby/mixins/count.py,sha256=Wcn6CeWrYSgsTTmYQ4J-CEiM4630rUSwRP9iKwbCl6c,2193
|
|
10
|
+
scruby/mixins/custom_task.py,sha256=DL-pQZninz7CJUyRYlVV7SNPC60qMD3ZQyMLnC3zVTM,2294
|
|
11
|
+
scruby/mixins/delete.py,sha256=BmfQH68iX7kzC20w16xzFcLO3uLxYKdNyqZqIbXb1M0,3240
|
|
12
|
+
scruby/mixins/docs.py,sha256=UHawXUjIkDBtik6MIQwbPF3DZKSOG8WI4Da9_i_-9R4,5533
|
|
13
|
+
scruby/mixins/find.py,sha256=va1hTm6Poua7_TMcZW2iqI-xmL1HcCUOx8pkKvTvu6U,5063
|
|
14
|
+
scruby/mixins/update.py,sha256=A9V4PjA3INnqLTGoBxIvC8y8Wo-nLxlFejkPUhsebzQ,3428
|
|
15
|
+
scruby-0.26.0.dist-info/METADATA,sha256=CluDLzRgB952ZDbbxdsXuHGE598BzIiF5eYXpGaNdj4,10483
|
|
16
|
+
scruby-0.26.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
scruby-0.26.0.dist-info/licenses/LICENSE,sha256=mS0Wz0yGNB63gEcWEnuIb_lldDYV0sjRaO-o_GL6CWE,1074
|
|
18
|
+
scruby-0.26.0.dist-info/RECORD,,
|
scruby-0.24.4.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
scruby/__init__.py,sha256=elrW_AWMyl3kuTpEqGPaYFSpF8iVzjpivF6MxVNlqoQ,855
|
|
2
|
-
scruby/aggregation.py,sha256=SYGcnMy2eq9vJb-pW3xR9LLAQIQ55TK-LGW_yKQ-7sU,3318
|
|
3
|
-
scruby/constants.py,sha256=KInSZ_4dsQNXilrs7DvtQXevKEYibnNzl69a7XiWG4k,1099
|
|
4
|
-
scruby/db.py,sha256=ggYW4dQPtr7m9-GM4QeYMMDZm5eUYN5bTAdz2Tj0hlw,5980
|
|
5
|
-
scruby/errors.py,sha256=aj1zQlfxGwZC-bZZ07DRX2vHx31SpyWPqXHMpQ9kRVY,1124
|
|
6
|
-
scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
scruby/mixins/__init__.py,sha256=-rRZE-JZwGmEkC0wS_X0hs8OXsEYyvgSNIfil8wmjFA,454
|
|
8
|
-
scruby/mixins/collection.py,sha256=eMnfHFdzk7LWILMmDbzugcOYSeIKp0DlEEqCmmGQRwA,1222
|
|
9
|
-
scruby/mixins/count.py,sha256=Wcn6CeWrYSgsTTmYQ4J-CEiM4630rUSwRP9iKwbCl6c,2193
|
|
10
|
-
scruby/mixins/custom_task.py,sha256=Ib1G1I7NyDGbow4SeafkYd9C0r6u6EDgUK0NxjhsEa0,2297
|
|
11
|
-
scruby/mixins/delete.py,sha256=BmfQH68iX7kzC20w16xzFcLO3uLxYKdNyqZqIbXb1M0,3240
|
|
12
|
-
scruby/mixins/find.py,sha256=va1hTm6Poua7_TMcZW2iqI-xmL1HcCUOx8pkKvTvu6U,5063
|
|
13
|
-
scruby/mixins/keys.py,sha256=Hbb0AX68ph--fA43AXDWoM72PzSmS48h3iVwlQwQH0c,4971
|
|
14
|
-
scruby/mixins/update.py,sha256=A9V4PjA3INnqLTGoBxIvC8y8Wo-nLxlFejkPUhsebzQ,3428
|
|
15
|
-
scruby-0.24.4.dist-info/METADATA,sha256=RDE0Fa_IXd2hx2MDjdsI-5iwGhZOCZ9zOqqQuOe8k5g,9643
|
|
16
|
-
scruby-0.24.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
-
scruby-0.24.4.dist-info/licenses/LICENSE,sha256=mS0Wz0yGNB63gEcWEnuIb_lldDYV0sjRaO-o_GL6CWE,1074
|
|
18
|
-
scruby-0.24.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|