scruby 0.6.3__py3-none-any.whl → 0.7.1__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/constants.py +12 -13
- scruby/db.py +90 -7
- {scruby-0.6.3.dist-info → scruby-0.7.1.dist-info}/METADATA +72 -1
- scruby-0.7.1.dist-info/RECORD +8 -0
- scruby-0.6.3.dist-info/RECORD +0 -8
- {scruby-0.6.3.dist-info → scruby-0.7.1.dist-info}/WHEEL +0 -0
- {scruby-0.6.3.dist-info → scruby-0.7.1.dist-info}/licenses/LICENSE +0 -0
scruby/constants.py
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
The module contains the following variables:
|
|
4
4
|
|
|
5
5
|
- `DB_ROOT` - Path to root directory of database. `By default = "ScrubyDB"` (*in root of project*).
|
|
6
|
-
- `
|
|
7
|
-
- `
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
6
|
+
- `LENGTH_REDUCTION_HASH` - The length of the hash reduction on the left side.
|
|
7
|
+
- `0` - 4294967296 keys (by default).
|
|
8
|
+
- `2` - 16777216 keys.
|
|
9
|
+
- `4` - 65536 keys.
|
|
10
|
+
- `6` - 256 keys (main purpose is tests).
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
__all__ = (
|
|
16
16
|
"DB_ROOT",
|
|
17
|
-
"
|
|
17
|
+
"LENGTH_REDUCTION_HASH",
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
from typing import Literal
|
|
@@ -23,10 +23,9 @@ from typing import Literal
|
|
|
23
23
|
# By default = "ScrubyDB" (in root of project).
|
|
24
24
|
DB_ROOT: str = "ScrubyDB"
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# 2 =
|
|
29
|
-
# 4 = 65536
|
|
30
|
-
# 6 =
|
|
31
|
-
|
|
32
|
-
LENGTH_SEPARATED_HASH: Literal[2, 4, 6, 8] = 8
|
|
26
|
+
# The length of the hash reduction on the left side.
|
|
27
|
+
# 0 = 4294967296 keys (by default).
|
|
28
|
+
# 2 = 16777216 keys.
|
|
29
|
+
# 4 = 65536 keys.
|
|
30
|
+
# 6 = 256 keys (main purpose is tests).
|
|
31
|
+
LENGTH_REDUCTION_HASH: Literal[0, 2, 4, 6] = 0
|
scruby/db.py
CHANGED
|
@@ -4,10 +4,13 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
__all__ = ("Scruby",)
|
|
6
6
|
|
|
7
|
+
import concurrent.futures
|
|
7
8
|
import contextlib
|
|
8
9
|
import zlib
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from pathlib import Path as SyncPath
|
|
9
12
|
from shutil import rmtree
|
|
10
|
-
from typing import TypeVar
|
|
13
|
+
from typing import Any, Never, TypeVar, assert_never
|
|
11
14
|
|
|
12
15
|
import orjson
|
|
13
16
|
from anyio import Path, to_thread
|
|
@@ -29,6 +32,20 @@ class Scruby[T]:
|
|
|
29
32
|
class_model: T,
|
|
30
33
|
) -> None:
|
|
31
34
|
self.__class_model = class_model
|
|
35
|
+
self.__db_root = constants.DB_ROOT
|
|
36
|
+
self.__length_reduction_hash = constants.LENGTH_REDUCTION_HASH
|
|
37
|
+
# The maximum number of keys.
|
|
38
|
+
match self.__length_reduction_hash:
|
|
39
|
+
case 0:
|
|
40
|
+
self.__max_num_keys = 4294967296
|
|
41
|
+
case 2:
|
|
42
|
+
self.__max_num_keys = 16777216
|
|
43
|
+
case 4:
|
|
44
|
+
self.__max_num_keys = 65536
|
|
45
|
+
case 6:
|
|
46
|
+
self.__max_num_keys = 256
|
|
47
|
+
case _ as unreachable:
|
|
48
|
+
assert_never(Never(unreachable))
|
|
32
49
|
|
|
33
50
|
async def get_leaf_path(self, key: str) -> Path:
|
|
34
51
|
"""Asynchronous method for getting path to collection cell by key.
|
|
@@ -40,16 +57,14 @@ class Scruby[T]:
|
|
|
40
57
|
raise KeyError("The key is not a type of `str`.")
|
|
41
58
|
if len(key) == 0:
|
|
42
59
|
raise KeyError("The key should not be empty.")
|
|
43
|
-
# Get length of hash.
|
|
44
|
-
length_hash = constants.LENGTH_SEPARATED_HASH
|
|
45
60
|
# Key to crc32 sum.
|
|
46
|
-
key_as_hash: str = f"{zlib.crc32(key.encode('utf-8')):08x}"[
|
|
61
|
+
key_as_hash: str = f"{zlib.crc32(key.encode('utf-8')):08x}"[self.__length_reduction_hash :]
|
|
47
62
|
# Convert crc32 sum in the segment of path.
|
|
48
63
|
separated_hash: str = "/".join(list(key_as_hash))
|
|
49
64
|
# The path of the branch to the database.
|
|
50
65
|
branch_path: Path = Path(
|
|
51
66
|
*(
|
|
52
|
-
|
|
67
|
+
self.__db_root,
|
|
53
68
|
self.__class_model.__name__,
|
|
54
69
|
separated_hash,
|
|
55
70
|
),
|
|
@@ -138,8 +153,8 @@ class Scruby[T]:
|
|
|
138
153
|
return
|
|
139
154
|
raise KeyError()
|
|
140
155
|
|
|
141
|
-
@
|
|
142
|
-
async def napalm(
|
|
156
|
+
@staticmethod
|
|
157
|
+
async def napalm() -> None:
|
|
143
158
|
"""Asynchronous method for full database deletion.
|
|
144
159
|
|
|
145
160
|
The main purpose is tests.
|
|
@@ -150,3 +165,71 @@ class Scruby[T]:
|
|
|
150
165
|
with contextlib.suppress(FileNotFoundError):
|
|
151
166
|
await to_thread.run_sync(rmtree, constants.DB_ROOT)
|
|
152
167
|
return
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def search_task(
|
|
171
|
+
key: int,
|
|
172
|
+
filter_fn: Callable,
|
|
173
|
+
length_reduction_hash: str,
|
|
174
|
+
db_root: str,
|
|
175
|
+
class_model: T,
|
|
176
|
+
) -> dict[str, Any] | None:
|
|
177
|
+
"""Search task."""
|
|
178
|
+
key_as_hash: str = f"{key:08x}"[length_reduction_hash:]
|
|
179
|
+
separated_hash: str = "/".join(list(key_as_hash))
|
|
180
|
+
leaf_path: SyncPath = SyncPath(
|
|
181
|
+
*(
|
|
182
|
+
db_root,
|
|
183
|
+
class_model.__name__,
|
|
184
|
+
separated_hash,
|
|
185
|
+
"leaf.json",
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
if leaf_path.exists():
|
|
189
|
+
data_json: bytes = leaf_path.read_bytes()
|
|
190
|
+
data: dict[str, str] = orjson.loads(data_json) or {}
|
|
191
|
+
for _, val in data.items():
|
|
192
|
+
doc = class_model.model_validate_json(val)
|
|
193
|
+
if filter_fn(doc):
|
|
194
|
+
return doc
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def find_one(
|
|
198
|
+
self,
|
|
199
|
+
filter_fn: Callable,
|
|
200
|
+
max_workers: int | None = None,
|
|
201
|
+
timeout: float | None = None,
|
|
202
|
+
) -> T | None:
|
|
203
|
+
"""Find a single document.
|
|
204
|
+
|
|
205
|
+
The search is based on the effect of a quantum loop.
|
|
206
|
+
The search effectiveness depends on the number of processor threads.
|
|
207
|
+
Ideally, hundreds and even thousands of streams are required.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
filter_fn: A function that execute the conditions of filtering.
|
|
211
|
+
max_workers: The maximum number of processes that can be used to
|
|
212
|
+
execute the given calls. If None or not given then as many
|
|
213
|
+
worker processes will be created as the machine has processors.
|
|
214
|
+
timeout: The number of seconds to wait for the result if the future isn't done.
|
|
215
|
+
If None, then there is no limit on the wait time.
|
|
216
|
+
"""
|
|
217
|
+
keys: range = range(1, self.__max_num_keys)
|
|
218
|
+
search_task_fn: Callable = self.search_task
|
|
219
|
+
length_reduction_hash: int = self.__length_reduction_hash
|
|
220
|
+
db_root: str = self.__db_root
|
|
221
|
+
class_model: T = self.__class_model
|
|
222
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
|
|
223
|
+
for key in keys:
|
|
224
|
+
future = executor.submit(
|
|
225
|
+
search_task_fn,
|
|
226
|
+
key,
|
|
227
|
+
filter_fn,
|
|
228
|
+
length_reduction_hash,
|
|
229
|
+
db_root,
|
|
230
|
+
class_model,
|
|
231
|
+
)
|
|
232
|
+
result = future.result(timeout)
|
|
233
|
+
if result is not None:
|
|
234
|
+
return result
|
|
235
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scruby
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: A fast key-value storage library.
|
|
5
5
|
Project-URL: Homepage, https://github.com/kebasyaty/scruby
|
|
6
6
|
Project-URL: Repository, https://github.com/kebasyaty/scruby
|
|
@@ -104,6 +104,8 @@ uv add scruby
|
|
|
104
104
|
## Usage
|
|
105
105
|
|
|
106
106
|
```python
|
|
107
|
+
"""Working with keys."""
|
|
108
|
+
|
|
107
109
|
import anyio
|
|
108
110
|
import datetime
|
|
109
111
|
from pydantic import BaseModel, EmailStr
|
|
@@ -153,6 +155,75 @@ if __name__ == "__main__":
|
|
|
153
155
|
anyio.run(main)
|
|
154
156
|
```
|
|
155
157
|
|
|
158
|
+
```python
|
|
159
|
+
"""Find a single document.
|
|
160
|
+
|
|
161
|
+
The search is based on the effect of a quantum loop.
|
|
162
|
+
The search effectiveness depends on the number of processor threads.
|
|
163
|
+
Ideally, hundreds and even thousands of streams are required.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
import anyio
|
|
167
|
+
import datetime
|
|
168
|
+
from pydantic import BaseModel, EmailStr
|
|
169
|
+
from pydantic_extra_types.phone_numbers import PhoneNumber
|
|
170
|
+
from scruby import Scruby, constants
|
|
171
|
+
from pprint import pprint as pp
|
|
172
|
+
|
|
173
|
+
constants.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
|
|
174
|
+
constants.LENGTH_REDUCTION_HASH = 6 # 256 keys (main purpose is tests).
|
|
175
|
+
|
|
176
|
+
class User(BaseModel):
|
|
177
|
+
"""Model of User."""
|
|
178
|
+
first_name: str
|
|
179
|
+
last_name: str
|
|
180
|
+
birthday: datetime.datetime
|
|
181
|
+
email: EmailStr
|
|
182
|
+
phone: PhoneNumber
|
|
183
|
+
|
|
184
|
+
async def main() -> None:
|
|
185
|
+
"""Example."""
|
|
186
|
+
# Get collection of `User`.
|
|
187
|
+
user_coll = Scruby(User)
|
|
188
|
+
|
|
189
|
+
# Create user.
|
|
190
|
+
user = User(
|
|
191
|
+
first_name="John",
|
|
192
|
+
last_name="Smith",
|
|
193
|
+
birthday=datetime.datetime(1970, 1, 1),
|
|
194
|
+
email="John_Smith@gmail.com",
|
|
195
|
+
phone="+447986123456",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Add user to collection.
|
|
199
|
+
await user_coll.set_key("+447986123456", user)
|
|
200
|
+
|
|
201
|
+
# Find user by email.
|
|
202
|
+
user_details: User | None = user_coll.find_one(
|
|
203
|
+
filter_fn=lambda doc: doc.email == "John_Smith@gmail.com",
|
|
204
|
+
)
|
|
205
|
+
if user_details is not None:
|
|
206
|
+
pp(user_details)
|
|
207
|
+
else:
|
|
208
|
+
print("No User!")
|
|
209
|
+
|
|
210
|
+
# Find user by birthday.
|
|
211
|
+
user_details: User | None = user_coll.find_one(
|
|
212
|
+
filter_fn=lambda doc: doc.birthday == datetime.datetime(1970, 1, 1),
|
|
213
|
+
)
|
|
214
|
+
if user_details is not None:
|
|
215
|
+
pp(user_details)
|
|
216
|
+
else:
|
|
217
|
+
print("No User!")
|
|
218
|
+
|
|
219
|
+
# Full database deletion.
|
|
220
|
+
# Hint: The main purpose is tests.
|
|
221
|
+
await Scruby.napalm()
|
|
222
|
+
|
|
223
|
+
if __name__ == "__main__":
|
|
224
|
+
anyio.run(main)
|
|
225
|
+
```
|
|
226
|
+
|
|
156
227
|
## Changelog
|
|
157
228
|
|
|
158
229
|
[View the change history](https://github.com/kebasyaty/scruby/blob/v0/CHANGELOG.md "Changelog").
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
scruby/__init__.py,sha256=myX7sG-7oAQZGdgfZtTGXYCCraTeuwi7SjBoltftpnM,648
|
|
2
|
+
scruby/constants.py,sha256=XcBbK_gkDC0makWE34M8gUotwj3smi2sWY4mMnfzUt4,874
|
|
3
|
+
scruby/db.py,sha256=FkYBsVYERtauTlWjl8b8cDTTIaH1w7lOzAneWUtNLDE,8114
|
|
4
|
+
scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
scruby-0.7.1.dist-info/METADATA,sha256=qkiNaSRnF2pnWMImFLsAz16MMRD_amaDImy9DbFwNoM,8616
|
|
6
|
+
scruby-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
scruby-0.7.1.dist-info/licenses/LICENSE,sha256=2zZINd6m_jNYlowdQImlEizyhSui5cBAJZRhWQURcEc,1095
|
|
8
|
+
scruby-0.7.1.dist-info/RECORD,,
|
scruby-0.6.3.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
scruby/__init__.py,sha256=myX7sG-7oAQZGdgfZtTGXYCCraTeuwi7SjBoltftpnM,648
|
|
2
|
-
scruby/constants.py,sha256=7Px7BDQozlvfSKSAN4Rme4uJHLY_OsT3H0Wq6A_810k,942
|
|
3
|
-
scruby/db.py,sha256=Rt9YDe0lSJwtREHFiqdQ6CQ664FL6YRyczbgKFhpsa4,4871
|
|
4
|
-
scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
scruby-0.6.3.dist-info/METADATA,sha256=8TTKyS4FsDr9PEvL7U5DKtAuRSxUR-s_cMiJ9F7Wwzw,6813
|
|
6
|
-
scruby-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
scruby-0.6.3.dist-info/licenses/LICENSE,sha256=2zZINd6m_jNYlowdQImlEizyhSui5cBAJZRhWQURcEc,1095
|
|
8
|
-
scruby-0.6.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|