objbase 0.4.0__tar.gz → 0.5.1__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.
- {objbase-0.4.0 → objbase-0.5.1}/PKG-INFO +89 -88
- {objbase-0.4.0 → objbase-0.5.1}/README.md +88 -87
- objbase-0.5.1/examples/async_directory_example.py +35 -0
- objbase-0.5.1/examples/async_example.py +28 -0
- objbase-0.5.1/examples/async_file_example.py +30 -0
- {objbase-0.4.0 → objbase-0.5.1}/examples/async_mongodb_example.py +9 -9
- {objbase-0.4.0 → objbase-0.5.1}/examples/async_pydantic_example.py +9 -9
- objbase-0.5.1/examples/async_sqlite_example.py +30 -0
- {objbase-0.4.0 → objbase-0.5.1}/examples/dict_example.py +7 -7
- {objbase-0.4.0 → objbase-0.5.1}/examples/mongodb_example.py +9 -9
- {objbase-0.4.0 → objbase-0.5.1}/examples/pydantic_example.py +7 -7
- {objbase-0.4.0 → objbase-0.5.1}/pyproject.toml +1 -1
- objbase-0.5.1/src/objbase/__init__.py +58 -0
- objbase-0.4.0/src/objbase/asyncio/inventory.py → objbase-0.5.1/src/objbase/asyncio/collection.py +9 -9
- objbase-0.4.0/src/objbase/asyncio/storage/file_storage.py → objbase-0.5.1/src/objbase/asyncio/storage/local.py +13 -13
- objbase-0.4.0/src/objbase/asyncio/storage/mongodb_storage.py → objbase-0.5.1/src/objbase/asyncio/storage/mongodb.py +4 -2
- objbase-0.4.0/src/objbase/asyncio/storage/redis_storage.py → objbase-0.5.1/src/objbase/asyncio/storage/redis.py +1 -1
- objbase-0.4.0/src/objbase/asyncio/storage/sqlite_storage.py → objbase-0.5.1/src/objbase/asyncio/storage/sqlite.py +2 -2
- objbase-0.4.0/src/objbase/inventory.py → objbase-0.5.1/src/objbase/collection.py +6 -6
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/errors.py +2 -2
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/pydantic.py +31 -31
- objbase-0.4.0/src/objbase/storage/inmemory_storage.py → objbase-0.5.1/src/objbase/storage/inmemory.py +1 -1
- objbase-0.4.0/src/objbase/storage/file_storage.py → objbase-0.5.1/src/objbase/storage/local.py +10 -10
- objbase-0.4.0/src/objbase/storage/mongodb_storage.py → objbase-0.5.1/src/objbase/storage/mongodb.py +9 -3
- objbase-0.4.0/src/objbase/storage/redis_storage.py → objbase-0.5.1/src/objbase/storage/redis.py +1 -1
- objbase-0.4.0/tests/test_async_inventory.py → objbase-0.5.1/tests/test_async_collection.py +33 -33
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_async_file_storage.py +12 -12
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_async_mongodb_storage.py +13 -4
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_async_redis_storage.py +4 -4
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_async_sqlite_storage.py +5 -5
- objbase-0.4.0/tests/test_inventory.py → objbase-0.5.1/tests/test_collection.py +30 -30
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_file_storage.py +37 -37
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_inmemory_storage.py +1 -1
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_mongodb_storage.py +30 -3
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_package.py +7 -7
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_redis_storage.py +4 -4
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_sqlite_storage.py +1 -1
- {objbase-0.4.0 → objbase-0.5.1}/tests/test_storage_contract.py +19 -19
- {objbase-0.4.0 → objbase-0.5.1}/uv.lock +1 -1
- objbase-0.4.0/examples/async_directory_example.py +0 -35
- objbase-0.4.0/examples/async_example.py +0 -28
- objbase-0.4.0/examples/async_file_example.py +0 -30
- objbase-0.4.0/examples/async_sqlite_example.py +0 -30
- objbase-0.4.0/src/objbase/__init__.py +0 -58
- {objbase-0.4.0 → objbase-0.5.1}/.github/dependabot.yml +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/.github/workflows/ci.yml +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/.github/workflows/release.yml +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/.gitignore +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/DEVELOPER.md +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/LICENSE +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/release.sh +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/asyncio/__init__.py +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/asyncio/storage/__init__.py +0 -0
- /objbase-0.4.0/src/objbase/asyncio/storage/threaded_storage.py → /objbase-0.5.1/src/objbase/asyncio/storage/threaded.py +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/interface.py +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/py.typed +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/storage/__init__.py +0 -0
- /objbase-0.4.0/src/objbase/storage/sqlite_storage.py → /objbase-0.5.1/src/objbase/storage/sqlite.py +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/util/__init__.py +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/util/file_util.py +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/util/mongodb_util.py +0 -0
- {objbase-0.4.0 → objbase-0.5.1}/src/objbase/util/redis_util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: objbase
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Damn simple object store for Python dicts and Pydantic models across multiple backends
|
|
5
5
|
Project-URL: Homepage, https://github.com/fm-labs/objbase
|
|
6
6
|
Project-URL: Issues, https://github.com/fm-labs/objbase/issues
|
|
@@ -41,10 +41,10 @@ __No thrills__ - **just a simple key-value store for serializable Python objects
|
|
|
41
41
|
|
|
42
42
|
## What you get
|
|
43
43
|
|
|
44
|
-
- Basic CRUD operations: `save`, `get`, `
|
|
44
|
+
- Basic CRUD operations: `save`, `get`, `items`, `keys`, `patch`, `delete`
|
|
45
45
|
- Multiple storage adapters (in-memory, file-based, SQLite, Redis, MongoDB)
|
|
46
|
-
- Optional Pydantic model validation with `
|
|
47
|
-
- Async support via `
|
|
46
|
+
- Optional Pydantic model validation with `PydanticCollection` / `AsyncPydanticCollection`
|
|
47
|
+
- Async support via `AsyncCollection` with async storage adapters (in-memory, file-based, SQLite, Redis, MongoDB)
|
|
48
48
|
- Easy FastAPI integration with dependency injection
|
|
49
49
|
- Fully typed (ships `py.typed`), checked with `mypy --strict`
|
|
50
50
|
|
|
@@ -60,7 +60,7 @@ and SQLite storage work out of the box. Install extras for the other backends:
|
|
|
60
60
|
pip install objbase # core only
|
|
61
61
|
pip install "objbase[redis]" # + redis-py, for (Async)RedisStorage
|
|
62
62
|
pip install "objbase[mongodb]" # + pymongo, for (Async)MongoDBStorage
|
|
63
|
-
pip install "objbase[pydantic]" # + pydantic, for (Async)
|
|
63
|
+
pip install "objbase[pydantic]" # + pydantic, for (Async)PydanticCollection
|
|
64
64
|
pip install "objbase[all]" # everything
|
|
65
65
|
# or with uv
|
|
66
66
|
uv add "objbase[redis]"
|
|
@@ -70,25 +70,25 @@ uv add "objbase[redis]"
|
|
|
70
70
|
|
|
71
71
|
## Quick Start
|
|
72
72
|
|
|
73
|
-
Every item must have an `"id"` field. Use `
|
|
73
|
+
Every item must have an `"id"` field. Use `Collection` with any storage adapter:
|
|
74
74
|
|
|
75
75
|
```python
|
|
76
|
-
from objbase import
|
|
76
|
+
from objbase import Collection, InMemoryStorage
|
|
77
77
|
|
|
78
78
|
storage = InMemoryStorage()
|
|
79
|
-
todos =
|
|
79
|
+
todos = Collection(item_type="todo", storage=storage)
|
|
80
80
|
|
|
81
81
|
todos.save({"id": "1", "title": "Buy milk", "done": False})
|
|
82
82
|
todos.save({"id": "2", "title": "Walk dog", "done": False})
|
|
83
83
|
|
|
84
84
|
todos.get("1") # → {"id": "1", "title": "Buy milk", "done": False}
|
|
85
|
-
todos.
|
|
85
|
+
todos.items() # → [{"id": "1", ...}, {"id": "2", ...}]
|
|
86
86
|
todos.keys() # → ["1", "2"] (order unspecified)
|
|
87
87
|
todos.patch("1", {"done": True}) # → {"id": "1", ..., "done": True}
|
|
88
88
|
todos.delete("1") # → True
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Swapping the backend requires only changing the `storage` argument — the `
|
|
91
|
+
Swapping the backend requires only changing the `storage` argument — the `Collection`
|
|
92
92
|
API stays identical.
|
|
93
93
|
|
|
94
94
|
All public classes can be imported from the top-level `objbase` package, as above,
|
|
@@ -97,21 +97,21 @@ or from their submodules as in the examples below:
|
|
|
97
97
|
| Module | Contents |
|
|
98
98
|
|---|---|
|
|
99
99
|
| `objbase.interface` | `Storage`, `AsyncStorage` protocols and the `Item` type |
|
|
100
|
-
| `objbase.
|
|
101
|
-
| `objbase.errors` | `
|
|
102
|
-
| `objbase.pydantic` | `
|
|
103
|
-
| `objbase.storage.{inmemory,
|
|
104
|
-
| `objbase.asyncio.
|
|
105
|
-
| `objbase.asyncio.storage.{
|
|
100
|
+
| `objbase.collection` | `Collection` |
|
|
101
|
+
| `objbase.errors` | `CollectionError`, `ItemNotFoundError` |
|
|
102
|
+
| `objbase.pydantic` | `PydanticCollection`, `AsyncPydanticCollection` (needs `objbase[pydantic]`) |
|
|
103
|
+
| `objbase.storage.{inmemory,local,sqlite,redis,mongodb}` | Sync storage adapters |
|
|
104
|
+
| `objbase.asyncio.collection` | `AsyncCollection` |
|
|
105
|
+
| `objbase.asyncio.storage.{local,sqlite,redis,mongodb}` | Async storage adapters |
|
|
106
106
|
|
|
107
|
-
`import objbase` works without Pydantic installed; `
|
|
108
|
-
`
|
|
107
|
+
`import objbase` works without Pydantic installed; `PydanticCollection` and
|
|
108
|
+
`AsyncPydanticCollection` are loaded on first access.
|
|
109
109
|
|
|
110
110
|
### Behaviour
|
|
111
111
|
|
|
112
112
|
All adapters follow the same contract (verified by a shared test suite):
|
|
113
113
|
|
|
114
|
-
- `get` returns `None` for a missing item; `
|
|
114
|
+
- `get` returns `None` for a missing item; `items` and `keys` return `[]` for an empty type.
|
|
115
115
|
- `save` inserts a new item or **replaces** an existing one entirely (it does not merge fields).
|
|
116
116
|
- `patch` merges the given fields into an existing item. It cannot change the item's `id`.
|
|
117
117
|
- `delete` returns `True` if the item was removed, `False` if it did not exist.
|
|
@@ -123,10 +123,10 @@ Errors are raised, not returned:
|
|
|
123
123
|
|---|---|
|
|
124
124
|
| `save` an item without an `id` (or with an empty one) | `ValueError` |
|
|
125
125
|
| `patch` with data that changes the item's `id` | `ValueError` |
|
|
126
|
-
| `patch` a missing item | `objbase.ItemNotFoundError` (
|
|
127
|
-
| The storage backend reports a failed write | `objbase.
|
|
126
|
+
| `patch` a missing item | `objbase.ItemNotFoundError` (a `CollectionError` and a `LookupError`) |
|
|
127
|
+
| The storage backend reports a failed write | `objbase.CollectionError` |
|
|
128
128
|
|
|
129
|
-
`
|
|
129
|
+
`CollectionError` is the base class of all objbase errors.
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
@@ -135,8 +135,8 @@ Errors are raised, not returned:
|
|
|
135
135
|
| Adapter | Sync class | Async class | When to use |
|
|
136
136
|
|---|---|---|---|
|
|
137
137
|
| In-Memory | `InMemoryStorage` | same class | Testing / prototyping — volatile |
|
|
138
|
-
| File (one file per type) | `
|
|
139
|
-
| File (one file per item) | `
|
|
138
|
+
| File (one file per type) | `LocalFileStorage` | `AsyncLocalFileStorage` | Simple persistence for small datasets |
|
|
139
|
+
| File (one file per item) | `LocalDirectoryStorage` | `AsyncLocalDirectoryStorage` | Medium datasets; per-item file operations |
|
|
140
140
|
| SQLite | `SQLiteStorage` | `AsyncSQLiteStorage` | ACID persistence with zero external deps |
|
|
141
141
|
| Redis | `RedisStorage` | `AsyncRedisStorage` | High-performance / distributed access |
|
|
142
142
|
| MongoDB | `MongoDBStorage` | `AsyncMongoDBStorage` | Document-oriented storage and complex queries |
|
|
@@ -149,7 +149,7 @@ MongoDB ones use the drivers' native async clients.
|
|
|
149
149
|
### In-Memory
|
|
150
150
|
|
|
151
151
|
```python
|
|
152
|
-
from objbase.storage.
|
|
152
|
+
from objbase.storage.inmemory import InMemoryStorage
|
|
153
153
|
|
|
154
154
|
storage = InMemoryStorage()
|
|
155
155
|
```
|
|
@@ -161,9 +161,9 @@ sync counterparts.
|
|
|
161
161
|
### File-Based (single file per type)
|
|
162
162
|
|
|
163
163
|
```python
|
|
164
|
-
from objbase.storage.
|
|
164
|
+
from objbase.storage.local import LocalFileStorage
|
|
165
165
|
|
|
166
|
-
storage =
|
|
166
|
+
storage = LocalFileStorage(base_dir="/var/data/myapp")
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
All items of one type are stored in `{base_dir}/{item_type}.json`.
|
|
@@ -179,16 +179,16 @@ mid-write cannot corrupt data. Lock files are left in place after use.
|
|
|
179
179
|
Every write rewrites the whole type file, so this adapter suits small datasets.
|
|
180
180
|
Locks are advisory and may not work on network file systems (NFS, SMB).
|
|
181
181
|
|
|
182
|
-
`
|
|
182
|
+
`AsyncLocalFileStorage(base_dir=...)` is the async counterpart. It uses the
|
|
183
183
|
same files and locks, running each call in a worker thread (`asyncio.to_thread`),
|
|
184
184
|
so it can share a directory with the sync adapter.
|
|
185
185
|
|
|
186
186
|
### File-Based (one file per item)
|
|
187
187
|
|
|
188
188
|
```python
|
|
189
|
-
from objbase.storage.
|
|
189
|
+
from objbase.storage.local import LocalDirectoryStorage
|
|
190
190
|
|
|
191
|
-
storage =
|
|
191
|
+
storage = LocalDirectoryStorage(base_dir="/var/data/myapp")
|
|
192
192
|
```
|
|
193
193
|
|
|
194
194
|
Items are stored at `{base_dir}/{item_type}/{id}.json`.
|
|
@@ -211,14 +211,14 @@ correct with concurrent writers across threads and processes; concurrent writes
|
|
|
211
211
|
to the same item are last-writer-wins. Locks are advisory and may not work on
|
|
212
212
|
network file systems (NFS, SMB).
|
|
213
213
|
|
|
214
|
-
`
|
|
214
|
+
`AsyncLocalDirectoryStorage(base_dir=...)` is the async counterpart. It uses
|
|
215
215
|
the same files, index and locks, running each call in a worker thread
|
|
216
216
|
(`asyncio.to_thread`); rebuild its index with `await storage.arebuild_index(item_type)`.
|
|
217
217
|
|
|
218
218
|
### Path safety
|
|
219
219
|
|
|
220
220
|
Both file-based adapters build file paths from item types (and, for
|
|
221
|
-
`
|
|
221
|
+
`LocalDirectoryStorage`, ids), so they guard against path traversal:
|
|
222
222
|
|
|
223
223
|
- Names must be a single path component: empty names, `.`, `..`, and names
|
|
224
224
|
containing `/`, `\`, NUL or newlines are rejected. On Windows, `< > : " | ? *`
|
|
@@ -237,7 +237,7 @@ check and the operation. Don't give untrusted users write access to it.
|
|
|
237
237
|
### SQLite
|
|
238
238
|
|
|
239
239
|
```python
|
|
240
|
-
from objbase.storage.
|
|
240
|
+
from objbase.storage.sqlite import SQLiteStorage
|
|
241
241
|
|
|
242
242
|
storage = SQLiteStorage(db_path="myapp.db")
|
|
243
243
|
```
|
|
@@ -252,15 +252,15 @@ also needs no extra dependencies.
|
|
|
252
252
|
|
|
253
253
|
```python
|
|
254
254
|
import redis
|
|
255
|
-
from objbase.storage.
|
|
255
|
+
from objbase.storage.redis import RedisStorage
|
|
256
256
|
|
|
257
257
|
client = redis.Redis(host="localhost", port=6379, decode_responses=True)
|
|
258
258
|
storage = RedisStorage(redis_client=client)
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
Each item type is one Redis hash, `
|
|
261
|
+
Each item type is one Redis hash, `collection:{item_type}`, mapping item ids to
|
|
262
262
|
JSON-encoded items, so value types (numbers, booleans, lists, nested dicts) are
|
|
263
|
-
preserved. Pass `key_prefix="myapp:"` to use a different prefix than `
|
|
263
|
+
preserved. Pass `key_prefix="myapp:"` to use a different prefix than `collection:`.
|
|
264
264
|
|
|
265
265
|
Pass a pre-configured `redis.Redis` client (sync); `decode_responses` may be on or off.
|
|
266
266
|
Requires `redis-py`. `AsyncRedisStorage` takes a `redis.asyncio.Redis` client
|
|
@@ -270,29 +270,30 @@ and uses the same layout, so sync and async adapters can share data.
|
|
|
270
270
|
|
|
271
271
|
```python
|
|
272
272
|
import pymongo
|
|
273
|
-
from objbase.storage.
|
|
273
|
+
from objbase.storage.mongodb import MongoDBStorage
|
|
274
274
|
|
|
275
275
|
client = pymongo.MongoClient("mongodb://localhost:27017")
|
|
276
276
|
storage = MongoDBStorage(mongo_client=client)
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
Items are stored in the `
|
|
279
|
+
Items are stored in the `collection` database, one collection per `item_type`.
|
|
280
|
+
Pass `db_name="myapp"` to use a different database.
|
|
280
281
|
The MongoDB `_id` field is stripped from results automatically.
|
|
281
282
|
Pass a pre-configured `pymongo.MongoClient`. Requires `pymongo`. `AsyncMongoDBStorage`
|
|
282
|
-
takes a `pymongo.AsyncMongoClient` and uses the same layout, so sync and async adapters
|
|
283
|
+
takes a `pymongo.AsyncMongoClient` (and the same `db_name` option) and uses the same layout, so sync and async adapters
|
|
283
284
|
can share data. Both accept an optional MongoDB `query` in `items` / `aitems` to filter results.
|
|
284
285
|
|
|
285
286
|
---
|
|
286
287
|
|
|
287
288
|
## Pydantic Models
|
|
288
289
|
|
|
289
|
-
Use `
|
|
290
|
+
Use `PydanticCollection` to validate items against a Pydantic `BaseModel`.
|
|
290
291
|
`save` and `get` return typed model instances instead of plain dicts.
|
|
291
292
|
|
|
292
293
|
```python
|
|
293
294
|
from pydantic import BaseModel
|
|
294
|
-
from objbase.pydantic import
|
|
295
|
-
from objbase.storage.
|
|
295
|
+
from objbase.pydantic import PydanticCollection
|
|
296
|
+
from objbase.storage.inmemory import InMemoryStorage
|
|
296
297
|
|
|
297
298
|
|
|
298
299
|
class Todo(BaseModel):
|
|
@@ -301,7 +302,7 @@ class Todo(BaseModel):
|
|
|
301
302
|
done: bool = False
|
|
302
303
|
|
|
303
304
|
|
|
304
|
-
todos =
|
|
305
|
+
todos = PydanticCollection(
|
|
305
306
|
item_type="todo",
|
|
306
307
|
storage=InMemoryStorage(),
|
|
307
308
|
model_class=Todo,
|
|
@@ -314,7 +315,7 @@ if item is not None:
|
|
|
314
315
|
```
|
|
315
316
|
|
|
316
317
|
The model type is inferred from `model_class`, so type checkers know that
|
|
317
|
-
`todos.get()` returns `Todo | None` and `todos.
|
|
318
|
+
`todos.get()` returns `Todo | None` and `todos.items()` returns `list[Todo]`.
|
|
318
319
|
`todos.keys()` returns the item ids (`list[str]`) without loading or validating
|
|
319
320
|
any items.
|
|
320
321
|
|
|
@@ -331,15 +332,15 @@ so values Pydantic coerces are stored normalized: patching `{"done": "true"}` st
|
|
|
331
332
|
|
|
332
333
|
### Async
|
|
333
334
|
|
|
334
|
-
`
|
|
335
|
+
`AsyncPydanticCollection` has the same methods and behaviour, as coroutines, and
|
|
335
336
|
takes an async storage adapter:
|
|
336
337
|
|
|
337
338
|
```python
|
|
338
339
|
import redis.asyncio
|
|
339
|
-
from objbase.asyncio.storage.
|
|
340
|
-
from objbase.pydantic import
|
|
340
|
+
from objbase.asyncio.storage.redis import AsyncRedisStorage
|
|
341
|
+
from objbase.pydantic import AsyncPydanticCollection
|
|
341
342
|
|
|
342
|
-
todos =
|
|
343
|
+
todos = AsyncPydanticCollection(
|
|
343
344
|
item_type="todo",
|
|
344
345
|
storage=AsyncRedisStorage(redis.asyncio.Redis()),
|
|
345
346
|
model_class=Todo,
|
|
@@ -353,31 +354,31 @@ item = await todos.get("1") # Todo | None
|
|
|
353
354
|
|
|
354
355
|
## Async Usage
|
|
355
356
|
|
|
356
|
-
`
|
|
357
|
+
`AsyncCollection` has the same methods and behaviour as `Collection`, but every
|
|
357
358
|
method is a coroutine. It works with any `AsyncStorage` adapter:
|
|
358
|
-
`
|
|
359
|
+
`AsyncLocalFileStorage`, `AsyncLocalDirectoryStorage`, `AsyncSQLiteStorage`,
|
|
359
360
|
`AsyncRedisStorage`, `AsyncMongoDBStorage`,
|
|
360
361
|
or `InMemoryStorage` for tests.
|
|
361
362
|
|
|
362
363
|
```python
|
|
363
364
|
import redis.asyncio
|
|
364
|
-
from objbase.asyncio.
|
|
365
|
-
from objbase.asyncio.storage.
|
|
365
|
+
from objbase.asyncio.collection import AsyncCollection
|
|
366
|
+
from objbase.asyncio.storage.redis import AsyncRedisStorage
|
|
366
367
|
|
|
367
368
|
client = redis.asyncio.Redis(host="localhost", port=6379)
|
|
368
|
-
todos =
|
|
369
|
+
todos = AsyncCollection(item_type="todo", storage=AsyncRedisStorage(client))
|
|
369
370
|
|
|
370
371
|
await todos.save({"id": "1", "title": "Buy milk", "done": False})
|
|
371
372
|
await todos.get("1") # → {"id": "1", "title": "Buy milk", "done": False}
|
|
372
|
-
await todos.
|
|
373
|
+
await todos.items() # → [{"id": "1", ...}]
|
|
373
374
|
await todos.keys() # → ["1"]
|
|
374
375
|
await todos.patch("1", {"done": True}) # → {"id": "1", ..., "done": True}
|
|
375
376
|
await todos.delete("1") # → True
|
|
376
377
|
```
|
|
377
378
|
|
|
378
|
-
For Pydantic models, use `
|
|
379
|
+
For Pydantic models, use `AsyncPydanticCollection` (see [Pydantic Models: Async](#async)).
|
|
379
380
|
|
|
380
|
-
Passing a sync-only adapter (e.g. `SQLiteStorage`) to `
|
|
381
|
+
Passing a sync-only adapter (e.g. `SQLiteStorage`) to `AsyncCollection`
|
|
381
382
|
raises `TypeError`; use its async counterpart (e.g. `AsyncSQLiteStorage`) instead.
|
|
382
383
|
The adapter methods (`akeys`, `aitems`, `aread`, `awrite`, `adelete`)
|
|
383
384
|
can also be called directly on the storage.
|
|
@@ -390,12 +391,12 @@ Runnable scripts are in [`examples/`](examples/):
|
|
|
390
391
|
|
|
391
392
|
| Script | Shows |
|
|
392
393
|
|---|---|
|
|
393
|
-
| `dict_example.py` | `
|
|
394
|
-
| `pydantic_example.py` | `
|
|
395
|
-
| `async_example.py` | `
|
|
396
|
-
| `async_pydantic_example.py` | `
|
|
397
|
-
| `async_file_example.py` | `
|
|
398
|
-
| `async_directory_example.py` | `
|
|
394
|
+
| `dict_example.py` | `Collection` with plain dicts (in-memory) |
|
|
395
|
+
| `pydantic_example.py` | `PydanticCollection` (in-memory) |
|
|
396
|
+
| `async_example.py` | `AsyncCollection` (in-memory) |
|
|
397
|
+
| `async_pydantic_example.py` | `AsyncPydanticCollection` (in-memory) |
|
|
398
|
+
| `async_file_example.py` | `AsyncLocalFileStorage` |
|
|
399
|
+
| `async_directory_example.py` | `AsyncLocalDirectoryStorage`, incl. concurrent saves and `arebuild_index` |
|
|
399
400
|
| `async_sqlite_example.py` | `AsyncSQLiteStorage` |
|
|
400
401
|
| `mongodb_example.py` | `MongoDBStorage`, incl. a MongoDB `query` filter |
|
|
401
402
|
| `async_mongodb_example.py` | `AsyncMongoDBStorage`, incl. a MongoDB `query` filter |
|
|
@@ -405,7 +406,7 @@ uv run python examples/async_sqlite_example.py
|
|
|
405
406
|
```
|
|
406
407
|
|
|
407
408
|
The file-based and SQLite examples write to `data/` in the current directory
|
|
408
|
-
(ignored by git); set `
|
|
409
|
+
(ignored by git); set `OBJBASE_DATA_DIR` or `SQLITE_DB_PATH` to change that. The MongoDB
|
|
409
410
|
examples need a running server — `docker run --rm -p 27017:27017 mongo:7.0` — and
|
|
410
411
|
connect to `MONGODB_URI` (default `mongodb://localhost:27017`).
|
|
411
412
|
|
|
@@ -422,7 +423,7 @@ once at startup and tear them down cleanly on shutdown.
|
|
|
422
423
|
from contextlib import asynccontextmanager
|
|
423
424
|
from fastapi import FastAPI
|
|
424
425
|
import redis.asyncio
|
|
425
|
-
from objbase.asyncio.storage.
|
|
426
|
+
from objbase.asyncio.storage.redis import AsyncRedisStorage
|
|
426
427
|
|
|
427
428
|
|
|
428
429
|
@asynccontextmanager
|
|
@@ -436,28 +437,28 @@ async def lifespan(app: FastAPI):
|
|
|
436
437
|
app = FastAPI(lifespan=lifespan)
|
|
437
438
|
```
|
|
438
439
|
|
|
439
|
-
### 2. Inject `
|
|
440
|
+
### 2. Inject `AsyncCollection` with `Depends`
|
|
440
441
|
|
|
441
|
-
Wrap the `
|
|
442
|
+
Wrap the `AsyncCollection` construction in a dependency function so routes stay clean
|
|
442
443
|
and the storage adapter is easy to swap out (e.g. in tests).
|
|
443
444
|
|
|
444
445
|
```python
|
|
445
446
|
from fastapi import Depends, HTTPException, Request
|
|
446
|
-
from objbase.asyncio.
|
|
447
|
+
from objbase.asyncio.collection import AsyncCollection
|
|
447
448
|
from objbase.errors import ItemNotFoundError
|
|
448
449
|
|
|
449
450
|
|
|
450
|
-
def get_todos(request: Request) ->
|
|
451
|
-
return
|
|
451
|
+
def get_todos(request: Request) -> AsyncCollection:
|
|
452
|
+
return AsyncCollection(item_type="todo", storage=request.app.state.storage)
|
|
452
453
|
|
|
453
454
|
|
|
454
455
|
@app.get("/todos")
|
|
455
|
-
async def list_todos(todos:
|
|
456
|
-
return await todos.
|
|
456
|
+
async def list_todos(todos: AsyncCollection = Depends(get_todos)):
|
|
457
|
+
return await todos.items()
|
|
457
458
|
|
|
458
459
|
|
|
459
460
|
@app.get("/todos/{todo_id}")
|
|
460
|
-
async def get_todo(todo_id: str, todos:
|
|
461
|
+
async def get_todo(todo_id: str, todos: AsyncCollection = Depends(get_todos)):
|
|
461
462
|
item = await todos.get(todo_id)
|
|
462
463
|
if item is None:
|
|
463
464
|
raise HTTPException(status_code=404)
|
|
@@ -465,12 +466,12 @@ async def get_todo(todo_id: str, todos: AsyncInventory = Depends(get_todos)):
|
|
|
465
466
|
|
|
466
467
|
|
|
467
468
|
@app.post("/todos")
|
|
468
|
-
async def create_todo(item: dict, todos:
|
|
469
|
+
async def create_todo(item: dict, todos: AsyncCollection = Depends(get_todos)):
|
|
469
470
|
return await todos.save(item)
|
|
470
471
|
|
|
471
472
|
|
|
472
473
|
@app.patch("/todos/{todo_id}")
|
|
473
|
-
async def update_todo(todo_id: str, data: dict, todos:
|
|
474
|
+
async def update_todo(todo_id: str, data: dict, todos: AsyncCollection = Depends(get_todos)):
|
|
474
475
|
try:
|
|
475
476
|
return await todos.patch(todo_id, data)
|
|
476
477
|
except ItemNotFoundError:
|
|
@@ -486,8 +487,8 @@ automatically, keeping the event loop unblocked.
|
|
|
486
487
|
```python
|
|
487
488
|
from contextlib import asynccontextmanager
|
|
488
489
|
from fastapi import FastAPI, Depends, Request
|
|
489
|
-
from objbase.
|
|
490
|
-
from objbase.storage.
|
|
490
|
+
from objbase.collection import Collection
|
|
491
|
+
from objbase.storage.sqlite import SQLiteStorage
|
|
491
492
|
|
|
492
493
|
|
|
493
494
|
@asynccontextmanager
|
|
@@ -499,13 +500,13 @@ async def lifespan(app: FastAPI):
|
|
|
499
500
|
app = FastAPI(lifespan=lifespan)
|
|
500
501
|
|
|
501
502
|
|
|
502
|
-
def get_todos(request: Request) ->
|
|
503
|
-
return
|
|
503
|
+
def get_todos(request: Request) -> Collection:
|
|
504
|
+
return Collection(item_type="todo", storage=request.app.state.storage)
|
|
504
505
|
|
|
505
506
|
|
|
506
507
|
@app.get("/todos") # sync — runs in threadpool
|
|
507
|
-
def list_todos(todos:
|
|
508
|
-
return todos.
|
|
508
|
+
def list_todos(todos: Collection = Depends(get_todos)):
|
|
509
|
+
return todos.items()
|
|
509
510
|
```
|
|
510
511
|
|
|
511
512
|
### 4. Override the dependency in tests
|
|
@@ -515,15 +516,15 @@ Swap the storage backend for the entire test run without touching any route code
|
|
|
515
516
|
for Redis. Create it once so data persists across requests:
|
|
516
517
|
|
|
517
518
|
```python
|
|
518
|
-
from objbase.asyncio.
|
|
519
|
-
from objbase.storage.
|
|
519
|
+
from objbase.asyncio.collection import AsyncCollection
|
|
520
|
+
from objbase.storage.inmemory import InMemoryStorage
|
|
520
521
|
from fastapi.testclient import TestClient
|
|
521
522
|
|
|
522
523
|
test_storage = InMemoryStorage()
|
|
523
524
|
|
|
524
525
|
|
|
525
526
|
def override_todos():
|
|
526
|
-
return
|
|
527
|
+
return AsyncCollection(item_type="todo", storage=test_storage)
|
|
527
528
|
|
|
528
529
|
|
|
529
530
|
app.dependency_overrides[get_todos] = override_todos
|
|
@@ -536,8 +537,8 @@ client = TestClient(app)
|
|
|
536
537
|
|---|---|
|
|
537
538
|
| Single-process, low traffic | `SQLiteStorage` — zero deps, ACID, simple |
|
|
538
539
|
| Multi-worker / multi-process | `RedisStorage` or `MongoDBStorage` |
|
|
539
|
-
| Async routes | `
|
|
540
|
-
| Async routes, single process, no infrastructure | `
|
|
540
|
+
| Async routes | `AsyncCollection` + `AsyncRedisStorage` or `AsyncMongoDBStorage` — non-blocking, fits the event loop |
|
|
541
|
+
| Async routes, single process, no infrastructure | `AsyncCollection` + `AsyncSQLiteStorage` — zero deps, runs in a worker thread |
|
|
541
542
|
| Testing / local dev | `InMemoryStorage` — fast, no infrastructure needed |
|
|
542
543
|
|
|
543
544
|
---
|
|
@@ -617,7 +618,7 @@ class MyCustomStorage:
|
|
|
617
618
|
|
|
618
619
|
|
|
619
620
|
# Works — no explicit inheritance required
|
|
620
|
-
todos =
|
|
621
|
+
todos = Collection(item_type="todo", storage=MyCustomStorage())
|
|
621
622
|
```
|
|
622
623
|
|
|
623
624
|
### Optional explicit inheritance
|
|
@@ -654,8 +655,8 @@ The package ships a `py.typed` marker, so mypy, Pyright and IDEs use its type
|
|
|
654
655
|
hints. The library itself is checked with `mypy --strict`.
|
|
655
656
|
|
|
656
657
|
- Items are typed as `objbase.Item`, an alias for `dict[str, Any]`.
|
|
657
|
-
- `
|
|
658
|
-
- `
|
|
658
|
+
- `Collection` and `AsyncCollection` accept and return `Item`; `get` returns `Item | None`.
|
|
659
|
+
- `PydanticCollection` and `AsyncPydanticCollection` are generic over their model class, which is inferred from
|
|
659
660
|
`model_class` (see [Pydantic Models](#pydantic-models)).
|
|
660
661
|
- Storage adapters accept any structurally compatible client. For example,
|
|
661
662
|
`RedisStorage` and `AsyncRedisStorage` take anything with Redis's
|