scruby 0.10.3__py3-none-any.whl → 0.11.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
@@ -3,7 +3,7 @@
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
- - `LENGTH_REDUCTION_HASH` - The length of the hash reduction on the left side.
6
+ - `HASH_REDUCE_LEFT` - The length of the hash reduction on the left side.
7
7
  - `0` - 4294967296 branches in collection (by default).
8
8
  - `2` - 16777216 branches in collectionю
9
9
  - `4` - 65536 branches in collectionю
@@ -14,7 +14,7 @@ from __future__ import annotations
14
14
 
15
15
  __all__ = (
16
16
  "DB_ROOT",
17
- "LENGTH_REDUCTION_HASH",
17
+ "HASH_REDUCE_LEFT",
18
18
  )
19
19
 
20
20
  from typing import Literal
@@ -28,4 +28,4 @@ DB_ROOT: str = "ScrubyDB"
28
28
  # 2 = 16777216 branches in collectionю
29
29
  # 4 = 65536 branches in collectionю
30
30
  # 6 = 256 branches in collection (main purpose is tests).
31
- LENGTH_REDUCTION_HASH: Literal[0, 2, 4, 6] = 0
31
+ HASH_REDUCE_LEFT: Literal[0, 2, 4, 6] = 0
scruby/db.py CHANGED
@@ -11,7 +11,7 @@ import zlib
11
11
  from collections.abc import Callable
12
12
  from pathlib import Path as SyncPath
13
13
  from shutil import rmtree
14
- from typing import Any, Literal, Never, TypeVar, assert_never
14
+ from typing import Any, Never, TypeVar, assert_never
15
15
 
16
16
  import orjson
17
17
  from anyio import Path, to_thread
@@ -27,9 +27,6 @@ T = TypeVar("T")
27
27
  class _Meta(BaseModel):
28
28
  """Metadata of Collection."""
29
29
 
30
- db_root: str
31
- model_name: str
32
- length_reduction_hash: int
33
30
  counter_documents: int
34
31
 
35
32
 
@@ -47,19 +44,19 @@ class Scruby[T]:
47
44
  self.__meta = _Meta
48
45
  self.__class_model = class_model
49
46
  self.__db_root = constants.DB_ROOT
50
- self.__length_reduction_hash = constants.LENGTH_REDUCTION_HASH
47
+ self.__hash_reduce_left = constants.HASH_REDUCE_LEFT
51
48
  # The maximum number of keys.
52
- match self.__length_reduction_hash:
49
+ match self.__hash_reduce_left:
53
50
  case 0:
54
- self.__max_num_keys = 4294967296
51
+ self.__max_branch_number = 4294967296
55
52
  case 2:
56
- self.__max_num_keys = 16777216
53
+ self.__max_branch_number = 16777216
57
54
  case 4:
58
- self.__max_num_keys = 65536
55
+ self.__max_branch_number = 65536
59
56
  case 6:
60
- self.__max_num_keys = 256
57
+ self.__max_branch_number = 256
61
58
  case _ as unreachable:
62
- msg: str = f"{unreachable} - Unacceptable value for LENGTH_REDUCTION_HASH."
59
+ msg: str = f"{unreachable} - Unacceptable value for HASH_REDUCE_LEFT."
63
60
  logger.critical(msg)
64
61
  assert_never(Never(unreachable))
65
62
  # 1.Create metadata if absent.
@@ -71,8 +68,8 @@ class Scruby[T]:
71
68
 
72
69
  This method is for internal use.
