objbase 0.5.0__tar.gz → 0.5.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.
- {objbase-0.5.0 → objbase-0.5.2}/PKG-INFO +97 -17
- {objbase-0.5.0 → objbase-0.5.2}/README.md +96 -16
- {objbase-0.5.0 → objbase-0.5.2}/examples/async_directory_example.py +9 -9
- {objbase-0.5.0 → objbase-0.5.2}/examples/async_example.py +5 -5
- objbase-0.5.2/examples/async_file_example.py +30 -0
- {objbase-0.5.0 → objbase-0.5.2}/examples/async_mongodb_example.py +7 -7
- {objbase-0.5.0 → objbase-0.5.2}/examples/async_pydantic_example.py +7 -7
- {objbase-0.5.0 → objbase-0.5.2}/examples/async_sqlite_example.py +7 -7
- {objbase-0.5.0 → objbase-0.5.2}/examples/dict_example.py +5 -5
- {objbase-0.5.0 → objbase-0.5.2}/examples/mongodb_example.py +7 -7
- {objbase-0.5.0 → objbase-0.5.2}/examples/pydantic_example.py +5 -5
- {objbase-0.5.0 → objbase-0.5.2}/pyproject.toml +1 -1
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/__init__.py +7 -1
- objbase-0.5.2/src/objbase/actions.py +55 -0
- objbase-0.5.2/src/objbase/asyncio/collection.py +93 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/asyncio/storage/local.py +4 -4
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/asyncio/storage/mongodb.py +4 -2
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/collection.py +35 -1
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/errors.py +9 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/pydantic.py +22 -22
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/storage/inmemory.py +1 -1
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/storage/local.py +8 -8
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/storage/mongodb.py +9 -3
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/storage/redis.py +1 -1
- objbase-0.5.2/tests/test_actions.py +229 -0
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_async_collection.py +8 -8
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_async_file_storage.py +3 -3
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_async_mongodb_storage.py +11 -2
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_async_redis_storage.py +2 -2
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_async_sqlite_storage.py +1 -1
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_collection.py +6 -6
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_file_storage.py +2 -2
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_mongodb_storage.py +30 -3
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_redis_storage.py +3 -3
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_storage_contract.py +4 -4
- {objbase-0.5.0 → objbase-0.5.2}/uv.lock +1 -1
- objbase-0.5.0/examples/async_file_example.py +0 -30
- objbase-0.5.0/src/objbase/asyncio/collection.py +0 -46
- {objbase-0.5.0 → objbase-0.5.2}/.github/dependabot.yml +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/.github/workflows/ci.yml +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/.github/workflows/release.yml +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/.gitignore +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/DEVELOPER.md +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/LICENSE +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/release.sh +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/asyncio/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/asyncio/storage/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/asyncio/storage/redis.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/asyncio/storage/sqlite.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/asyncio/storage/threaded.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/interface.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/py.typed +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/storage/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/storage/sqlite.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/util/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/util/file_util.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/util/mongodb_util.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/src/objbase/util/redis_util.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_inmemory_storage.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_package.py +0 -0
- {objbase-0.5.0 → objbase-0.5.2}/tests/test_sqlite_storage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: objbase
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
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,7 +41,7 @@ __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
46
|
- Optional Pydantic model validation with `PydanticCollection` / `AsyncPydanticCollection`
|
|
47
47
|
- Async support via `AsyncCollection` with async storage adapters (in-memory, file-based, SQLite, Redis, MongoDB)
|
|
@@ -97,12 +97,13 @@ 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` | `CollectionError`, `ItemNotFoundError` |
|
|
100
|
+
| `objbase.collection` | `Collection` |
|
|
101
|
+
| `objbase.errors` | `CollectionError`, `ItemNotFoundError`, `ActionNotFoundError` |
|
|
102
|
+
| `objbase.actions` | `ActionHandler`, `AsyncActionHandler`, `ActionParams`, `load_action_handler` |
|
|
102
103
|
| `objbase.pydantic` | `PydanticCollection`, `AsyncPydanticCollection` (needs `objbase[pydantic]`) |
|
|
103
|
-
| `objbase.storage.{inmemory,
|
|
104
|
-
| `objbase.asyncio.
|
|
105
|
-
| `objbase.asyncio.storage.{
|
|
104
|
+
| `objbase.storage.{inmemory,local,sqlite,redis,mongodb}` | Sync storage adapters |
|
|
105
|
+
| `objbase.asyncio.collection` | `AsyncCollection` |
|
|
106
|
+
| `objbase.asyncio.storage.{local,sqlite,redis,mongodb}` | Async storage adapters |
|
|
106
107
|
|
|
107
108
|
`import objbase` works without Pydantic installed; `PydanticCollection` and
|
|
108
109
|
`AsyncPydanticCollection` are loaded on first access.
|
|
@@ -111,7 +112,7 @@ or from their submodules as in the examples below:
|
|
|
111
112
|
|
|
112
113
|
All adapters follow the same contract (verified by a shared test suite):
|
|
113
114
|
|
|
114
|
-
- `get` returns `None` for a missing item; `
|
|
115
|
+
- `get` returns `None` for a missing item; `items` and `keys` return `[]` for an empty type.
|
|
115
116
|
- `save` inserts a new item or **replaces** an existing one entirely (it does not merge fields).
|
|
116
117
|
- `patch` merges the given fields into an existing item. It cannot change the item's `id`.
|
|
117
118
|
- `delete` returns `True` if the item was removed, `False` if it did not exist.
|
|
@@ -123,8 +124,9 @@ Errors are raised, not returned:
|
|
|
123
124
|
|---|---|
|
|
124
125
|
| `save` an item without an `id` (or with an empty one) | `ValueError` |
|
|
125
126
|
| `patch` with data that changes the item's `id` | `ValueError` |
|
|
126
|
-
| `patch` a missing item | `objbase.ItemNotFoundError` (
|
|
127
|
+
| `patch` a missing item | `objbase.ItemNotFoundError` (a `CollectionError` and a `LookupError`) |
|
|
127
128
|
| The storage backend reports a failed write | `objbase.CollectionError` |
|
|
129
|
+
| `run_action` with an action that isn't registered | `objbase.ActionNotFoundError` (a `CollectionError` and a `LookupError`) |
|
|
128
130
|
|
|
129
131
|
`CollectionError` is the base class of all objbase errors.
|
|
130
132
|
|
|
@@ -258,9 +260,9 @@ client = redis.Redis(host="localhost", port=6379, decode_responses=True)
|
|
|
258
260
|
storage = RedisStorage(redis_client=client)
|
|
259
261
|
```
|
|
260
262
|
|
|
261
|
-
Each item type is one Redis hash, `
|
|
263
|
+
Each item type is one Redis hash, `collection:{item_type}`, mapping item ids to
|
|
262
264
|
JSON-encoded items, so value types (numbers, booleans, lists, nested dicts) are
|
|
263
|
-
preserved. Pass `key_prefix="myapp:"` to use a different prefix than `
|
|
265
|
+
preserved. Pass `key_prefix="myapp:"` to use a different prefix than `collection:`.
|
|
264
266
|
|
|
265
267
|
Pass a pre-configured `redis.Redis` client (sync); `decode_responses` may be on or off.
|
|
266
268
|
Requires `redis-py`. `AsyncRedisStorage` takes a `redis.asyncio.Redis` client
|
|
@@ -276,10 +278,11 @@ client = pymongo.MongoClient("mongodb://localhost:27017")
|
|
|
276
278
|
storage = MongoDBStorage(mongo_client=client)
|
|
277
279
|
```
|
|
278
280
|
|
|
279
|
-
Items are stored in the `collection
|
|
281
|
+
Items are stored in the `collection` database, one collection per `item_type`.
|
|
282
|
+
Pass `db_name="myapp"` to use a different database.
|
|
280
283
|
The MongoDB `_id` field is stripped from results automatically.
|
|
281
284
|
Pass a pre-configured `pymongo.MongoClient`. Requires `pymongo`. `AsyncMongoDBStorage`
|
|
282
|
-
takes a `pymongo.AsyncMongoClient` and uses the same layout, so sync and async adapters
|
|
285
|
+
takes a `pymongo.AsyncMongoClient` (and the same `db_name` option) and uses the same layout, so sync and async adapters
|
|
283
286
|
can share data. Both accept an optional MongoDB `query` in `items` / `aitems` to filter results.
|
|
284
287
|
|
|
285
288
|
---
|
|
@@ -314,7 +317,7 @@ if item is not None:
|
|
|
314
317
|
```
|
|
315
318
|
|
|
316
319
|
The model type is inferred from `model_class`, so type checkers know that
|
|
317
|
-
`todos.get()` returns `Todo | None` and `todos.
|
|
320
|
+
`todos.get()` returns `Todo | None` and `todos.items()` returns `list[Todo]`.
|
|
318
321
|
`todos.keys()` returns the item ids (`list[str]`) without loading or validating
|
|
319
322
|
any items.
|
|
320
323
|
|
|
@@ -369,7 +372,7 @@ todos = AsyncCollection(item_type="todo", storage=AsyncRedisStorage(client))
|
|
|
369
372
|
|
|
370
373
|
await todos.save({"id": "1", "title": "Buy milk", "done": False})
|
|
371
374
|
await todos.get("1") # → {"id": "1", "title": "Buy milk", "done": False}
|
|
372
|
-
await todos.
|
|
375
|
+
await todos.items() # → [{"id": "1", ...}]
|
|
373
376
|
await todos.keys() # → ["1"]
|
|
374
377
|
await todos.patch("1", {"done": True}) # → {"id": "1", ..., "done": True}
|
|
375
378
|
await todos.delete("1") # → True
|
|
@@ -384,6 +387,83 @@ can also be called directly on the storage.
|
|
|
384
387
|
|
|
385
388
|
---
|
|
386
389
|
|
|
390
|
+
## Actions
|
|
391
|
+
|
|
392
|
+
An action is a named operation on a single item. Register a handler on a
|
|
393
|
+
collection, then run it by item id:
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
from objbase import Collection, InMemoryStorage
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def set_status(item, params):
|
|
400
|
+
return {**item, "status": params["status"]}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
todos = Collection(item_type="todo", storage=InMemoryStorage())
|
|
404
|
+
todos.register_action("set_status", set_status)
|
|
405
|
+
|
|
406
|
+
todos.save({"id": "1", "title": "Buy milk", "status": "pending"})
|
|
407
|
+
todos.run_action("1", "set_status", {"status": "done"}) # → {"id": "1", ..., "status": "done"}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
A handler is called as `handler(item, params)` with the stored item and the
|
|
411
|
+
params (`{}` if none are given):
|
|
412
|
+
|
|
413
|
+
- If it returns an item, that item **replaces** the stored one (like `save`) and is
|
|
414
|
+
returned. It must keep the item's `id`, otherwise `ValueError` is raised and nothing is saved.
|
|
415
|
+
- If it returns `None`, nothing is saved and the stored item is returned.
|
|
416
|
+
- Exceptions raised by the handler propagate unchanged.
|
|
417
|
+
|
|
418
|
+
`run_action` raises `ActionNotFoundError` for an unregistered action and
|
|
419
|
+
`ItemNotFoundError` for a missing item. Actions are registered per collection
|
|
420
|
+
instance; registering a name again replaces its handler. Like `patch`, an action
|
|
421
|
+
reads, changes and writes the item, so it is not atomic across concurrent writers.
|
|
422
|
+
|
|
423
|
+
On `AsyncCollection`, `run_action` is a coroutine and handlers may be sync or
|
|
424
|
+
async: async handlers are awaited, sync handlers run in a worker thread
|
|
425
|
+
(`asyncio.to_thread`). `Collection` accepts only sync handlers and raises
|
|
426
|
+
`TypeError` for an async one.
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
async def set_status(item, params):
|
|
430
|
+
return {**item, "status": params["status"]}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
todos = AsyncCollection(item_type="todo", storage=storage)
|
|
434
|
+
todos.register_action("set_status", set_status)
|
|
435
|
+
await todos.run_action("1", "set_status", {"status": "done"})
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Loading handlers from a module
|
|
439
|
+
|
|
440
|
+
`load_action_handler(module_name, action_name)` imports a module and returns
|
|
441
|
+
the handler from its `actions` mapping (pass `attr_name=` to use another name):
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
# myapp/todo_actions.py
|
|
445
|
+
def set_status(item, params):
|
|
446
|
+
return {**item, "status": params["status"]}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
actions = {"set_status": set_status}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
from objbase import load_action_handler
|
|
454
|
+
|
|
455
|
+
todos.register_action("set_status", load_action_handler("myapp.todo_actions", "set_status"))
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
It raises `ActionNotFoundError` if the module has no such mapping or action, and
|
|
459
|
+
`TypeError` if the entry isn't callable. Errors from importing the module itself
|
|
460
|
+
propagate unchanged. Only pass module names you trust: importing a module runs its code.
|
|
461
|
+
|
|
462
|
+
Runs are logged at `DEBUG` level on the `objbase.collection` and
|
|
463
|
+
`objbase.asyncio.collection` loggers.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
387
467
|
## Examples
|
|
388
468
|
|
|
389
469
|
Runnable scripts are in [`examples/`](examples/):
|
|
@@ -405,7 +485,7 @@ uv run python examples/async_sqlite_example.py
|
|
|
405
485
|
```
|
|
406
486
|
|
|
407
487
|
The file-based and SQLite examples write to `data/` in the current directory
|
|
408
|
-
(ignored by git); set `
|
|
488
|
+
(ignored by git); set `OBJBASE_DATA_DIR` or `SQLITE_DB_PATH` to change that. The MongoDB
|
|
409
489
|
examples need a running server — `docker run --rm -p 27017:27017 mongo:7.0` — and
|
|
410
490
|
connect to `MONGODB_URI` (default `mongodb://localhost:27017`).
|
|
411
491
|
|
|
@@ -453,7 +533,7 @@ def get_todos(request: Request) -> AsyncCollection:
|
|
|
453
533
|
|
|
454
534
|
@app.get("/todos")
|
|
455
535
|
async def list_todos(todos: AsyncCollection = Depends(get_todos)):
|
|
456
|
-
return await todos.
|
|
536
|
+
return await todos.items()
|
|
457
537
|
|
|
458
538
|
|
|
459
539
|
@app.get("/todos/{todo_id}")
|
|
@@ -9,7 +9,7 @@ __No thrills__ - **just a simple key-value store for serializable Python objects
|
|
|
9
9
|
|
|
10
10
|
## What you get
|
|
11
11
|
|
|
12
|
-
- Basic CRUD operations: `save`, `get`, `
|
|
12
|
+
- Basic CRUD operations: `save`, `get`, `items`, `keys`, `patch`, `delete`
|
|
13
13
|
- Multiple storage adapters (in-memory, file-based, SQLite, Redis, MongoDB)
|
|
14
14
|
- Optional Pydantic model validation with `PydanticCollection` / `AsyncPydanticCollection`
|
|
15
15
|
- Async support via `AsyncCollection` with async storage adapters (in-memory, file-based, SQLite, Redis, MongoDB)
|
|
@@ -65,12 +65,13 @@ or from their submodules as in the examples below:
|
|
|
65
65
|
| Module | Contents |
|
|
66
66
|
|---|---|
|
|
67
67
|
| `objbase.interface` | `Storage`, `AsyncStorage` protocols and the `Item` type |
|
|
68
|
-
| `objbase.
|
|
69
|
-
| `objbase.errors` | `CollectionError`, `ItemNotFoundError` |
|
|
68
|
+
| `objbase.collection` | `Collection` |
|
|
69
|
+
| `objbase.errors` | `CollectionError`, `ItemNotFoundError`, `ActionNotFoundError` |
|
|
70
|
+
| `objbase.actions` | `ActionHandler`, `AsyncActionHandler`, `ActionParams`, `load_action_handler` |
|
|
70
71
|
| `objbase.pydantic` | `PydanticCollection`, `AsyncPydanticCollection` (needs `objbase[pydantic]`) |
|
|
71
|
-
| `objbase.storage.{inmemory,
|
|
72
|
-
| `objbase.asyncio.
|
|
73
|
-
| `objbase.asyncio.storage.{
|
|
72
|
+
| `objbase.storage.{inmemory,local,sqlite,redis,mongodb}` | Sync storage adapters |
|
|
73
|
+
| `objbase.asyncio.collection` | `AsyncCollection` |
|
|
74
|
+
| `objbase.asyncio.storage.{local,sqlite,redis,mongodb}` | Async storage adapters |
|
|
74
75
|
|
|
75
76
|
`import objbase` works without Pydantic installed; `PydanticCollection` and
|
|
76
77
|
`AsyncPydanticCollection` are loaded on first access.
|
|
@@ -79,7 +80,7 @@ or from their submodules as in the examples below:
|
|
|
79
80
|
|
|
80
81
|
All adapters follow the same contract (verified by a shared test suite):
|
|
81
82
|
|
|
82
|
-
- `get` returns `None` for a missing item; `
|
|
83
|
+
- `get` returns `None` for a missing item; `items` and `keys` return `[]` for an empty type.
|
|
83
84
|
- `save` inserts a new item or **replaces** an existing one entirely (it does not merge fields).
|
|
84
85
|
- `patch` merges the given fields into an existing item. It cannot change the item's `id`.
|
|
85
86
|
- `delete` returns `True` if the item was removed, `False` if it did not exist.
|
|
@@ -91,8 +92,9 @@ Errors are raised, not returned:
|
|
|
91
92
|
|---|---|
|
|
92
93
|
| `save` an item without an `id` (or with an empty one) | `ValueError` |
|
|
93
94
|
| `patch` with data that changes the item's `id` | `ValueError` |
|
|
94
|
-
| `patch` a missing item | `objbase.ItemNotFoundError` (
|
|
95
|
+
| `patch` a missing item | `objbase.ItemNotFoundError` (a `CollectionError` and a `LookupError`) |
|
|
95
96
|
| The storage backend reports a failed write | `objbase.CollectionError` |
|
|
97
|
+
| `run_action` with an action that isn't registered | `objbase.ActionNotFoundError` (a `CollectionError` and a `LookupError`) |
|
|
96
98
|
|
|
97
99
|
`CollectionError` is the base class of all objbase errors.
|
|
98
100
|
|
|
@@ -226,9 +228,9 @@ client = redis.Redis(host="localhost", port=6379, decode_responses=True)
|
|
|
226
228
|
storage = RedisStorage(redis_client=client)
|
|
227
229
|
```
|
|
228
230
|
|
|
229
|
-
Each item type is one Redis hash, `
|
|
231
|
+
Each item type is one Redis hash, `collection:{item_type}`, mapping item ids to
|
|
230
232
|
JSON-encoded items, so value types (numbers, booleans, lists, nested dicts) are
|
|
231
|
-
preserved. Pass `key_prefix="myapp:"` to use a different prefix than `
|
|
233
|
+
preserved. Pass `key_prefix="myapp:"` to use a different prefix than `collection:`.
|
|
232
234
|
|
|
233
235
|
Pass a pre-configured `redis.Redis` client (sync); `decode_responses` may be on or off.
|
|
234
236
|
Requires `redis-py`. `AsyncRedisStorage` takes a `redis.asyncio.Redis` client
|
|
@@ -244,10 +246,11 @@ client = pymongo.MongoClient("mongodb://localhost:27017")
|
|
|
244
246
|
storage = MongoDBStorage(mongo_client=client)
|
|
245
247
|
```
|
|
246
248
|
|
|
247
|
-
Items are stored in the `collection
|
|
249
|
+
Items are stored in the `collection` database, one collection per `item_type`.
|
|
250
|
+
Pass `db_name="myapp"` to use a different database.
|
|
248
251
|
The MongoDB `_id` field is stripped from results automatically.
|
|
249
252
|
Pass a pre-configured `pymongo.MongoClient`. Requires `pymongo`. `AsyncMongoDBStorage`
|
|
250
|
-
takes a `pymongo.AsyncMongoClient` and uses the same layout, so sync and async adapters
|
|
253
|
+
takes a `pymongo.AsyncMongoClient` (and the same `db_name` option) and uses the same layout, so sync and async adapters
|
|
251
254
|
can share data. Both accept an optional MongoDB `query` in `items` / `aitems` to filter results.
|
|
252
255
|
|
|
253
256
|
---
|
|
@@ -282,7 +285,7 @@ if item is not None:
|
|
|
282
285
|
```
|
|
283
286
|
|
|
284
287
|
The model type is inferred from `model_class`, so type checkers know that
|
|
285
|
-
`todos.get()` returns `Todo | None` and `todos.
|
|
288
|
+
`todos.get()` returns `Todo | None` and `todos.items()` returns `list[Todo]`.
|
|
286
289
|
`todos.keys()` returns the item ids (`list[str]`) without loading or validating
|
|
287
290
|
any items.
|
|
288
291
|
|
|
@@ -337,7 +340,7 @@ todos = AsyncCollection(item_type="todo", storage=AsyncRedisStorage(client))
|
|
|
337
340
|
|
|
338
341
|
await todos.save({"id": "1", "title": "Buy milk", "done": False})
|
|
339
342
|
await todos.get("1") # → {"id": "1", "title": "Buy milk", "done": False}
|
|
340
|
-
await todos.
|
|
343
|
+
await todos.items() # → [{"id": "1", ...}]
|
|
341
344
|
await todos.keys() # → ["1"]
|
|
342
345
|
await todos.patch("1", {"done": True}) # → {"id": "1", ..., "done": True}
|
|
343
346
|
await todos.delete("1") # → True
|
|
@@ -352,6 +355,83 @@ can also be called directly on the storage.
|
|
|
352
355
|
|
|
353
356
|
---
|
|
354
357
|
|
|
358
|
+
## Actions
|
|
359
|
+
|
|
360
|
+
An action is a named operation on a single item. Register a handler on a
|
|
361
|
+
collection, then run it by item id:
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
from objbase import Collection, InMemoryStorage
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def set_status(item, params):
|
|
368
|
+
return {**item, "status": params["status"]}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
todos = Collection(item_type="todo", storage=InMemoryStorage())
|
|
372
|
+
todos.register_action("set_status", set_status)
|
|
373
|
+
|
|
374
|
+
todos.save({"id": "1", "title": "Buy milk", "status": "pending"})
|
|
375
|
+
todos.run_action("1", "set_status", {"status": "done"}) # → {"id": "1", ..., "status": "done"}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
A handler is called as `handler(item, params)` with the stored item and the
|
|
379
|
+
params (`{}` if none are given):
|
|
380
|
+
|
|
381
|
+
- If it returns an item, that item **replaces** the stored one (like `save`) and is
|
|
382
|
+
returned. It must keep the item's `id`, otherwise `ValueError` is raised and nothing is saved.
|
|
383
|
+
- If it returns `None`, nothing is saved and the stored item is returned.
|
|
384
|
+
- Exceptions raised by the handler propagate unchanged.
|
|
385
|
+
|
|
386
|
+
`run_action` raises `ActionNotFoundError` for an unregistered action and
|
|
387
|
+
`ItemNotFoundError` for a missing item. Actions are registered per collection
|
|
388
|
+
instance; registering a name again replaces its handler. Like `patch`, an action
|
|
389
|
+
reads, changes and writes the item, so it is not atomic across concurrent writers.
|
|
390
|
+
|
|
391
|
+
On `AsyncCollection`, `run_action` is a coroutine and handlers may be sync or
|
|
392
|
+
async: async handlers are awaited, sync handlers run in a worker thread
|
|
393
|
+
(`asyncio.to_thread`). `Collection` accepts only sync handlers and raises
|
|
394
|
+
`TypeError` for an async one.
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
async def set_status(item, params):
|
|
398
|
+
return {**item, "status": params["status"]}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
todos = AsyncCollection(item_type="todo", storage=storage)
|
|
402
|
+
todos.register_action("set_status", set_status)
|
|
403
|
+
await todos.run_action("1", "set_status", {"status": "done"})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Loading handlers from a module
|
|
407
|
+
|
|
408
|
+
`load_action_handler(module_name, action_name)` imports a module and returns
|
|
409
|
+
the handler from its `actions` mapping (pass `attr_name=` to use another name):
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
# myapp/todo_actions.py
|
|
413
|
+
def set_status(item, params):
|
|
414
|
+
return {**item, "status": params["status"]}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
actions = {"set_status": set_status}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
from objbase import load_action_handler
|
|
422
|
+
|
|
423
|
+
todos.register_action("set_status", load_action_handler("myapp.todo_actions", "set_status"))
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
It raises `ActionNotFoundError` if the module has no such mapping or action, and
|
|
427
|
+
`TypeError` if the entry isn't callable. Errors from importing the module itself
|
|
428
|
+
propagate unchanged. Only pass module names you trust: importing a module runs its code.
|
|
429
|
+
|
|
430
|
+
Runs are logged at `DEBUG` level on the `objbase.collection` and
|
|
431
|
+
`objbase.asyncio.collection` loggers.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
355
435
|
## Examples
|
|
356
436
|
|
|
357
437
|
Runnable scripts are in [`examples/`](examples/):
|
|
@@ -373,7 +453,7 @@ uv run python examples/async_sqlite_example.py
|
|
|
373
453
|
```
|
|
374
454
|
|
|
375
455
|
The file-based and SQLite examples write to `data/` in the current directory
|
|
376
|
-
(ignored by git); set `
|
|
456
|
+
(ignored by git); set `OBJBASE_DATA_DIR` or `SQLITE_DB_PATH` to change that. The MongoDB
|
|
377
457
|
examples need a running server — `docker run --rm -p 27017:27017 mongo:7.0` — and
|
|
378
458
|
connect to `MONGODB_URI` (default `mongodb://localhost:27017`).
|
|
379
459
|
|
|
@@ -421,7 +501,7 @@ def get_todos(request: Request) -> AsyncCollection:
|
|
|
421
501
|
|
|
422
502
|
@app.get("/todos")
|
|
423
503
|
async def list_todos(todos: AsyncCollection = Depends(get_todos)):
|
|
424
|
-
return await todos.
|
|
504
|
+
return await todos.items()
|
|
425
505
|
|
|
426
506
|
|
|
427
507
|
@app.get("/todos/{todo_id}")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Async to-do list stored in one JSON file per item ({base_dir}/todo/{id}.json). No extra dependencies needed.
|
|
2
|
-
# Set
|
|
2
|
+
# Set OBJBASE_DATA_DIR to use a different directory.
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
5
5
|
|
|
@@ -8,28 +8,28 @@ from objbase.asyncio.storage.local import AsyncLocalDirectoryStorage
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
async def main() -> None:
|
|
11
|
-
base_dir = os.getenv("
|
|
11
|
+
base_dir = os.getenv("OBJBASE_DATA_DIR", "data")
|
|
12
12
|
os.makedirs(base_dir, exist_ok=True) # the base directory must exist
|
|
13
13
|
storage = AsyncLocalDirectoryStorage(base_dir=base_dir)
|
|
14
|
-
|
|
14
|
+
todos_collection = AsyncCollection(item_type="todo", storage=storage)
|
|
15
15
|
|
|
16
16
|
# Create some to-do items concurrently; the adapter's file locks keep the index consistent
|
|
17
17
|
await asyncio.gather(
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
todos_collection.save({"id": "1", "name": "Buy groceries", "status": "pending"}),
|
|
19
|
+
todos_collection.save({"id": "2", "name": "Walk the dog", "status": "pending"}),
|
|
20
20
|
)
|
|
21
|
-
print("To-do ids:", sorted(await
|
|
21
|
+
print("To-do ids:", sorted(await todos_collection.keys()))
|
|
22
22
|
|
|
23
23
|
# Update a to-do item
|
|
24
|
-
updated_todo = await
|
|
24
|
+
updated_todo = await todos_collection.patch("1", {"status": "completed"})
|
|
25
25
|
print("Updated To-do:", updated_todo)
|
|
26
26
|
|
|
27
27
|
# Rebuild the index, e.g. after item files were added or removed by hand
|
|
28
28
|
await storage.arebuild_index("todo")
|
|
29
29
|
|
|
30
30
|
# Delete the to-do items
|
|
31
|
-
for todo_id in await
|
|
32
|
-
print(f"Deleted To-do {todo_id}:", await
|
|
31
|
+
for todo_id in await todos_collection.keys():
|
|
32
|
+
print(f"Deleted To-do {todo_id}:", await todos_collection.delete(todo_id))
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
asyncio.run(main())
|
|
@@ -6,22 +6,22 @@ from objbase.storage.inmemory import InMemoryStorage
|
|
|
6
6
|
|
|
7
7
|
async def main():
|
|
8
8
|
# Replace with AsyncRedisStorage(redis.asyncio.Redis(...)) for real persistence
|
|
9
|
-
|
|
9
|
+
todos_collection = AsyncCollection(item_type="todo", storage=InMemoryStorage())
|
|
10
10
|
|
|
11
11
|
# Create a new to-do item
|
|
12
|
-
created_todo = await
|
|
12
|
+
created_todo = await todos_collection.save({"id": "1", "name": "Buy groceries", "status": "pending"})
|
|
13
13
|
print("Created To-do:", created_todo)
|
|
14
14
|
|
|
15
15
|
# Read the to-do item
|
|
16
|
-
fetched_todo = await
|
|
16
|
+
fetched_todo = await todos_collection.get("1")
|
|
17
17
|
print("Fetched To-do:", fetched_todo)
|
|
18
18
|
|
|
19
19
|
# Update the to-do item
|
|
20
|
-
updated_todo = await
|
|
20
|
+
updated_todo = await todos_collection.patch("1", {"status": "completed"})
|
|
21
21
|
print("Updated To-do:", updated_todo)
|
|
22
22
|
|
|
23
23
|
# Delete the to-do item
|
|
24
|
-
delete_result = await
|
|
24
|
+
delete_result = await todos_collection.delete("1")
|
|
25
25
|
print("Deleted To-do:", delete_result)
|
|
26
26
|
|
|
27
27
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Async to-do list stored in one JSON file per item type ({base_dir}/todo.json). No extra dependencies needed.
|
|
2
|
+
# Set OBJBASE_DATA_DIR to use a different directory.
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from objbase.asyncio.collection import AsyncCollection
|
|
7
|
+
from objbase.asyncio.storage.local import AsyncLocalFileStorage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def main() -> None:
|
|
11
|
+
base_dir = os.getenv("OBJBASE_DATA_DIR", "data")
|
|
12
|
+
os.makedirs(base_dir, exist_ok=True) # the base directory must exist
|
|
13
|
+
storage = AsyncLocalFileStorage(base_dir=base_dir)
|
|
14
|
+
todos_collection = AsyncCollection(item_type="todo", storage=storage)
|
|
15
|
+
|
|
16
|
+
# Create some to-do items
|
|
17
|
+
await todos_collection.save({"id": "1", "name": "Buy groceries", "status": "pending"})
|
|
18
|
+
await todos_collection.save({"id": "2", "name": "Walk the dog", "status": "pending"})
|
|
19
|
+
print("All To-dos:", await todos_collection.items())
|
|
20
|
+
|
|
21
|
+
# Update a to-do item
|
|
22
|
+
updated_todo = await todos_collection.patch("1", {"status": "completed"})
|
|
23
|
+
print("Updated To-do:", updated_todo)
|
|
24
|
+
|
|
25
|
+
# Delete the to-do items
|
|
26
|
+
for todo_id in await todos_collection.keys():
|
|
27
|
+
print(f"Deleted To-do {todo_id}:", await todos_collection.delete(todo_id))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
asyncio.run(main())
|
|
@@ -15,23 +15,23 @@ async def main() -> None:
|
|
|
15
15
|
os.getenv("MONGODB_URI", "mongodb://localhost:27017")
|
|
16
16
|
)
|
|
17
17
|
storage = AsyncMongoDBStorage(mongo_client=client)
|
|
18
|
-
|
|
18
|
+
todos_collection = AsyncCollection(item_type="todo", storage=storage)
|
|
19
19
|
|
|
20
20
|
# Create some to-do items
|
|
21
|
-
await
|
|
22
|
-
await
|
|
23
|
-
print("All To-dos:", await
|
|
21
|
+
await todos_collection.save({"id": "1", "name": "Buy groceries", "status": "pending"})
|
|
22
|
+
await todos_collection.save({"id": "2", "name": "Walk the dog", "status": "pending"})
|
|
23
|
+
print("All To-dos:", await todos_collection.items())
|
|
24
24
|
|
|
25
25
|
# Update a to-do item
|
|
26
|
-
updated_todo = await
|
|
26
|
+
updated_todo = await todos_collection.patch("1", {"status": "completed"})
|
|
27
27
|
print("Updated To-do:", updated_todo)
|
|
28
28
|
|
|
29
29
|
# Filter with a MongoDB query (a MongoDB-only extension of the storage adapter)
|
|
30
30
|
print("Pending To-dos:", await storage.aitems("todo", query={"status": "pending"}))
|
|
31
31
|
|
|
32
32
|
# Delete the to-do items
|
|
33
|
-
for todo_id in await
|
|
34
|
-
print(f"Deleted To-do {todo_id}:", await
|
|
33
|
+
for todo_id in await todos_collection.keys():
|
|
34
|
+
print(f"Deleted To-do {todo_id}:", await todos_collection.delete(todo_id))
|
|
35
35
|
|
|
36
36
|
await client.close()
|
|
37
37
|
|
|
@@ -14,30 +14,30 @@ class Todo(pydantic.BaseModel):
|
|
|
14
14
|
|
|
15
15
|
async def main() -> None:
|
|
16
16
|
# Replace with AsyncRedisStorage(redis.asyncio.Redis(...)) for real persistence
|
|
17
|
-
|
|
17
|
+
todos_collection = AsyncPydanticCollection(item_type="todos", storage=InMemoryStorage(), model_class=Todo)
|
|
18
18
|
|
|
19
19
|
# Create a new to-do item
|
|
20
|
-
created_todo = await
|
|
20
|
+
created_todo = await todos_collection.save(Todo(id="1", title="Buy milk"))
|
|
21
21
|
print("Created To-do:", created_todo)
|
|
22
22
|
|
|
23
23
|
# Read the to-do item
|
|
24
|
-
fetched_todo = await
|
|
24
|
+
fetched_todo = await todos_collection.get("1")
|
|
25
25
|
print("Fetched To-do:", fetched_todo)
|
|
26
26
|
assert fetched_todo is not None # get() returns None for a missing id
|
|
27
27
|
|
|
28
28
|
# Update the to-do item
|
|
29
29
|
fetched_todo.completed = True
|
|
30
|
-
updated_todo = await
|
|
30
|
+
updated_todo = await todos_collection.patch("1", fetched_todo)
|
|
31
31
|
print("Updated To-do:", updated_todo)
|
|
32
32
|
|
|
33
33
|
# Invalid data is rejected and never stored
|
|
34
34
|
try:
|
|
35
|
-
await
|
|
35
|
+
await todos_collection.patch("1", {"completed": "not a bool"})
|
|
36
36
|
except pydantic.ValidationError:
|
|
37
|
-
print("Rejected invalid patch; stored item unchanged:", await
|
|
37
|
+
print("Rejected invalid patch; stored item unchanged:", await todos_collection.get("1"))
|
|
38
38
|
|
|
39
39
|
# Delete the to-do item
|
|
40
|
-
delete_result = await
|
|
40
|
+
delete_result = await todos_collection.delete("1")
|
|
41
41
|
print("Deleted To-do:", delete_result)
|
|
42
42
|
|
|
43
43
|
|
|
@@ -11,20 +11,20 @@ async def main() -> None:
|
|
|
11
11
|
db_path = os.getenv("SQLITE_DB_PATH", os.path.join("data", "todos.db"))
|
|
12
12
|
os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True) # sqlite3 doesn't create missing directories
|
|
13
13
|
storage = AsyncSQLiteStorage(db_path=db_path)
|
|
14
|
-
|
|
14
|
+
todos_collection = AsyncCollection(item_type="todo", storage=storage)
|
|
15
15
|
|
|
16
16
|
# Create some to-do items
|
|
17
|
-
await
|
|
18
|
-
await
|
|
19
|
-
print("All To-dos:", await
|
|
17
|
+
await todos_collection.save({"id": "1", "name": "Buy groceries", "status": "pending"})
|
|
18
|
+
await todos_collection.save({"id": "2", "name": "Walk the dog", "status": "pending"})
|
|
19
|
+
print("All To-dos:", await todos_collection.items())
|
|
20
20
|
|
|
21
21
|
# Update a to-do item
|
|
22
|
-
updated_todo = await
|
|
22
|
+
updated_todo = await todos_collection.patch("1", {"status": "completed"})
|
|
23
23
|
print("Updated To-do:", updated_todo)
|
|
24
24
|
|
|
25
25
|
# Delete the to-do items
|
|
26
|
-
for todo_id in await
|
|
27
|
-
print(f"Deleted To-do {todo_id}:", await
|
|
26
|
+
for todo_id in await todos_collection.keys():
|
|
27
|
+
print(f"Deleted To-do {todo_id}:", await todos_collection.delete(todo_id))
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
asyncio.run(main())
|
|
@@ -3,21 +3,21 @@ from objbase.collection import Collection
|
|
|
3
3
|
from objbase.storage.inmemory import InMemoryStorage
|
|
4
4
|
|
|
5
5
|
# Replace with actual storage instance
|
|
6
|
-
|
|
6
|
+
todos_collection = Collection(item_type="todo", storage=InMemoryStorage())
|
|
7
7
|
|
|
8
8
|
# Create a new to-do item
|
|
9
9
|
new_todo = {"id": "1", "name": "Buy groceries", "status": "pending"}
|
|
10
|
-
created_todo =
|
|
10
|
+
created_todo = todos_collection.save(new_todo)
|
|
11
11
|
print("Created To-do:", created_todo)
|
|
12
12
|
|
|
13
13
|
# Read the to-do item
|
|
14
|
-
fetched_todo =
|
|
14
|
+
fetched_todo = todos_collection.get("1")
|
|
15
15
|
print("Fetched To-do:", fetched_todo)
|
|
16
16
|
|
|
17
17
|
# Update the to-do item
|
|
18
|
-
updated_todo =
|
|
18
|
+
updated_todo = todos_collection.patch("1", {"status": "completed"})
|
|
19
19
|
print("Updated To-do:", updated_todo)
|
|
20
20
|
|
|
21
21
|
# Delete the to-do item
|
|
22
|
-
delete_result =
|
|
22
|
+
delete_result = todos_collection.delete("1")
|
|
23
23
|
print("Deleted To-do:", delete_result)
|
|
@@ -10,22 +10,22 @@ from objbase.storage.mongodb import MongoDBStorage
|
|
|
10
10
|
|
|
11
11
|
client: pymongo.MongoClient[Item] = pymongo.MongoClient(os.getenv("MONGODB_URI", "mongodb://localhost:27017"))
|
|
12
12
|
storage = MongoDBStorage(mongo_client=client)
|
|
13
|
-
|
|
13
|
+
todos_collection = Collection(item_type="todo", storage=storage)
|
|
14
14
|
|
|
15
15
|
# Create some to-do items
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
print("All To-dos:",
|
|
16
|
+
todos_collection.save({"id": "1", "name": "Buy groceries", "status": "pending"})
|
|
17
|
+
todos_collection.save({"id": "2", "name": "Walk the dog", "status": "pending"})
|
|
18
|
+
print("All To-dos:", todos_collection.items())
|
|
19
19
|
|
|
20
20
|
# Update a to-do item
|
|
21
|
-
updated_todo =
|
|
21
|
+
updated_todo = todos_collection.patch("1", {"status": "completed"})
|
|
22
22
|
print("Updated To-do:", updated_todo)
|
|
23
23
|
|
|
24
24
|
# Filter with a MongoDB query (a MongoDB-only extension of the storage adapter)
|
|
25
25
|
print("Pending To-dos:", storage.items("todo", query={"status": "pending"}))
|
|
26
26
|
|
|
27
27
|
# Delete the to-do items
|
|
28
|
-
for todo_id in
|
|
29
|
-
print(f"Deleted To-do {todo_id}:",
|
|
28
|
+
for todo_id in todos_collection.keys():
|
|
29
|
+
print(f"Deleted To-do {todo_id}:", todos_collection.delete(todo_id))
|
|
30
30
|
|
|
31
31
|
client.close()
|