scruby 0.6.3__py3-none-any.whl → 0.7.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/constants.py CHANGED
@@ -4,10 +4,10 @@ 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
6
  - `LENGTH_SEPARATED_HASH` - Length of separated hash for create path inside collection.
7
- - `2` - 256 branches (main purpose is tests).
8
- - `4` - 65536 branches.
9
- - `6` - 16777216 branches.
10
- - `8` - 4294967296 branches (by default).
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
@@ -24,9 +24,8 @@ from typing import Literal
24
24
  DB_ROOT: str = "ScrubyDB"
25
25
 
26
26
  # Length of separated hash for create path inside collection.
27
- # By default = 8
28
- # 2 = 256 branches (main purpose is tests).
29
- # 4 = 65536 branches.
30
- # 6 = 16777216 branches.
31
- # 8 = 4294967296 branches (by default).
32
- LENGTH_SEPARATED_HASH: Literal[2, 4, 6, 8] = 8
27
+ # 0 = 4294967296 keys (by default).
28
+ # 2 = 16777216 keys.
29
+ # 4 = 65536 keys.
30
+ # 6 = 256 keys (main purpose is tests).
31
+ LENGTH_SEPARATED_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_hash = constants.LENGTH_SEPARATED_HASH
37
+ # The maximum number of keys.
38
+ match self.__length_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}"[0:length_hash]
61
+ key_as_hash: str = f"{zlib.crc32(key.encode('utf-8')):08x}"[self.__length_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
- constants.DB_ROOT,
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
- @classmethod
142
- async def napalm(cls) -> None:
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_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_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_hash: int = self.__length_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_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.6.3
3
+ Version: 0.7.0
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_SEPARATED_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=1Po5FSYj1qst8F05L4cPyKsDHNmReXDx_IKYgNa06eI,892
3
+ scruby/db.py,sha256=qelkW6MzI4HQOtkXDb8qOJHfqsads3vILSnddboQEOc,8034
4
+ scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ scruby-0.7.0.dist-info/METADATA,sha256=uH-rBdWc5tDUhFxTYac6Z3EDua6z-H-mWBXo9gB47Mc,8616
6
+ scruby-0.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ scruby-0.7.0.dist-info/licenses/LICENSE,sha256=2zZINd6m_jNYlowdQImlEizyhSui5cBAJZRhWQURcEc,1095
8
+ scruby-0.7.0.dist-info/RECORD,,
@@ -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