73
70
  """
74
- key: int = 0
75
- key_as_hash: str = f"{key:08x}"[self.__length_reduction_hash :]
71
+ branch_number: int = 0
72
+ key_as_hash: str = f"{branch_number:08x}"[self.__hash_reduce_left :]
76
73
  separated_hash: str = "/".join(list(key_as_hash))
77
74
  branch_path = SyncPath(
78
75
  *(
@@ -84,9 +81,6 @@ class Scruby[T]:
84
81
  if not branch_path.exists():
85
82
  branch_path.mkdir(parents=True)
86
83
  meta = _Meta(
87
- db_root=self.__db_root,
88
- model_name=self.__class_model.__name__,
89
- length_reduction_hash=self.__length_reduction_hash,
90
84
  counter_documents=0,
91
85
  )
92
86
  meta_json = meta.model_dump_json()
@@ -98,9 +92,9 @@ class Scruby[T]:
98
92
 
99
93
  This method is for internal use.
100
94
  """
101
- key: int = 0
102
- key_as_hash: str = f"{key:08x}"[self.__length_reduction_hash :]
103
- separated_hash: str = "/".join(list(key_as_hash))
95
+ branch_number: int = 0
96
+ branch_number_as_hash: str = f"{branch_number:08x}"[self.__hash_reduce_left :]
97
+ separated_hash: str = "/".join(list(branch_number_as_hash))
104
98
  return Path(
105
99
  *(
106
100
  self.__db_root,
@@ -129,16 +123,43 @@ class Scruby[T]:
129
123
  meta_json = meta.model_dump_json()
130
124
  await meta_path.write_text(meta_json, "utf-8")
131
125
 
132
- async def _counter_documents(self, step: Literal[1, -1]) -> None:
126
+ async def _counter_documents(self, number: int) -> None:
127
+ """Asynchronous method for management of documents in metadata of collection.
128
+
129
+ This method is for internal use.
130
+ """
131
+ meta_path = await self._get_meta_path()
132
+ meta_json = await meta_path.read_text("utf-8")
133
+ meta: _Meta = self.__meta.model_validate_json(meta_json)
134
+ meta.counter_documents += number
135
+ if meta.counter_documents < 0:
136
+ meta.counter_documents = 0
137
+ meta_json = meta.model_dump_json()
138
+ await meta_path.write_text(meta_json, "utf-8")
139
+
140
+ def _sync_counter_documents(self, number: int) -> None:
133
141
  """Management of documents in metadata of collection.
134
142
 
135
143
  This method is for internal use.
136
144
  """
137
- meta = await self._get_meta()
138
- meta.counter_documents += step
145
+ branch_number: int = 0
146
+ branch_number_as_hash: str = f"{branch_number:08x}"[self.__hash_reduce_left :]
147
+ separated_hash: str = "/".join(list(branch_number_as_hash))
148
+ meta_path = SyncPath(
149
+ *(
150
+ self.__db_root,
151
+ self.__class_model.__name__,
152
+ separated_hash,
153
+ "meta.json",
154
+ ),
155
+ )
156
+ meta_json = meta_path.read_text("utf-8")
157
+ meta: _Meta = self.__meta.model_validate_json(meta_json)
158
+ meta.counter_documents += number
139
159
  if meta.counter_documents < 0:
140
160
  meta.counter_documents = 0
141
- await self._set_meta(meta)
161
+ meta_json = meta.model_dump_json()
162
+ meta_path.write_text(meta_json, "utf-8")
142
163
 
143
164
  async def _get_leaf_path(self, key: str) -> Path:
144
165
  """Asynchronous method for getting path to collection cell by key.
@@ -155,7 +176,7 @@ class Scruby[T]:
155
176
  logger.error("The key should not be empty.")
156
177
  raise KeyError("The key should not be empty.")
157
178
  # Key to crc32 sum.
158
- key_as_hash: str = f"{zlib.crc32(key.encode('utf-8')):08x}"[self.__length_reduction_hash :]
179
+ key_as_hash: str = f"{zlib.crc32(key.encode('utf-8')):08x}"[self.__hash_reduce_left :]
159
180
  # Convert crc32 sum in the segment of path.
160
181
  separated_hash: str = "/".join(list(key_as_hash))
161
182
  # The path of the branch to the database.
@@ -273,18 +294,18 @@ class Scruby[T]:
273
294
 
274
295
  @staticmethod
275
296
  def _task_find(
276
- key: int,
297
+ branch_number: int,
277
298
  filter_fn: Callable,
278
- length_reduction_hash: str,
299
+ hash_reduce_left: str,
279
300
  db_root: str,
280
301
  class_model: T,
281
302
  ) -> dict[str, Any] | None:
282
- """Task for searching for documents.
303
+ """Task for find documents.
283
304
 
284
305
  This method is for internal use.
285
306
  """
286
- key_as_hash: str = f"{key:08x}"[length_reduction_hash:]
287
- separated_hash: str = "/".join(list(key_as_hash))
307
+ branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
308
+ separated_hash: str = "/".join(list(branch_number_as_hash))
288
309
  leaf_path: SyncPath = SyncPath(
289
310
  *(
290
311
  db_root,
@@ -308,7 +329,7 @@ class Scruby[T]:
308
329
  max_workers: int | None = None,
309
330
  timeout: float | None = None,
310
331
  ) -> T | None:
311
- """Find a single document matching the filter.
332
+ """Finds a single document matching the filter.
312
333
 
313
334
  The search is based on the effect of a quantum loop.
314
335
  The search effectiveness depends on the number of processor threads.
@@ -322,18 +343,18 @@ class Scruby[T]:
322
343
  timeout: The number of seconds to wait for the result if the future isn't done.
323
344
  If None, then there is no limit on the wait time.
324
345
  """
325
- keys: range = range(1, self.__max_num_keys)
346
+ branch_numbers: range = range(1, self.__max_branch_number)
326
347
  search_task_fn: Callable = self._task_find
327
- length_reduction_hash: int = self.__length_reduction_hash
348
+ hash_reduce_left: int = self.__hash_reduce_left
328
349
  db_root: str = self.__db_root
329
350
  class_model: T = self.__class_model
330
351
  with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
331
- for key in keys:
352
+ for branch_number in branch_numbers:
332
353
  future = executor.submit(
333
354
  search_task_fn,
334
- key,
355
+ branch_number,
335
356
  filter_fn,
336
- length_reduction_hash,
357
+ hash_reduce_left,
337
358
  db_root,
338
359
  class_model,
339
360
  )
@@ -342,14 +363,14 @@ class Scruby[T]:
342
363
  return doc
343
364
  return None
344
365
 
345
- def find(
366
+ def find_many(
346
367
  self,
347
368
  filter_fn: Callable,
348
369
  db_query_docs_limit: int = 1000,
349
370
  max_workers: int | None = None,
350
371
  timeout: float | None = None,
351
372
  ) -> list[T] | None:
352
- """Find one or more documents matching the filter.
373
+ """Finds one or more documents matching the filter.
353
374
 
354
375
  The search is based on the effect of a quantum loop.
355
376
  The search effectiveness depends on the number of processor threads.
@@ -364,22 +385,22 @@ class Scruby[T]:
364
385
  timeout: The number of seconds to wait for the result if the future isn't done.
365
386
  If None, then there is no limit on the wait time.
366
387
  """
367
- keys: range = range(1, self.__max_num_keys)
388
+ branch_numbers: range = range(1, self.__max_branch_number)
368
389
  search_task_fn: Callable = self._task_find
369
- length_reduction_hash: int = self.__length_reduction_hash
390
+ hash_reduce_left: int = self.__hash_reduce_left
370
391
  db_root: str = self.__db_root
371
392
  class_model: T = self.__class_model
372
393
  counter: int = 0
373
394
  with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
374
395
  results = []
375
- for key in keys:
396
+ for branch_number in branch_numbers:
376
397
  if counter == db_query_docs_limit:
377
398
  break
378
399
  future = executor.submit(
379
400
  search_task_fn,
380
- key,
401
+ branch_number,
381
402
  filter_fn,
382
- length_reduction_hash,
403
+ hash_reduce_left,
383
404
  db_root,
384
405
  class_model,
385
406
  )
@@ -422,22 +443,99 @@ class Scruby[T]:
422
443
  timeout: The number of seconds to wait for the result if the future isn't done.
423
444
  If None, then there is no limit on the wait time.
424
445
  """
425
- keys: range = range(1, self.__max_num_keys)
446
+ branch_numbers: range = range(1, self.__max_branch_number)
426
447
  search_task_fn: Callable = self._task_find
427
- length_reduction_hash: int = self.__length_reduction_hash
448
+ hash_reduce_left: int = self.__hash_reduce_left
428
449
  db_root: str = self.__db_root
429
450
  class_model: T = self.__class_model
430
451
  counter: int = 0
431
452
  with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
432
- for key in keys:
453
+ for branch_number in branch_numbers:
433
454
  future = executor.submit(
434
455
  search_task_fn,
435
- key,
456
+ branch_number,
436
457
  filter_fn,
437
- length_reduction_hash,
458
+ hash_reduce_left,
438
459
  db_root,
439
460
  class_model,
440
461
  )
441
462
  if future.result(timeout) is not None:
442
463
  counter += 1
443
464
  return counter
465
+
466
+ @staticmethod
467
+ def _task_delete(
468
+ branch_number: int,
469
+ filter_fn: Callable,
470
+ hash_reduce_left: str,
471
+ db_root: str,
472
+ class_model: T,
473
+ ) -> int:
474
+ """Task for find and delete documents.
475
+
476
+ This method is for internal use.
477
+ """
478
+ branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
479
+ separated_hash: str = "/".join(list(branch_number_as_hash))
480
+ leaf_path: SyncPath = SyncPath(
481
+ *(
482
+ db_root,
483
+ class_model.__name__,
484
+ separated_hash,
485
+ "leaf.json",
486
+ ),
487
+ )
488
+ counter: int = 0
489
+ if leaf_path.exists():
490
+ data_json: bytes = leaf_path.read_bytes()
491
+ data: dict[str, str] = orjson.loads(data_json) or {}
492
+ new_data: dict[str, str] = {}
493
+ for key, val in data.items():
494
+ doc = class_model.model_validate_json(val)
495
+ if filter_fn(doc):
496
+ counter -= 1
497
+ else:
498
+ new_data[key] = val
499
+ leaf_path.write_bytes(orjson.dumps(new_data))
500
+ return counter
501
+
502
+ def find_many_and_delete(
503
+ self,
504
+ filter_fn: Callable,
505
+ max_workers: int | None = None,
506
+ timeout: float | None = None,
507
+ ) -> int:
508
+ """Finds one or more documents matching the filter and deletes their.
509
+
510
+ The search is based on the effect of a quantum loop.
511
+ The search effectiveness depends on the number of processor threads.
512
+ Ideally, hundreds and even thousands of threads are required.
513
+
514
+ Args:
515
+ filter_fn: A function that execute the conditions of filtering.
516
+ max_workers: The maximum number of processes that can be used to
517
+ execute the given calls. If None or not given then as many
518
+ worker processes will be created as the machine has processors.
519
+ timeout: The number of seconds to wait for the result if the future isn't done.
520
+ If None, then there is no limit on the wait time.
521
+ """
522
+ branch_numbers: range = range(1, self.__max_branch_number)
523
+ search_task_fn: Callable = self._task_delete
524
+ hash_reduce_left: int = self.__hash_reduce_left
525
+ db_root: str = self.__db_root
526
+ class_model: T = self.__class_model
527
+ counter: int = 0
528
+ with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
529
+ for branch_number in branch_numbers:
530
+ future = executor.submit(
531
+ search_task_fn,
532
+ branch_number,
533
+ filter_fn,
534
+ hash_reduce_left,
535
+ db_root,
536
+ class_model,
537
+ )
538
+ counter += future.result(timeout)
539
+ if counter < 0:
540
+ self._sync_counter_documents(counter)
541
+ return abs(counter)
scruby/errors.py CHANGED
@@ -1,14 +1,17 @@
1
- """XLOT Exceptions."""
1
+ """Scruby Exceptions."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __all__ = ("MetadataValueError",)
5
+ __all__ = (
6
+ "ScrubyException",
7
+ "MetadataValueError",
8
+ )
6
9
 
7
10
 
8
11
  class ScrubyException(Exception):
9
12
  """Root Custom Exception."""
10
13
 
11
- def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
14
+ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] # noqa: D107
12
15
  super().__init__(*args, **kwargs)
13
16
 
14
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scruby
3
- Version: 0.10.3
3
+ Version: 0.11.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
@@ -175,7 +175,7 @@ from scruby import Scruby, constants
175
175
  from pprint import pprint as pp
176
176
 
177
177
  constants.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
178
- constants.LENGTH_REDUCTION_HASH = 6 # 256 branches in collection
178
+ constants.HASH_REDUCE_LEFT = 6 # 256 branches in collection
179
179
  # (main purpose is tests).
180
180
 
181
181
  class User(BaseModel):
@@ -246,7 +246,7 @@ from scruby import Scruby, constants
246
246
  from pprint import pprint as pp
247
247
 
248
248
  constants.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
249
- constants.LENGTH_REDUCTION_HASH = 6 # 256 branches in collection
249
+ constants.HASH_REDUCE_LEFT = 6 # 256 branches in collection
250
250
  # (main purpose is tests).
251
251
 
252
252
  class User(BaseModel):
@@ -274,7 +274,7 @@ async def main() -> None:
274
274
  await db.set_key(f"+44798612345{num}", user)
275
275
 
276
276
  # Find users by email.
277
- users: list[User] | None = user_coll.find(
277
+ users: list[User] | None = user_coll.find_many(
278
278
  filter_fn=lambda doc: doc.email == "John_Smith_5@gmail.com" or doc.email == "John_Smith_8@gmail.com",
279
279
  )
280
280
  if users is not None:
@@ -0,0 +1,9 @@
1
+ scruby/__init__.py,sha256=wFwUS1KcLxfIopXOVS8gPue9fNzIIU2cVj_RgK5drz4,849
2
+ scruby/constants.py,sha256=3LZfcxcuRqwzoB0-iogLMjKBZRdxfWJmTbyPwVRhQgY,1007
3
+ scruby/db.py,sha256=Q7J4OKS2emiF0KzZClSjpBBLjohnccZ81T4pgoWNxqA,20269
4
+ scruby/errors.py,sha256=aHQri4LNcFVQrSHwjyzb1fL8O49SwjYEU4QgMOo4uyA,622
5
+ scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ scruby-0.11.0.dist-info/METADATA,sha256=yS9LDAYAqmxxn8cXHKbO7BmPEsX4o7th3VVFtgr9aoo,10824
7
+ scruby-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ scruby-0.11.0.dist-info/licenses/LICENSE,sha256=2zZINd6m_jNYlowdQImlEizyhSui5cBAJZRhWQURcEc,1095
9
+ scruby-0.11.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- scruby/__init__.py,sha256=wFwUS1KcLxfIopXOVS8gPue9fNzIIU2cVj_RgK5drz4,849
2
- scruby/constants.py,sha256=GbB-O0qaVdi5EHUp-zRAppFXLR-oHxpXUFVAOCpS0C8,1022
3
- scruby/db.py,sha256=J14Xjyc6iyb-cwBKiH8rJuioEHoYfNkLTezzvQBsJng,16181
4
- scruby/errors.py,sha256=4G0zNVzulBE9mM2iJLdg0EXP_W8n-L6EjZrkCCErvAU,574
5
- scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- scruby-0.10.3.dist-info/METADATA,sha256=JGgVH8QKtA-iGifWhNdSczfuglIT2RRw5njRuNKvG3M,10829
7
- scruby-0.10.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- scruby-0.10.3.dist-info/licenses/LICENSE,sha256=2zZINd6m_jNYlowdQImlEizyhSui5cBAJZRhWQURcEc,1095
9
- scruby-0.10.3.dist-info/RECORD,,