mongospec 0.2__tar.gz → 0.2.2__tar.gz
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.
- {mongospec-0.2/mongospec.egg-info → mongospec-0.2.2}/PKG-INFO +63 -1
- {mongospec-0.2 → mongospec-0.2.2}/README.md +62 -0
- mongospec-0.2.2/mongospec/contrib/__init__.py +0 -0
- mongospec-0.2.2/mongospec/contrib/kv_store.py +280 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/document.py +17 -1
- {mongospec-0.2 → mongospec-0.2.2/mongospec.egg-info}/PKG-INFO +63 -1
- {mongospec-0.2 → mongospec-0.2.2}/mongospec.egg-info/SOURCES.txt +2 -0
- {mongospec-0.2 → mongospec-0.2.2}/pyproject.toml +1 -1
- {mongospec-0.2 → mongospec-0.2.2}/LICENSE +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/atomic_updates.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/batch_operations.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/collection_binding.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/connection_management.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/count_documents.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/create_documents.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/delete_documents.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/document_models.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/index_creation.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/quick_start.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/read_documents.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/update_documents.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/upsert_operations.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/examples/working_with_cursors.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/__init__.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/_connection.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/core.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/__init__.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/operations/__init__.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/operations/base.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/operations/count.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/operations/delete.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/operations/find.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/operations/insert.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/document/operations/update.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec/utils.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec.egg-info/dependency_links.txt +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec.egg-info/requires.txt +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/mongospec.egg-info/top_level.txt +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/setup.cfg +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/tests/conftest.py +0 -0
- {mongospec-0.2 → mongospec-0.2.2}/tests/test_connection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mongospec
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Async MongoDB ODM with msgspec integration and automatic collection binding
|
|
5
5
|
Author-email: Diprog <diprog991@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -51,6 +51,7 @@ Minimal **async** MongoDB ODM built for *speed* and *simplicity*, featuring auto
|
|
|
51
51
|
- [CRUD Operations](#crud-operations)
|
|
52
52
|
- [Indexes](#indexes)
|
|
53
53
|
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
54
|
+
- [Contrib: KV Store](#contrib-kv-store)
|
|
54
55
|
6. [Contributing](#contributing)
|
|
55
56
|
7. [License](#license)
|
|
56
57
|
|
|
@@ -271,6 +272,67 @@ in sync — no caller-side boilerplate needed.
|
|
|
271
272
|
|
|
272
273
|
---
|
|
273
274
|
|
|
275
|
+
### Contrib: KV Store
|
|
276
|
+
|
|
277
|
+
`mongospec.contrib.kv_store` provides a ready-made async key-value store
|
|
278
|
+
backed by a MongoDB collection. Designed for multiple inheritance with
|
|
279
|
+
project-specific base documents.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from mongospec.contrib.kv_store import KVStore, KVStoreItem
|
|
283
|
+
from myapp.db import Document # your base with timestamps, hooks, etc.
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class AppStorage(KVStore, Document):
|
|
287
|
+
__collection_name__ = "app_storage"
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
A unique index on `key` is created automatically at init time.
|
|
291
|
+
|
|
292
|
+
**Direct usage:**
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
await AppStorage.set("theme", "dark")
|
|
296
|
+
theme = await AppStorage.get("theme") # "dark"
|
|
297
|
+
theme = await AppStorage.get_or_default("x", 0) # 0 (no KeyError)
|
|
298
|
+
await AppStorage.set_default("theme", "light") # "dark" (atomic, no overwrite)
|
|
299
|
+
|
|
300
|
+
await AppStorage.set_many({"a": 1, "b": 2})
|
|
301
|
+
all_pairs = await AppStorage.get_all() # {"theme": "dark", "a": 1, "b": 2}
|
|
302
|
+
all_keys = await AppStorage.keys() # ["theme", "a", "b"]
|
|
303
|
+
|
|
304
|
+
await AppStorage.has("theme") # True
|
|
305
|
+
await AppStorage.delete_key("theme") # True
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Typed accessor (`KVStoreItem`):**
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
AppStorageItem = KVStoreItem.of(AppStorage)
|
|
312
|
+
|
|
313
|
+
max_retries = AppStorageItem[int]("max_retries", default=3)
|
|
314
|
+
|
|
315
|
+
value = await max_retries.get() # 3 (default, not persisted)
|
|
316
|
+
await max_retries.set_default() # atomically persist default if missing
|
|
317
|
+
await max_retries.set(10)
|
|
318
|
+
await max_retries.has() # True
|
|
319
|
+
await max_retries.delete() # True
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
| `KVStore` method | Description |
|
|
323
|
+
|------------------|-------------|
|
|
324
|
+
| `set(key, value)` | Upsert a value by key |
|
|
325
|
+
| `get(key)` | Get value or raise `KeyError` |
|
|
326
|
+
| `get_or_default(key, default)` | Get value or return default |
|
|
327
|
+
| `set_default(key, value)` | Atomic insert-if-absent (`$setOnInsert`) |
|
|
328
|
+
| `delete_key(key)` | Delete a key, return `True` if existed |
|
|
329
|
+
| `has(key)` | Check key existence |
|
|
330
|
+
| `get_all()` | Return all pairs as `dict` |
|
|
331
|
+
| `keys()` | Return all key names |
|
|
332
|
+
| `set_many(items)` | Upsert multiple pairs |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
274
336
|
## Contributing
|
|
275
337
|
|
|
276
338
|
Contributions, issues and feature requests are welcome.
|
|
@@ -24,6 +24,7 @@ Minimal **async** MongoDB ODM built for *speed* and *simplicity*, featuring auto
|
|
|
24
24
|
- [CRUD Operations](#crud-operations)
|
|
25
25
|
- [Indexes](#indexes)
|
|
26
26
|
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
27
|
+
- [Contrib: KV Store](#contrib-kv-store)
|
|
27
28
|
6. [Contributing](#contributing)
|
|
28
29
|
7. [License](#license)
|
|
29
30
|
|
|
@@ -244,6 +245,67 @@ in sync — no caller-side boilerplate needed.
|
|
|
244
245
|
|
|
245
246
|
---
|
|
246
247
|
|
|
248
|
+
### Contrib: KV Store
|
|
249
|
+
|
|
250
|
+
`mongospec.contrib.kv_store` provides a ready-made async key-value store
|
|
251
|
+
backed by a MongoDB collection. Designed for multiple inheritance with
|
|
252
|
+
project-specific base documents.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from mongospec.contrib.kv_store import KVStore, KVStoreItem
|
|
256
|
+
from myapp.db import Document # your base with timestamps, hooks, etc.
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class AppStorage(KVStore, Document):
|
|
260
|
+
__collection_name__ = "app_storage"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A unique index on `key` is created automatically at init time.
|
|
264
|
+
|
|
265
|
+
**Direct usage:**
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
await AppStorage.set("theme", "dark")
|
|
269
|
+
theme = await AppStorage.get("theme") # "dark"
|
|
270
|
+
theme = await AppStorage.get_or_default("x", 0) # 0 (no KeyError)
|
|
271
|
+
await AppStorage.set_default("theme", "light") # "dark" (atomic, no overwrite)
|
|
272
|
+
|
|
273
|
+
await AppStorage.set_many({"a": 1, "b": 2})
|
|
274
|
+
all_pairs = await AppStorage.get_all() # {"theme": "dark", "a": 1, "b": 2}
|
|
275
|
+
all_keys = await AppStorage.keys() # ["theme", "a", "b"]
|
|
276
|
+
|
|
277
|
+
await AppStorage.has("theme") # True
|
|
278
|
+
await AppStorage.delete_key("theme") # True
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Typed accessor (`KVStoreItem`):**
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
AppStorageItem = KVStoreItem.of(AppStorage)
|
|
285
|
+
|
|
286
|
+
max_retries = AppStorageItem[int]("max_retries", default=3)
|
|
287
|
+
|
|
288
|
+
value = await max_retries.get() # 3 (default, not persisted)
|
|
289
|
+
await max_retries.set_default() # atomically persist default if missing
|
|
290
|
+
await max_retries.set(10)
|
|
291
|
+
await max_retries.has() # True
|
|
292
|
+
await max_retries.delete() # True
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
| `KVStore` method | Description |
|
|
296
|
+
|------------------|-------------|
|
|
297
|
+
| `set(key, value)` | Upsert a value by key |
|
|
298
|
+
| `get(key)` | Get value or raise `KeyError` |
|
|
299
|
+
| `get_or_default(key, default)` | Get value or return default |
|
|
300
|
+
| `set_default(key, value)` | Atomic insert-if-absent (`$setOnInsert`) |
|
|
301
|
+
| `delete_key(key)` | Delete a key, return `True` if existed |
|
|
302
|
+
| `has(key)` | Check key existence |
|
|
303
|
+
| `get_all()` | Return all pairs as `dict` |
|
|
304
|
+
| `keys()` | Return all key names |
|
|
305
|
+
| `set_many(items)` | Upsert multiple pairs |
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
247
309
|
## Contributing
|
|
248
310
|
|
|
249
311
|
Contributions, issues and feature requests are welcome.
|
|
File without changes
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Key-value store built on top of MongoDocument.
|
|
3
|
+
|
|
4
|
+
Provides a simple async key-value interface over a MongoDB collection.
|
|
5
|
+
Designed for use via multiple inheritance with project-specific base documents.
|
|
6
|
+
|
|
7
|
+
.. code-block:: python
|
|
8
|
+
|
|
9
|
+
from mongospec.contrib.kv_store import KVStoreMixin, KVStoreItem
|
|
10
|
+
|
|
11
|
+
class AppStorage(KVStoreMixin, Document):
|
|
12
|
+
__collection_name__ = "app_storage"
|
|
13
|
+
|
|
14
|
+
max_retries = KVStoreItem[int](AppStorage, "max_retries", default=3)
|
|
15
|
+
value = await max_retries.get()
|
|
16
|
+
await max_retries.set(5)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from typing import Any, ClassVar, Generic, TypeVar
|
|
23
|
+
|
|
24
|
+
import mongojet
|
|
25
|
+
|
|
26
|
+
from mongospec.document.document import MongoDocument
|
|
27
|
+
|
|
28
|
+
T = TypeVar("T")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class KVStoreMixin:
|
|
32
|
+
"""
|
|
33
|
+
Mixin providing async key-value store methods.
|
|
34
|
+
|
|
35
|
+
Does **not** define struct fields — safe for multiple inheritance
|
|
36
|
+
with any ``MongoDocument`` subclass. The concrete class must define
|
|
37
|
+
``key: str`` and ``value: Any | None`` fields.
|
|
38
|
+
|
|
39
|
+
.. code-block:: python
|
|
40
|
+
|
|
41
|
+
class AppStorage(KVStoreMixin, Document):
|
|
42
|
+
__collection_name__ = "app_storage"
|
|
43
|
+
|
|
44
|
+
await AppStorage.set("theme", "dark")
|
|
45
|
+
theme = await AppStorage.get("theme")
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
__indexes__: ClassVar[Sequence[mongojet.IndexModel]] = [
|
|
49
|
+
mongojet.IndexModel(keys=[("key", 1)], unique=True), # type: ignore[call-arg]
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
async def set(cls, key: str, value: Any | None) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Upsert a value by key.
|
|
56
|
+
|
|
57
|
+
:param key: Unique setting key.
|
|
58
|
+
:param value: Value to store; may be ``None``.
|
|
59
|
+
"""
|
|
60
|
+
await cls.update_one(
|
|
61
|
+
{"key": key},
|
|
62
|
+
{"$set": {"value": value}},
|
|
63
|
+
upsert=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
async def get(cls, key: str) -> Any | None:
|
|
68
|
+
"""
|
|
69
|
+
Retrieve a value by key.
|
|
70
|
+
|
|
71
|
+
:param key: Unique setting key.
|
|
72
|
+
:return: The stored value (may be ``None``).
|
|
73
|
+
:raises KeyError: If the key does not exist.
|
|
74
|
+
"""
|
|
75
|
+
document = await cls.find_one({"key": key})
|
|
76
|
+
if document is None:
|
|
77
|
+
raise KeyError(key)
|
|
78
|
+
return document.value
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
async def get_or_default(cls, key: str, default: Any | None = None) -> Any | None:
|
|
82
|
+
"""
|
|
83
|
+
Retrieve a value by key, returning *default* if missing.
|
|
84
|
+
|
|
85
|
+
Unlike :meth:`get`, this never raises :exc:`KeyError`.
|
|
86
|
+
|
|
87
|
+
:param key: Unique setting key.
|
|
88
|
+
:param default: Fallback value when the key is absent.
|
|
89
|
+
:return: The stored value or *default*.
|
|
90
|
+
"""
|
|
91
|
+
document = await cls.find_one({"key": key})
|
|
92
|
+
if document is None:
|
|
93
|
+
return default
|
|
94
|
+
return document.value
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
async def set_default(cls, key: str, value: Any | None) -> Any | None:
|
|
98
|
+
"""
|
|
99
|
+
Atomically set a value only if the key does not already exist.
|
|
100
|
+
|
|
101
|
+
:param key: Unique setting key.
|
|
102
|
+
:param value: Value to store when the key is missing.
|
|
103
|
+
:return: The existing value if present, otherwise *value*.
|
|
104
|
+
"""
|
|
105
|
+
doc = await cls.find_one_and_update(
|
|
106
|
+
{"key": key},
|
|
107
|
+
{"$setOnInsert": {"key": key, "value": value}},
|
|
108
|
+
upsert=True,
|
|
109
|
+
return_updated=True,
|
|
110
|
+
)
|
|
111
|
+
return doc.value if doc is not None else value
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
async def delete_key(cls, key: str) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Delete a key-value pair.
|
|
117
|
+
|
|
118
|
+
:param key: Unique setting key.
|
|
119
|
+
:return: ``True`` if the key existed and was deleted.
|
|
120
|
+
"""
|
|
121
|
+
return await cls.delete_one({"key": key}) > 0
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
async def has(cls, key: str) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Check whether a key exists.
|
|
127
|
+
|
|
128
|
+
:param key: Unique setting key.
|
|
129
|
+
:return: ``True`` if the key is present.
|
|
130
|
+
"""
|
|
131
|
+
return await cls.exists({"key": key})
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
async def get_all(cls) -> dict[str, Any | None]:
|
|
135
|
+
"""
|
|
136
|
+
Retrieve all key-value pairs as a dictionary.
|
|
137
|
+
|
|
138
|
+
:return: Mapping of all stored keys to their values.
|
|
139
|
+
"""
|
|
140
|
+
docs = await cls.find_all({})
|
|
141
|
+
return {doc.key: doc.value for doc in docs}
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
async def keys(cls) -> list[str]:
|
|
145
|
+
"""
|
|
146
|
+
Retrieve all stored keys.
|
|
147
|
+
|
|
148
|
+
:return: List of key names.
|
|
149
|
+
"""
|
|
150
|
+
docs = await cls.find_all({})
|
|
151
|
+
return [doc.key for doc in docs]
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
async def set_many(cls, items: dict[str, Any | None]) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Upsert multiple key-value pairs at once.
|
|
157
|
+
|
|
158
|
+
:param items: Mapping of keys to values.
|
|
159
|
+
"""
|
|
160
|
+
for key, value in items.items():
|
|
161
|
+
await cls.set(key, value)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class KVStore(KVStoreMixin, MongoDocument, kw_only=True):
|
|
165
|
+
"""
|
|
166
|
+
Standalone async key-value store backed by a MongoDB collection.
|
|
167
|
+
|
|
168
|
+
Includes ``key`` and ``value`` fields. Use this directly if you
|
|
169
|
+
don't need to combine with a custom base document.
|
|
170
|
+
For multiple inheritance with your own ``Document`` base, use
|
|
171
|
+
:class:`KVStoreMixin` instead.
|
|
172
|
+
|
|
173
|
+
.. code-block:: python
|
|
174
|
+
|
|
175
|
+
class AppStorage(KVStore):
|
|
176
|
+
__collection_name__ = "app_storage"
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
key: str
|
|
180
|
+
value: Any | None = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class KVStoreItem(Generic[T]):
|
|
184
|
+
"""
|
|
185
|
+
Typed accessor for a single key in a :class:`KVStoreMixin` collection.
|
|
186
|
+
|
|
187
|
+
.. code-block:: python
|
|
188
|
+
|
|
189
|
+
max_retries = KVStoreItem[int](AppStorage, "max_retries", default=3)
|
|
190
|
+
|
|
191
|
+
value = await max_retries.get() # int | None
|
|
192
|
+
await max_retries.set(10)
|
|
193
|
+
|
|
194
|
+
:param store: The ``KVStoreMixin`` subclass to use.
|
|
195
|
+
:param key: Unique setting key.
|
|
196
|
+
:param default: Default value returned (and persisted) when the key is missing.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
store: type[KVStoreMixin],
|
|
202
|
+
key: str,
|
|
203
|
+
default: T | None = None,
|
|
204
|
+
) -> None:
|
|
205
|
+
self._store = store
|
|
206
|
+
self._key = key
|
|
207
|
+
self._default = default
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def of(cls, store: type[KVStoreMixin]) -> type[KVStoreItem]:
|
|
211
|
+
"""
|
|
212
|
+
Create a ``KVStoreItem`` subclass bound to a specific store.
|
|
213
|
+
|
|
214
|
+
.. code-block:: python
|
|
215
|
+
|
|
216
|
+
AppStorageItem = KVStoreItem.of(AppStorage)
|
|
217
|
+
max_retries = AppStorageItem[int]("max_retries", default=3)
|
|
218
|
+
|
|
219
|
+
:param store: The ``KVStoreMixin`` subclass to bind.
|
|
220
|
+
:return: A new ``KVStoreItem`` subclass with *store* pre-filled.
|
|
221
|
+
"""
|
|
222
|
+
bound_store = store
|
|
223
|
+
|
|
224
|
+
class BoundKVStoreItem(cls): # type: ignore[misc]
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
key: str,
|
|
228
|
+
default: T | None = None,
|
|
229
|
+
) -> None:
|
|
230
|
+
super().__init__(store=bound_store, key=key, default=default)
|
|
231
|
+
|
|
232
|
+
def __class_getitem__(cls, item: Any) -> type:
|
|
233
|
+
return cls
|
|
234
|
+
|
|
235
|
+
BoundKVStoreItem.__name__ = f"{store.__name__}Item"
|
|
236
|
+
BoundKVStoreItem.__qualname__ = f"{store.__name__}Item"
|
|
237
|
+
return BoundKVStoreItem
|
|
238
|
+
|
|
239
|
+
async def get(self) -> T | None:
|
|
240
|
+
"""
|
|
241
|
+
Get the stored value, returning *default* if missing.
|
|
242
|
+
|
|
243
|
+
Does **not** persist the default — use :meth:`set_default`
|
|
244
|
+
to atomically initialize a key.
|
|
245
|
+
|
|
246
|
+
:return: The stored value cast to ``T``, or the default.
|
|
247
|
+
"""
|
|
248
|
+
return await self._store.get_or_default(self._key, self._default) # type: ignore[return-value]
|
|
249
|
+
|
|
250
|
+
async def set(self, value: T | None) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Set the stored value.
|
|
253
|
+
|
|
254
|
+
:param value: New value to store; may be ``None``.
|
|
255
|
+
"""
|
|
256
|
+
await self._store.set(self._key, value)
|
|
257
|
+
|
|
258
|
+
async def set_default(self) -> T | None:
|
|
259
|
+
"""
|
|
260
|
+
Atomically persist *default* only if the key is missing.
|
|
261
|
+
|
|
262
|
+
:return: The existing value if present, otherwise the default.
|
|
263
|
+
"""
|
|
264
|
+
return await self._store.set_default(self._key, self._default) # type: ignore[return-value]
|
|
265
|
+
|
|
266
|
+
async def delete(self) -> bool:
|
|
267
|
+
"""
|
|
268
|
+
Delete the key from the store.
|
|
269
|
+
|
|
270
|
+
:return: ``True`` if the key existed.
|
|
271
|
+
"""
|
|
272
|
+
return await self._store.delete_key(self._key)
|
|
273
|
+
|
|
274
|
+
async def has(self) -> bool:
|
|
275
|
+
"""
|
|
276
|
+
Check whether the key exists.
|
|
277
|
+
|
|
278
|
+
:return: ``True`` if the key is present.
|
|
279
|
+
"""
|
|
280
|
+
return await self._store.has(self._key)
|
|
@@ -10,6 +10,7 @@ from typing import Any, ClassVar, Self, Sequence, final
|
|
|
10
10
|
import mongojet
|
|
11
11
|
import msgspec
|
|
12
12
|
from bson import ObjectId, int64
|
|
13
|
+
from msgspec._core import StructMeta
|
|
13
14
|
|
|
14
15
|
from .operations import (
|
|
15
16
|
CountOperationsMixin, DeleteOperationsMixin, FindOperationsMixin,
|
|
@@ -17,6 +18,20 @@ from .operations import (
|
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
class _AutoKwOnlyMeta(StructMeta):
|
|
22
|
+
"""Metaclass that propagates ``kw_only=True`` to all subclasses.
|
|
23
|
+
|
|
24
|
+
``msgspec.Struct`` only inherits ``kw_only`` one level deep.
|
|
25
|
+
This metaclass ensures it is applied at every depth automatically,
|
|
26
|
+
unless explicitly overridden with ``kw_only=False``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __new__(mcs, name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any) -> type:
|
|
30
|
+
if any(isinstance(b, type) and issubclass(b, msgspec.Struct) for b in bases):
|
|
31
|
+
kwargs.setdefault("kw_only", True)
|
|
32
|
+
return super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
33
|
+
|
|
34
|
+
|
|
20
35
|
def default_dec_hook(expected_type: type, obj: Any) -> Any:
|
|
21
36
|
"""Default decoding hook for basic type conversion.
|
|
22
37
|
|
|
@@ -56,7 +71,8 @@ class MongoDocument(
|
|
|
56
71
|
FindOperationsMixin,
|
|
57
72
|
InsertOperationsMixin,
|
|
58
73
|
UpdateOperationsMixin,
|
|
59
|
-
|
|
74
|
+
metaclass=_AutoKwOnlyMeta,
|
|
75
|
+
kw_only=True,
|
|
60
76
|
):
|
|
61
77
|
"""
|
|
62
78
|
Abstract base document for MongoDB collections with automatic binding.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mongospec
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Async MongoDB ODM with msgspec integration and automatic collection binding
|
|
5
5
|
Author-email: Diprog <diprog991@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -51,6 +51,7 @@ Minimal **async** MongoDB ODM built for *speed* and *simplicity*, featuring auto
|
|
|
51
51
|
- [CRUD Operations](#crud-operations)
|
|
52
52
|
- [Indexes](#indexes)
|
|
53
53
|
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
54
|
+
- [Contrib: KV Store](#contrib-kv-store)
|
|
54
55
|
6. [Contributing](#contributing)
|
|
55
56
|
7. [License](#license)
|
|
56
57
|
|
|
@@ -271,6 +272,67 @@ in sync — no caller-side boilerplate needed.
|
|
|
271
272
|
|
|
272
273
|
---
|
|
273
274
|
|
|
275
|
+
### Contrib: KV Store
|
|
276
|
+
|
|
277
|
+
`mongospec.contrib.kv_store` provides a ready-made async key-value store
|
|
278
|
+
backed by a MongoDB collection. Designed for multiple inheritance with
|
|
279
|
+
project-specific base documents.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from mongospec.contrib.kv_store import KVStore, KVStoreItem
|
|
283
|
+
from myapp.db import Document # your base with timestamps, hooks, etc.
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class AppStorage(KVStore, Document):
|
|
287
|
+
__collection_name__ = "app_storage"
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
A unique index on `key` is created automatically at init time.
|
|
291
|
+
|
|
292
|
+
**Direct usage:**
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
await AppStorage.set("theme", "dark")
|
|
296
|
+
theme = await AppStorage.get("theme") # "dark"
|
|
297
|
+
theme = await AppStorage.get_or_default("x", 0) # 0 (no KeyError)
|
|
298
|
+
await AppStorage.set_default("theme", "light") # "dark" (atomic, no overwrite)
|
|
299
|
+
|
|
300
|
+
await AppStorage.set_many({"a": 1, "b": 2})
|
|
301
|
+
all_pairs = await AppStorage.get_all() # {"theme": "dark", "a": 1, "b": 2}
|
|
302
|
+
all_keys = await AppStorage.keys() # ["theme", "a", "b"]
|
|
303
|
+
|
|
304
|
+
await AppStorage.has("theme") # True
|
|
305
|
+
await AppStorage.delete_key("theme") # True
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Typed accessor (`KVStoreItem`):**
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
AppStorageItem = KVStoreItem.of(AppStorage)
|
|
312
|
+
|
|
313
|
+
max_retries = AppStorageItem[int]("max_retries", default=3)
|
|
314
|
+
|
|
315
|
+
value = await max_retries.get() # 3 (default, not persisted)
|
|
316
|
+
await max_retries.set_default() # atomically persist default if missing
|
|
317
|
+
await max_retries.set(10)
|
|
318
|
+
await max_retries.has() # True
|
|
319
|
+
await max_retries.delete() # True
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
| `KVStore` method | Description |
|
|
323
|
+
|------------------|-------------|
|
|
324
|
+
| `set(key, value)` | Upsert a value by key |
|
|
325
|
+
| `get(key)` | Get value or raise `KeyError` |
|
|
326
|
+
| `get_or_default(key, default)` | Get value or return default |
|
|
327
|
+
| `set_default(key, value)` | Atomic insert-if-absent (`$setOnInsert`) |
|
|
328
|
+
| `delete_key(key)` | Delete a key, return `True` if existed |
|
|
329
|
+
| `has(key)` | Check key existence |
|
|
330
|
+
| `get_all()` | Return all pairs as `dict` |
|
|
331
|
+
| `keys()` | Return all key names |
|
|
332
|
+
| `set_many(items)` | Upsert multiple pairs |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
274
336
|
## Contributing
|
|
275
337
|
|
|
276
338
|
Contributions, issues and feature requests are welcome.
|
|
@@ -24,6 +24,8 @@ mongospec.egg-info/SOURCES.txt
|
|
|
24
24
|
mongospec.egg-info/dependency_links.txt
|
|
25
25
|
mongospec.egg-info/requires.txt
|
|
26
26
|
mongospec.egg-info/top_level.txt
|
|
27
|
+
mongospec/contrib/__init__.py
|
|
28
|
+
mongospec/contrib/kv_store.py
|
|
27
29
|
mongospec/document/__init__.py
|
|
28
30
|
mongospec/document/document.py
|
|
29
31
|
mongospec/document/operations/__init__.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|