scruby 0.26.0__py3-none-any.whl → 0.30.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.

Potentially problematic release.


This version of scruby might be problematic. Click here for more details.

scruby/__init__.py CHANGED
@@ -1,16 +1,11 @@
1
1
  #
2
- # .|'''|                        '||
3
- # ||                             ||
4
- # `|'''|, .|'', '||''| '||  ||`  ||''|, '||  ||`
5
- #  .   || ||     ||     ||  ||   ||  ||  `|..||
6
- #  |...|' `|..' .||.    `|..'|. .||..|'      ||
7
- #                                         ,  |'
8
- #                                          ''
9
- #
2
+ # .dP"Y8  dP""b8 88""Yb 88   88 88""Yb Yb  dP
3
+ # `Ybo." dP   `" 88__dP 88   88 88__dP  YbdP
4
+ # o.`Y8b Yb      88"Yb  Y8   8P 88""Yb   8P
5
+ # 8bodP'  YboodP 88  Yb `YbodP' 88oodP  dP
10
6
  #
11
7
  # Copyright (c) 2025 Gennady Kostyunin
12
- # Scruby is free software under terms of the MIT License.
13
- # Repository https://github.com/kebasyaty/scruby
8
+ # SPDX-License-Identifier: MIT
14
9
  #
15
10
  """Asynchronous library for building and managing a hybrid database, by scheme of key-value.
16
11
 
@@ -30,14 +25,11 @@ requires a large number of processor threads.
30
25
 
31
26
  from __future__ import annotations
32
27
 
33
- __all__ = ("Scruby",)
34
-
35
- import logging
36
-
37
- from scruby.db import Scruby
38
-
39
- logging.basicConfig(
40
- level=logging.INFO,
41
- datefmt="%Y-%m-%d %H:%M:%S",
42
- format="[%(asctime)s.%(msecs)03d] %(module)10s:%(lineno)-3d %(levelname)-7s - %(message)s",
28
+ __all__ = (
29
+ "settings",
30
+ "Scruby",
31
+ "ScrubyModel",
43
32
  )
33
+
34
+ from scruby import settings
35
+ from scruby.db import Scruby, ScrubyModel
scruby/aggregation.py CHANGED
@@ -1,3 +1,7 @@
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
+ #
1
5
  """Aggregation classes."""
2
6
 
3
7
  from __future__ import annotations
scruby/db.py CHANGED
@@ -1,22 +1,28 @@
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
+ #
1
5
  """Creation and management of the database."""
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
- __all__ = ("Scruby",)
9
+ __all__ = (
10
+ "Scruby",
11
+ "ScrubyModel",
12
+ )
6
13
 
7
14
  import contextlib
8
15
  import logging
9
16
  import re
10
17
  import zlib
18
+ from datetime import datetime
11
19
  from shutil import rmtree
12
20
  from typing import Any, Literal, Never, assert_never
13
21
 
14
22
  from anyio import Path
15
23
  from pydantic import BaseModel
16
24
 
17
- from scruby import constants, mixins
18
-
19
- logger = logging.getLogger(__name__)
25
+ from scruby import mixins, settings
20
26
 
21
27
 
22
28
  class _Meta(BaseModel):
@@ -29,8 +35,15 @@ class _Meta(BaseModel):
29
35
  counter_documents: int
30
36
 
31
37
 
38
+ class ScrubyModel(BaseModel):
39
+ """Additional fields for models."""
40
+
41
+ created_at: datetime | None = None
42
+ updated_at: datetime | None = None
43
+
44
+
32
45
  class Scruby(
33
- mixins.Docs,
46
+ mixins.Keys,
34
47
  mixins.Find,
35
48
  mixins.CustomTask,
36
49
  mixins.Collection,
@@ -45,21 +58,22 @@ class Scruby(
45
58
  ) -> None:
46
59
  super().__init__()
47
60
  self._meta = _Meta
48
- self._db_root = constants.DB_ROOT
49
- self._hash_reduce_left = constants.HASH_REDUCE_LEFT
61
+ self._db_root = settings.DB_ROOT
62
+ self._hash_reduce_left = settings.HASH_REDUCE_LEFT
63
+ self._max_workers = settings.MAX_WORKERS
50
64
  # The maximum number of branches.
51
65
  match self._hash_reduce_left:
52
66
  case 0:
53
- self._max_branch_number = 4294967296
67
+ self._max_number_branch = 4294967296
54
68
  case 2:
55
- self._max_branch_number = 16777216
69
+ self._max_number_branch = 16777216
56
70
  case 4:
57
- self._max_branch_number = 65536
71
+ self._max_number_branch = 65536
58
72
  case 6:
59
- self._max_branch_number = 256
73
+ self._max_number_branch = 256
60
74
  case _ as unreachable:
61
75
  msg: str = f"{unreachable} - Unacceptable value for HASH_REDUCE_LEFT."
62
- logger.critical(msg)
76
+ logging.critical(msg)
63
77
  assert_never(Never(unreachable)) # pyrefly: ignore[not-callable]
64
78
 
65
79
  @classmethod
@@ -67,42 +81,67 @@ class Scruby(
67
81
  """Get an object to access a collection.
68
82
 
69
83
  Args:
70
- class_model: Class of Model (pydantic.BaseModel).
84
+ class_model (Any): Class of Model (ScrubyModel).
71
85
 
72
86
  Returns:
73
87
  Instance of Scruby for access a collection.
