scruby 0.28.3__py3-none-any.whl → 0.31.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 +2 -3
- scruby/aggregation.py +0 -1
- scruby/db.py +55 -20
- scruby/errors.py +0 -1
- scruby/mixins/__init__.py +0 -1
- scruby/mixins/collection.py +0 -1
- scruby/mixins/count.py +5 -5
- scruby/mixins/custom_task.py +20 -50
- scruby/mixins/delete.py +5 -5
- scruby/mixins/find.py +9 -9
- scruby/mixins/keys.py +27 -17
- scruby/mixins/update.py +10 -8
- scruby/settings.py +0 -1
- {scruby-0.28.3.dist-info → scruby-0.31.0.dist-info}/METADATA +31 -23
- scruby-0.31.0.dist-info/RECORD +18 -0
- scruby-0.28.3.dist-info/RECORD +0 -18
- {scruby-0.28.3.dist-info → scruby-0.31.0.dist-info}/WHEEL +0 -0
- {scruby-0.28.3.dist-info → scruby-0.31.0.dist-info}/licenses/LICENSE +0 -0
scruby/__init__.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#
|
|
2
1
|
# .dP"Y8 dP""b8 88""Yb 88 88 88""Yb Yb dP
|
|
3
2
|
# `Ybo." dP `" 88__dP 88 88 88__dP YbdP
|
|
4
3
|
# o.`Y8b Yb 88"Yb Y8 8P 88""Yb 8P
|
|
@@ -6,7 +5,6 @@
|
|
|
6
5
|
#
|
|
7
6
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
8
7
|
# SPDX-License-Identifier: MIT
|
|
9
|
-
#
|
|
10
8
|
"""Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
11
9
|
|
|
12
10
|
The library uses fractal-tree addressing and
|
|
@@ -28,7 +26,8 @@ from __future__ import annotations
|
|
|
28
26
|
__all__ = (
|
|
29
27
|
"settings",
|
|
30
28
|
"Scruby",
|
|
29
|
+
"ScrubyModel",
|
|
31
30
|
)
|
|
32
31
|
|
|
33
32
|
from scruby import settings
|
|
34
|
-
from scruby.db import Scruby
|
|
33
|
+
from scruby.db import Scruby, ScrubyModel
|
scruby/aggregation.py
CHANGED
scruby/db.py
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
2
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
#
|
|
5
4
|
"""Creation and management of the database."""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
8
7
|
|
|
9
|
-
__all__ = (
|
|
8
|
+
__all__ = (
|
|
9
|
+
"Scruby",
|
|
10
|
+
"ScrubyModel",
|
|
11
|
+
)
|
|
10
12
|
|
|
11
13
|
import contextlib
|
|
12
14
|
import logging
|
|
13
15
|
import re
|
|
14
16
|
import zlib
|
|
17
|
+
from datetime import datetime
|
|
15
18
|
from shutil import rmtree
|
|
16
19
|
from typing import Any, Literal, Never, assert_never
|
|
17
20
|
|
|
@@ -31,6 +34,13 @@ class _Meta(BaseModel):
|
|
|
31
34
|
counter_documents: int
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
class ScrubyModel(BaseModel):
|
|
38
|
+
"""Additional fields for models."""
|
|
39
|
+
|
|
40
|
+
created_at: datetime | None = None
|
|
41
|
+
updated_at: datetime | None = None
|
|
42
|
+
|
|
43
|
+
|
|
34
44
|
class Scruby(
|
|
35
45
|
mixins.Keys,
|
|
36
46
|
mixins.Find,
|
|
@@ -53,13 +63,13 @@ class Scruby(
|
|
|
53
63
|
# The maximum number of branches.
|
|
54
64
|
match self._hash_reduce_left:
|
|
55
65
|
case 0:
|
|
56
|
-
self.
|
|
66
|
+
self._max_number_branch = 4294967296
|
|
57
67
|
case 2:
|
|
58
|
-
self.
|
|
68
|
+
self._max_number_branch = 16777216
|
|
59
69
|
case 4:
|
|
60
|
-
self.
|
|
70
|
+
self._max_number_branch = 65536
|
|
61
71
|
case 6:
|
|
62
|
-
self.
|
|
72
|
+
self._max_number_branch = 256
|
|
63
73
|
case _ as unreachable:
|
|
64
74
|
msg: str = f"{unreachable} - Unacceptable value for HASH_REDUCE_LEFT."
|
|
65
75
|
logging.critical(msg)
|
|
@@ -70,42 +80,67 @@ class Scruby(
|
|
|
70
80
|
"""Get an object to access a collection.
|
|
71
81
|
|
|
72
82
|
Args:
|
|
73
|
-
class_model: Class of Model (
|
|
83
|
+
class_model (Any): Class of Model (ScrubyModel).
|
|
74
84
|
|
|
75
85
|
Returns:
|
|
76
86
|
Instance of Scruby for access a collection.
|
|
77
87
|
"""
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
if __debug__:
|
|
89
|
+
# Check if the object belongs to the class `ScrubyModel`
|
|
90
|
+
if ScrubyModel not in class_model.__bases__:
|
|
91
|
+
msg = (
|
|
92
|
+
"Method: `collection` => argument `class_model` " + "does not contain the base class `ScrubyModel`!"
|
|
93
|
+
)
|
|
94
|
+
raise AssertionError(msg)
|
|
95
|
+
# Checking the model for the presence of a key.
|
|
96
|
+
model_fields = list(class_model.model_fields.keys())
|
|
97
|
+
if "key" not in model_fields:
|
|
98
|
+
msg = f"Model: {class_model.__name__} => The `key` field is missing!"
|
|
99
|
+
raise AssertionError(msg)
|
|
100
|
+
if "created_at" not in model_fields:
|
|
101
|
+
msg = f"Model: {class_model.__name__} => The `created_at` field is missing!"
|
|
102
|
+
raise AssertionError(msg)
|
|
103
|
+
if "updated_at" not in model_fields:
|
|
104
|
+
msg = f"Model: {class_model.__name__} => The `updated_at` field is missing!"
|
|
105
|
+
raise AssertionError(msg)
|
|
106
|
+
# Check the length of the collection name for an acceptable size.
|
|
107
|
+
len_db_root_absolut_path = len(str(await Path(settings.DB_ROOT).resolve()).encode("utf-8"))
|
|
108
|
+
len_model_name = len(class_model.__name__)
|
|
109
|
+
len_full_path_leaf = len_db_root_absolut_path + len_model_name + 26
|
|
110
|
+
if len_full_path_leaf > 255:
|
|
111
|
+
excess = len_full_path_leaf - 255
|
|
112
|
+
msg = (
|
|
113
|
+
f"Model: {class_model.__name__} => The collection name is too long, "
|
|
114
|
+
+ f"it exceeds the limit of {excess} characters!"
|
|
115
|
+
)
|
|
116
|
+
raise AssertionError(msg)
|
|
117
|
+
# Create instance of Scruby
|
|
80
118
|
instance = cls()
|
|
119
|
+
# Add model class to Scruby
|
|
81
120
|
instance.__dict__["_class_model"] = class_model
|
|
82
|
-
#
|
|
83
|
-
# The zero branch is reserved for metadata.
|
|
84
|
-
branch_number: int = 0
|
|
85
|
-
branch_number_as_hash: str = f"{branch_number:08x}"[settings.HASH_REDUCE_LEFT :]
|
|
86
|
-
separated_hash: str = "/".join(list(branch_number_as_hash))
|
|
121
|
+
# Create a path for metadata.
|
|
87
122
|
meta_dir_path_tuple = (
|
|
88
123
|
settings.DB_ROOT,
|
|
89
124
|
class_model.__name__,
|
|
90
|
-
|
|
125
|
+
"meta",
|
|
91
126
|
)
|
|
92
127
|
instance.__dict__["_meta_path"] = Path(
|
|
93
128
|
*meta_dir_path_tuple,
|
|
94
129
|
"meta.json",
|
|
95
130
|
)
|
|
96
131
|
# Create metadata for collection, if missing.
|
|
97
|
-
|
|
98
|
-
if not await
|
|
99
|
-
await
|
|
132
|
+
meta_dir_path = Path(*meta_dir_path_tuple)
|
|
133
|
+
if not await meta_dir_path.exists():
|
|
134
|
+
await meta_dir_path.mkdir(parents=True)
|
|
100
135
|
meta = _Meta(
|
|
101
136
|
db_root=settings.DB_ROOT,
|
|
102
137
|
collection_name=class_model.__name__,
|
|
103
138
|
hash_reduce_left=settings.HASH_REDUCE_LEFT,
|
|
104
|
-
max_branch_number=instance.__dict__["
|
|
139
|
+
max_branch_number=instance.__dict__["_max_number_branch"],
|
|
105
140
|
counter_documents=0,
|
|
106
141
|
)
|
|
107
142
|
meta_json = meta.model_dump_json()
|
|
108
|
-
meta_path = Path(*(
|
|
143
|
+
meta_path = Path(*(meta_dir_path, "meta.json"))
|
|
109
144
|
await meta_path.write_text(meta_json, "utf-8")
|
|
110
145
|
return instance
|
|
111
146
|
|
scruby/errors.py
CHANGED
scruby/mixins/__init__.py
CHANGED
scruby/mixins/collection.py
CHANGED
scruby/mixins/count.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
2
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
#
|
|
5
4
|
"""Methods for counting the number of documents."""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -31,18 +30,19 @@ class Count:
|
|
|
31
30
|
) -> int:
|
|
32
31
|
"""Count the number of documents a matching the filter in this collection.
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
Attention:
|
|
34
|
+
- The search is based on the effect of a quantum loop.
|
|
35
|
+
- The search effectiveness depends on the number of processor threads.
|
|
36
36
|
|
|
37
37
|
Args:
|
|
38
|
-
filter_fn: A function that execute the conditions of filtering.
|
|
38
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
39
39
|
|
|
40
40
|
Returns:
|
|
41
41
|
The number of documents.
|
|
42
42
|
"""
|
|
43
43
|
# Variable initialization
|
|
44
44
|
search_task_fn: Callable = self._task_find
|
|
45
|
-
branch_numbers: range = range(
|
|
45
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
46
46
|
hash_reduce_left: int = self._hash_reduce_left
|
|
47
47
|
db_root: str = self._db_root
|
|
48
48
|
class_model: Any = self._class_model
|
scruby/mixins/custom_task.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
2
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
#
|
|
5
4
|
"""Quantum methods for running custom tasks."""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -11,64 +10,35 @@ __all__ = ("CustomTask",)
|
|
|
11
10
|
from collections.abc import Callable
|
|
12
11
|
from typing import Any
|
|
13
12
|
|
|
14
|
-
import orjson
|
|
15
|
-
from anyio import Path
|
|
16
|
-
|
|
17
13
|
|
|
18
14
|
class CustomTask:
|
|
19
15
|
"""Quantum methods for running custom tasks."""
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
) -> list[Any]:
|
|
28
|
-
"""Get documents for custom task.
|
|
29
|
-
|
|
30
|
-
This method is for internal use.
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
List of documents.
|
|
34
|
-
"""
|
|
35
|
-
branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
|
|
36
|
-
separated_hash: str = "/".join(list(branch_number_as_hash))
|
|
37
|
-
leaf_path: Path = Path(
|
|
38
|
-
*(
|
|
39
|
-
db_root,
|
|
40
|
-
class_model.__name__,
|
|
41
|
-
separated_hash,
|
|
42
|
-
"leaf.json",
|
|
43
|
-
),
|
|
44
|
-
)
|
|
45
|
-
docs: list[Any] = []
|
|
46
|
-
if await leaf_path.exists():
|
|
47
|
-
data_json: bytes = await leaf_path.read_bytes()
|
|
48
|
-
data: dict[str, str] = orjson.loads(data_json) or {}
|
|
49
|
-
for _, val in data.items():
|
|
50
|
-
docs.append(class_model.model_validate_json(val))
|
|
51
|
-
return docs
|
|
52
|
-
|
|
53
|
-
async def run_custom_task(self, custom_task_fn: Callable, limit_docs: int = 1000) -> Any:
|
|
17
|
+
async def run_custom_task(
|
|
18
|
+
self,
|
|
19
|
+
custom_task_fn: Callable,
|
|
20
|
+
filter_fn: Callable = lambda _: True,
|
|
21
|
+
**kwargs,
|
|
22
|
+
) -> Any:
|
|
54
23
|
"""Running custom task.
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
|
|
25
|
+
Attention:
|
|
26
|
+
- The search is based on the effect of a quantum loop.
|
|
27
|
+
- The search effectiveness depends on the number of processor threads.
|
|
58
28
|
|
|
59
29
|
Args:
|
|
60
|
-
custom_task_fn: A function that execute the custom task.
|
|
61
|
-
limit_docs: Limiting the number of documents. By default = 1000.
|
|
30
|
+
custom_task_fn (Callable): A function that execute the custom task.
|
|
62
31
|
|
|
63
32
|
Returns:
|
|
64
33
|
The result of a custom task.
|
|
65
34
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
35
|
+
return await custom_task_fn(
|
|
36
|
+
search_task_fn=self._task_find,
|
|
37
|
+
filter_fn=filter_fn,
|
|
38
|
+
branch_numbers=range(self._max_number_branch),
|
|
39
|
+
hash_reduce_left=self._hash_reduce_left,
|
|
40
|
+
db_root=self._db_root,
|
|
41
|
+
class_model=self._class_model,
|
|
42
|
+
max_workers=self._max_workers,
|
|
43
|
+
**kwargs,
|
|
44
|
+
)
|
scruby/mixins/delete.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
2
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
#
|
|
5
4
|
"""Methods for deleting documents."""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -64,18 +63,19 @@ class Delete:
|
|
|
64
63
|
) -> int:
|
|
65
64
|
"""Delete one or more documents matching the filter.
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
Attention:
|
|
67
|
+
- The search is based on the effect of a quantum loop.
|
|
68
|
+
- The search effectiveness depends on the number of processor threads.
|
|
69
69
|
|
|
70
70
|
Args:
|
|
71
|
-
filter_fn: A function that execute the conditions of filtering.
|
|
71
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
72
72
|
|
|
73
73
|
Returns:
|
|
74
74
|
The number of deleted documents.
|
|
75
75
|
"""
|
|
76
76
|
# Variable initialization
|
|
77
77
|
search_task_fn: Callable = self._task_delete
|
|
78
|
-
branch_numbers: range = range(
|
|
78
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
79
79
|
hash_reduce_left: int = self._hash_reduce_left
|
|
80
80
|
db_root: str = self._db_root
|
|
81
81
|
class_model: Any = self._class_model
|
scruby/mixins/find.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
2
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
#
|
|
5
4
|
"""Quantum methods for searching documents."""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -34,7 +33,6 @@ class Find:
|
|
|
34
33
|
Returns:
|
|
35
34
|
List of documents or None.
|
|
36
35
|
"""
|
|
37
|
-
# Variable initialization
|
|
38
36
|
branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
|
|
39
37
|
separated_hash: str = "/".join(list(branch_number_as_hash))
|
|
40
38
|
leaf_path: Path = Path(
|
|
@@ -61,8 +59,9 @@ class Find:
|
|
|
61
59
|
) -> Any | None:
|
|
62
60
|
"""Find one document matching the filter.
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
Attention:
|
|
63
|
+
- The search is based on the effect of a quantum loop.
|
|
64
|
+
- The search effectiveness depends on the number of processor threads.
|
|
66
65
|
|
|
67
66
|
Args:
|
|
68
67
|
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
@@ -72,7 +71,7 @@ class Find:
|
|
|
72
71
|
"""
|
|
73
72
|
# Variable initialization
|
|
74
73
|
search_task_fn: Callable = self._task_find
|
|
75
|
-
branch_numbers: range = range(
|
|
74
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
76
75
|
hash_reduce_left: int = self._hash_reduce_left
|
|
77
76
|
db_root: str = self._db_root
|
|
78
77
|
class_model: Any = self._class_model
|
|
@@ -100,14 +99,15 @@ class Find:
|
|
|
100
99
|
) -> list[Any] | None:
|
|
101
100
|
"""Find many documents matching the filter.
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
Attention:
|
|
103
|
+
- The search is based on the effect of a quantum loop.
|
|
104
|
+
- The search effectiveness depends on the number of processor threads.
|
|
105
105
|
|
|
106
106
|
Args:
|
|
107
107
|
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
108
108
|
By default it searches for all documents.
|
|
109
109
|
limit_docs (int): Limiting the number of documents. By default = 1000.
|
|
110
|
-
page_number (int): For pagination
|
|
110
|
+
page_number (int): For pagination. By default = 1.
|
|
111
111
|
Number of documents per page = limit_docs.
|
|
112
112
|
|
|
113
113
|
Returns:
|
|
@@ -117,7 +117,7 @@ class Find:
|
|
|
117
117
|
assert page_number > 0, "`find_many` => The `page_number` parameter must not be less than one."
|
|
118
118
|
# Variable initialization
|
|
119
119
|
search_task_fn: Callable = self._task_find
|
|
120
|
-
branch_numbers: range = range(
|
|
120
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
121
121
|
hash_reduce_left: int = self._hash_reduce_left
|
|
122
122
|
db_root: str = self._db_root
|
|
123
123
|
class_model: Any = self._class_model
|
scruby/mixins/keys.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
2
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
#
|
|
5
4
|
"""Methods for working with keys."""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -9,7 +8,9 @@ from __future__ import annotations
|
|
|
9
8
|
__all__ = ("Keys",)
|
|
10
9
|
|
|
11
10
|
import logging
|
|
11
|
+
from datetime import datetime
|
|
12
12
|
from typing import Any
|
|
13
|
+
from zoneinfo import ZoneInfo
|
|
13
14
|
|
|
14
15
|
import orjson
|
|
15
16
|
|
|
@@ -26,7 +27,7 @@ class Keys:
|
|
|
26
27
|
"""Asynchronous method for adding document to collection.
|
|
27
28
|
|
|
28
29
|
Args:
|
|
29
|
-
doc: Value of key. Type, derived from `
|
|
30
|
+
doc (Any): Value of key. Type, derived from `ScrubyModel`.
|
|
30
31
|
|
|
31
32
|
Returns:
|
|
32
33
|
None.
|
|
@@ -42,6 +43,11 @@ class Keys:
|
|
|
42
43
|
raise TypeError(msg)
|
|
43
44
|
# The path to cell of collection.
|
|
44
45
|
leaf_path, prepared_key = await self._get_leaf_path(doc.key)
|
|
46
|
+
# Init a `created_at` and `updated_at` fields
|
|
47
|
+
tz = ZoneInfo("UTC")
|
|
48
|
+
doc.created_at = datetime.now(tz)
|
|
49
|
+
doc.updated_at = datetime.now(tz)
|
|
50
|
+
# Convert doc to json
|
|
45
51
|
doc_json: str = doc.model_dump_json()
|
|
46
52
|
# Write key-value to collection.
|
|
47
53
|
if await leaf_path.exists():
|
|
@@ -63,10 +69,10 @@ class Keys:
|
|
|
63
69
|
await self._counter_documents(1)
|
|
64
70
|
|
|
65
71
|
async def update_doc(self, doc: Any) -> None:
|
|
66
|
-
"""Asynchronous method for updating
|
|
72
|
+
"""Asynchronous method for updating document to collection.
|
|
67
73
|
|
|
68
74
|
Args:
|
|
69
|
-
doc: Value of key. Type `
|
|
75
|
+
doc (Any): Value of key. Type `ScrubyModel`.
|
|
70
76
|
|
|
71
77
|
Returns:
|
|
72
78
|
None.
|
|
@@ -83,6 +89,9 @@ class Keys:
|
|
|
83
89
|
raise TypeError(msg)
|
|
84
90
|
# The path to cell of collection.
|
|
85
91
|
leaf_path, prepared_key = await self._get_leaf_path(doc.key)
|
|
92
|
+
# Update a `updated_at` field
|
|
93
|
+
doc.updated_at = datetime.now(ZoneInfo("UTC"))
|
|
94
|
+
# Convert doc to json
|
|
86
95
|
doc_json: str = doc.model_dump_json()
|
|
87
96
|
# Update the existing key.
|
|
88
97
|
if await leaf_path.exists():
|
|
@@ -98,14 +107,15 @@ class Keys:
|
|
|
98
107
|
logging.error(err.message)
|
|
99
108
|
raise err from None
|
|
100
109
|
else:
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
msg: str = f"`update_doc` - The key `{doc.key}` is missing!"
|
|
111
|
+
logging.error(msg)
|
|
112
|
+
raise KeyError(msg)
|
|
103
113
|
|
|
104
|
-
async def
|
|
105
|
-
"""Asynchronous method for getting
|
|
114
|
+
async def get_doc(self, key: str) -> Any:
|
|
115
|
+
"""Asynchronous method for getting document from collection the by key.
|
|
106
116
|
|
|
107
117
|
Args:
|
|
108
|
-
key: Key name.
|
|
118
|
+
key (str): Key name.
|
|
109
119
|
|
|
110
120
|
Returns:
|
|
111
121
|
Value of key or KeyError.
|
|
@@ -118,15 +128,15 @@ class Keys:
|
|
|
118
128
|
data: dict = orjson.loads(data_json) or {}
|
|
119
129
|
obj: Any = self._class_model.model_validate_json(data[prepared_key])
|
|
120
130
|
return obj
|
|
121
|
-
msg: str = "`
|
|
131
|
+
msg: str = f"`get_doc` - The key `{key}` is missing!"
|
|
122
132
|
logging.error(msg)
|
|
123
|
-
raise KeyError()
|
|
133
|
+
raise KeyError(msg)
|
|
124
134
|
|
|
125
135
|
async def has_key(self, key: str) -> bool:
|
|
126
136
|
"""Asynchronous method for checking presence of key in collection.
|
|
127
137
|
|
|
128
138
|
Args:
|
|
129
|
-
key: Key name.
|
|
139
|
+
key (str): Key name.
|
|
130
140
|
|
|
131
141
|
Returns:
|
|
132
142
|
True, if the key is present.
|
|
@@ -144,11 +154,11 @@ class Keys:
|
|
|
144
154
|
return False
|
|
145
155
|
return False
|
|
146
156
|
|
|
147
|
-
async def
|
|
148
|
-
"""Asynchronous method for deleting
|
|
157
|
+
async def delete_doc(self, key: str) -> None:
|
|
158
|
+
"""Asynchronous method for deleting document from collection the by key.
|
|
149
159
|
|
|
150
160
|
Args:
|
|
151
|
-
key: Key name.
|
|
161
|
+
key (str): Key name.
|
|
152
162
|
|
|
153
163
|
Returns:
|
|
154
164
|
None.
|
|
@@ -163,6 +173,6 @@ class Keys:
|
|
|
163
173
|
await leaf_path.write_bytes(orjson.dumps(data))
|
|
164
174
|
await self._counter_documents(-1)
|
|
165
175
|
return
|
|
166
|
-
msg: str = "`
|
|
176
|
+
msg: str = f"`delete_doc` - The key `{key}` is missing!"
|
|
167
177
|
logging.error(msg)
|
|
168
|
-
raise KeyError()
|
|
178
|
+
raise KeyError(msg)
|
scruby/mixins/update.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Scruby - Asynchronous library for building and managing a hybrid database, by scheme of key-value.
|
|
2
2
|
# Copyright (c) 2025 Gennady Kostyunin
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
#
|
|
5
4
|
"""Methods for updating documents."""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -62,24 +61,27 @@ class Update:
|
|
|
62
61
|
|
|
63
62
|
async def update_many(
|
|
64
63
|
self,
|
|
65
|
-
filter_fn: Callable,
|
|
66
64
|
new_data: dict[str, Any],
|
|
65
|
+
filter_fn: Callable = lambda _: True,
|
|
67
66
|
) -> int:
|
|
68
|
-
"""Updates
|
|
67
|
+
"""Updates many documents matching the filter.
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
Attention:
|
|
70
|
+
- For a complex case, a custom task may be needed.
|
|
71
|
+
- See documentation on creating custom tasks.
|
|
72
|
+
- The search is based on the effect of a quantum loop.
|
|
73
|
+
- The search effectiveness depends on the number of processor threads.
|
|
72
74
|
|
|
73
75
|
Args:
|
|
74
|
-
filter_fn: A function that execute the conditions of filtering.
|
|
75
|
-
new_data: New data for the fields that need to be updated.
|
|
76
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
77
|
+
new_data (dict[str, Any]): New data for the fields that need to be updated.
|
|
76
78
|
|
|
77
79
|
Returns:
|
|
78
80
|
The number of updated documents.
|
|
79
81
|
"""
|
|
80
82
|
# Variable initialization
|
|
81
83
|
update_task_fn: Callable = self._task_update
|
|
82
|
-
branch_numbers: range = range(
|
|
84
|
+
branch_numbers: range = range(self._max_number_branch)
|
|
83
85
|
hash_reduce_left: int = self._hash_reduce_left
|
|
84
86
|
db_root: str = self._db_root
|
|
85
87
|
class_model: Any = self._class_model
|
scruby/settings.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scruby
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.0
|
|
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
|
|
@@ -99,6 +99,15 @@ Online browsable documentation is available at [https://kebasyaty.github.io/scru
|
|
|
99
99
|
uv add scruby
|
|
100
100
|
```
|
|
101
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
|
+
|
|
102
111
|
## Usage
|
|
103
112
|
|
|
104
113
|
See more examples here [https://kebasyaty.github.io/scruby/latest/pages/usage/](https://kebasyaty.github.io/scruby/latest/pages/usage/ "Examples").
|
|
@@ -107,24 +116,25 @@ See more examples here [https://kebasyaty.github.io/scruby/latest/pages/usage/](
|
|
|
107
116
|
"""Working with keys."""
|
|
108
117
|
|
|
109
118
|
import anyio
|
|
110
|
-
import datetime
|
|
119
|
+
from datetime import datetime
|
|
120
|
+
from zoneinfo import ZoneInfo
|
|
111
121
|
from typing import Annotated
|
|
112
|
-
from pydantic import
|
|
122
|
+
from pydantic import EmailStr, Field
|
|
113
123
|
from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator
|
|
114
|
-
from scruby import Scruby, settings
|
|
124
|
+
from scruby import Scruby, ScrubyModel, settings
|
|
115
125
|
|
|
116
126
|
settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
|
|
117
127
|
settings.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
118
128
|
settings.MAX_WORKERS = None # By default = None
|
|
119
129
|
|
|
120
|
-
class User(
|
|
130
|
+
class User(ScrubyModel):
|
|
121
131
|
"""User model."""
|
|
122
132
|
first_name: str = Field(strict=True)
|
|
123
133
|
last_name: str = Field(strict=True)
|
|
124
|
-
birthday: datetime
|
|
134
|
+
birthday: datetime = Field(strict=True)
|
|
125
135
|
email: EmailStr = Field(strict=True)
|
|
126
136
|
phone: Annotated[PhoneNumber, PhoneNumberValidator(number_format="E164")] = Field(frozen=True)
|
|
127
|
-
#
|
|
137
|
+
# key is always at bottom
|
|
128
138
|
key: str = Field(
|
|
129
139
|
strict=True,
|
|
130
140
|
frozen=True,
|
|
@@ -140,7 +150,7 @@ async def main() -> None:
|
|
|
140
150
|
user = User(
|
|
141
151
|
first_name="John",
|
|
142
152
|
last_name="Smith",
|
|
143
|
-
birthday=datetime
|
|
153
|
+
birthday=datetime(1970, 1, 1, tzinfo=ZoneInfo("UTC")),
|
|
144
154
|
email="John_Smith@gmail.com",
|
|
145
155
|
phone="+447986123456",
|
|
146
156
|
)
|
|
@@ -149,15 +159,15 @@ async def main() -> None:
|
|
|
149
159
|
|
|
150
160
|
await user_coll.update_doc(user)
|
|
151
161
|
|
|
152
|
-
await user_coll.
|
|
153
|
-
await user_coll.
|
|
162
|
+
await user_coll.get_doc("+447986123456") # => user
|
|
163
|
+
await user_coll.get_doc("key missing") # => KeyError
|
|
154
164
|
|
|
155
165
|
await user_coll.has_key("+447986123456") # => True
|
|
156
166
|
await user_coll.has_key("key missing") # => False
|
|
157
167
|
|
|
158
|
-
await user_coll.
|
|
159
|
-
await user_coll.
|
|
160
|
-
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
|
|
161
171
|
|
|
162
172
|
# Full database deletion.
|
|
163
173
|
# Hint: The main purpose is tests.
|
|
@@ -176,10 +186,9 @@ The search effectiveness depends on the number of processor threads.
|
|
|
176
186
|
"""
|
|
177
187
|
|
|
178
188
|
import anyio
|
|
179
|
-
import datetime
|
|
180
189
|
from typing import Annotated
|
|
181
|
-
from pydantic import
|
|
182
|
-
from scruby import Scruby, settings
|
|
190
|
+
from pydantic import Field
|
|
191
|
+
from scruby import Scruby, ScrubyModel, settings
|
|
183
192
|
from pprint import pprint as pp
|
|
184
193
|
|
|
185
194
|
settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
|
|
@@ -187,13 +196,13 @@ settings.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
|
187
196
|
settings.MAX_WORKERS = None # By default = None
|
|
188
197
|
|
|
189
198
|
|
|
190
|
-
class Phone(
|
|
199
|
+
class Phone(ScrubyModel):
|
|
191
200
|
"""Phone model."""
|
|
192
201
|
brand: str = Field(strict=True, frozen=True)
|
|
193
202
|
model: str = Field(strict=True, frozen=True)
|
|
194
203
|
screen_diagonal: float = Field(strict=True)
|
|
195
204
|
matrix_type: str = Field(strict=True)
|
|
196
|
-
#
|
|
205
|
+
# key is always at bottom
|
|
197
206
|
key: str = Field(
|
|
198
207
|
strict=True,
|
|
199
208
|
frozen=True,
|
|
@@ -252,10 +261,9 @@ The search effectiveness depends on the number of processor threads.
|
|
|
252
261
|
"""
|
|
253
262
|
|
|
254
263
|
import anyio
|
|
255
|
-
import datetime
|
|
256
264
|
from typing import Annotated
|
|
257
|
-
from pydantic import
|
|
258
|
-
from scruby import Scruby, settings
|
|
265
|
+
from pydantic import Field
|
|
266
|
+
from scruby import Scruby, ScrubyModel, settings
|
|
259
267
|
from pprint import pprint as pp
|
|
260
268
|
|
|
261
269
|
settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
|
|
@@ -263,13 +271,13 @@ settings.HASH_REDUCE_LEFT = 6 # By default = 6
|
|
|
263
271
|
settings.MAX_WORKERS = None # By default = None
|
|
264
272
|
|
|
265
273
|
|
|
266
|
-
class Car(
|
|
274
|
+
class Car(ScrubyModel):
|
|
267
275
|
"""Car model."""
|
|
268
276
|
brand: str = Field(strict=True, frozen=True)
|
|
269
277
|
model: str = Field(strict=True, frozen=True)
|
|
270
278
|
year: int = Field(strict=True)
|
|
271
279
|
power_reserve: int = Field(strict=True)
|
|
272
|
-
#
|
|
280
|
+
# key is always at bottom
|
|
273
281
|
key: str = Field(
|
|
274
282
|
strict=True,
|
|
275
283
|
frozen=True,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
scruby/__init__.py,sha256=m5VkmOTqVzfHsLb7eIfGTdBpWPwEayrRDpAilUuksGc,1042
|
|
2
|
+
scruby/aggregation.py,sha256=ooDN2KShP_JwIpIqxz7wL6gtBdd4oCSFk_3BhnD2o50,3489
|
|
3
|
+
scruby/db.py,sha256=EVrMfHYWZr9ZEyU7b2fMBqSKeh0fEJnjbihQhMNVJak,8374
|
|
4
|
+
scruby/errors.py,sha256=F51l5eyJuuMrwPeiIy6IGc1GqMsrGuS6l_B0e0Y6xWs,1295
|
|
5
|
+
scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
scruby/settings.py,sha256=g2AYH8QorjoTYxMceWotFL9X3F2mQnKDl9dlgmSLa5c,1578
|
|
7
|
+
scruby/mixins/__init__.py,sha256=9geOyHcfWV4hilYs76whiEUOK2aZtgeC0fg2K0qvUOk,625
|
|
8
|
+
scruby/mixins/collection.py,sha256=SZ2yvPPPPu_Cg9pfqu_RM0YB6hkhikwmuE13sATeUtE,1380
|
|
9
|
+
scruby/mixins/count.py,sha256=gYX7ttHjHINZjSWa6FjzMZnqwEQO9kj5Ehl7Hkcs-2E,2087
|
|
10
|
+
scruby/mixins/custom_task.py,sha256=tHZSo6IdZL5Yu46S-c7Ac3ImxGhwzPoQm4Dp4O8bqLs,1320
|
|
11
|
+
scruby/mixins/delete.py,sha256=ng0xXM9mKQy5iq04WCyDm2AseETzEV7PgQOeDzUe-uU,3083
|
|
12
|
+
scruby/mixins/find.py,sha256=5X0PNvLtsPq8s9J-lNFrhT9Pmqn4qSYUmSsV9yCl3VE,5353
|
|
13
|
+
scruby/mixins/keys.py,sha256=ybCHefGgxXNCYMKxgRhlXXW1CLcv2BGKff3S8E1y2JI,6177
|
|
14
|
+
scruby/mixins/update.py,sha256=PWrF1lvuMOHIlsrfXMB6jfCFEGbpqXor-CzH5X0b3VE,3414
|
|
15
|
+
scruby-0.31.0.dist-info/METADATA,sha256=f1Ad9XPgW1euUdbxe0SbxpKQ0qjiybSMADCu_e4QxPw,10963
|
|
16
|
+
scruby-0.31.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
scruby-0.31.0.dist-info/licenses/LICENSE,sha256=mS0Wz0yGNB63gEcWEnuIb_lldDYV0sjRaO-o_GL6CWE,1074
|
|
18
|
+
scruby-0.31.0.dist-info/RECORD,,
|
scruby-0.28.3.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
scruby/__init__.py,sha256=bw2Le5ULYlf2nFQT_617rmEumu66Ll-QCLCxqDFERWw,1014
|
|
2
|
-
scruby/aggregation.py,sha256=bd70J1Xye6faNHD8LS3lVQoHWKtPdPV_cqT_i7oui38,3491
|
|
3
|
-
scruby/db.py,sha256=djo4JkfuKCcV3jRbd2L3mIwENS3ptqJBt7SlAuiRhGY,6794
|
|
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=PGzRtgLvseQnHg6wt-A81s30pnsdY1d8cr-EQRnbfyU,2050
|
|
10
|
-
scruby/mixins/custom_task.py,sha256=ZhvCDiYnJ8BTIWlnRu6cTH-9G9o7dHSixjMIsxAtDpw,2316
|
|
11
|
-
scruby/mixins/delete.py,sha256=B2loiowj8ToO0euumDRxpHUVrLQx0iTcRys0jszn-rA,3046
|
|
12
|
-
scruby/mixins/find.py,sha256=gnHjnm0MZbzMHmWOBUJbMm8LZFBqdJ6yWA6Pxfap51Q,5340
|
|
13
|
-
scruby/mixins/keys.py,sha256=OUByWbHfNVWJVkUrhCsJZdVqf0zez_an6Gti2n5iKnM,5671
|
|
14
|
-
scruby/mixins/update.py,sha256=YkUiz1gcVtNXdgf7Mmda-0g3vJm3jL09v-msGy2tAWg,3229
|
|
15
|
-
scruby-0.28.3.dist-info/METADATA,sha256=DoDzTEj_wrZu3EfAhBcKBtCfbr9pb74FS3spyDpFfzY,10849
|
|
16
|
-
scruby-0.28.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
-
scruby-0.28.3.dist-info/licenses/LICENSE,sha256=mS0Wz0yGNB63gEcWEnuIb_lldDYV0sjRaO-o_GL6CWE,1074
|
|
18
|
-
scruby-0.28.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|