scruby 0.26.0__py3-none-any.whl → 0.30.2__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 +12 -20
- scruby/aggregation.py +4 -0
- scruby/db.py +77 -32
- scruby/errors.py +4 -0
- scruby/mixins/__init__.py +6 -2
- scruby/mixins/collection.py +8 -7
- scruby/mixins/count.py +15 -15
- scruby/mixins/custom_task.py +14 -16
- scruby/mixins/delete.py +16 -19
- scruby/mixins/find.py +50 -44
- scruby/mixins/{docs.py → keys.py} +40 -27
- scruby/mixins/update.py +21 -22
- scruby/{constants.py → settings.py} +13 -2
- {scruby-0.26.0.dist-info → scruby-0.30.2.dist-info}/METADATA +66 -40
- scruby-0.30.2.dist-info/RECORD +18 -0
- scruby-0.26.0.dist-info/RECORD +0 -18
- {scruby-0.26.0.dist-info → scruby-0.30.2.dist-info}/WHEEL +0 -0
- {scruby-0.26.0.dist-info → scruby-0.30.2.dist-info}/licenses/LICENSE +0 -0
scruby/__init__.py
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# |...|' `|..' .||. `|..'|. .||..|' ||
|
|
7
|
-
# , |'
|
|
8
|
-
# ''
|
|
9
|
-
#
|
|
2
|
+
# .dP"Y8 dP""b8 88""Yb 88 88 88""Yb Yb dP
|
|
3
|
+
# `Ybo." dP `" 88__dP 88 88 88__dP YbdP
|
|
4
|
+
# o.`Y8b Yb 88"Yb Y8 8P 88""Yb 8P
|
|
5
|
+
# 8bodP' YboodP 88 Yb `YbodP' 88oodP dP
|
|
10
6
|
#
|
|
11
7
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
12
|
-
#
|
|
13
|
-
# Repository https://github.com/kebasyaty/scruby
|
|
8
|
+
# SPDX-License-Identifier: MIT
|
|
14
9
|
#
|
|
15
10
|
"""Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
16
11
|
|
|
@@ -30,14 +25,11 @@ requires a large number of processor threads.
|
|
|
30
25
|
|
|
31
26
|
from __future__ import annotations
|
|
32
27
|
|
|
33
|
-
__all__ = (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
from scruby.db import Scruby
|
|
38
|
-
|
|
39
|
-
logging.basicConfig(
|
|
40
|
-
level=logging.INFO,
|
|
41
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
|
42
|
-
format="[%(asctime)s.%(msecs)03d] %(module)10s:%(lineno)-3d %(levelname)-7s - %(message)s",
|
|
28
|
+
__all__ = (
|
|
29
|
+
"settings",
|
|
30
|
+
"Scruby",
|
|
31
|
+
"ScrubyModel",
|
|
43
32
|
)
|
|
33
|
+
|
|
34
|
+
from scruby import settings
|
|
35
|
+
from scruby.db import Scruby, ScrubyModel
|
scruby/aggregation.py
CHANGED
scruby/db.py
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Creation and management of the database."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
|
-
__all__ = (
|
|
9
|
+
__all__ = (
|
|
10
|
+
"Scruby",
|
|
11
|
+
"ScrubyModel",
|
|
12
|
+
)
|
|
6
13
|
|
|
7
14
|
import contextlib
|
|
8
15
|
import logging
|
|
9
16
|
import re
|
|
10
17
|
import zlib
|
|
18
|
+
from datetime import datetime
|
|
11
19
|
from shutil import rmtree
|
|
12
20
|
from typing import Any, Literal, Never, assert_never
|
|
13
21
|
|
|
14
22
|
from anyio import Path
|
|
15
23
|
from pydantic import BaseModel
|
|
16
24
|
|
|
17
|
-
from scruby import
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
25
|
+
from scruby import mixins, settings
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
class _Meta(BaseModel):
|
|
@@ -29,8 +35,15 @@ class _Meta(BaseModel):
|
|
|
29
35
|
counter_documents: int
|
|
30
36
|
|
|
31
37
|
|
|
38
|
+
class ScrubyModel(BaseModel):
|
|
39
|
+
"""Additional fields for models."""
|
|
40
|
+
|
|
41
|
+
created_at: datetime | None = None
|
|
42
|
+
updated_at: datetime | None = None
|
|
43
|
+
|
|
44
|
+
|
|
32
45
|
class Scruby(
|
|
33
|
-
mixins.
|
|
46
|
+
mixins.Keys,
|
|
34
47
|
mixins.Find,
|
|
35
48
|
mixins.CustomTask,
|
|
36
49
|
mixins.Collection,
|
|
@@ -45,21 +58,22 @@ class Scruby(
|
|
|
45
58
|
) -> None:
|
|
46
59
|
super().__init__()
|
|
47
60
|
self._meta = _Meta
|
|
48
|
-
self._db_root =
|
|
49
|
-
self._hash_reduce_left =
|
|
61
|
+
self._db_root = settings.DB_ROOT
|
|
62
|
+
self._hash_reduce_left = settings.HASH_REDUCE_LEFT
|
|
63
|
+
self._max_workers = settings.MAX_WORKERS
|
|
50
64
|
# The maximum number of branches.
|
|
51
65
|
match self._hash_reduce_left:
|
|
52
66
|
case 0:
|
|
53
|
-
self.
|
|
67
|
+
self._max_number_branch = 4294967296
|
|
54
68
|
case 2:
|
|
55
|
-
self.
|
|
69
|
+
self._max_number_branch = 16777216
|
|
56
70
|
case 4:
|
|
57
|
-
self.
|
|
71
|
+
self._max_number_branch = 65536
|
|
58
72
|
case 6:
|
|
59
|
-
self.
|
|
73
|
+
self._max_number_branch = 256
|
|
60
74
|
case _ as unreachable:
|
|
61
75
|
msg: str = f"{unreachable} - Unacceptable value for HASH_REDUCE_LEFT."
|
|
62
|
-
|
|
76
|
+
logging.critical(msg)
|
|
63
77
|
assert_never(Never(unreachable)) # pyrefly: ignore[not-callable]
|
|
64
78
|
|
|
65
79
|
@classmethod
|
|
@@ -67,42 +81,67 @@ class Scruby(
|
|
|
67
81
|
"""Get an object to access a collection.
|
|
68
82
|
|
|
69
83
|
Args:
|
|
70
|
-
class_model: Class of Model (
|
|
84
|
+
class_model (Any): Class of Model (ScrubyModel).
|
|
71
85
|
|
|
72
86
|
Returns:
|
|
73
87
|
Instance of Scruby for access a collection.
|
|
74
88
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
if __debug__:
|
|
90
|
+
# Check if the object belongs to the class `ScrubyModel`
|
|
91
|
+
if ScrubyModel not in class_model.__bases__:
|
|
92
|
+
msg = (
|
|
93
|
+
"Method: `collection` => argument `class_model` " + "does not contain the base class `ScrubyModel`!"
|
|
94
|
+
)
|
|
95
|
+
raise AssertionError(msg)
|
|
96
|
+
# Checking the model for the presence of a key.
|
|
97
|
+
model_fields = list(class_model.model_fields.keys())
|
|
98
|
+
if "key" not in model_fields:
|
|
99
|
+
msg = f"Model: {class_model.__name__} => The `key` field is missing!"
|
|
100
|
+
raise AssertionError(msg)
|
|
101
|
+
if "created_at" not in model_fields:
|
|
102
|
+
msg = f"Model: {class_model.__name__} => The `created_at` field is missing!"
|
|
103
|
+
raise AssertionError(msg)
|
|
104
|
+
if "updated_at" not in model_fields:
|
|
105
|
+
msg = f"Model: {class_model.__name__} => The `updated_at` field is missing!"
|
|
106
|
+
raise AssertionError(msg)
|
|
107
|
+
# Check the length of the collection name for an acceptable size.
|
|
108
|
+
len_db_root_absolut_path = len(str(await Path(settings.DB_ROOT).resolve()).encode("utf-8"))
|
|
109
|
+
len_model_name = len(class_model.__name__)
|
|
110
|
+
len_full_path_leaf = len_db_root_absolut_path + len_model_name + 26
|
|
111
|
+
if len_full_path_leaf > 255:
|
|
112
|
+
excess = len_full_path_leaf - 255
|
|
113
|
+
msg = (
|
|
114
|
+
f"Model: {class_model.__name__} => The collection name is too long, "
|
|
115
|
+
+ f"it exceeds the limit of {excess} characters!"
|
|
116
|
+
)
|
|
117
|
+
raise AssertionError(msg)
|
|
118
|
+
# Create instance of Scruby
|
|
77
119
|
instance = cls()
|
|
120
|
+
# Add model class to Scruby
|
|
78
121
|
instance.__dict__["_class_model"] = class_model
|
|
79
|
-
#
|
|
80
|
-
# The zero branch is reserved for metadata.
|
|
81
|
-
branch_number: int = 0
|
|
82
|
-
branch_number_as_hash: str = f"{branch_number:08x}"[constants.HASH_REDUCE_LEFT :]
|
|
83
|
-
separated_hash: str = "/".join(list(branch_number_as_hash))
|
|
122
|
+
# Create a path for metadata.
|
|
84
123
|
meta_dir_path_tuple = (
|
|
85
|
-
|
|
124
|
+
settings.DB_ROOT,
|
|
86
125
|
class_model.__name__,
|
|
87
|
-
|
|
126
|
+
"meta",
|
|
88
127
|
)
|
|
89
128
|
instance.__dict__["_meta_path"] = Path(
|
|
90
129
|
*meta_dir_path_tuple,
|
|
91
130
|
"meta.json",
|
|
92
131
|
)
|
|
93
132
|
# Create metadata for collection, if missing.
|
|
94
|
-
|
|
95
|
-
if not await
|
|
96
|
-
await
|
|
133
|
+
meta_dir_path = Path(*meta_dir_path_tuple)
|
|
134
|
+
if not await meta_dir_path.exists():
|
|
135
|
+
await meta_dir_path.mkdir(parents=True)
|
|
97
136
|
meta = _Meta(
|
|
98
|
-
db_root=
|
|
137
|
+
db_root=settings.DB_ROOT,
|
|
99
138
|
collection_name=class_model.__name__,
|
|
100
|
-
hash_reduce_left=
|
|
101
|
-
max_branch_number=instance.__dict__["
|
|
139
|
+
hash_reduce_left=settings.HASH_REDUCE_LEFT,
|
|
140
|
+
max_branch_number=instance.__dict__["_max_number_branch"],
|
|
102
141
|
counter_documents=0,
|
|
103
142
|
)
|
|
104
143
|
meta_json = meta.model_dump_json()
|
|
105
|
-
meta_path = Path(*(
|
|
144
|
+
meta_path = Path(*(meta_dir_path, "meta.json"))
|
|
106
145
|
await meta_path.write_text(meta_json, "utf-8")
|
|
107
146
|
return instance
|
|
108
147
|
|
|
@@ -123,6 +162,9 @@ class Scruby(
|
|
|
123
162
|
|
|
124
163
|
This method is for internal use.
|
|
125
164
|
|
|
165
|
+
Args:
|
|
166
|
+
meta (_Meta): Metadata of Collection.
|
|
167
|
+
|
|
126
168
|
Returns:
|
|
127
169
|
None.
|
|
128
170
|
"""
|
|
@@ -134,6 +176,9 @@ class Scruby(
|
|
|
134
176
|
|
|
135
177
|
This method is for internal use.
|
|
136
178
|
|
|
179
|
+
Args:
|
|
180
|
+
step (Literal[1, -1]): Number of documents added or removed.
|
|
181
|
+
|
|
137
182
|
Returns:
|
|
138
183
|
None.
|
|
139
184
|
"""
|
|
@@ -157,7 +202,7 @@ class Scruby(
|
|
|
157
202
|
"""
|
|
158
203
|
if not isinstance(key, str):
|
|
159
204
|
msg = "The key is not a string."
|
|
160
|
-
|
|
205
|
+
logging.error(msg)
|
|
161
206
|
raise KeyError(msg)
|
|
162
207
|
# Prepare key.
|
|
163
208
|
# Removes spaces at the beginning and end of a string.
|
|
@@ -166,7 +211,7 @@ class Scruby(
|
|
|
166
211
|
# Check the key for an empty string.
|
|
167
212
|
if len(prepared_key) == 0:
|
|
168
213
|
msg = "The key should not be empty."
|
|
169
|
-
|
|
214
|
+
logging.error(msg)
|
|
170
215
|
raise KeyError(msg)
|
|
171
216
|
# Key to crc32 sum.
|
|
172
217
|
key_as_hash: str = f"{zlib.crc32(prepared_key.encode('utf-8')):08x}"[self._hash_reduce_left :]
|
|
@@ -200,5 +245,5 @@ class Scruby(
|
|
|
200
245
|
None.
|
|
201
246
|
"""
|
|
202
247
|
with contextlib.suppress(FileNotFoundError):
|
|
203
|
-
rmtree(
|
|
248
|
+
rmtree(settings.DB_ROOT)
|
|
204
249
|
return
|
scruby/errors.py
CHANGED
scruby/mixins/__init__.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Mixins."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
@@ -8,7 +12,7 @@ __all__ = (
|
|
|
8
12
|
"CustomTask",
|
|
9
13
|
"Delete",
|
|
10
14
|
"Find",
|
|
11
|
-
"
|
|
15
|
+
"Keys",
|
|
12
16
|
"Update",
|
|
13
17
|
)
|
|
14
18
|
|
|
@@ -16,6 +20,6 @@ from scruby.mixins.collection import Collection
|
|
|
16
20
|
from scruby.mixins.count import Count
|
|
17
21
|
from scruby.mixins.custom_task import CustomTask
|
|
18
22
|
from scruby.mixins.delete import Delete
|
|
19
|
-
from scruby.mixins.docs import Docs
|
|
20
23
|
from scruby.mixins.find import Find
|
|
24
|
+
from scruby.mixins.keys import Keys
|
|
21
25
|
from scruby.mixins.update import Update
|
scruby/mixins/collection.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Methods for working with collections."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
@@ -5,16 +9,13 @@ from __future__ import annotations
|
|
|
5
9
|
__all__ = ("Collection",)
|
|
6
10
|
|
|
7
11
|
from shutil import rmtree
|
|
8
|
-
from typing import TypeVar
|
|
9
12
|
|
|
10
13
|
from anyio import Path, to_thread
|
|
11
14
|
|
|
12
|
-
from scruby import
|
|
15
|
+
from scruby import settings
|
|
13
16
|
|
|
14
|
-
T = TypeVar("T")
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
class Collection[T]:
|
|
18
|
+
class Collection:
|
|
18
19
|
"""Methods for working with collections."""
|
|
19
20
|
|
|
20
21
|
def collection_name(self) -> str:
|
|
@@ -28,7 +29,7 @@ class Collection[T]:
|
|
|
28
29
|
@staticmethod
|
|
29
30
|
async def collection_list() -> list[str]:
|
|
30
31
|
"""Get collection list."""
|
|
31
|
-
target_directory = Path(
|
|
32
|
+
target_directory = Path(settings.DB_ROOT)
|
|
32
33
|
# Get all entries in the directory
|
|
33
34
|
all_entries = Path.iterdir(target_directory)
|
|
34
35
|
directory_names: list[str] = [entry.name async for entry in all_entries]
|
|
@@ -44,6 +45,6 @@ class Collection[T]:
|
|
|
44
45
|
Returns:
|
|
45
46
|
None.
|
|
46
47
|
"""
|
|
47
|
-
target_directory = f"{
|
|
48
|
+
target_directory = f"{settings.DB_ROOT}/{name}"
|
|
48
49
|
await to_thread.run_sync(rmtree, target_directory) # pyrefly: ignore[bad-argument-type]
|
|
49
50
|
return
|
scruby/mixins/count.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Methods for counting the number of documents."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
@@ -6,12 +10,10 @@ __all__ = ("Count",)
|
|
|
6
10
|
|
|
7
11
|
import concurrent.futures
|
|
8
12
|
from collections.abc import Callable
|
|
9
|
-
from typing import
|
|
13
|
+
from typing import Any
|
|
10
14
|
|
|
11
|
-
T = TypeVar("T")
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
class Count[T]:
|
|
16
|
+
class Count:
|
|
15
17
|
"""Methods for counting the number of documents."""
|
|
16
18
|
|
|
17
19
|
async def estimated_document_count(self) -> int:
|
|
@@ -26,30 +28,28 @@ class Count[T]:
|
|
|
26
28
|
async def count_documents(
|
|
27
29
|
self,
|
|
28
30
|
filter_fn: Callable,
|
|
29
|
-
max_workers: int | None = None,
|
|
30
31
|
) -> int:
|
|
31
32
|
"""Count the number of documents a matching the filter in this collection.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
Attention:
|
|
35
|
+
- The search is based on the effect of a quantum loop.
|
|
36
|
+
- The search effectiveness depends on the number of processor threads.
|
|
36
37
|
|
|
37
38
|
Args:
|
|
38
|
-
filter_fn: A function that execute the conditions of filtering.
|
|
39
|
-
max_workers: The maximum number of processes that can be used to
|
|
40
|
-
execute the given calls. If None or not given then as many
|
|
41
|
-
worker processes will be created as the machine has processors.
|
|
39
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
42
40
|
|
|
43
41
|
Returns:
|
|
44
42
|
The number of documents.
|
|
45
43
|
"""
|
|
46
|
-
|
|
44
|
+
# Variable initialization
|
|
47
45
|
search_task_fn: Callable = self._task_find
|
|
46
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
48
47
|
hash_reduce_left: int = self._hash_reduce_left
|
|
49
48
|
db_root: str = self._db_root
|
|
50
|
-
class_model:
|
|
49
|
+
class_model: Any = self._class_model
|
|
51
50
|
counter: int = 0
|
|
52
|
-
|
|
51
|
+
# Run quantum loop
|
|
52
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
53
53
|
for branch_number in branch_numbers:
|
|
54
54
|
future = executor.submit(
|
|
55
55
|
search_task_fn,
|
scruby/mixins/custom_task.py
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Quantum methods for running custom tasks."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
9
|
__all__ = ("CustomTask",)
|
|
6
10
|
|
|
7
|
-
import logging
|
|
8
11
|
from collections.abc import Callable
|
|
9
|
-
from typing import Any
|
|
12
|
+
from typing import Any
|
|
10
13
|
|
|
11
14
|
import orjson
|
|
12
15
|
from anyio import Path
|
|
13
16
|
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class CustomTask[T]:
|
|
18
|
+
class CustomTask:
|
|
20
19
|
"""Quantum methods for running custom tasks."""
|
|
21
20
|
|
|
22
21
|
@staticmethod
|
|
@@ -24,7 +23,7 @@ class CustomTask[T]:
|
|
|
24
23
|
branch_number: int,
|
|
25
24
|
hash_reduce_left: int,
|
|
26
25
|
db_root: str,
|
|
27
|
-
class_model:
|
|
26
|
+
class_model: Any,
|
|
28
27
|
) -> list[Any]:
|
|
29
28
|
"""Get documents for custom task.
|
|
30
29
|
|
|
@@ -51,26 +50,25 @@ class CustomTask[T]:
|
|
|
51
50
|
docs.append(class_model.model_validate_json(val))
|
|
52
51
|
return docs
|
|
53
52
|
|
|
54
|
-
async def run_custom_task(self, custom_task_fn: Callable
|
|
53
|
+
async def run_custom_task(self, custom_task_fn: Callable) -> Any:
|
|
55
54
|
"""Running custom task.
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
Attention:
|
|
57
|
+
- The search is based on the effect of a quantum loop.
|
|
58
|
+
- The search effectiveness depends on the number of processor threads.
|
|
60
59
|
|
|
61
60
|
Args:
|
|
62
|
-
custom_task_fn: A function that execute the custom task.
|
|
63
|
-
limit_docs: Limiting the number of documents. By default = 1000.
|
|
61
|
+
custom_task_fn (Callable): A function that execute the custom task.
|
|
64
62
|
|
|
65
63
|
Returns:
|
|
66
64
|
The result of a custom task.
|
|
67
65
|
"""
|
|
68
66
|
kwargs = {
|
|
69
67
|
"get_docs_fn": self._task_get_docs,
|
|
70
|
-
"branch_numbers": range(
|
|
68
|
+
"branch_numbers": range(self._max_number_branch),
|
|
71
69
|
"hash_reduce_left": self._hash_reduce_left,
|
|
72
70
|
"db_root": self._db_root,
|
|
73
71
|
"class_model": self._class_model,
|
|
74
|
-
"
|
|
72
|
+
"max_workers": self._max_workers,
|
|
75
73
|
}
|
|
76
74
|
return await custom_task_fn(**kwargs)
|
scruby/mixins/delete.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Methods for deleting documents."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
@@ -5,19 +9,14 @@ from __future__ import annotations
|
|
|
5
9
|
__all__ = ("Delete",)
|
|
6
10
|
|
|
7
11
|
import concurrent.futures
|
|
8
|
-
import logging
|
|
9
12
|
from collections.abc import Callable
|
|
10
|
-
from typing import
|
|
13
|
+
from typing import Any
|
|
11
14
|
|
|
12
15
|
import orjson
|
|
13
16
|
from anyio import Path
|
|
14
17
|
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class Delete[T]:
|
|
19
|
+
class Delete:
|
|
21
20
|
"""Methods for deleting documents."""
|
|
22
21
|
|
|
23
22
|
@staticmethod
|
|
@@ -26,7 +25,7 @@ class Delete[T]:
|
|
|
26
25
|
filter_fn: Callable,
|
|
27
26
|
hash_reduce_left: int,
|
|
28
27
|
db_root: str,
|
|
29
|
-
class_model:
|
|
28
|
+
class_model: Any,
|
|
30
29
|
) -> int:
|
|
31
30
|
"""Task for find and delete documents.
|
|
32
31
|
|
|
@@ -62,30 +61,28 @@ class Delete[T]:
|
|
|
62
61
|
async def delete_many(
|
|
63
62
|
self,
|
|
64
63
|
filter_fn: Callable,
|
|
65
|
-
max_workers: int | None = None,
|
|
66
64
|
) -> int:
|
|
67
65
|
"""Delete one or more documents matching the filter.
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
Attention:
|
|
68
|
+
- The search is based on the effect of a quantum loop.
|
|
69
|
+
- The search effectiveness depends on the number of processor threads.
|
|
72
70
|
|
|
73
71
|
Args:
|
|
74
|
-
filter_fn: A function that execute the conditions of filtering.
|
|
75
|
-
max_workers: The maximum number of processes that can be used to
|
|
76
|
-
execute the given calls. If None or not given then as many
|
|
77
|
-
worker processes will be created as the machine has processors.
|
|
72
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
78
73
|
|
|
79
74
|
Returns:
|
|
80
75
|
The number of deleted documents.
|
|
81
76
|
"""
|
|
82
|
-
|
|
77
|
+
# Variable initialization
|
|
83
78
|
search_task_fn: Callable = self._task_delete
|
|
79
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
84
80
|
hash_reduce_left: int = self._hash_reduce_left
|
|
85
81
|
db_root: str = self._db_root
|
|
86
|
-
class_model:
|
|
82
|
+
class_model: Any = self._class_model
|
|
87
83
|
counter: int = 0
|
|
88
|
-
|
|
84
|
+
# Run quantum loop
|
|
85
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
89
86
|
for branch_number in branch_numbers:
|
|
90
87
|
future = executor.submit(
|
|
91
88
|
search_task_fn,
|
scruby/mixins/find.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Quantum methods for searching documents."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
@@ -5,19 +9,14 @@ from __future__ import annotations
|
|
|
5
9
|
__all__ = ("Find",)
|
|
6
10
|
|
|
7
11
|
import concurrent.futures
|
|
8
|
-
import logging
|
|
9
12
|
from collections.abc import Callable
|
|
10
|
-
from typing import
|
|
13
|
+
from typing import Any
|
|
11
14
|
|
|
12
15
|
import orjson
|
|
13
16
|
from anyio import Path
|
|
14
17
|
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class Find[T]:
|
|
19
|
+
class Find:
|
|
21
20
|
"""Quantum methods for searching documents."""
|
|
22
21
|
|
|
23
22
|
@staticmethod
|
|
@@ -26,8 +25,8 @@ class Find[T]:
|
|
|
26
25
|
filter_fn: Callable,
|
|
27
26
|
hash_reduce_left: str,
|
|
28
27
|
db_root: str,
|
|
29
|
-
class_model:
|
|
30
|
-
) -> list[
|
|
28
|
+
class_model: Any,
|
|
29
|
+
) -> list[Any] | None:
|
|
31
30
|
"""Task for find documents.
|
|
32
31
|
|
|
33
32
|
This method is for internal use.
|
|
@@ -35,6 +34,7 @@ class Find[T]:
|
|
|
35
34
|
Returns:
|
|
36
35
|
List of documents or None.
|
|
37
36
|
"""
|
|
37
|
+
# Variable initialization
|
|
38
38
|
branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
|
|
39
39
|
separated_hash: str = "/".join(list(branch_number_as_hash))
|
|
40
40
|
leaf_path: Path = Path(
|
|
@@ -45,7 +45,7 @@ class Find[T]:
|
|
|
45
45
|
"leaf.json",
|
|
46
46
|
),
|
|
47
47
|
)
|
|
48
|
-
docs: list[
|
|
48
|
+
docs: list[Any] = []
|
|
49
49
|
if await leaf_path.exists():
|
|
50
50
|
data_json: bytes = await leaf_path.read_bytes()
|
|
51
51
|
data: dict[str, str] = orjson.loads(data_json) or {}
|
|
@@ -58,29 +58,27 @@ class Find[T]:
|
|
|
58
58
|
async def find_one(
|
|
59
59
|
self,
|
|
60
60
|
filter_fn: Callable,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"""Finds a single document matching the filter.
|
|
61
|
+
) -> Any | None:
|
|
62
|
+
"""Find one document matching the filter.
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
Attention:
|
|
65
|
+
- The search is based on the effect of a quantum loop.
|
|
66
|
+
- The search effectiveness depends on the number of processor threads.
|
|
68
67
|
|
|
69
68
|
Args:
|
|
70
|
-
filter_fn: A function that execute the conditions of filtering.
|
|
71
|
-
max_workers: The maximum number of processes that can be used to
|
|
72
|
-
execute the given calls. If None or not given then as many
|
|
73
|
-
worker processes will be created as the machine has processors.
|
|
69
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
74
70
|
|
|
75
71
|
Returns:
|
|
76
72
|
Document or None.
|
|
77
73
|
"""
|
|
78
|
-
|
|
74
|
+
# Variable initialization
|
|
79
75
|
search_task_fn: Callable = self._task_find
|
|
76
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
80
77
|
hash_reduce_left: int = self._hash_reduce_left
|
|
81
78
|
db_root: str = self._db_root
|
|
82
|
-
class_model:
|
|
83
|
-
|
|
79
|
+
class_model: Any = self._class_model
|
|
80
|
+
# Run quantum loop
|
|
81
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
84
82
|
for branch_number in branch_numbers:
|
|
85
83
|
future = executor.submit(
|
|
86
84
|
search_task_fn,
|
|
@@ -97,36 +95,41 @@ class Find[T]:
|
|
|
97
95
|
|
|
98
96
|
async def find_many(
|
|
99
97
|
self,
|
|
100
|
-
filter_fn: Callable,
|
|
98
|
+
filter_fn: Callable = lambda _: True,
|
|
101
99
|
limit_docs: int = 1000,
|
|
102
|
-
|
|
103
|
-
) -> list[
|
|
104
|
-
"""
|
|
100
|
+
page_number: int = 1,
|
|
101
|
+
) -> list[Any] | None:
|
|
102
|
+
"""Find many documents matching the filter.
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
Attention:
|
|
105
|
+
- The search is based on the effect of a quantum loop.
|
|
106
|
+
- The search effectiveness depends on the number of processor threads.
|
|
109
107
|
|
|
110
108
|
Args:
|
|
111
|
-
filter_fn: A function that execute the conditions of filtering.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
110
|
+
By default it searches for all documents.
|
|
111
|
+
limit_docs (int): Limiting the number of documents. By default = 1000.
|
|
112
|
+
page_number (int): For pagination output. By default = 1.
|
|
113
|
+
Number of documents per page = limit_docs.
|
|
116
114
|
|
|
117
115
|
Returns:
|
|
118
116
|
List of documents or None.
|
|
119
117
|
"""
|
|
120
|
-
|
|
118
|
+
# The `page_number` parameter must not be less than one
|
|
119
|
+
assert page_number > 0, "`find_many` => The `page_number` parameter must not be less than one."
|
|
120
|
+
# Variable initialization
|
|
121
121
|
search_task_fn: Callable = self._task_find
|
|
122
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
122
123
|
hash_reduce_left: int = self._hash_reduce_left
|
|
123
124
|
db_root: str = self._db_root
|
|
124
|
-
class_model:
|
|
125
|
+
class_model: Any = self._class_model
|
|
125
126
|
counter: int = 0
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
number_docs_skippe: int = limit_docs * (page_number - 1) if page_number > 1 else 0
|
|
128
|
+
result: list[Any] = []
|
|
129
|
+
# Run quantum loop
|
|
130
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
128
131
|
for branch_number in branch_numbers:
|
|
129
|
-
if counter >= limit_docs:
|
|
132
|
+
if number_docs_skippe == 0 and counter >= limit_docs:
|
|
130
133
|
return result[:limit_docs]
|
|
131
134
|
future = executor.submit(
|
|
132
135
|
search_task_fn,
|
|
@@ -139,8 +142,11 @@ class Find[T]:
|
|
|
139
142
|
docs = await future.result()
|
|
140
143
|
if docs is not None:
|
|
141
144
|
for doc in docs:
|
|
142
|
-
if
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
if number_docs_skippe == 0:
|
|
146
|
+
if counter >= limit_docs:
|
|
147
|
+
return result[:limit_docs]
|
|
148
|
+
result.append(doc)
|
|
149
|
+
counter += 1
|
|
150
|
+
else:
|
|
151
|
+
number_docs_skippe -= 1
|
|
146
152
|
return result or None
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Methods for working with keys."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
|
-
__all__ = ("
|
|
9
|
+
__all__ = ("Keys",)
|
|
6
10
|
|
|
7
11
|
import logging
|
|
12
|
+
from datetime import datetime
|
|
8
13
|
from typing import Any
|
|
14
|
+
from zoneinfo import ZoneInfo
|
|
9
15
|
|
|
10
16
|
import orjson
|
|
11
17
|
|
|
@@ -14,17 +20,15 @@ from scruby.errors import (
|
|
|
14
20
|
KeyNotExistsError,
|
|
15
21
|
)
|
|
16
22
|
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"""Methods for working with document."""
|
|
24
|
+
class Keys:
|
|
25
|
+
"""Methods for working with keys."""
|
|
22
26
|
|
|
23
27
|
async def add_doc(self, doc: Any) -> None:
|
|
24
28
|
"""Asynchronous method for adding document to collection.
|
|
25
29
|
|
|
26
30
|
Args:
|
|
27
|
-
doc: Value of key. Type, derived from `
|
|
31
|
+
doc (Any): Value of key. Type, derived from `ScrubyModel`.
|
|
28
32
|
|
|
29
33
|
Returns:
|
|
30
34
|
None.
|
|
@@ -36,10 +40,15 @@ class Docs:
|
|
|
36
40
|
msg = (
|
|
37
41
|
f"(add_doc) Parameter `doc` => Model `{doc_class_name}` does not match collection `{collection_name}`!"
|
|
38
42
|
)
|
|
39
|
-
|
|
43
|
+
logging.error(msg)
|
|
40
44
|
raise TypeError(msg)
|
|
41
45
|
# The path to cell of collection.
|
|
42
46
|
leaf_path, prepared_key = await self._get_leaf_path(doc.key)
|
|
47
|
+
# Init a `created_at` and `updated_at` fields
|
|
48
|
+
tz = ZoneInfo("UTC")
|
|
49
|
+
doc.created_at = datetime.now(tz)
|
|
50
|
+
doc.updated_at = datetime.now(tz)
|
|
51
|
+
# Convert doc to json
|
|
43
52
|
doc_json: str = doc.model_dump_json()
|
|
44
53
|
# Write key-value to collection.
|
|
45
54
|
if await leaf_path.exists():
|
|
@@ -53,7 +62,7 @@ class Docs:
|
|
|
53
62
|
await leaf_path.write_bytes(orjson.dumps(data))
|
|
54
63
|
else:
|
|
55
64
|
err = KeyAlreadyExistsError()
|
|
56
|
-
|
|
65
|
+
logging.error(err.message)
|
|
57
66
|
raise err
|
|
58
67
|
else:
|
|
59
68
|
# Add new document to a blank leaf.
|
|
@@ -61,10 +70,10 @@ class Docs:
|
|
|
61
70
|
await self._counter_documents(1)
|
|
62
71
|
|
|
63
72
|
async def update_doc(self, doc: Any) -> None:
|
|
64
|
-
"""Asynchronous method for updating
|
|
73
|
+
"""Asynchronous method for updating document to collection.
|
|
65
74
|
|
|
66
75
|
Args:
|
|
67
|
-
doc: Value of key. Type `
|
|
76
|
+
doc (Any): Value of key. Type `ScrubyModel`.
|
|
68
77
|
|
|
69
78
|
Returns:
|
|
70
79
|
None.
|
|
@@ -77,10 +86,13 @@ class Docs:
|
|
|
77
86
|
f"(update_doc) Parameter `doc` => Model `{doc_class_name}` "
|
|
78
87
|
f"does not match collection `{collection_name}`!"
|
|
79
88
|
)
|
|
80
|
-
|
|
89
|
+
logging.error(msg)
|
|
81
90
|
raise TypeError(msg)
|
|
82
91
|
# The path to cell of collection.
|
|
83
92
|
leaf_path, prepared_key = await self._get_leaf_path(doc.key)
|
|
93
|
+
# Update a `updated_at` field
|
|
94
|
+
doc.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
95
|
+
# Convert doc to json
|
|
84
96
|
doc_json: str = doc.model_dump_json()
|
|
85
97
|
# Update the existing key.
|
|
86
98
|
if await leaf_path.exists():
|
|
@@ -93,17 +105,18 @@ class Docs:
|
|
|
93
105
|
await leaf_path.write_bytes(orjson.dumps(data))
|
|
94
106
|
except KeyError:
|
|
95
107
|
err = KeyNotExistsError()
|
|
96
|
-
|
|
108
|
+
logging.error(err.message)
|
|
97
109
|
raise err from None
|
|
98
110
|
else:
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
msg: str = f"`update_doc` - The key `{doc.key}` is missing!"
|
|
112
|
+
logging.error(msg)
|
|
113
|
+
raise KeyError(msg)
|
|
101
114
|
|
|
102
|
-
async def
|
|
103
|
-
"""Asynchronous method for getting
|
|
115
|
+
async def get_doc(self, key: str) -> Any:
|
|
116
|
+
"""Asynchronous method for getting document from collection the by key.
|
|
104
117
|
|
|
105
118
|
Args:
|
|
106
|
-
key: Key name.
|
|
119
|
+
key (str): Key name.
|
|
107
120
|
|
|
108
121
|
Returns:
|
|
109
122
|
Value of key or KeyError.
|
|
@@ -116,15 +129,15 @@ class Docs:
|
|
|
116
129
|
data: dict = orjson.loads(data_json) or {}
|
|
117
130
|
obj: Any = self._class_model.model_validate_json(data[prepared_key])
|
|
118
131
|
return obj
|
|
119
|
-
msg: str = "`
|
|
120
|
-
|
|
121
|
-
raise KeyError()
|
|
132
|
+
msg: str = f"`get_doc` - The key `{key}` is missing!"
|
|
133
|
+
logging.error(msg)
|
|
134
|
+
raise KeyError(msg)
|
|
122
135
|
|
|
123
136
|
async def has_key(self, key: str) -> bool:
|
|
124
137
|
"""Asynchronous method for checking presence of key in collection.
|
|
125
138
|
|
|
126
139
|
Args:
|
|
127
|
-
key: Key name.
|
|
140
|
+
key (str): Key name.
|
|
128
141
|
|
|
129
142
|
Returns:
|
|
130
143
|
True, if the key is present.
|
|
@@ -142,11 +155,11 @@ class Docs:
|
|
|
142
155
|
return False
|
|
143
156
|
return False
|
|
144
157
|
|
|
145
|
-
async def
|
|
146
|
-
"""Asynchronous method for deleting
|
|
158
|
+
async def delete_doc(self, key: str) -> None:
|
|
159
|
+
"""Asynchronous method for deleting document from collection the by key.
|
|
147
160
|
|
|
148
161
|
Args:
|
|
149
|
-
key: Key name.
|
|
162
|
+
key (str): Key name.
|
|
150
163
|
|
|
151
164
|
Returns:
|
|
152
165
|
None.
|
|
@@ -161,6 +174,6 @@ class Docs:
|
|
|
161
174
|
await leaf_path.write_bytes(orjson.dumps(data))
|
|
162
175
|
await self._counter_documents(-1)
|
|
163
176
|
return
|
|
164
|
-
msg: str = "`
|
|
165
|
-
|
|
166
|
-
raise KeyError()
|
|
177
|
+
msg: str = f"`delete_doc` - The key `{key}` is missing!"
|
|
178
|
+
logging.error(msg)
|
|
179
|
+
raise KeyError(msg)
|
scruby/mixins/update.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
1
5
|
"""Methods for updating documents."""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
@@ -5,19 +9,14 @@ from __future__ import annotations
|
|
|
5
9
|
__all__ = ("Update",)
|
|
6
10
|
|
|
7
11
|
import concurrent.futures
|
|
8
|
-
import logging
|
|
9
12
|
from collections.abc import Callable
|
|
10
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
11
14
|
|
|
12
15
|
import orjson
|
|
13
16
|
from anyio import Path
|
|
14
17
|
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class Update[T]:
|
|
19
|
+
class Update:
|
|
21
20
|
"""Methods for updating documents."""
|
|
22
21
|
|
|
23
22
|
@staticmethod
|
|
@@ -26,7 +25,7 @@ class Update[T]:
|
|
|
26
25
|
filter_fn: Callable,
|
|
27
26
|
hash_reduce_left: str,
|
|
28
27
|
db_root: str,
|
|
29
|
-
class_model:
|
|
28
|
+
class_model: Any,
|
|
30
29
|
new_data: dict[str, Any],
|
|
31
30
|
) -> int:
|
|
32
31
|
"""Task for find documents.
|
|
@@ -63,33 +62,33 @@ class Update[T]:
|
|
|
63
62
|
|
|
64
63
|
async def update_many(
|
|
65
64
|
self,
|
|
66
|
-
filter_fn: Callable,
|
|
67
65
|
new_data: dict[str, Any],
|
|
68
|
-
|
|
66
|
+
filter_fn: Callable = lambda _: True,
|
|
69
67
|
) -> int:
|
|
70
|
-
"""Updates
|
|
68
|
+
"""Updates many documents matching the filter.
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
Attention:
|
|
71
|
+
- For a complex case, a custom task may be needed.
|
|
72
|
+
- See documentation on creating custom tasks.
|
|
73
|
+
- The search is based on the effect of a quantum loop.
|
|
74
|
+
- The search effectiveness depends on the number of processor threads.
|
|
75
75
|
|
|
76
76
|
Args:
|
|
77
|
-
filter_fn: A function that execute the conditions of filtering.
|
|
78
|
-
new_data: New data for the fields that need to be updated.
|
|
79
|
-
max_workers: The maximum number of processes that can be used to
|
|
80
|
-
execute the given calls. If None or not given then as many
|
|
81
|
-
worker processes will be created as the machine has processors.
|
|
77
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
78
|
+
new_data (dict[str, Any]): New data for the fields that need to be updated.
|
|
82
79
|
|
|
83
80
|
Returns:
|
|
84
81
|
The number of updated documents.
|
|
85
82
|
"""
|
|
86
|
-
|
|
83
|
+
# Variable initialization
|
|
87
84
|
update_task_fn: Callable = self._task_update
|
|
85
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
88
86
|
hash_reduce_left: int = self._hash_reduce_left
|
|
89
87
|
db_root: str = self._db_root
|
|
90
|
-
class_model:
|
|
88
|
+
class_model: Any = self._class_model
|
|
91
89
|
counter: int = 0
|
|
92
|
-
|
|
90
|
+
# Run quantum loop
|
|
91
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
93
92
|
for branch_number in branch_numbers:
|
|
94
93
|
future = executor.submit(
|
|
95
94
|
update_task_fn,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
|
+
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
5
|
+
"""Database settings.
|
|
2
6
|
|
|
3
|
-
The module contains the following
|
|
7
|
+
The module contains the following parameters:
|
|
4
8
|
|
|
5
9
|
- `DB_ROOT` - Path to root directory of database. `By default = "ScrubyDB" (in root of project)`.
|
|
6
10
|
- `HASH_REDUCE_LEFT` - The length of the hash reduction on the left side.
|
|
@@ -8,6 +12,7 @@ The module contains the following variables:
|
|
|
8
12
|
- `2` - 16777216 branches in collection.
|
|
9
13
|
- `4` - 65536 branches in collection.
|
|
10
14
|
- `6` - 256 branches in collection (by default).
|
|
15
|
+
- `MAX_WORKERS` - The maximum number of processes that can be used `By default = None`.
|
|
11
16
|
"""
|
|
12
17
|
|
|
13
18
|
from __future__ import annotations
|
|
@@ -15,6 +20,7 @@ from __future__ import annotations
|
|
|
15
20
|
__all__ = (
|
|
16
21
|
"DB_ROOT",
|
|
17
22
|
"HASH_REDUCE_LEFT",
|
|
23
|
+
"MAX_WORKERS",
|
|
18
24
|
)
|
|
19
25
|
|
|
20
26
|
from typing import Literal
|
|
@@ -31,3 +37,8 @@ DB_ROOT: str = "ScrubyDB"
|
|
|
31
37
|
# Number of branches is number of requests to the hard disk during quantum operations.
|
|
32
38
|
# Quantum operations: find_one, find_many, count_documents, delete_many, run_custom_task.
|
|
33
39
|
HASH_REDUCE_LEFT: Literal[0, 2, 4, 6] = 6
|
|
40
|
+
|
|
41
|
+
# The maximum number of processes that can be used to execute the given calls.
|
|
42
|
+
# If None, then as many worker processes will be
|
|
43
|
+
# created as the machine has processors.
|
|
44
|
+
MAX_WORKERS: int | None = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scruby
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.30.2
|
|
4
4
|
Summary: Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
5
5
|
Project-URL: Homepage, https://kebasyaty.github.io/scruby/
|
|
6
6
|
Project-URL: Repository, https://github.com/kebasyaty/scruby
|
|
@@ -17,6 +17,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
17
17
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
18
|
Classifier: Operating System :: Microsoft :: Windows
|
|
19
19
|
Classifier: Operating System :: POSIX
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
21
|
Classifier: Programming Language :: Python :: 3
|
|
21
22
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -98,6 +99,15 @@ Online browsable documentation is available at [https://kebasyaty.github.io/scru
|
|
|
98
99
|
uv add scruby
|
|
99
100
|
```
|
|
100
101
|
|
|
102
|
+
## Run
|
|
103
|
+
|
|
104
|
+
```shell
|
|
105
|
+
# Run Development:
|
|
106
|
+
uv run python main.py
|
|
107
|
+
# Run Production:
|
|
108
|
+
uv run python -OOP main.py
|
|
109
|
+
```
|
|
110
|
+
|
|
101
111
|
## Usage
|
|
102
112
|
|
|
103
113
|
See more examples here [https://kebasyaty.github.io/scruby/latest/pages/usage/](https://kebasyaty.github.io/scruby/latest/pages/usage/ "Examples").
|
|
@@ -106,23 +116,25 @@ See more examples here [https://kebasyaty.github.io/scruby/latest/pages/usage/](
|
|
|
106
116
|
"""Working with keys."""
|
|
107
117
|
|
|
108
118
|
import anyio
|
|
109
|
-
import datetime
|
|
119
|
+
from datetime import datetime
|
|
120
|
+
from zoneinfo import ZoneInfo
|
|
110
121
|
from typing import Annotated
|
|
111
|
-
from pydantic import
|
|
122
|
+
from pydantic import EmailStr, Field
|
|
112
123
|
from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator
|
|
113
|
-
from scruby import Scruby,
|
|
124
|
+
from scruby import Scruby, ScrubyModel, settings
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
|
|
127
|
+
settings.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
128
|
+
settings.MAX_WORKERS = None # By default = None
|
|
117
129
|
|
|
118
|
-
class User(
|
|
130
|
+
class User(ScrubyModel):
|
|
119
131
|
"""User model."""
|
|
120
132
|
first_name: str = Field(strict=True)
|
|
121
133
|
last_name: str = Field(strict=True)
|
|
122
|
-
birthday: datetime
|
|
134
|
+
birthday: datetime = Field(strict=True)
|
|
123
135
|
email: EmailStr = Field(strict=True)
|
|
124
136
|
phone: Annotated[PhoneNumber, PhoneNumberValidator(number_format="E164")] = Field(frozen=True)
|
|
125
|
-
#
|
|
137
|
+
# key is always at bottom
|
|
126
138
|
key: str = Field(
|
|
127
139
|
strict=True,
|
|
128
140
|
frozen=True,
|
|
@@ -132,13 +144,13 @@ class User(BaseModel):
|
|
|
132
144
|
|
|
133
145
|
async def main() -> None:
|
|
134
146
|
"""Example."""
|
|
135
|
-
# Get collection
|
|
147
|
+
# Get collection `User`.
|
|
136
148
|
user_coll = await Scruby.collection(User)
|
|
137
149
|
|
|
138
150
|
user = User(
|
|
139
151
|
first_name="John",
|
|
140
152
|
last_name="Smith",
|
|
141
|
-
birthday=datetime.datetime(1970, 1, 1),
|
|
153
|
+
birthday=datetime.datetime(1970, 1, 1, tzinfo=ZoneInfo("UTC")),
|
|
142
154
|
email="John_Smith@gmail.com",
|
|
143
155
|
phone="+447986123456",
|
|
144
156
|
)
|
|
@@ -147,15 +159,15 @@ async def main() -> None:
|
|
|
147
159
|
|
|
148
160
|
await user_coll.update_doc(user)
|
|
149
161
|
|
|
150
|
-
await user_coll.
|
|
151
|
-
await user_coll.
|
|
162
|
+
await user_coll.get_doc("+447986123456") # => user
|
|
163
|
+
await user_coll.get_doc("key missing") # => KeyError
|
|
152
164
|
|
|
153
165
|
await user_coll.has_key("+447986123456") # => True
|
|
154
166
|
await user_coll.has_key("key missing") # => False
|
|
155
167
|
|
|
156
|
-
await user_coll.
|
|
157
|
-
await user_coll.
|
|
158
|
-
await user_coll.
|
|
168
|
+
await user_coll.delete_doc("+447986123456")
|
|
169
|
+
await user_coll.delete_doc("+447986123456") # => KeyError
|
|
170
|
+
await user_coll.delete_doc("key missing") # => KeyError
|
|
159
171
|
|
|
160
172
|
# Full database deletion.
|
|
161
173
|
# Hint: The main purpose is tests.
|
|
@@ -167,31 +179,31 @@ if __name__ == "__main__":
|
|
|
167
179
|
```
|
|
168
180
|
|
|
169
181
|
```python
|
|
170
|
-
"""Find
|
|
182
|
+
"""Find one document matching the filter.
|
|
171
183
|
|
|
172
184
|
The search is based on the effect of a quantum loop.
|
|
173
185
|
The search effectiveness depends on the number of processor threads.
|
|
174
|
-
Ideally, hundreds and even thousands of threads are required.
|
|
175
186
|
"""
|
|
176
187
|
|
|
177
188
|
import anyio
|
|
178
|
-
import datetime
|
|
189
|
+
from datetime import datetime
|
|
179
190
|
from typing import Annotated
|
|
180
|
-
from pydantic import
|
|
181
|
-
from scruby import Scruby,
|
|
191
|
+
from pydantic import Field
|
|
192
|
+
from scruby import Scruby, ScrubyModel, settings
|
|
182
193
|
from pprint import pprint as pp
|
|
183
194
|
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
|
|
196
|
+
settings.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
197
|
+
settings.MAX_WORKERS = None # By default = None
|
|
186
198
|
|
|
187
199
|
|
|
188
|
-
class Phone(
|
|
200
|
+
class Phone(ScrubyModel):
|
|
189
201
|
"""Phone model."""
|
|
190
202
|
brand: str = Field(strict=True, frozen=True)
|
|
191
203
|
model: str = Field(strict=True, frozen=True)
|
|
192
204
|
screen_diagonal: float = Field(strict=True)
|
|
193
205
|
matrix_type: str = Field(strict=True)
|
|
194
|
-
#
|
|
206
|
+
# key is always at bottom
|
|
195
207
|
key: str = Field(
|
|
196
208
|
strict=True,
|
|
197
209
|
frozen=True,
|
|
@@ -201,7 +213,7 @@ class Phone(BaseModel):
|
|
|
201
213
|
|
|
202
214
|
async def main() -> None:
|
|
203
215
|
"""Example."""
|
|
204
|
-
# Get collection
|
|
216
|
+
# Get collection `Phone`.
|
|
205
217
|
phone_coll = await Scruby.collection(Phone)
|
|
206
218
|
|
|
207
219
|
# Create phone.
|
|
@@ -243,31 +255,31 @@ if __name__ == "__main__":
|
|
|
243
255
|
```
|
|
244
256
|
|
|
245
257
|
```python
|
|
246
|
-
"""Find
|
|
258
|
+
"""Find many documents matching the filter.
|
|
247
259
|
|
|
248
260
|
The search is based on the effect of a quantum loop.
|
|
249
261
|
The search effectiveness depends on the number of processor threads.
|
|
250
|
-
Ideally, hundreds and even thousands of threads are required.
|
|
251
262
|
"""
|
|
252
263
|
|
|
253
264
|
import anyio
|
|
254
|
-
import datetime
|
|
265
|
+
from datetime import datetime
|
|
255
266
|
from typing import Annotated
|
|
256
|
-
from pydantic import
|
|
257
|
-
from scruby import Scruby,
|
|
267
|
+
from pydantic import Field
|
|
268
|
+
from scruby import Scruby, ScrubyModel, settings
|
|
258
269
|
from pprint import pprint as pp
|
|
259
270
|
|
|
260
|
-
|
|
261
|
-
|
|
271
|
+
settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
|
|
272
|
+
settings.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
273
|
+
settings.MAX_WORKERS = None # By default = None
|
|
262
274
|
|
|
263
275
|
|
|
264
|
-
class Car(
|
|
276
|
+
class Car(ScrubyModel):
|
|
265
277
|
"""Car model."""
|
|
266
278
|
brand: str = Field(strict=True, frozen=True)
|
|
267
279
|
model: str = Field(strict=True, frozen=True)
|
|
268
280
|
year: int = Field(strict=True)
|
|
269
281
|
power_reserve: int = Field(strict=True)
|
|
270
|
-
#
|
|
282
|
+
# key is always at bottom
|
|
271
283
|
key: str = Field(
|
|
272
284
|
strict=True,
|
|
273
285
|
frozen=True,
|
|
@@ -277,11 +289,11 @@ class Car(BaseModel):
|
|
|
277
289
|
|
|
278
290
|
async def main() -> None:
|
|
279
291
|
"""Example."""
|
|
280
|
-
# Get collection
|
|
292
|
+
# Get collection `Car`.
|
|
281
293
|
car_coll = await Scruby.collection(Car)
|
|
282
294
|
|
|
283
295
|
# Create cars.
|
|
284
|
-
for
|
|
296
|
+
for num in range(1, 10):
|
|
285
297
|
car = Car(
|
|
286
298
|
brand="Mazda",
|
|
287
299
|
model=f"EZ-6 {num}",
|
|
@@ -299,9 +311,23 @@ async def main() -> None:
|
|
|
299
311
|
else:
|
|
300
312
|
print("No cars!")
|
|
301
313
|
|
|
302
|
-
#
|
|
303
|
-
|
|
304
|
-
|
|
314
|
+
# Find all cars.
|
|
315
|
+
car_list: list[Car] | None = await car_coll.find_many()
|
|
316
|
+
if car_list is not None:
|
|
317
|
+
pp(car_list)
|
|
318
|
+
else:
|
|
319
|
+
print("No cars!")
|
|
320
|
+
|
|
321
|
+
# For pagination output.
|
|
322
|
+
car_list: list[Car] | None = await car_coll.find_many(
|
|
323
|
+
filter_fn=lambda doc: doc.brand == "Mazda",
|
|
324
|
+
limit_docs=5,
|
|
325
|
+
page_number=2,
|
|
326
|
+
)
|
|
327
|
+
if car_list is not None:
|
|
328
|
+
pp(car_list)
|
|
329
|
+
else:
|
|
330
|
+
print("No cars!")
|
|
305
331
|
|
|
306
332
|
# Full database deletion.
|
|
307
333
|
# Hint: The main purpose is tests.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
scruby/__init__.py,sha256=WuIA27d3uUK0Vo0fXg92hge7i6v_2DBndhfepK16_y8,1046
|
|
2
|
+
scruby/aggregation.py,sha256=bd70J1Xye6faNHD8LS3lVQoHWKtPdPV_cqT_i7oui38,3491
|
|
3
|
+
scruby/db.py,sha256=4bSPMh0fYil0j9qhHhRu1g4U_0WaCuaGPthnmi2QpBI,8376
|
|
4
|
+
scruby/errors.py,sha256=D0jisudUsZk9iXp4nRSymaSMwyqHPVshsSlxx4HDVVk,1297
|
|
5
|
+
scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
scruby/settings.py,sha256=_uVdZIGWoi6q9zcu0c2PS51OBEBNASRRrxfzaF7Nwy0,1580
|
|
7
|
+
scruby/mixins/__init__.py,sha256=XPMjJvOZN7dLpTE1FfGMBGQ_0421HXug-0rWKMU5fRQ,627
|
|
8
|
+
scruby/mixins/collection.py,sha256=coF-IOhicV_EihDwnYf6SW5Mfi3nOFR0gAhCc619NmI,1382
|
|
9
|
+
scruby/mixins/count.py,sha256=YHjonxAtroSc5AlbDX1vpCvbe3vsTD0LfYnjEDB001A,2089
|
|
10
|
+
scruby/mixins/custom_task.py,sha256=ENr3FkCsPWRblOWv8jGMkkGKw4hvp9mMP2YjQvIeqzE,2246
|
|
11
|
+
scruby/mixins/delete.py,sha256=b7RYiTLUqVu70ep15CN1VTyKs13P7f-1YDGRc3ke-2g,3085
|
|
12
|
+
scruby/mixins/find.py,sha256=-rpILkxhfywJ5E3ceYo9dSPabma47ouceGajUVNh23Q,5396
|
|
13
|
+
scruby/mixins/keys.py,sha256=waxye5n0-oTWIhdDXGQkUTGaV9vLSCc9lCryTCuexkw,6179
|
|
14
|
+
scruby/mixins/update.py,sha256=WeNk2qZToQS3-r1_ahazBc2ErLMU9K5RRNgCxbu3JMg,3416
|
|
15
|
+
scruby-0.30.2.dist-info/METADATA,sha256=OgV4Foj3-X97NKFLpw_8Qb_osGwmYIINO2UZkgdLaY0,11032
|
|
16
|
+
scruby-0.30.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
scruby-0.30.2.dist-info/licenses/LICENSE,sha256=mS0Wz0yGNB63gEcWEnuIb_lldDYV0sjRaO-o_GL6CWE,1074
|
|
18
|
+
scruby-0.30.2.dist-info/RECORD,,
|
scruby-0.26.0.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|