74
88
  """
75
- assert BaseModel in class_model.__bases__, "`class_model` does not contain the base class `pydantic.BaseModel`!"
76
-
89
+ if __debug__:
90
+ # Check if the object belongs to the class `ScrubyModel`
91
+ if ScrubyModel not in class_model.__bases__:
92
+ msg = (
93
+ "Method: `collection` => argument `class_model` " + "does not contain the base class `ScrubyModel`!"
94
+ )
95
+ raise AssertionError(msg)
96
+ # Checking the model for the presence of a key.
97
+ model_fields = list(class_model.model_fields.keys())
98
+ if "key" not in model_fields:
99
+ msg = f"Model: {class_model.__name__} => The `key` field is missing!"
100
+ raise AssertionError(msg)
101
+ if "created_at" not in model_fields:
102
+ msg = f"Model: {class_model.__name__} => The `created_at` field is missing!"
103
+ raise AssertionError(msg)
104
+ if "updated_at" not in model_fields:
105
+ msg = f"Model: {class_model.__name__} => The `updated_at` field is missing!"
106
+ raise AssertionError(msg)
107
+ # Check the length of the collection name for an acceptable size.
108
+ len_db_root_absolut_path = len(str(await Path(settings.DB_ROOT).resolve()).encode("utf-8"))
109
+ len_model_name = len(class_model.__name__)
110
+ len_full_path_leaf = len_db_root_absolut_path + len_model_name + 26
111
+ if len_full_path_leaf > 255:
112
+ excess = len_full_path_leaf - 255
113
+ msg = (
114
+ f"Model: {class_model.__name__} => The collection name is too long, "
115
+ + f"it exceeds the limit of {excess} characters!"
116
+ )
117
+ raise AssertionError(msg)
118
+ # Create instance of Scruby
77
119
  instance = cls()
120
+ # Add model class to Scruby
78
121
  instance.__dict__["_class_model"] = class_model
79
- # Caching a pati for metadata.
80
- # The zero branch is reserved for metadata.
81
- branch_number: int = 0
82
- branch_number_as_hash: str = f"{branch_number:08x}"[constants.HASH_REDUCE_LEFT :]
83
- separated_hash: str = "/".join(list(branch_number_as_hash))
122
+ # Create a path for metadata.
84
123
  meta_dir_path_tuple = (
85
- constants.DB_ROOT,
124
+ settings.DB_ROOT,
86
125
  class_model.__name__,
87
- separated_hash,
126
+ "meta",
88
127
  )
89
128
  instance.__dict__["_meta_path"] = Path(
90
129
  *meta_dir_path_tuple,
91
130
  "meta.json",
92
131
  )
93
132
  # Create metadata for collection, if missing.
94
- branch_path = Path(*meta_dir_path_tuple)
95
- if not await branch_path.exists():
96
- await branch_path.mkdir(parents=True)
133
+ meta_dir_path = Path(*meta_dir_path_tuple)
134
+ if not await meta_dir_path.exists():
135
+ await meta_dir_path.mkdir(parents=True)
97
136
  meta = _Meta(
98
- db_root=constants.DB_ROOT,
137
+ db_root=settings.DB_ROOT,
99
138
  collection_name=class_model.__name__,
100
- hash_reduce_left=constants.HASH_REDUCE_LEFT,
101
- max_branch_number=instance.__dict__["_max_branch_number"],
139
+ hash_reduce_left=settings.HASH_REDUCE_LEFT,
140
+ max_branch_number=instance.__dict__["_max_number_branch"],
102
141
  counter_documents=0,
103
142
  )
104
143
  meta_json = meta.model_dump_json()
105
- meta_path = Path(*(branch_path, "meta.json"))
144
+ meta_path = Path(*(meta_dir_path, "meta.json"))
106
145
  await meta_path.write_text(meta_json, "utf-8")
107
146
  return instance
108
147
 
@@ -123,6 +162,9 @@ class Scruby(
123
162
 
124
163
  This method is for internal use.
125
164
 
165
+ Args:
166
+ meta (_Meta): Metadata of Collection.
167
+
126
168
  Returns:
127
169
  None.
128
170
  """
