scruby 0.9.3__py3-none-any.whl → 0.27.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.
- scruby/__init__.py +37 -29
- scruby/aggregation.py +152 -0
- scruby/db.py +213 -301
- scruby/errors.py +45 -0
- scruby/mixins/__init__.py +25 -0
- scruby/mixins/collection.py +50 -0
- scruby/mixins/count.py +62 -0
- scruby/mixins/custom_task.py +75 -0
- scruby/mixins/delete.py +96 -0
- scruby/mixins/docs.py +168 -0
- scruby/mixins/find.py +149 -0
- scruby/mixins/update.py +99 -0
- scruby/settings.py +44 -0
- {scruby-0.9.3.dist-info → scruby-0.27.2.dist-info}/METADATA +138 -112
- scruby-0.27.2.dist-info/RECORD +18 -0
- {scruby-0.9.3.dist-info → scruby-0.27.2.dist-info}/WHEEL +1 -1
- {scruby-0.9.3.dist-info → scruby-0.27.2.dist-info}/licenses/LICENSE +21 -21
- scruby/constants.py +0 -31
- scruby-0.9.3.dist-info/RECORD +0 -8
scruby/mixins/count.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
"""Methods for counting the number of documents."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = ("Count",)
|
|
10
|
+
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Count:
|
|
17
|
+
"""Methods for counting the number of documents."""
|
|
18
|
+
|
|
19
|
+
async def estimated_document_count(self) -> int:
|
|
20
|
+
"""Get an estimate of the number of documents in this collection using collection metadata.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The number of documents.
|
|
24
|
+
"""
|
|
25
|
+
meta = await self.get_meta()
|
|
26
|
+
return meta.counter_documents
|
|
27
|
+
|
|
28
|
+
async def count_documents(
|
|
29
|
+
self,
|
|
30
|
+
filter_fn: Callable,
|
|
31
|
+
) -> int:
|
|
32
|
+
"""Count the number of documents a matching the filter in this collection.
|
|
33
|
+
|
|
34
|
+
The search is based on the effect of a quantum loop.
|
|
35
|
+
The search effectiveness depends on the number of processor threads.
|
|
36
|
+
Ideally, hundreds and even thousands of threads are required.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
filter_fn: A function that execute the conditions of filtering.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The number of documents.
|
|
43
|
+
"""
|
|
44
|
+
branch_numbers: range = range(1, self._max_branch_number)
|
|
45
|
+
search_task_fn: Callable = self._task_find
|
|
46
|
+
hash_reduce_left: int = self._hash_reduce_left
|
|
47
|
+
db_root: str = self._db_root
|
|
48
|
+
class_model: Any = self._class_model
|
|
49
|
+
counter: int = 0
|
|
50
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
51
|
+
for branch_number in branch_numbers:
|
|
52
|
+
future = executor.submit(
|
|
53
|
+
search_task_fn,
|
|
54
|
+
branch_number,
|
|
55
|
+
filter_fn,
|
|
56
|
+
hash_reduce_left,
|
|
57
|
+
db_root,
|
|
58
|
+
class_model,
|
|
59
|
+
)
|
|
60
|
+
if await future.result() is not None:
|
|
61
|
+
counter += 1
|
|
62
|
+
return counter
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
"""Quantum methods for running custom tasks."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = ("CustomTask",)
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import orjson
|
|
15
|
+
from anyio import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CustomTask:
|
|
19
|
+
"""Quantum methods for running custom tasks."""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
async def _task_get_docs(
|
|
23
|
+
branch_number: int,
|
|
24
|
+
hash_reduce_left: int,
|
|
25
|
+
db_root: str,
|
|
26
|
+
class_model: Any,
|
|
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:
|
|
54
|
+
"""Running custom task.
|
|
55
|
+
|
|
56
|
+
This method running a task created on the basis of a quantum loop.
|
|
57
|
+
Effectiveness running task depends on the number of processor threads.
|
|
58
|
+
Ideally, hundreds and even thousands of threads are required.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
custom_task_fn: A function that execute the custom task.
|
|
62
|
+
limit_docs: Limiting the number of documents. By default = 1000.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The result of a custom task.
|
|
66
|
+
"""
|
|
67
|
+
kwargs = {
|
|
68
|
+
"get_docs_fn": self._task_get_docs,
|
|
69
|
+
"branch_numbers": range(1, self._max_branch_number),
|
|
70
|
+
"hash_reduce_left": self._hash_reduce_left,
|
|
71
|
+
"db_root": self._db_root,
|
|
72
|
+
"class_model": self._class_model,
|
|
73
|
+
"limit_docs": limit_docs,
|
|
74
|
+
}
|
|
75
|
+
return await custom_task_fn(**kwargs)
|
scruby/mixins/delete.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
"""Methods for deleting documents."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = ("Delete",)
|
|
10
|
+
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import orjson
|
|
16
|
+
from anyio import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Delete:
|
|
20
|
+
"""Methods for deleting documents."""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
async def _task_delete(
|
|
24
|
+
branch_number: int,
|
|
25
|
+
filter_fn: Callable,
|
|
26
|
+
hash_reduce_left: int,
|
|
27
|
+
db_root: str,
|
|
28
|
+
class_model: Any,
|
|
29
|
+
) -> int:
|
|
30
|
+
"""Task for find and delete documents.
|
|
31
|
+
|
|
32
|
+
This method is for internal use.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The number of deleted documents.
|
|
36
|
+
"""
|
|
37
|
+
branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
|
|
38
|
+
separated_hash: str = "/".join(list(branch_number_as_hash))
|
|
39
|
+
leaf_path: Path = Path(
|
|
40
|
+
*(
|
|
41
|
+
db_root,
|
|
42
|
+
class_model.__name__,
|
|
43
|
+
separated_hash,
|
|
44
|
+
"leaf.json",
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
counter: int = 0
|
|
48
|
+
if await leaf_path.exists():
|
|
49
|
+
data_json: bytes = await leaf_path.read_bytes()
|
|
50
|
+
data: dict[str, str] = orjson.loads(data_json) or {}
|
|
51
|
+
new_state: dict[str, str] = {}
|
|
52
|
+
for key, val in data.items():
|
|
53
|
+
doc = class_model.model_validate_json(val)
|
|
54
|
+
if filter_fn(doc):
|
|
55
|
+
counter -= 1
|
|
56
|
+
else:
|
|
57
|
+
new_state[key] = val
|
|
58
|
+
await leaf_path.write_bytes(orjson.dumps(new_state))
|
|
59
|
+
return counter
|
|
60
|
+
|
|
61
|
+
async def delete_many(
|
|
62
|
+
self,
|
|
63
|
+
filter_fn: Callable,
|
|
64
|
+
) -> int:
|
|
65
|
+
"""Delete one or more documents matching the filter.
|
|
66
|
+
|
|
67
|
+
The search is based on the effect of a quantum loop.
|
|
68
|
+
The search effectiveness depends on the number of processor threads.
|
|
69
|
+
Ideally, hundreds and even thousands of threads are required.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
filter_fn: A function that execute the conditions of filtering.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The number of deleted documents.
|
|
76
|
+
"""
|
|
77
|
+
branch_numbers: range = range(1, self._max_branch_number)
|
|
78
|
+
search_task_fn: Callable = self._task_delete
|
|
79
|
+
hash_reduce_left: int = self._hash_reduce_left
|
|
80
|
+
db_root: str = self._db_root
|
|
81
|
+
class_model: Any = self._class_model
|
|
82
|
+
counter: int = 0
|
|
83
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
84
|
+
for branch_number in branch_numbers:
|
|
85
|
+
future = executor.submit(
|
|
86
|
+
search_task_fn,
|
|
87
|
+
branch_number,
|
|
88
|
+
filter_fn,
|
|
89
|
+
hash_reduce_left,
|
|
90
|
+
db_root,
|
|
91
|
+
class_model,
|
|
92
|
+
)
|
|
93
|
+
counter += await future.result()
|
|
94
|
+
if counter < 0:
|
|
95
|
+
await self._counter_documents(counter)
|
|
96
|
+
return abs(counter)
|
scruby/mixins/docs.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
"""Methods for working with keys."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = ("Keys",)
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import orjson
|
|
15
|
+
|
|
16
|
+
from scruby.errors import (
|
|
17
|
+
KeyAlreadyExistsError,
|
|
18
|
+
KeyNotExistsError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Keys:
|
|
23
|
+
"""Methods for working with keys."""
|
|
24
|
+
|
|
25
|
+
async def add_doc(self, doc: Any) -> None:
|
|
26
|
+
"""Asynchronous method for adding document to collection.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
doc: Value of key. Type, derived from `BaseModel`.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
None.
|
|
33
|
+
"""
|
|
34
|
+
# Check if the Model matches the collection
|
|
35
|
+
if not isinstance(doc, self._class_model):
|
|
36
|
+
doc_class_name = doc.__class__.__name__
|
|
37
|
+
collection_name = self._class_model.__name__
|
|
38
|
+
msg = (
|
|
39
|
+
f"(add_doc) Parameter `doc` => Model `{doc_class_name}` does not match collection `{collection_name}`!"
|
|
40
|
+
)
|
|
41
|
+
logging.error(msg)
|
|
42
|
+
raise TypeError(msg)
|
|
43
|
+
# The path to cell of collection.
|
|
44
|
+
leaf_path, prepared_key = await self._get_leaf_path(doc.key)
|
|
45
|
+
doc_json: str = doc.model_dump_json()
|
|
46
|
+
# Write key-value to collection.
|
|
47
|
+
if await leaf_path.exists():
|
|
48
|
+
# Add new key.
|
|
49
|
+
data_json: bytes = await leaf_path.read_bytes()
|
|
50
|
+
data: dict = orjson.loads(data_json) or {}
|
|
51
|
+
try:
|
|
52
|
+
data[prepared_key]
|
|
53
|
+
except KeyError:
|
|
54
|
+
data[prepared_key] = doc_json
|
|
55
|
+
await leaf_path.write_bytes(orjson.dumps(data))
|
|
56
|
+
else:
|
|
57
|
+
err = KeyAlreadyExistsError()
|
|
58
|
+
logging.error(err.message)
|
|
59
|
+
raise err
|
|
60
|
+
else:
|
|
61
|
+
# Add new document to a blank leaf.
|
|
62
|
+
await leaf_path.write_bytes(orjson.dumps({prepared_key: doc_json}))
|
|
63
|
+
await self._counter_documents(1)
|
|
64
|
+
|
|
65
|
+
async def update_doc(self, doc: Any) -> None:
|
|
66
|
+
"""Asynchronous method for updating key to collection.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
doc: Value of key. Type `BaseModel`.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
None.
|
|
73
|
+
"""
|
|
74
|
+
# Check if the Model matches the collection
|
|
75
|
+
if not isinstance(doc, self._class_model):
|
|
76
|
+
doc_class_name = doc.__class__.__name__
|
|
77
|
+
collection_name = self._class_model.__name__
|
|
78
|
+
msg = (
|
|
79
|
+
f"(update_doc) Parameter `doc` => Model `{doc_class_name}` "
|
|
80
|
+
f"does not match collection `{collection_name}`!"
|
|
81
|
+
)
|
|
82
|
+
logging.error(msg)
|
|
83
|
+
raise TypeError(msg)
|
|
84
|
+
# The path to cell of collection.
|
|
85
|
+
leaf_path, prepared_key = await self._get_leaf_path(doc.key)
|
|
86
|
+
doc_json: str = doc.model_dump_json()
|
|
87
|
+
# Update the existing key.
|
|
88
|
+
if await leaf_path.exists():
|
|
89
|
+
# Update the existing key.
|
|
90
|
+
data_json: bytes = await leaf_path.read_bytes()
|
|
91
|
+
data: dict = orjson.loads(data_json) or {}
|
|
92
|
+
try:
|
|
93
|
+
data[prepared_key]
|
|
94
|
+
data[prepared_key] = doc_json
|
|
95
|
+
await leaf_path.write_bytes(orjson.dumps(data))
|
|
96
|
+
except KeyError:
|
|
97
|
+
err = KeyNotExistsError()
|
|
98
|
+
logging.error(err.message)
|
|
99
|
+
raise err from None
|
|
100
|
+
else:
|
|
101
|
+
logging.error("The key not exists.")
|
|
102
|
+
raise KeyError()
|
|
103
|
+
|
|
104
|
+
async def get_key(self, key: str) -> Any:
|
|
105
|
+
"""Asynchronous method for getting value of key from collection.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
key: Key name.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Value of key or KeyError.
|
|
112
|
+
"""
|
|
113
|
+
# The path to the database cell.
|
|
114
|
+
leaf_path, prepared_key = await self._get_leaf_path(key)
|
|
115
|
+
# Get value of key.
|
|
116
|
+
if await leaf_path.exists():
|
|
117
|
+
data_json: bytes = await leaf_path.read_bytes()
|
|
118
|
+
data: dict = orjson.loads(data_json) or {}
|
|
119
|
+
obj: Any = self._class_model.model_validate_json(data[prepared_key])
|
|
120
|
+
return obj
|
|
121
|
+
msg: str = "`get_key` - The unacceptable key value."
|
|
122
|
+
logging.error(msg)
|
|
123
|
+
raise KeyError()
|
|
124
|
+
|
|
125
|
+
async def has_key(self, key: str) -> bool:
|
|
126
|
+
"""Asynchronous method for checking presence of key in collection.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
key: Key name.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True, if the key is present.
|
|
133
|
+
"""
|
|
134
|
+
# Get path to cell of collection.
|
|
135
|
+
leaf_path, prepared_key = await self._get_leaf_path(key)
|
|
136
|
+
# Checking whether there is a key.
|
|
137
|
+
if await leaf_path.exists():
|
|
138
|
+
data_json: bytes = await leaf_path.read_bytes()
|
|
139
|
+
data: dict = orjson.loads(data_json) or {}
|
|
140
|
+
try:
|
|
141
|
+
data[prepared_key]
|
|
142
|
+
return True
|
|
143
|
+
except KeyError:
|
|
144
|
+
return False
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
async def delete_key(self, key: str) -> None:
|
|
148
|
+
"""Asynchronous method for deleting key from collection.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
key: Key name.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
None.
|
|
155
|
+
"""
|
|
156
|
+
# The path to the database cell.
|
|
157
|
+
leaf_path, prepared_key = await self._get_leaf_path(key)
|
|
158
|
+
# Deleting key.
|
|
159
|
+
if await leaf_path.exists():
|
|
160
|
+
data_json: bytes = await leaf_path.read_bytes()
|
|
161
|
+
data: dict = orjson.loads(data_json) or {}
|
|
162
|
+
del data[prepared_key]
|
|
163
|
+
await leaf_path.write_bytes(orjson.dumps(data))
|
|
164
|
+
await self._counter_documents(-1)
|
|
165
|
+
return
|
|
166
|
+
msg: str = "`delete_key` - The unacceptable key value."
|
|
167
|
+
logging.error(msg)
|
|
168
|
+
raise KeyError()
|
scruby/mixins/find.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
"""Quantum methods for searching documents."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = ("Find",)
|
|
10
|
+
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import orjson
|
|
16
|
+
from anyio import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Find:
|
|
20
|
+
"""Quantum methods for searching documents."""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
async def _task_find(
|
|
24
|
+
branch_number: int,
|
|
25
|
+
filter_fn: Callable,
|
|
26
|
+
hash_reduce_left: str,
|
|
27
|
+
db_root: str,
|
|
28
|
+
class_model: Any,
|
|
29
|
+
filter_is_checking: bool = True,
|
|
30
|
+
) -> list[Any] | None:
|
|
31
|
+
"""Task for find documents.
|
|
32
|
+
|
|
33
|
+
This method is for internal use.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of documents or None.
|
|
37
|
+
"""
|
|
38
|
+
branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
|
|
39
|
+
separated_hash: str = "/".join(list(branch_number_as_hash))
|
|
40
|
+
leaf_path: Path = Path(
|
|
41
|
+
*(
|
|
42
|
+
db_root,
|
|
43
|
+
class_model.__name__,
|
|
44
|
+
separated_hash,
|
|
45
|
+
"leaf.json",
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
docs: list[Any] = []
|
|
49
|
+
if await leaf_path.exists():
|
|
50
|
+
data_json: bytes = await leaf_path.read_bytes()
|
|
51
|
+
data: dict[str, str] = orjson.loads(data_json) or {}
|
|
52
|
+
for _, val in data.items():
|
|
53
|
+
doc = class_model.model_validate_json(val)
|
|
54
|
+
if not filter_is_checking or filter_fn(doc):
|
|
55
|
+
docs.append(doc)
|
|
56
|
+
return docs or None
|
|
57
|
+
|
|
58
|
+
async def find_one(
|
|
59
|
+
self,
|
|
60
|
+
filter_fn: Callable,
|
|
61
|
+
) -> Any | None:
|
|
62
|
+
"""Finds a single document matching the filter.
|
|
63
|
+
|
|
64
|
+
The search is based on the effect of a quantum loop.
|
|
65
|
+
The search effectiveness depends on the number of processor threads.
|
|
66
|
+
Ideally, hundreds and even thousands of threads are required.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Document or None.
|
|
73
|
+
"""
|
|
74
|
+
branch_numbers: range = range(1, self._max_branch_number)
|
|
75
|
+
search_task_fn: Callable = self._task_find
|
|
76
|
+
hash_reduce_left: int = self._hash_reduce_left
|
|
77
|
+
db_root: str = self._db_root
|
|
78
|
+
class_model: Any = self._class_model
|
|
79
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
80
|
+
for branch_number in branch_numbers:
|
|
81
|
+
future = executor.submit(
|
|
82
|
+
search_task_fn,
|
|
83
|
+
branch_number,
|
|
84
|
+
filter_fn,
|
|
85
|
+
hash_reduce_left,
|
|
86
|
+
db_root,
|
|
87
|
+
class_model,
|
|
88
|
+
)
|
|
89
|
+
docs = await future.result()
|
|
90
|
+
if docs is not None:
|
|
91
|
+
return docs[0]
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
async def find_many(
|
|
95
|
+
self,
|
|
96
|
+
filter_fn: Callable = lambda _: True,
|
|
97
|
+
limit_docs: int = 1000,
|
|
98
|
+
page_number: int = 1,
|
|
99
|
+
) -> list[Any] | None:
|
|
100
|
+
"""Finds one or more documents matching the filter.
|
|
101
|
+
|
|
102
|
+
The search is based on the effect of a quantum loop.
|
|
103
|
+
The search effectiveness depends on the number of processor threads.
|
|
104
|
+
Ideally, hundreds and even thousands of threads are required.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
filter_fn (Callable): A function that execute the conditions of filtering.
|
|
108
|
+
By default it searches for all documents.
|
|
109
|
+
limit_docs (int): Limiting the number of documents. By default = 1000.
|
|
110
|
+
page_number (int): For pagination output. By default = 1.
|
|
111
|
+
Number of documents per page = limit_docs.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of documents or None.
|
|
115
|
+
"""
|
|
116
|
+
branch_numbers: range = range(1, self._max_branch_number)
|
|
117
|
+
search_task_fn: Callable = self._task_find
|
|
118
|
+
hash_reduce_left: int = self._hash_reduce_left
|
|
119
|
+
db_root: str = self._db_root
|
|
120
|
+
class_model: Any = self._class_model
|
|
121
|
+
counter: int = 0
|
|
122
|
+
number_docs_skippe: int = limit_docs * (page_number - 1) if page_number > 1 else 0
|
|
123
|
+
result: list[Any] = []
|
|
124
|
+
filter_is_checking: bool = False
|
|
125
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
126
|
+
for branch_number in branch_numbers:
|
|
127
|
+
if number_docs_skippe == 0 and counter >= limit_docs:
|
|
128
|
+
return result[:limit_docs]
|
|
129
|
+
future = executor.submit(
|
|
130
|
+
search_task_fn,
|
|
131
|
+
branch_number,
|
|
132
|
+
filter_fn,
|
|
133
|
+
hash_reduce_left,
|
|
134
|
+
db_root,
|
|
135
|
+
class_model,
|
|
136
|
+
filter_is_checking,
|
|
137
|
+
)
|
|
138
|
+
docs = await future.result()
|
|
139
|
+
if docs is not None:
|
|
140
|
+
for doc in docs:
|
|
141
|
+
if number_docs_skippe == 0:
|
|
142
|
+
if counter >= limit_docs:
|
|
143
|
+
return result[:limit_docs]
|
|
144
|
+
if filter_fn(doc):
|
|
145
|
+
result.append(doc)
|
|
146
|
+
counter += 1
|
|
147
|
+
else:
|
|
148
|
+
number_docs_skippe -= 1
|
|
149
|
+
return result or None
|
scruby/mixins/update.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
"""Methods for updating documents."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__all__ = ("Update",)
|
|
10
|
+
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import orjson
|
|
16
|
+
from anyio import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Update:
|
|
20
|
+
"""Methods for updating documents."""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
async def _task_update(
|
|
24
|
+
branch_number: int,
|
|
25
|
+
filter_fn: Callable,
|
|
26
|
+
hash_reduce_left: str,
|
|
27
|
+
db_root: str,
|
|
28
|
+
class_model: Any,
|
|
29
|
+
new_data: dict[str, Any],
|
|
30
|
+
) -> int:
|
|
31
|
+
"""Task for find documents.
|
|
32
|
+
|
|
33
|
+
This method is for internal use.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The number of updated documents.
|
|
37
|
+
"""
|
|
38
|
+
branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
|
|
39
|
+
separated_hash: str = "/".join(list(branch_number_as_hash))
|
|
40
|
+
leaf_path: Path = Path(
|
|
41
|
+
*(
|
|
42
|
+
db_root,
|
|
43
|
+
class_model.__name__,
|
|
44
|
+
separated_hash,
|
|
45
|
+
"leaf.json",
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
counter: int = 0
|
|
49
|
+
if await leaf_path.exists():
|
|
50
|
+
data_json: bytes = await leaf_path.read_bytes()
|
|
51
|
+
data: dict[str, str] = orjson.loads(data_json) or {}
|
|
52
|
+
new_state: dict[str, str] = {}
|
|
53
|
+
for _, val in data.items():
|
|
54
|
+
doc = class_model.model_validate_json(val)
|
|
55
|
+
if filter_fn(doc):
|
|
56
|
+
for key, value in new_data.items():
|
|
57
|
+
doc.__dict__[key] = value
|
|
58
|
+
new_state[key] = doc.model_dump_json()
|
|
59
|
+
counter += 1
|
|
60
|
+
await leaf_path.write_bytes(orjson.dumps(new_state))
|
|
61
|
+
return counter
|
|
62
|
+
|
|
63
|
+
async def update_many(
|
|
64
|
+
self,
|
|
65
|
+
filter_fn: Callable,
|
|
66
|
+
new_data: dict[str, Any],
|
|
67
|
+
) -> int:
|
|
68
|
+
"""Updates one or more documents matching the filter.
|
|
69
|
+
|
|
70
|
+
The search is based on the effect of a quantum loop.
|
|
71
|
+
The search effectiveness depends on the number of processor threads.
|
|
72
|
+
Ideally, hundreds and even thousands of threads are required.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
filter_fn: A function that execute the conditions of filtering.
|
|
76
|
+
new_data: New data for the fields that need to be updated.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The number of updated documents.
|
|
80
|
+
"""
|
|
81
|
+
branch_numbers: range = range(1, self._max_branch_number)
|
|
82
|
+
update_task_fn: Callable = self._task_update
|
|
83
|
+
hash_reduce_left: int = self._hash_reduce_left
|
|
84
|
+
db_root: str = self._db_root
|
|
85
|
+
class_model: Any = self._class_model
|
|
86
|
+
counter: int = 0
|
|
87
|
+
with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
|
|
88
|
+
for branch_number in branch_numbers:
|
|
89
|
+
future = executor.submit(
|
|
90
|
+
update_task_fn,
|
|
91
|
+
branch_number,
|
|
92
|
+
filter_fn,
|
|
93
|
+
hash_reduce_left,
|
|
94
|
+
db_root,
|
|
95
|
+
class_model,
|
|
96
|
+
new_data,
|
|
97
|
+
)
|
|
98
|
+
counter += await future.result()
|
|
99
|
+
return counter
|
scruby/settings.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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.
|
|
6
|
+
|
|
7
|
+
The module contains the following parameters:
|
|
8
|
+
|
|
9
|
+
- `DB_ROOT` - Path to root directory of database. `By default = "ScrubyDB" (in root of project)`.
|
|
10
|
+
- `HASH_REDUCE_LEFT` - The length of the hash reduction on the left side.
|
|
11
|
+
- `0` - 4294967296 branches in collection.
|
|
12
|
+
- `2` - 16777216 branches in collection.
|
|
13
|
+
- `4` - 65536 branches in collection.
|
|
14
|
+
- `6` - 256 branches in collection (by default).
|
|
15
|
+
- `MAX_WORKERS` - The maximum number of processes that can be used `By default = None`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
__all__ = (
|
|
21
|
+
"DB_ROOT",
|
|
22
|
+
"HASH_REDUCE_LEFT",
|
|
23
|
+
"MAX_WORKERS",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from typing import Literal
|
|
27
|
+
|
|
28
|
+
# Path to root directory of database
|
|
29
|
+
# By default = "ScrubyDB" (in root of project).
|
|
30
|
+
DB_ROOT: str = "ScrubyDB"
|
|
31
|
+
|
|
32
|
+
# The length of the hash reduction on the left side.
|
|
33
|
+
# 0 = 4294967296 branches in collection.
|
|
34
|
+
# 2 = 16777216 branches in collection.
|
|
35
|
+
# 4 = 65536 branches in collection.
|
|
36
|
+
# 6 = 256 branches in collection (by default).
|
|
37
|
+
# Number of branches is number of requests to the hard disk during quantum operations.
|
|
38
|
+
# Quantum operations: find_one, find_many, count_documents, delete_many, run_custom_task.
|
|
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
|