objbase 0.4.0__tar.gz → 0.5.0__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.0}/PKG-INFO +77 -77
- {objbase-0.4.0 → objbase-0.5.0}/README.md +76 -76
- {objbase-0.4.0 → objbase-0.5.0}/examples/async_directory_example.py +4 -4
- {objbase-0.4.0 → objbase-0.5.0}/examples/async_example.py +3 -3
- {objbase-0.4.0 → objbase-0.5.0}/examples/async_file_example.py +4 -4
- {objbase-0.4.0 → objbase-0.5.0}/examples/async_mongodb_example.py +3 -3
- {objbase-0.4.0 → objbase-0.5.0}/examples/async_pydantic_example.py +3 -3
- {objbase-0.4.0 → objbase-0.5.0}/examples/async_sqlite_example.py +3 -3
- {objbase-0.4.0 → objbase-0.5.0}/examples/dict_example.py +3 -3
- {objbase-0.4.0 → objbase-0.5.0}/examples/mongodb_example.py +4 -4
- {objbase-0.4.0 → objbase-0.5.0}/examples/pydantic_example.py +3 -3
- {objbase-0.4.0 → objbase-0.5.0}/pyproject.toml +1 -1
- objbase-0.5.0/src/objbase/__init__.py +58 -0
- objbase-0.4.0/src/objbase/asyncio/inventory.py → objbase-0.5.0/src/objbase/asyncio/collection.py +8 -8
- objbase-0.4.0/src/objbase/asyncio/storage/file_storage.py → objbase-0.5.0/src/objbase/asyncio/storage/local.py +9 -9
- objbase-0.4.0/src/objbase/asyncio/storage/redis_storage.py → objbase-0.5.0/src/objbase/asyncio/storage/redis.py +1 -1
- objbase-0.4.0/src/objbase/asyncio/storage/sqlite_storage.py → objbase-0.5.0/src/objbase/asyncio/storage/sqlite.py +2 -2
- objbase-0.4.0/src/objbase/inventory.py → objbase-0.5.0/src/objbase/collection.py +6 -6
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/errors.py +2 -2
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/pydantic.py +12 -12
- objbase-0.4.0/src/objbase/storage/file_storage.py → objbase-0.5.0/src/objbase/storage/local.py +2 -2
- objbase-0.4.0/tests/test_async_inventory.py → objbase-0.5.0/tests/test_async_collection.py +25 -25
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_async_file_storage.py +9 -9
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_async_mongodb_storage.py +2 -2
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_async_redis_storage.py +2 -2
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_async_sqlite_storage.py +4 -4
- objbase-0.4.0/tests/test_inventory.py → objbase-0.5.0/tests/test_collection.py +24 -24
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_file_storage.py +35 -35
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_inmemory_storage.py +1 -1
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_mongodb_storage.py +1 -1
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_package.py +7 -7
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_redis_storage.py +1 -1
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_sqlite_storage.py +1 -1
- {objbase-0.4.0 → objbase-0.5.0}/tests/test_storage_contract.py +16 -16
- {objbase-0.4.0 → objbase-0.5.0}/uv.lock +1 -1
- objbase-0.4.0/src/objbase/__init__.py +0 -58
- {objbase-0.4.0 → objbase-0.5.0}/.github/dependabot.yml +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/.github/workflows/ci.yml +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/.github/workflows/release.yml +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/.gitignore +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/DEVELOPER.md +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/LICENSE +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/release.sh +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/asyncio/__init__.py +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/asyncio/storage/__init__.py +0 -0
- /objbase-0.4.0/src/objbase/asyncio/storage/mongodb_storage.py → /objbase-0.5.0/src/objbase/asyncio/storage/mongodb.py +0 -0
- /objbase-0.4.0/src/objbase/asyncio/storage/threaded_storage.py → /objbase-0.5.0/src/objbase/asyncio/storage/threaded.py +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/interface.py +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/py.typed +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/storage/__init__.py +0 -0
- /objbase-0.4.0/src/objbase/storage/inmemory_storage.py → /objbase-0.5.0/src/objbase/storage/inmemory.py +0 -0
- /objbase-0.4.0/src/objbase/storage/mongodb_storage.py → /objbase-0.5.0/src/objbase/storage/mongodb.py +0 -0
- /objbase-0.4.0/src/objbase/storage/redis_storage.py → /objbase-0.5.0/src/objbase/storage/redis.py +0 -0
- /objbase-0.4.0/src/objbase/storage/sqlite_storage.py → /objbase-0.5.0/src/objbase/storage/sqlite.py +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/util/__init__.py +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/util/file_util.py +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/src/objbase/util/mongodb_util.py +0 -0
- {objbase-0.4.0 → objbase-0.5.0}/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.0
|
|
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
|
|
@@ -43,8 +43,8 @@ __No thrills__ - **just a simple key-value store for serializable Python objects
|
|
|
43
43
|
|
|
44
44
|
- Basic CRUD operations: `save`, `get`, `filter`, `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,15 +97,15 @@ 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.inventory` | `
|
|
101
|
-
| `objbase.errors` | `
|
|
102
|
-
| `objbase.pydantic` | `
|
|
100
|
+
| `objbase.inventory` | `Collection` |
|
|
101
|
+
| `objbase.errors` | `CollectionError`, `ItemNotFoundError` |
|
|
102
|
+
| `objbase.pydantic` | `PydanticCollection`, `AsyncPydanticCollection` (needs `objbase[pydantic]`) |
|
|
103
103
|
| `objbase.storage.{inmemory,file,sqlite,redis,mongodb}_storage` | Sync storage adapters |
|
|
104
|
-
| `objbase.asyncio.inventory` | `
|
|
104
|
+
| `objbase.asyncio.inventory` | `AsyncCollection` |
|
|
105
105
|
| `objbase.asyncio.storage.{file,sqlite,redis,mongodb}_storage` | 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
|
|
|
@@ -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` (an `
|
|
127
|
-
| The storage backend reports a failed write | `objbase.
|
|
126
|
+
| `patch` a missing item | `objbase.ItemNotFoundError` (an `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,7 +252,7 @@ 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)
|
|
@@ -270,13 +270,13 @@ 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.py` database, one collection per `item_type`.
|
|
280
280
|
The MongoDB `_id` field is stripped from results automatically.
|
|
281
281
|
Pass a pre-configured `pymongo.MongoClient`. Requires `pymongo`. `AsyncMongoDBStorage`
|
|
282
282
|
takes a `pymongo.AsyncMongoClient` and uses the same layout, so sync and async adapters
|
|
@@ -286,13 +286,13 @@ can share data. Both accept an optional MongoDB `query` in `items` / `aitems` to
|
|
|
286
286
|
|
|
287
287
|
## Pydantic Models
|
|
288
288
|
|
|
289
|
-
Use `
|
|
289
|
+
Use `PydanticCollection` to validate items against a Pydantic `BaseModel`.
|
|
290
290
|
`save` and `get` return typed model instances instead of plain dicts.
|
|
291
291
|
|
|
292
292
|
```python
|
|
293
293
|
from pydantic import BaseModel
|
|
294
|
-
from objbase.pydantic import
|
|
295
|
-
from objbase.storage.
|
|
294
|
+
from objbase.pydantic import PydanticCollection
|
|
295
|
+
from objbase.storage.inmemory import InMemoryStorage
|
|
296
296
|
|
|
297
297
|
|
|
298
298
|
class Todo(BaseModel):
|
|
@@ -301,7 +301,7 @@ class Todo(BaseModel):
|
|
|
301
301
|
done: bool = False
|
|
302
302
|
|
|
303
303
|
|
|
304
|
-
todos =
|
|
304
|
+
todos = PydanticCollection(
|
|
305
305
|
item_type="todo",
|
|
306
306
|
storage=InMemoryStorage(),
|
|
307
307
|
model_class=Todo,
|
|
@@ -331,15 +331,15 @@ so values Pydantic coerces are stored normalized: patching `{"done": "true"}` st
|
|
|
331
331
|
|
|
332
332
|
### Async
|
|
333
333
|
|
|
334
|
-
`
|
|
334
|
+
`AsyncPydanticCollection` has the same methods and behaviour, as coroutines, and
|
|
335
335
|
takes an async storage adapter:
|
|
336
336
|
|
|
337
337
|
```python
|
|
338
338
|
import redis.asyncio
|
|
339
|
-
from objbase.asyncio.storage.
|
|
340
|
-
from objbase.pydantic import
|
|
339
|
+
from objbase.asyncio.storage.redis import AsyncRedisStorage
|
|
340
|
+
from objbase.pydantic import AsyncPydanticCollection
|
|
341
341
|
|
|
342
|
-
todos =
|
|
342
|
+
todos = AsyncPydanticCollection(
|
|
343
343
|
item_type="todo",
|
|
344
344
|
storage=AsyncRedisStorage(redis.asyncio.Redis()),
|
|
345
345
|
model_class=Todo,
|
|
@@ -353,19 +353,19 @@ item = await todos.get("1") # Todo | None
|
|
|
353
353
|
|
|
354
354
|
## Async Usage
|
|
355
355
|
|
|
356
|
-
`
|
|
356
|
+
`AsyncCollection` has the same methods and behaviour as `Collection`, but every
|
|
357
357
|
method is a coroutine. It works with any `AsyncStorage` adapter:
|
|
358
|
-
`
|
|
358
|
+
`AsyncLocalFileStorage`, `AsyncLocalDirectoryStorage`, `AsyncSQLiteStorage`,
|
|
359
359
|
`AsyncRedisStorage`, `AsyncMongoDBStorage`,
|
|
360
360
|
or `InMemoryStorage` for tests.
|
|
361
361
|
|
|
362
362
|
```python
|
|
363
363
|
import redis.asyncio
|
|
364
|
-
from objbase.asyncio.
|
|
365
|
-
from objbase.asyncio.storage.
|
|
364
|
+
from objbase.asyncio.collection import AsyncCollection
|
|
365
|
+
from objbase.asyncio.storage.redis import AsyncRedisStorage
|
|
366
366
|
|
|
367
367
|
client = redis.asyncio.Redis(host="localhost", port=6379)
|
|
368
|
-
todos =
|
|
368
|
+
todos = AsyncCollection(item_type="todo", storage=AsyncRedisStorage(client))
|
|
369
369
|
|
|
370
370
|
await todos.save({"id": "1", "title": "Buy milk", "done": False})
|
|
371
371
|
await todos.get("1") # → {"id": "1", "title": "Buy milk", "done": False}
|
|
@@ -375,9 +375,9 @@ await todos.patch("1", {"done": True}) # → {"id": "1", ..., "done": True}
|
|
|
375
375
|
await todos.delete("1") # → True
|
|
376
376
|
```
|
|
377
377
|
|
|
378
|
-
For Pydantic models, use `
|
|
378
|
+
For Pydantic models, use `AsyncPydanticCollection` (see [Pydantic Models: Async](#async)).
|
|
379
379
|
|
|
380
|
-
Passing a sync-only adapter (e.g. `SQLiteStorage`) to `
|
|
380
|
+
Passing a sync-only adapter (e.g. `SQLiteStorage`) to `AsyncCollection`
|
|
381
381
|
raises `TypeError`; use its async counterpart (e.g. `AsyncSQLiteStorage`) instead.
|
|
382
382
|
The adapter methods (`akeys`, `aitems`, `aread`, `awrite`, `adelete`)
|
|
383
383
|
can also be called directly on the storage.
|
|
@@ -390,12 +390,12 @@ Runnable scripts are in [`examples/`](examples/):
|
|
|
390
390
|
|
|
391
391
|
| Script | Shows |
|
|
392
392
|
|---|---|
|
|
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` | `
|
|
393
|
+
| `dict_example.py` | `Collection` with plain dicts (in-memory) |
|
|
394
|
+
| `pydantic_example.py` | `PydanticCollection` (in-memory) |
|
|
395
|
+
| `async_example.py` | `AsyncCollection` (in-memory) |
|
|
396
|
+
| `async_pydantic_example.py` | `AsyncPydanticCollection` (in-memory) |
|
|
397
|
+
| `async_file_example.py` | `AsyncLocalFileStorage` |
|
|
398
|
+
| `async_directory_example.py` | `AsyncLocalDirectoryStorage`, incl. concurrent saves and `arebuild_index` |
|
|
399
399
|
| `async_sqlite_example.py` | `AsyncSQLiteStorage` |
|
|
400
400
|
| `mongodb_example.py` | `MongoDBStorage`, incl. a MongoDB `query` filter |
|
|
401
401
|
| `async_mongodb_example.py` | `AsyncMongoDBStorage`, incl. a MongoDB `query` filter |
|
|
@@ -422,7 +422,7 @@ once at startup and tear them down cleanly on shutdown.
|
|
|
422
422
|
from contextlib import asynccontextmanager
|
|
423
423
|
from fastapi import FastAPI
|
|
424
424
|
import redis.asyncio
|
|
425
|
-
from objbase.asyncio.storage.
|
|
425
|
+
from objbase.asyncio.storage.redis import AsyncRedisStorage
|
|
426
426
|
|
|
427
427
|
|
|
428
428
|
@asynccontextmanager
|
|
@@ -436,28 +436,28 @@ async def lifespan(app: FastAPI):
|
|
|
436
436
|
app = FastAPI(lifespan=lifespan)
|
|
437
437
|
```
|
|
438
438
|
|
|
439
|
-
### 2. Inject `
|
|
439
|
+
### 2. Inject `AsyncCollection` with `Depends`
|
|
440
440
|
|
|
441
|
-
Wrap the `
|
|
441
|
+
Wrap the `AsyncCollection` construction in a dependency function so routes stay clean
|
|
442
442
|
and the storage adapter is easy to swap out (e.g. in tests).
|
|
443
443
|
|
|
444
444
|
```python
|
|
445
445
|
from fastapi import Depends, HTTPException, Request
|
|
446
|
-
from objbase.asyncio.
|
|
446
|
+
from objbase.asyncio.collection import AsyncCollection
|
|
447
447
|
from objbase.errors import ItemNotFoundError
|
|
448
448
|
|
|
449
449
|
|
|
450
|
-
def get_todos(request: Request) ->
|
|
451
|
-
return
|
|
450
|
+
def get_todos(request: Request) -> AsyncCollection:
|
|
451
|
+
return AsyncCollection(item_type="todo", storage=request.app.state.storage)
|
|
452
452
|
|
|
453
453
|
|
|
454
454
|
@app.get("/todos")
|
|
455
|
-
async def list_todos(todos:
|
|
455
|
+
async def list_todos(todos: AsyncCollection = Depends(get_todos)):
|
|
456
456
|
return await todos.filter()
|
|
457
457
|
|
|
458
458
|
|
|
459
459
|
@app.get("/todos/{todo_id}")
|
|
460
|
-
async def get_todo(todo_id: str, todos:
|
|
460
|
+
async def get_todo(todo_id: str, todos: AsyncCollection = Depends(get_todos)):
|
|
461
461
|
item = await todos.get(todo_id)
|
|
462
462
|
if item is None:
|
|
463
463
|
raise HTTPException(status_code=404)
|
|
@@ -465,12 +465,12 @@ async def get_todo(todo_id: str, todos: AsyncInventory = Depends(get_todos)):
|
|
|
465
465
|
|
|
466
466
|
|
|
467
467
|
@app.post("/todos")
|
|
468
|
-
async def create_todo(item: dict, todos:
|
|
468
|
+
async def create_todo(item: dict, todos: AsyncCollection = Depends(get_todos)):
|
|
469
469
|
return await todos.save(item)
|
|
470
470
|
|
|
471
471
|
|
|
472
472
|
@app.patch("/todos/{todo_id}")
|
|
473
|
-
async def update_todo(todo_id: str, data: dict, todos:
|
|
473
|
+
async def update_todo(todo_id: str, data: dict, todos: AsyncCollection = Depends(get_todos)):
|
|
474
474
|
try:
|
|
475
475
|
return await todos.patch(todo_id, data)
|
|
476
476
|
except ItemNotFoundError:
|
|
@@ -486,8 +486,8 @@ automatically, keeping the event loop unblocked.
|
|
|
486
486
|
```python
|
|
487
487
|
from contextlib import asynccontextmanager
|
|
488
488
|
from fastapi import FastAPI, Depends, Request
|
|
489
|
-
from objbase.
|
|
490
|
-
from objbase.storage.
|
|
489
|
+
from objbase.collection import Collection
|
|
490
|
+
from objbase.storage.sqlite import SQLiteStorage
|
|
491
491
|
|
|
492
492
|
|
|
493
493
|
@asynccontextmanager
|
|
@@ -499,13 +499,13 @@ async def lifespan(app: FastAPI):
|
|
|
499
499
|
app = FastAPI(lifespan=lifespan)
|
|
500
500
|
|
|
501
501
|
|
|
502
|
-
def get_todos(request: Request) ->
|
|
503
|
-
return
|
|
502
|
+
def get_todos(request: Request) -> Collection:
|
|
503
|
+
return Collection(item_type="todo", storage=request.app.state.storage)
|
|
504
504
|
|
|
505
505
|
|
|
506
506
|
@app.get("/todos") # sync — runs in threadpool
|
|
507
|
-
def list_todos(todos:
|
|
508
|
-
return todos.
|
|
507
|
+
def list_todos(todos: Collection = Depends(get_todos)):
|
|
508
|
+
return todos.items()
|
|
509
509
|
```
|
|
510
510
|
|
|
511
511
|
### 4. Override the dependency in tests
|
|
@@ -515,15 +515,15 @@ Swap the storage backend for the entire test run without touching any route code
|
|
|
515
515
|
for Redis. Create it once so data persists across requests:
|
|
516
516
|
|
|
517
517
|
```python
|
|
518
|
-
from objbase.asyncio.
|
|
519
|
-
from objbase.storage.
|
|
518
|
+
from objbase.asyncio.collection import AsyncCollection
|
|
519
|
+
from objbase.storage.inmemory import InMemoryStorage
|
|
520
520
|
from fastapi.testclient import TestClient
|
|
521
521
|
|
|
522
522
|
test_storage = InMemoryStorage()
|
|
523
523
|
|
|
524
524
|
|
|
525
525
|
def override_todos():
|
|
526
|
-
return
|
|
526
|
+
return AsyncCollection(item_type="todo", storage=test_storage)
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
app.dependency_overrides[get_todos] = override_todos
|
|
@@ -536,8 +536,8 @@ client = TestClient(app)
|
|
|
536
536
|
|---|---|
|
|
537
537
|
| Single-process, low traffic | `SQLiteStorage` — zero deps, ACID, simple |
|
|
538
538
|
| Multi-worker / multi-process | `RedisStorage` or `MongoDBStorage` |
|
|
539
|
-
| Async routes | `
|
|
540
|
-
| Async routes, single process, no infrastructure | `
|
|
539
|
+
| Async routes | `AsyncCollection` + `AsyncRedisStorage` or `AsyncMongoDBStorage` — non-blocking, fits the event loop |
|
|
540
|
+
| Async routes, single process, no infrastructure | `AsyncCollection` + `AsyncSQLiteStorage` — zero deps, runs in a worker thread |
|
|
541
541
|
| Testing / local dev | `InMemoryStorage` — fast, no infrastructure needed |
|
|
542
542
|
|
|
543
543
|
---
|
|
@@ -617,7 +617,7 @@ class MyCustomStorage:
|
|
|
617
617
|
|
|
618
618
|
|
|
619
619
|
# Works — no explicit inheritance required
|
|
620
|
-
todos =
|
|
620
|
+
todos = Collection(item_type="todo", storage=MyCustomStorage())
|
|
621
621
|
```
|
|
622
622
|
|
|
623
623
|
### Optional explicit inheritance
|
|
@@ -654,8 +654,8 @@ The package ships a `py.typed` marker, so mypy, Pyright and IDEs use its type
|
|
|
654
654
|
hints. The library itself is checked with `mypy --strict`.
|
|
655
655
|
|
|
656
656
|
- Items are typed as `objbase.Item`, an alias for `dict[str, Any]`.
|
|
657
|
-
- `
|
|
658
|
-
- `
|
|
657
|
+
- `Collection` and `AsyncCollection` accept and return `Item`; `get` returns `Item | None`.
|
|
658
|
+
- `PydanticCollection` and `AsyncPydanticCollection` are generic over their model class, which is inferred from
|
|
659
659
|
`model_class` (see [Pydantic Models](#pydantic-models)).
|
|
660
660
|
- Storage adapters accept any structurally compatible client. For example,
|
|
661
661
|
`RedisStorage` and `AsyncRedisStorage` take anything with Redis's
|