@@ -134,6 +176,9 @@ class Scruby(
134
176
 
135
177
  This method is for internal use.
136
178
 
179
+ Args:
180
+ step (Literal[1, -1]): Number of documents added or removed.
181
+
137
182
  Returns:
138
183
  None.
139
184
  """
@@ -157,7 +202,7 @@ class Scruby(
157
202
  """
158
203
  if not isinstance(key, str):
159
204
  msg = "The key is not a string."
160
- logger.error(msg)
205
+ logging.error(msg)
161
206
  raise KeyError(msg)
162
207
  # Prepare key.
163
208
  # Removes spaces at the beginning and end of a string.
@@ -166,7 +211,7 @@ class Scruby(
166
211
  # Check the key for an empty string.
167
212
  if len(prepared_key) == 0:
168
213
  msg = "The key should not be empty."
169
- logger.error(msg)
214
+ logging.error(msg)
170
215
  raise KeyError(msg)
171
216
  # Key to crc32 sum.
172
217
  key_as_hash: str = f"{zlib.crc32(prepared_key.encode('utf-8')):08x}"[self._hash_reduce_left :]
@@ -200,5 +245,5 @@ class Scruby(
200
245
  None.
201
246
  """
202
247
  with contextlib.suppress(FileNotFoundError):
203
- rmtree(constants.DB_ROOT)
248
+ rmtree(settings.DB_ROOT)
204
249
  return
scruby/errors.py CHANGED
@@ -1,3 +1,7 @@
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
+ #
1
5
  """Scruby Exceptions."""
2
6
 
3
7
  from __future__ import annotations
scruby/mixins/__init__.py CHANGED
@@ -1,3 +1,7 @@
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
+ #
1
5
  """Mixins."""
2
6
 
3
7
  from __future__ import annotations
@@ -8,7 +12,7 @@ __all__ = (
8
12
  "CustomTask",
9
13
  "Delete",
10
14
  "Find",
11
- "Docs",
15
+ "Keys",
12
16
  "Update",
13
17
  )
14
18
 
@@ -16,6 +20,6 @@ from scruby.mixins.collection import Collection
16
20
  from scruby.mixins.count import Count
17
21
  from scruby.mixins.custom_task import CustomTask
18
22
  from scruby.mixins.delete import Delete
19
- from scruby.mixins.docs import Docs
20
23
  from scruby.mixins.find import Find
24
+ from scruby.mixins.keys import Keys
21
25
  from scruby.mixins.update import Update
@@ -1,3 +1,7 @@
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
+ #
1
5
  """Methods for working with collections."""
2
6
 
3
7
  from __future__ import annotations
@@ -5,16 +9,13 @@ from __future__ import annotations
5
9
  __all__ = ("Collection",)
6
10
 
7
11
  from shutil import rmtree
8
- from typing import TypeVar
9
12
 
10
13
  from anyio import Path, to_thread
11
14
 
12
- from scruby import constants
15
+ from scruby import settings
13
16
 
14
- T = TypeVar("T")
15
17
 
16
-
17
- class Collection[T]:
18
+ class Collection:
18
19
  """Methods for working with collections."""
19
20
 
20
21
  def collection_name(self) -> str:
@@ -28,7 +29,7 @@ class Collection[T]:
28
29
  @staticmethod
29
30
  async def collection_list() -> list[str]:
30
31
  """Get collection list."""
31
- target_directory = Path(constants.DB_ROOT)
32
+ target_directory = Path(settings.DB_ROOT)
32
33
  # Get all entries in the directory
33
34
  all_entries = Path.iterdir(target_directory)
34
35
  directory_names: list[str] = [entry.name async for entry in all_entries]
@@ -44,6 +45,6 @@ class Collection[T]:
44
45
  Returns:
45
46
  None.
46
47
  """
47
- target_directory = f"{constants.DB_ROOT}/{name}"
48
+ target_directory = f"{settings.DB_ROOT}/{name}"
48
49
  await to_thread.run_sync(rmtree, target_directory) # pyrefly: ignore[bad-argument-type]
49
50
  return
scruby/mixins/count.py CHANGED
@@ -1,3 +1,7 @@
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
+ #
1
5
  """Methods for counting the number of documents."""
2
6
 
3
7
  from __future__ import annotations
@@ -6,12 +10,10 @@ __all__ = ("Count",)
6
10
 
7
11
  import concurrent.futures
8
12
  from collections.abc import Callable
9
- from typing import TypeVar
13
+ from typing import Any
10
14
 
11
- T = TypeVar("T")
12
15
 
13
-
14
- class Count[T]:
16
+ class Count:
15
17
  """Methods for counting the number of documents."""
16
18
 
17
19
  async def estimated_document_count(self) -> int:
@@ -26,30 +28,28 @@ class Count[T]:
26
28
  async def count_documents(
27
29
  self,
28
30
  filter_fn: Callable,
29
- max_workers: int | None = None,
30
31
  ) -> int:
31
32
  """Count the number of documents a matching the filter in this collection.
32
33
 
33
- The search is based on the effect of a quantum loop.
34
- The search effectiveness depends on the number of processor threads.
35
- Ideally, hundreds and even thousands of threads are required.
34
+ Attention:
35
+ - The search is based on the effect of a quantum loop.
36
+ - The search effectiveness depends on the number of processor threads.
36
37
 
37
38
  Args:
38
- filter_fn: A function that execute the conditions of filtering.
39
- max_workers: The maximum number of processes that can be used to
40
- execute the given calls. If None or not given then as many
41
- worker processes will be created as the machine has processors.
39
+ filter_fn (Callable): A function that execute the conditions of filtering.
42
40
 
43
41
  Returns:
44
42
  The number of documents.
45
43
  """
46
- branch_numbers: range = range(1, self._max_branch_number)
44
+ # Variable initialization
47
45
  search_task_fn: Callable = self._task_find
46
+ branch_numbers: range = range(self._max_number_branch)
48
47
  hash_reduce_left: int = self._hash_reduce_left
49
48
  db_root: str = self._db_root
50
- class_model: T = self._class_model
49
+ class_model: Any = self._class_model
51
50
  counter: int = 0
52
- with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
51
+ # Run quantum loop
52
+ with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
53
53
  for branch_number in branch_numbers:
54
54
  future = executor.submit(
55
55
  search_task_fn,
@@ -1,22 +1,21 @@
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
+ #
1
5
  """Quantum methods for running custom tasks."""
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
9
  __all__ = ("CustomTask",)
6
10
 
7
- import logging
8
11
  from collections.abc import Callable
9
- from typing import Any, TypeVar
12
+ from typing import Any
10
13
 
11
14
  import orjson
12
15
  from anyio import Path
13
16
 
14
- logger = logging.getLogger(__name__)
15
17
 
16
- T = TypeVar("T")
17
-
18
-
19
- class CustomTask[T]:
18
+ class CustomTask:
20
19
  """Quantum methods for running custom tasks."""
21
20
 
22
21
  @staticmethod
@@ -24,7 +23,7 @@ class CustomTask[T]:
24
23
  branch_number: int,
25
24
  hash_reduce_left: int,
26
25
  db_root: str,
27
- class_model: T,
26
+ class_model: Any,
28
27
  ) -> list[Any]:
29
28
  """Get documents for custom task.
30
29
 
@@ -51,26 +50,25 @@ class CustomTask[T]:
51
50
  docs.append(class_model.model_validate_json(val))
52
51
  return docs
53
52
 
54
- async def run_custom_task(self, custom_task_fn: Callable, limit_docs: int = 1000) -> Any:
53
+ async def run_custom_task(self, custom_task_fn: Callable) -> Any:
55
54
  """Running custom task.
56
55
 
57
- This method running a task created on the basis of a quantum loop.
58
- Effectiveness running task depends on the number of processor threads.
59
- Ideally, hundreds and even thousands of threads are required.
56
+ Attention:
57
+ - The search is based on the effect of a quantum loop.
58
+ - The search effectiveness depends on the number of processor threads.
60
59
 
61
60
  Args:
62
- custom_task_fn: A function that execute the custom task.
63
- limit_docs: Limiting the number of documents. By default = 1000.
61
+ custom_task_fn (Callable): A function that execute the custom task.
64
62
 
65
63
  Returns:
66
64
  The result of a custom task.
67
65
  """
68
66
  kwargs = {
69
67
  "get_docs_fn": self._task_get_docs,
70
- "branch_numbers": range(1, self._max_branch_number),
68
+ "branch_numbers": range(self._max_number_branch),
71
69
  "hash_reduce_left": self._hash_reduce_left,
72
70
  "db_root": self._db_root,
73
71
  "class_model": self._class_model,
74
- "limit_docs": limit_docs,
72
+ "max_workers": self._max_workers,
75
73
  }
76
74
  return await custom_task_fn(**kwargs)
scruby/mixins/delete.py CHANGED
@@ -1,3 +1,7 @@
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
+ #
1
5
  """Methods for deleting documents."""
2
6
 
3
7
  from __future__ import annotations
@@ -5,19 +9,14 @@ from __future__ import annotations
5
9
  __all__ = ("Delete",)
6
10
 
7
11
  import concurrent.futures
8
- import logging
9
12
  from collections.abc import Callable
10
- from typing import TypeVar
13
+ from typing import Any
11
14
 
12
15
  import orjson
13
16
  from anyio import Path
14
17
 
15
- logger = logging.getLogger(__name__)
16
18
 
17
- T = TypeVar("T")
18
-
19
-
20
- class Delete[T]:
19
+ class Delete:
21
20
  """Methods for deleting documents."""
22
21
 
23
22
  @staticmethod
@@ -26,7 +25,7 @@ class Delete[T]:
26
25
  filter_fn: Callable,
27
26
  hash_reduce_left: int,
28
27
  db_root: str,
29
- class_model: T,
28
+ class_model: Any,
30
29
  ) -> int:
31
30
  """Task for find and delete documents.
32
31
 
@@ -62,30 +61,28 @@ class Delete[T]:
62
61
  async def delete_many(
63
62
  self,
64
63
  filter_fn: Callable,
65
- max_workers: int | None = None,
66
64
  ) -> int:
67
65
  """Delete one or more documents matching the filter.
68
66
 
69
- The search is based on the effect of a quantum loop.
70
- The search effectiveness depends on the number of processor threads.
71
- Ideally, hundreds and even thousands of threads are required.
67
+ Attention:
68
+ - The search is based on the effect of a quantum loop.
69
+ - The search effectiveness depends on the number of processor threads.
72
70
 
73
71
  Args:
74
- filter_fn: A function that execute the conditions of filtering.
75
- max_workers: The maximum number of processes that can be used to
76
- execute the given calls. If None or not given then as many
77
- worker processes will be created as the machine has processors.
72
+ filter_fn (Callable): A function that execute the conditions of filtering.
78
73
 
79
74
  Returns:
80
75
  The number of deleted documents.
81
76
  """
82
- branch_numbers: range = range(1, self._max_branch_number)
77
+ # Variable initialization
83
78
  search_task_fn: Callable = self._task_delete
79
+ branch_numbers: range = range(self._max_number_branch)
84
80
  hash_reduce_left: int = self._hash_reduce_left
85
81
  db_root: str = self._db_root
86
- class_model: T = self._class_model
82
+ class_model: Any = self._class_model
87
83
  counter: int = 0
88
- with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
84
+ # Run quantum loop
85
+ with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
89
86
  for branch_number in branch_numbers:
90
87
  future = executor.submit(
91
88
  search_task_fn,
scruby/mixins/find.py CHANGED
@@ -1,3 +1,7 @@
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
+ #
1
5
  """Quantum methods for searching documents."""
2
6
 
3
7
  from __future__ import annotations
@@ -5,19 +9,14 @@ from __future__ import annotations
5
9
  __all__ = ("Find",)
6
10
 
7
11
  import concurrent.futures
8
- import logging
9
12
  from collections.abc import Callable
10
- from typing import TypeVar
13
+ from typing import Any
11
14
 
12
15
  import orjson
13
16
  from anyio import Path
14
17
 
15
- logger = logging.getLogger(__name__)
16
18
 
17
- T = TypeVar("T")
18
-
19
-
20
- class Find[T]:
19
+ class Find:
21
20
  """Quantum methods for searching documents."""
22
21
 
23
22
  @staticmethod
@@ -26,8 +25,8 @@ class Find[T]:
26
25
  filter_fn: Callable,
27
26
  hash_reduce_left: str,
28
27
  db_root: str,
29
- class_model: T,
30
- ) -> list[T] | None:
28
+ class_model: Any,
29
+ ) -> list[Any] | None:
31
30
  """Task for find documents.
32
31
 
33
32
  This method is for internal use.
@@ -35,6 +34,7 @@ class Find[T]:
35
34
  Returns:
36
35
  List of documents or None.
37
36
  """
37
+ # Variable initialization
38
38
  branch_number_as_hash: str = f"{branch_number:08x}"[hash_reduce_left:]
39
39
  separated_hash: str = "/".join(list(branch_number_as_hash))
40
40
  leaf_path: Path = Path(
@@ -45,7 +45,7 @@ class Find[T]:
45
45
  "leaf.json",
46
46
  ),
47
47
  )
48
- docs: list[T] = []
48
+ docs: list[Any] = []
49
49
  if await leaf_path.exists():
50
50
  data_json: bytes = await leaf_path.read_bytes()
51
51
  data: dict[str, str] = orjson.loads(data_json) or {}
@@ -58,29 +58,27 @@ class Find[T]:
58
58
  async def find_one(
59
59
  self,
60
60
  filter_fn: Callable,
61
- max_workers: int | None = None,
62
- ) -> T | None:
63
- """Finds a single document matching the filter.
61
+ ) -> Any | None:
62
+ """Find one document matching the filter.
64
63
 
65
- The search is based on the effect of a quantum loop.
66
- The search effectiveness depends on the number of processor threads.
67
- Ideally, hundreds and even thousands of threads are required.
64
+ Attention:
65
+ - The search is based on the effect of a quantum loop.
66
+ - The search effectiveness depends on the number of processor threads.
68
67
 
69
68
  Args:
70
- filter_fn: A function that execute the conditions of filtering.
71
- max_workers: The maximum number of processes that can be used to
72
- execute the given calls. If None or not given then as many
73
- worker processes will be created as the machine has processors.
69
+ filter_fn (Callable): A function that execute the conditions of filtering.
74
70
 
75
71
  Returns:
76
72
  Document or None.
77
73
  """
78
- branch_numbers: range = range(1, self._max_branch_number)
74
+ # Variable initialization
79
75
  search_task_fn: Callable = self._task_find
76
+ branch_numbers: range = range(self._max_number_branch)
80
77
  hash_reduce_left: int = self._hash_reduce_left
81
78
  db_root: str = self._db_root
82
- class_model: T = self._class_model
83
- with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
79
+ class_model: Any = self._class_model
80
+ # Run quantum loop
81
+ with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
84
82
  for branch_number in branch_numbers:
85
83
  future = executor.submit(
86
84
  search_task_fn,
@@ -97,36 +95,41 @@ class Find[T]:
97
95
 
98
96
  async def find_many(
99
97
  self,
100
- filter_fn: Callable,
98
+ filter_fn: Callable = lambda _: True,
101
99
  limit_docs: int = 1000,
102
- max_workers: int | None = None,
103
- ) -> list[T] | None:
104
- """Finds one or more documents matching the filter.
100
+ page_number: int = 1,
101
+ ) -> list[Any] | None:
102
+ """Find many documents matching the filter.
105
103
 
106
- The search is based on the effect of a quantum loop.
107
- The search effectiveness depends on the number of processor threads.
108
- Ideally, hundreds and even thousands of threads are required.
104
+ Attention:
105
+ - The search is based on the effect of a quantum loop.
106
+ - The search effectiveness depends on the number of processor threads.
109
107
 
110
108
  Args:
111
- filter_fn: A function that execute the conditions of filtering.
112
- limit_docs: Limiting the number of documents. By default = 1000.
113
- max_workers: The maximum number of processes that can be used to
114
- execute the given calls. If None or not given then as many
115
- worker processes will be created as the machine has processors.
109
+ filter_fn (Callable): A function that execute the conditions of filtering.
110
+ By default it searches for all documents.
111
+ limit_docs (int): Limiting the number of documents. By default = 1000.
112
+ page_number (int): For pagination output. By default = 1.
113
+ Number of documents per page = limit_docs.
116
114
 
117
115
  Returns:
118
116
  List of documents or None.
119
117
  """
120
- branch_numbers: range = range(1, self._max_branch_number)
118
+ # The `page_number` parameter must not be less than one
119
+ assert page_number > 0, "`find_many` => The `page_number` parameter must not be less than one."
120
+ # Variable initialization
121
121
  search_task_fn: Callable = self._task_find
122
+ branch_numbers: range = range(self._max_number_branch)
122
123
  hash_reduce_left: int = self._hash_reduce_left
123
124
  db_root: str = self._db_root
124
- class_model: T = self._class_model
125
+ class_model: Any = self._class_model
125
126
  counter: int = 0
126
- result: list[T] = []
127
- with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
127
+ number_docs_skippe: int = limit_docs * (page_number - 1) if page_number > 1 else 0
128
+ result: list[Any] = []
129
+ # Run quantum loop
130
+ with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
128
131
  for branch_number in branch_numbers:
129
- if counter >= limit_docs:
132
+ if number_docs_skippe == 0 and counter >= limit_docs:
130
133
  return result[:limit_docs]
131
134
  future = executor.submit(
132
135
  search_task_fn,
@@ -139,8 +142,11 @@ class Find[T]:
139
142
  docs = await future.result()
140
143
  if docs is not None:
141
144
  for doc in docs:
142
- if counter >= limit_docs:
143
- return result[:limit_docs]
144
- result.append(doc)
145
- counter += 1
145
+ if number_docs_skippe == 0:
146
+ if counter >= limit_docs:
147
+ return result[:limit_docs]
148
+ result.append(doc)
149
+ counter += 1
150
+ else:
151
+ number_docs_skippe -= 1
146
152
  return result or None
@@ -1,11 +1,17 @@
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
+ #
1
5
  """Methods for working with keys."""
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
- __all__ = ("Docs",)
9
+ __all__ = ("Keys",)
6
10
 
7
11
  import logging
12
+ from datetime import datetime
8
13
  from typing import Any
14
+ from zoneinfo import ZoneInfo
9
15
 
10
16
  import orjson
11
17
 
@@ -14,17 +20,15 @@ from scruby.errors import (
14
20
  KeyNotExistsError,
15
21
  )
16
22
 
17
- logger = logging.getLogger(__name__)
18
23
 
19
-
20
- class Docs:
21
- """Methods for working with document."""
24
+ class Keys:
25
+ """Methods for working with keys."""
22
26
 
23
27
  async def add_doc(self, doc: Any) -> None:
24
28
  """Asynchronous method for adding document to collection.
25
29
 
26
30
  Args:
27
- doc: Value of key. Type, derived from `BaseModel`.
31
+ doc (Any): Value of key. Type, derived from `ScrubyModel`.
28
32
 
29
33
  Returns:
30
34
  None.
@@ -36,10 +40,15 @@ class Docs:
36
40
  msg = (
37
41
  f"(add_doc) Parameter `doc` => Model `{doc_class_name}` does not match collection `{collection_name}`!"
38
42
  )
39
- logger.error(msg)
43
+ logging.error(msg)
40
44
  raise TypeError(msg)
41
45
  # The path to cell of collection.
42
46
  leaf_path, prepared_key = await self._get_leaf_path(doc.key)
47
+ # Init a `created_at` and `updated_at` fields
48
+ tz = ZoneInfo("UTC")
49
+ doc.created_at = datetime.now(tz)
50
+ doc.updated_at = datetime.now(tz)
51
+ # Convert doc to json
43
52
  doc_json: str = doc.model_dump_json()
44
53
  # Write key-value to collection.
45
54
  if await leaf_path.exists():
@@ -53,7 +62,7 @@ class Docs:
53
62
  await leaf_path.write_bytes(orjson.dumps(data))
54
63
  else:
55
64
  err = KeyAlreadyExistsError()
56
- logger.error(err.message)
65
+ logging.error(err.message)
57
66
  raise err
58
67
  else:
59
68
  # Add new document to a blank leaf.
@@ -61,10 +70,10 @@ class Docs:
61
70
  await self._counter_documents(1)
62
71
 
63
72
  async def update_doc(self, doc: Any) -> None:
64
- """Asynchronous method for updating key to collection.
73
+ """Asynchronous method for updating document to collection.
65
74
 
66
75
  Args:
67
- doc: Value of key. Type `BaseModel`.
76
+ doc (Any): Value of key. Type `ScrubyModel`.
68
77
 
69
78
  Returns:
70
79
  None.
@@ -77,10 +86,13 @@ class Docs:
77
86
  f"(update_doc) Parameter `doc` => Model `{doc_class_name}` "
78
87
  f"does not match collection `{collection_name}`!"
79
88
  )
80
- logger.error(msg)
89
+ logging.error(msg)
81
90
  raise TypeError(msg)
82
91
  # The path to cell of collection.
83
92
  leaf_path, prepared_key = await self._get_leaf_path(doc.key)
93
+ # Update a `updated_at` field
94
+ doc.updated_at = datetime.now(ZoneInfo("UTC"))
95
+ # Convert doc to json
84
96
  doc_json: str = doc.model_dump_json()
85
97
  # Update the existing key.
86
98
  if await leaf_path.exists():
@@ -93,17 +105,18 @@ class Docs:
93
105
  await leaf_path.write_bytes(orjson.dumps(data))
94
106
  except KeyError:
95
107
  err = KeyNotExistsError()
96
- logger.error(err.message)
108
+ logging.error(err.message)
97
109
  raise err from None
98
110
  else:
99
- logger.error("The key not exists.")
100
- raise KeyError()
111
+ msg: str = f"`update_doc` - The key `{doc.key}` is missing!"
112
+ logging.error(msg)
113
+ raise KeyError(msg)
101
114
 
102
- async def get_key(self, key: str) -> Any:
103
- """Asynchronous method for getting value of key from collection.
115
+ async def get_doc(self, key: str) -> Any:
116
+ """Asynchronous method for getting document from collection the by key.
104
117
 
105
118
  Args:
106
- key: Key name.
119
+ key (str): Key name.
107
120
 
108
121
  Returns:
109
122
  Value of key or KeyError.
@@ -116,15 +129,15 @@ class Docs:
116
129
  data: dict = orjson.loads(data_json) or {}
117
130
  obj: Any = self._class_model.model_validate_json(data[prepared_key])
118
131
  return obj
119
- msg: str = "`get_key` - The unacceptable key value."
120
- logger.error(msg)
121
- raise KeyError()
132
+ msg: str = f"`get_doc` - The key `{key}` is missing!"
133
+ logging.error(msg)
134
+ raise KeyError(msg)
122
135
 
123
136
  async def has_key(self, key: str) -> bool:
124
137
  """Asynchronous method for checking presence of key in collection.
125
138
 
126
139
  Args:
127
- key: Key name.
140
+ key (str): Key name.
128
141
 
129
142
  Returns:
130
143
  True, if the key is present.
@@ -142,11 +155,11 @@ class Docs:
142
155
  return False
143
156
  return False
144
157
 
145
- async def delete_key(self, key: str) -> None:
146
- """Asynchronous method for deleting key from collection.
158
+ async def delete_doc(self, key: str) -> None:
159
+ """Asynchronous method for deleting document from collection the by key.
147
160
 
148
161
  Args:
149
- key: Key name.
162
+ key (str): Key name.
150
163
 
151
164
  Returns:
152
165
  None.
@@ -161,6 +174,6 @@ class Docs:
161
174
  await leaf_path.write_bytes(orjson.dumps(data))
162
175
  await self._counter_documents(-1)
163
176
  return
164
- msg: str = "`delete_key` - The unacceptable key value."
165
- logger.error(msg)
166
- raise KeyError()
177
+ msg: str = f"`delete_doc` - The key `{key}` is missing!"
178
+ logging.error(msg)
179
+ raise KeyError(msg)
scruby/mixins/update.py CHANGED
@@ -1,3 +1,7 @@
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
+ #
1
5
  """Methods for updating documents."""
2
6
 
3
7
  from __future__ import annotations
@@ -5,19 +9,14 @@ from __future__ import annotations
5
9
  __all__ = ("Update",)
6
10
 
7
11
  import concurrent.futures
8
- import logging
9
12
  from collections.abc import Callable
10
- from typing import Any, TypeVar
13
+ from typing import Any
11
14
 
12
15
  import orjson
13
16
  from anyio import Path
14
17
 
15
- logger = logging.getLogger(__name__)
16
18
 
17
- T = TypeVar("T")
18
-
19
-
20
- class Update[T]:
19
+ class Update:
21
20
  """Methods for updating documents."""
22
21
 
23
22
  @staticmethod
@@ -26,7 +25,7 @@ class Update[T]:
26
25
  filter_fn: Callable,
27
26
  hash_reduce_left: str,
28
27
  db_root: str,
29
- class_model: T,
28
+ class_model: Any,
30
29
  new_data: dict[str, Any],
31
30
  ) -> int:
32
31
  """Task for find documents.
@@ -63,33 +62,33 @@ class Update[T]:
63
62
 
64
63
  async def update_many(
65
64
  self,
66
- filter_fn: Callable,
67
65
  new_data: dict[str, Any],
68
- max_workers: int | None = None,
66
+ filter_fn: Callable = lambda _: True,
69
67
  ) -> int:
70
- """Updates one or more documents matching the filter.
68
+ """Updates many documents matching the filter.
71
69
 
72
- The search is based on the effect of a quantum loop.
73
- The search effectiveness depends on the number of processor threads.
74
- Ideally, hundreds and even thousands of threads are required.
70
+ Attention:
71
+ - For a complex case, a custom task may be needed.
72
+ - See documentation on creating custom tasks.
73
+ - The search is based on the effect of a quantum loop.
74
+ - The search effectiveness depends on the number of processor threads.
75
75
 
76
76
  Args:
77
- filter_fn: A function that execute the conditions of filtering.
78
- new_data: New data for the fields that need to be updated.
79
- max_workers: The maximum number of processes that can be used to
80
- execute the given calls. If None or not given then as many
81
- worker processes will be created as the machine has processors.
77
+ filter_fn (Callable): A function that execute the conditions of filtering.
78
+ new_data (dict[str, Any]): New data for the fields that need to be updated.
82
79
 
83
80
  Returns:
84
81
  The number of updated documents.
85
82
  """
86
- branch_numbers: range = range(1, self._max_branch_number)
83
+ # Variable initialization
87
84
  update_task_fn: Callable = self._task_update
85
+ branch_numbers: range = range(self._max_number_branch)
88
86
  hash_reduce_left: int = self._hash_reduce_left
89
87
  db_root: str = self._db_root
90
- class_model: T = self._class_model
88
+ class_model: Any = self._class_model
91
89
  counter: int = 0
92
- with concurrent.futures.ThreadPoolExecutor(max_workers) as executor:
90
+ # Run quantum loop
91
+ with concurrent.futures.ThreadPoolExecutor(self._max_workers) as executor:
93
92
  for branch_number in branch_numbers:
94
93
  future = executor.submit(
95
94
  update_task_fn,
@@ -1,6 +1,10 @@
1
- """Constant variables.
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.
2
6
 
3
- The module contains the following variables:
7
+ The module contains the following parameters:
4
8
 
5
9
  - `DB_ROOT` - Path to root directory of database. `By default = "ScrubyDB" (in root of project)`.
6
10
  - `HASH_REDUCE_LEFT` - The length of the hash reduction on the left side.
@@ -8,6 +12,7 @@ The module contains the following variables:
8
12
  - `2` - 16777216 branches in collection.
9
13
  - `4` - 65536 branches in collection.
10
14
  - `6` - 256 branches in collection (by default).
15
+ - `MAX_WORKERS` - The maximum number of processes that can be used `By default = None`.
11
16
  """
12
17
 
13
18
  from __future__ import annotations
@@ -15,6 +20,7 @@ from __future__ import annotations
15
20
  __all__ = (
16
21
  "DB_ROOT",
17
22
  "HASH_REDUCE_LEFT",
23
+ "MAX_WORKERS",
18
24
  )
19
25
 
20
26
  from typing import Literal
@@ -31,3 +37,8 @@ DB_ROOT: str = "ScrubyDB"
31
37
  # Number of branches is number of requests to the hard disk during quantum operations.
32
38
  # Quantum operations: find_one, find_many, count_documents, delete_many, run_custom_task.
33
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scruby
3
- Version: 0.26.0
3
+ Version: 0.30.2
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
@@ -17,6 +17,7 @@ Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: MacOS :: MacOS X
18
18
  Classifier: Operating System :: Microsoft :: Windows
19
19
  Classifier: Operating System :: POSIX
20
+ Classifier: Operating System :: POSIX :: Linux
20
21
  Classifier: Programming Language :: Python :: 3
21
22
  Classifier: Programming Language :: Python :: 3 :: Only
22
23
  Classifier: Programming Language :: Python :: 3.12
@@ -98,6 +99,15 @@ Online browsable documentation is available at [https://kebasyaty.github.io/scru
98
99
  uv add scruby
99
100
  ```
100
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
+
101
111
  ## Usage
102
112
 
103
113
  See more examples here [https://kebasyaty.github.io/scruby/latest/pages/usage/](https://kebasyaty.github.io/scruby/latest/pages/usage/ "Examples").
@@ -106,23 +116,25 @@ See more examples here [https://kebasyaty.github.io/scruby/latest/pages/usage/](
106
116
  """Working with keys."""
107
117
 
108
118
  import anyio
109
- import datetime
119
+ from datetime import datetime
120
+ from zoneinfo import ZoneInfo
110
121
  from typing import Annotated
111
- from pydantic import BaseModel, EmailStr, Field
122
+ from pydantic import EmailStr, Field
112
123
  from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator
113
- from scruby import Scruby, constants
124
+ from scruby import Scruby, ScrubyModel, settings
114
125
 
115
- constants.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
116
- constants.HASH_REDUCE_LEFT = 6 # By default = 6
126
+ settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
127
+ settings.HASH_REDUCE_LEFT = 6 # By default = 6
128
+ settings.MAX_WORKERS = None # By default = None
117
129
 
118
- class User(BaseModel):
130
+ class User(ScrubyModel):
119
131
  """User model."""
120
132
  first_name: str = Field(strict=True)
121
133
  last_name: str = Field(strict=True)
122
- birthday: datetime.datetime = Field(strict=True)
134
+ birthday: datetime = Field(strict=True)
123
135
  email: EmailStr = Field(strict=True)
124
136
  phone: Annotated[PhoneNumber, PhoneNumberValidator(number_format="E164")] = Field(frozen=True)
125
- # The key is always at the bottom
137
+ # key is always at bottom
126
138
  key: str = Field(
127
139
  strict=True,
128
140
  frozen=True,
@@ -132,13 +144,13 @@ class User(BaseModel):
132
144
 
133
145
  async def main() -> None:
134
146
  """Example."""
135
- # Get collection of `User`.
147
+ # Get collection `User`.
136
148
  user_coll = await Scruby.collection(User)
137
149
 
138
150
  user = User(
139
151
  first_name="John",
140
152
  last_name="Smith",
141
- birthday=datetime.datetime(1970, 1, 1),
153
+ birthday=datetime.datetime(1970, 1, 1, tzinfo=ZoneInfo("UTC")),
142
154
  email="John_Smith@gmail.com",
143
155
  phone="+447986123456",
144
156
  )
@@ -147,15 +159,15 @@ async def main() -> None:
147
159
 
148
160
  await user_coll.update_doc(user)
149
161
 
150
- await user_coll.get_key("+447986123456") # => user
151
- await user_coll.get_key("key missing") # => KeyError
162
+ await user_coll.get_doc("+447986123456") # => user
163
+ await user_coll.get_doc("key missing") # => KeyError
152
164
 
153
165
  await user_coll.has_key("+447986123456") # => True
154
166
  await user_coll.has_key("key missing") # => False
155
167
 
156
- await user_coll.delete_key("+447986123456")
157
- await user_coll.delete_key("+447986123456") # => KeyError
158
- await user_coll.delete_key("key missing") # => KeyError
168
+ await user_coll.delete_doc("+447986123456")
169
+ await user_coll.delete_doc("+447986123456") # => KeyError
170
+ await user_coll.delete_doc("key missing") # => KeyError
159
171
 
160
172
  # Full database deletion.
161
173
  # Hint: The main purpose is tests.
@@ -167,31 +179,31 @@ if __name__ == "__main__":
167
179
  ```
168
180
 
169
181
  ```python
170
- """Find a single document matching the filter.
182
+ """Find one document matching the filter.
171
183
 
172
184
  The search is based on the effect of a quantum loop.
173
185
  The search effectiveness depends on the number of processor threads.
174
- Ideally, hundreds and even thousands of threads are required.
175
186
  """
176
187
 
177
188
  import anyio
178
- import datetime
189
+ from datetime import datetime
179
190
  from typing import Annotated
180
- from pydantic import BaseModel, Field
181
- from scruby import Scruby, constants
191
+ from pydantic import Field
192
+ from scruby import Scruby, ScrubyModel, settings
182
193
  from pprint import pprint as pp
183
194
 
184
- constants.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
185
- constants.HASH_REDUCE_LEFT = 6 # By default = 6
195
+ settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
196
+ settings.HASH_REDUCE_LEFT = 6 # By default = 6
197
+ settings.MAX_WORKERS = None # By default = None
186
198
 
187
199
 
188
- class Phone(BaseModel):
200
+ class Phone(ScrubyModel):
189
201
  """Phone model."""
190
202
  brand: str = Field(strict=True, frozen=True)
191
203
  model: str = Field(strict=True, frozen=True)
192
204
  screen_diagonal: float = Field(strict=True)
193
205
  matrix_type: str = Field(strict=True)
194
- # The key is always at the bottom
206
+ # key is always at bottom
195
207
  key: str = Field(
196
208
  strict=True,
197
209
  frozen=True,
@@ -201,7 +213,7 @@ class Phone(BaseModel):
201
213
 
202
214
  async def main() -> None:
203
215
  """Example."""
204
- # Get collection of `Phone`.
216
+ # Get collection `Phone`.
205
217
  phone_coll = await Scruby.collection(Phone)
206
218
 
207
219
  # Create phone.
@@ -243,31 +255,31 @@ if __name__ == "__main__":
243
255
  ```
244
256
 
245
257
  ```python
246
- """Find one or more documents matching the filter.
258
+ """Find many documents matching the filter.
247
259
 
248
260
  The search is based on the effect of a quantum loop.
249
261
  The search effectiveness depends on the number of processor threads.
250
- Ideally, hundreds and even thousands of threads are required.
251
262
  """
252
263
 
253
264
  import anyio
254
- import datetime
265
+ from datetime import datetime
255
266
  from typing import Annotated
256
- from pydantic import BaseModel, Field
257
- from scruby import Scruby, constants
267
+ from pydantic import Field
268
+ from scruby import Scruby, ScrubyModel, settings
258
269
  from pprint import pprint as pp
259
270
 
260
- constants.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
261
- constants.HASH_REDUCE_LEFT = 6 # By default = 6
271
+ settings.DB_ROOT = "ScrubyDB" # By default = "ScrubyDB"
272
+ settings.HASH_REDUCE_LEFT = 6 # By default = 6
273
+ settings.MAX_WORKERS = None # By default = None
262
274
 
263
275
 
264
- class Car(BaseModel):
276
+ class Car(ScrubyModel):
265
277
  """Car model."""
266
278
  brand: str = Field(strict=True, frozen=True)
267
279
  model: str = Field(strict=True, frozen=True)
268
280
  year: int = Field(strict=True)
269
281
  power_reserve: int = Field(strict=True)
270
- # The key is always at the bottom
282
+ # key is always at bottom
271
283
  key: str = Field(
272
284
  strict=True,
273
285
  frozen=True,
@@ -277,11 +289,11 @@ class Car(BaseModel):
277
289
 
278
290
  async def main() -> None:
279
291
  """Example."""
280
- # Get collection of `Car`.
292
+ # Get collection `Car`.
281
293
  car_coll = await Scruby.collection(Car)
282
294
 
283
295
  # Create cars.
284
- for name in range(1, 10):
296
+ for num in range(1, 10):
285
297
  car = Car(
286
298
  brand="Mazda",
287
299
  model=f"EZ-6 {num}",
@@ -299,9 +311,23 @@ async def main() -> None:
299
311
  else:
300
312
  print("No cars!")
301
313
 
302
- # Get collection list.
303
- collection_list = await Scruby.collection_list()
304
- print(collection_list) # ["Car"]
314
+ # Find all cars.
315
+ car_list: list[Car] | None = await car_coll.find_many()
316
+ if car_list is not None:
317
+ pp(car_list)
318
+ else:
319
+ print("No cars!")
320
+
321
+ # For pagination output.
322
+ car_list: list[Car] | None = await car_coll.find_many(
323
+ filter_fn=lambda doc: doc.brand == "Mazda",
324
+ limit_docs=5,
325
+ page_number=2,
326
+ )
327
+ if car_list is not None:
328
+ pp(car_list)
329
+ else:
330
+ print("No cars!")
305
331
 
306
332
  # Full database deletion.
307
333
  # Hint: The main purpose is tests.
@@ -0,0 +1,18 @@
1
+ scruby/__init__.py,sha256=WuIA27d3uUK0Vo0fXg92hge7i6v_2DBndhfepK16_y8,1046
2
+ scruby/aggregation.py,sha256=bd70J1Xye6faNHD8LS3lVQoHWKtPdPV_cqT_i7oui38,3491
3
+ scruby/db.py,sha256=4bSPMh0fYil0j9qhHhRu1g4U_0WaCuaGPthnmi2QpBI,8376
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=YHjonxAtroSc5AlbDX1vpCvbe3vsTD0LfYnjEDB001A,2089
10
+ scruby/mixins/custom_task.py,sha256=ENr3FkCsPWRblOWv8jGMkkGKw4hvp9mMP2YjQvIeqzE,2246
11
+ scruby/mixins/delete.py,sha256=b7RYiTLUqVu70ep15CN1VTyKs13P7f-1YDGRc3ke-2g,3085
12
+ scruby/mixins/find.py,sha256=-rpILkxhfywJ5E3ceYo9dSPabma47ouceGajUVNh23Q,5396
13
+ scruby/mixins/keys.py,sha256=waxye5n0-oTWIhdDXGQkUTGaV9vLSCc9lCryTCuexkw,6179
14
+ scruby/mixins/update.py,sha256=WeNk2qZToQS3-r1_ahazBc2ErLMU9K5RRNgCxbu3JMg,3416
15
+ scruby-0.30.2.dist-info/METADATA,sha256=OgV4Foj3-X97NKFLpw_8Qb_osGwmYIINO2UZkgdLaY0,11032
16
+ scruby-0.30.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ scruby-0.30.2.dist-info/licenses/LICENSE,sha256=mS0Wz0yGNB63gEcWEnuIb_lldDYV0sjRaO-o_GL6CWE,1074
18
+ scruby-0.30.2.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- scruby/__init__.py,sha256=iYjvi002DeRh-U_ND2cCOHlbX2xwxN8IIhsposeotNw,1504
2
- scruby/aggregation.py,sha256=SYGcnMy2eq9vJb-pW3xR9LLAQIQ55TK-LGW_yKQ-7sU,3318
3
- scruby/constants.py,sha256=KInSZ_4dsQNXilrs7DvtQXevKEYibnNzl69a7XiWG4k,1099
4
- scruby/db.py,sha256=06GjnhN9lKvZo585nxKFd4z8Ox858Ep08c7eCbMA99k,6462
5
- scruby/errors.py,sha256=aj1zQlfxGwZC-bZZ07DRX2vHx31SpyWPqXHMpQ9kRVY,1124
6
- scruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- scruby/mixins/__init__.py,sha256=w1Be13FHAGSkdRfXcmoZ-eDn5Q8cFsPRAV7k1tXkIwY,454
8
- scruby/mixins/collection.py,sha256=kqUgzJbgG9pTZhlP7OD5DsOaArRzu0fl6fVibLAdNtk,1260
9
- scruby/mixins/count.py,sha256=Wcn6CeWrYSgsTTmYQ4J-CEiM4630rUSwRP9iKwbCl6c,2193
10
- scruby/mixins/custom_task.py,sha256=DL-pQZninz7CJUyRYlVV7SNPC60qMD3ZQyMLnC3zVTM,2294
11
- scruby/mixins/delete.py,sha256=BmfQH68iX7kzC20w16xzFcLO3uLxYKdNyqZqIbXb1M0,3240
12
- scruby/mixins/docs.py,sha256=UHawXUjIkDBtik6MIQwbPF3DZKSOG8WI4Da9_i_-9R4,5533
13
- scruby/mixins/find.py,sha256=va1hTm6Poua7_TMcZW2iqI-xmL1HcCUOx8pkKvTvu6U,5063
14
- scruby/mixins/update.py,sha256=A9V4PjA3INnqLTGoBxIvC8y8Wo-nLxlFejkPUhsebzQ,3428
15
- scruby-0.26.0.dist-info/METADATA,sha256=CluDLzRgB952ZDbbxdsXuHGE598BzIiF5eYXpGaNdj4,10483
16
- scruby-0.26.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
- scruby-0.26.0.dist-info/licenses/LICENSE,sha256=mS0Wz0yGNB63gEcWEnuIb_lldDYV0sjRaO-o_GL6CWE,1074
18
- scruby-0.26.0.dist-info/RECORD,,