objbase 0.5.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.5.0 → objbase-0.5.1}/PKG-INFO +17 -16
- {objbase-0.5.0 → objbase-0.5.1}/README.md +16 -15
- {objbase-0.5.0 → objbase-0.5.1}/examples/async_directory_example.py +9 -9
- {objbase-0.5.0 → objbase-0.5.1}/examples/async_example.py +5 -5
- objbase-0.5.1/examples/async_file_example.py +30 -0
- {objbase-0.5.0 → objbase-0.5.1}/examples/async_mongodb_example.py +7 -7
- {objbase-0.5.0 → objbase-0.5.1}/examples/async_pydantic_example.py +7 -7
- {objbase-0.5.0 → objbase-0.5.1}/examples/async_sqlite_example.py +7 -7
- {objbase-0.5.0 → objbase-0.5.1}/examples/dict_example.py +5 -5
- {objbase-0.5.0 → objbase-0.5.1}/examples/mongodb_example.py +7 -7
- {objbase-0.5.0 → objbase-0.5.1}/examples/pydantic_example.py +5 -5
- {objbase-0.5.0 → objbase-0.5.1}/pyproject.toml +1 -1
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/collection.py +1 -1
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/local.py +4 -4
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/mongodb.py +4 -2
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/pydantic.py +22 -22
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/inmemory.py +1 -1
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/local.py +8 -8
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/mongodb.py +9 -3
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/redis.py +1 -1
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_collection.py +8 -8
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_file_storage.py +3 -3
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_mongodb_storage.py +11 -2
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_redis_storage.py +2 -2
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_sqlite_storage.py +1 -1
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_collection.py +6 -6
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_file_storage.py +2 -2
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_mongodb_storage.py +30 -3
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_redis_storage.py +3 -3
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_storage_contract.py +4 -4
- {objbase-0.5.0 → objbase-0.5.1}/uv.lock +1 -1
- objbase-0.5.0/examples/async_file_example.py +0 -30
- {objbase-0.5.0 → objbase-0.5.1}/.github/dependabot.yml +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/.github/workflows/ci.yml +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/.github/workflows/release.yml +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/.gitignore +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/DEVELOPER.md +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/LICENSE +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/release.sh +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/redis.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/sqlite.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/threaded.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/collection.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/errors.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/interface.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/py.typed +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/sqlite.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/util/__init__.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/util/file_util.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/util/mongodb_util.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/src/objbase/util/redis_util.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_inmemory_storage.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/tests/test_package.py +0 -0
- {objbase-0.5.0 → objbase-0.5.1}/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.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,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,12 @@ 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.
|
|
100
|
+
| `objbase.collection` | `Collection` |
|
|
101
101
|
| `objbase.errors` | `CollectionError`, `ItemNotFoundError` |
|
|
102
102
|
| `objbase.pydantic` | `PydanticCollection`, `AsyncPydanticCollection` (needs `objbase[pydantic]`) |
|
|
103
|
-
| `objbase.storage.{inmemory,
|
|
104
|
-
| `objbase.asyncio.
|
|
105
|
-
| `objbase.asyncio.storage.{
|
|
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
107
|
`import objbase` works without Pydantic installed; `PydanticCollection` and
|
|
108
108
|
`AsyncPydanticCollection` are loaded on first access.
|
|
@@ -111,7 +111,7 @@ or from their submodules as in the examples below:
|
|
|
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,7 +123,7 @@ 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` (
|
|
126
|
+
| `patch` a missing item | `objbase.ItemNotFoundError` (a `CollectionError` and a `LookupError`) |
|
|
127
127
|
| The storage backend reports a failed write | `objbase.CollectionError` |
|
|
128
128
|
|
|
129
129
|
`CollectionError` is the base class of all objbase errors.
|
|
@@ -258,9 +258,9 @@ 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
|
|
@@ -276,10 +276,11 @@ client = pymongo.MongoClient("mongodb://localhost:27017")
|
|
|
276
276
|
storage = MongoDBStorage(mongo_client=client)
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
Items are stored in the `collection
|
|
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
|
---
|
|
@@ -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
|
|
|
@@ -369,7 +370,7 @@ 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
|
|
@@ -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
|
|
|
@@ -453,7 +454,7 @@ def get_todos(request: Request) -> AsyncCollection:
|
|
|
453
454
|
|
|
454
455
|
@app.get("/todos")
|
|
455
456
|
async def list_todos(todos: AsyncCollection = Depends(get_todos)):
|
|
456
|
-
return await todos.
|
|
457
|
+
return await todos.items()
|
|
457
458
|
|
|
458
459
|
|
|
459
460
|
@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,12 @@ 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.
|
|
68
|
+
| `objbase.collection` | `Collection` |
|
|
69
69
|
| `objbase.errors` | `CollectionError`, `ItemNotFoundError` |
|
|
70
70
|
| `objbase.pydantic` | `PydanticCollection`, `AsyncPydanticCollection` (needs `objbase[pydantic]`) |
|
|
71
|
-
| `objbase.storage.{inmemory,
|
|
72
|
-
| `objbase.asyncio.
|
|
73
|
-
| `objbase.asyncio.storage.{
|
|
71
|
+
| `objbase.storage.{inmemory,local,sqlite,redis,mongodb}` | Sync storage adapters |
|
|
72
|
+
| `objbase.asyncio.collection` | `AsyncCollection` |
|
|
73
|
+
| `objbase.asyncio.storage.{local,sqlite,redis,mongodb}` | Async storage adapters |
|
|
74
74
|
|
|
75
75
|
`import objbase` works without Pydantic installed; `PydanticCollection` and
|
|
76
76
|
`AsyncPydanticCollection` are loaded on first access.
|
|
@@ -79,7 +79,7 @@ or from their submodules as in the examples below:
|
|
|
79
79
|
|
|
80
80
|
All adapters follow the same contract (verified by a shared test suite):
|
|
81
81
|
|
|
82
|
-
- `get` returns `None` for a missing item; `
|
|
82
|
+
- `get` returns `None` for a missing item; `items` and `keys` return `[]` for an empty type.
|
|
83
83
|
- `save` inserts a new item or **replaces** an existing one entirely (it does not merge fields).
|
|
84
84
|
- `patch` merges the given fields into an existing item. It cannot change the item's `id`.
|
|
85
85
|
- `delete` returns `True` if the item was removed, `False` if it did not exist.
|
|
@@ -91,7 +91,7 @@ Errors are raised, not returned:
|
|
|
91
91
|
|---|---|
|
|
92
92
|
| `save` an item without an `id` (or with an empty one) | `ValueError` |
|
|
93
93
|
| `patch` with data that changes the item's `id` | `ValueError` |
|
|
94
|
-
| `patch` a missing item | `objbase.ItemNotFoundError` (
|
|
94
|
+
| `patch` a missing item | `objbase.ItemNotFoundError` (a `CollectionError` and a `LookupError`) |
|
|
95
95
|
| The storage backend reports a failed write | `objbase.CollectionError` |
|
|
96
96
|
|
|
97
97
|
`CollectionError` is the base class of all objbase errors.
|
|
@@ -226,9 +226,9 @@ client = redis.Redis(host="localhost", port=6379, decode_responses=True)
|
|
|
226
226
|
storage = RedisStorage(redis_client=client)
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
Each item type is one Redis hash, `
|
|
229
|
+
Each item type is one Redis hash, `collection:{item_type}`, mapping item ids to
|
|
230
230
|
JSON-encoded items, so value types (numbers, booleans, lists, nested dicts) are
|
|
231
|
-
preserved. Pass `key_prefix="myapp:"` to use a different prefix than `
|
|
231
|
+
preserved. Pass `key_prefix="myapp:"` to use a different prefix than `collection:`.
|
|
232
232
|
|
|
233
233
|
Pass a pre-configured `redis.Redis` client (sync); `decode_responses` may be on or off.
|
|
234
234
|
Requires `redis-py`. `AsyncRedisStorage` takes a `redis.asyncio.Redis` client
|
|
@@ -244,10 +244,11 @@ client = pymongo.MongoClient("mongodb://localhost:27017")
|
|
|
244
244
|
storage = MongoDBStorage(mongo_client=client)
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
-
Items are stored in the `collection
|
|
247
|
+
Items are stored in the `collection` database, one collection per `item_type`.
|
|
248
|
+
Pass `db_name="myapp"` to use a different database.
|
|
248
249
|
The MongoDB `_id` field is stripped from results automatically.
|
|
249
250
|
Pass a pre-configured `pymongo.MongoClient`. Requires `pymongo`. `AsyncMongoDBStorage`
|
|
250
|
-
takes a `pymongo.AsyncMongoClient` and uses the same layout, so sync and async adapters
|
|
251
|
+
takes a `pymongo.AsyncMongoClient` (and the same `db_name` option) and uses the same layout, so sync and async adapters
|
|
251
252
|
can share data. Both accept an optional MongoDB `query` in `items` / `aitems` to filter results.
|
|
252
253
|
|
|
253
254
|
---
|
|
@@ -282,7 +283,7 @@ if item is not None:
|
|
|
282
283
|
```
|
|
283
284
|
|
|
284
285
|
The model type is inferred from `model_class`, so type checkers know that
|
|
285
|
-
`todos.get()` returns `Todo | None` and `todos.
|
|
286
|
+
`todos.get()` returns `Todo | None` and `todos.items()` returns `list[Todo]`.
|
|
286
287
|
`todos.keys()` returns the item ids (`list[str]`) without loading or validating
|
|
287
288
|
any items.
|
|
288
289
|
|
|
@@ -337,7 +338,7 @@ todos = AsyncCollection(item_type="todo", storage=AsyncRedisStorage(client))
|
|
|
337
338
|
|
|
338
339
|
await todos.save({"id": "1", "title": "Buy milk", "done": False})
|
|
339
340
|
await todos.get("1") # → {"id": "1", "title": "Buy milk", "done": False}
|
|
340
|
-
await todos.
|
|
341
|
+
await todos.items() # → [{"id": "1", ...}]
|
|
341
342
|
await todos.keys() # → ["1"]
|
|
342
343
|
await todos.patch("1", {"done": True}) # → {"id": "1", ..., "done": True}
|
|
343
344
|
await todos.delete("1") # → True
|
|
@@ -373,7 +374,7 @@ uv run python examples/async_sqlite_example.py
|
|
|
373
374
|
```
|
|
374
375
|
|
|
375
376
|
The file-based and SQLite examples write to `data/` in the current directory
|
|
376
|
-
(ignored by git); set `
|
|
377
|
+
(ignored by git); set `OBJBASE_DATA_DIR` or `SQLITE_DB_PATH` to change that. The MongoDB
|
|
377
378
|
examples need a running server — `docker run --rm -p 27017:27017 mongo:7.0` — and
|
|
378
379
|
connect to `MONGODB_URI` (default `mongodb://localhost:27017`).
|
|
379
380
|
|
|
@@ -421,7 +422,7 @@ def get_todos(request: Request) -> AsyncCollection:
|
|
|
421
422
|
|
|
422
423
|
@app.get("/todos")
|
|
423
424
|
async def list_todos(todos: AsyncCollection = Depends(get_todos)):
|
|
424
|
-
return await todos.
|
|
425
|
+
return await todos.items()
|
|
425
426
|
|
|
426
427
|
|
|
427
428
|
@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()
|
|
@@ -11,22 +11,22 @@ class Todo(pydantic.BaseModel):
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
model_storage = InMemoryStorage()
|
|
14
|
-
|
|
14
|
+
todos_collection = PydanticCollection(item_type="todos", model_class=Todo, storage=model_storage)
|
|
15
15
|
|
|
16
16
|
# Create a new to-do item
|
|
17
|
-
created_todo =
|
|
17
|
+
created_todo = todos_collection.save(Todo(id="1", title="Buy milk", completed=False))
|
|
18
18
|
print("Created To-do:", created_todo)
|
|
19
19
|
|
|
20
20
|
# Read the to-do item
|
|
21
|
-
fetched_todo =
|
|
21
|
+
fetched_todo = todos_collection.get("1")
|
|
22
22
|
print("Fetched To-do:", fetched_todo)
|
|
23
23
|
assert fetched_todo is not None # get() returns None for a missing id
|
|
24
24
|
|
|
25
25
|
# Update the to-do item
|
|
26
26
|
fetched_todo.completed = True
|
|
27
|
-
updated_todo =
|
|
27
|
+
updated_todo = todos_collection.patch("1", fetched_todo)
|
|
28
28
|
print("Updated To-do:", updated_todo)
|
|
29
29
|
|
|
30
30
|
# Delete the to-do item
|
|
31
|
-
delete_result =
|
|
31
|
+
delete_result = todos_collection.delete("1")
|
|
32
32
|
print("Deleted To-do:", delete_result)
|
|
@@ -20,7 +20,7 @@ class AsyncCollection:
|
|
|
20
20
|
async def keys(self) -> list[str]:
|
|
21
21
|
return await self.storage.akeys(self.item_type)
|
|
22
22
|
|
|
23
|
-
async def
|
|
23
|
+
async def items(self) -> list[Item]:
|
|
24
24
|
return await self.storage.aitems(self.item_type)
|
|
25
25
|
|
|
26
26
|
async def get(self, id: str) -> Item | None:
|
|
@@ -16,8 +16,8 @@ class AsyncLocalFileStorage(ThreadedAsyncStorage[LocalFileStorage]):
|
|
|
16
16
|
super().__init__(LocalFileStorage(base_dir))
|
|
17
17
|
|
|
18
18
|
@property
|
|
19
|
-
def
|
|
20
|
-
return self.sync_storage.
|
|
19
|
+
def collection_dir(self) -> str:
|
|
20
|
+
return self.sync_storage.collection_dir
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class AsyncLocalDirectoryStorage(ThreadedAsyncStorage[LocalDirectoryStorage]):
|
|
@@ -32,8 +32,8 @@ class AsyncLocalDirectoryStorage(ThreadedAsyncStorage[LocalDirectoryStorage]):
|
|
|
32
32
|
super().__init__(LocalDirectoryStorage(base_dir))
|
|
33
33
|
|
|
34
34
|
@property
|
|
35
|
-
def
|
|
36
|
-
return self.sync_storage.
|
|
35
|
+
def collection_dir(self) -> str:
|
|
36
|
+
return self.sync_storage.collection_dir
|
|
37
37
|
|
|
38
38
|
async def arebuild_index(self, item_type: str) -> None:
|
|
39
39
|
"""Async counterpart of ``LocalDirectoryStorage.rebuild_index``."""
|
|
@@ -2,6 +2,7 @@ from collections.abc import Mapping
|
|
|
2
2
|
from typing import TYPE_CHECKING, Any
|
|
3
3
|
|
|
4
4
|
from objbase.interface import AsyncStorage, Item
|
|
5
|
+
from objbase.storage.mongodb import DEFAULT_DB_NAME
|
|
5
6
|
|
|
6
7
|
if TYPE_CHECKING:
|
|
7
8
|
from pymongo import AsyncMongoClient
|
|
@@ -14,11 +15,12 @@ class AsyncMongoDBStorage(AsyncStorage):
|
|
|
14
15
|
Takes an async client such as ``pymongo.AsyncMongoClient``.
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
|
-
def __init__(self, mongo_client: "AsyncMongoClient[Item]"):
|
|
18
|
+
def __init__(self, mongo_client: "AsyncMongoClient[Item]", db_name: str = DEFAULT_DB_NAME):
|
|
18
19
|
self.mongo_client = mongo_client
|
|
20
|
+
self.db_name = db_name
|
|
19
21
|
|
|
20
22
|
def get_mongo_collection(self, item_type: str) -> "AsyncCollection[Item]":
|
|
21
|
-
db = self.mongo_client[
|
|
23
|
+
db = self.mongo_client[self.db_name]
|
|
22
24
|
return db[item_type]
|
|
23
25
|
|
|
24
26
|
async def akeys(self, item_type: str) -> list[str]:
|
|
@@ -48,37 +48,37 @@ class PydanticCollection[M: pydantic.BaseModel]:
|
|
|
48
48
|
|
|
49
49
|
def __init__(self, item_type: str, storage: Storage, model_class: type[M]):
|
|
50
50
|
self.model_class = model_class
|
|
51
|
-
self.
|
|
51
|
+
self.collection = Collection(item_type, storage)
|
|
52
52
|
|
|
53
53
|
@property
|
|
54
54
|
def item_type(self) -> str:
|
|
55
|
-
return self.
|
|
55
|
+
return self.collection.item_type
|
|
56
56
|
|
|
57
57
|
@property
|
|
58
58
|
def storage(self) -> Storage:
|
|
59
|
-
return self.
|
|
59
|
+
return self.collection.storage
|
|
60
60
|
|
|
61
61
|
def keys(self) -> list[str]:
|
|
62
|
-
return self.
|
|
62
|
+
return self.collection.keys()
|
|
63
63
|
|
|
64
|
-
def
|
|
65
|
-
return [self.model_class.model_validate(item) for item in self.
|
|
64
|
+
def items(self) -> list[M]:
|
|
65
|
+
return [self.model_class.model_validate(item) for item in self.collection.items()]
|
|
66
66
|
|
|
67
67
|
def save(self, model: M) -> M:
|
|
68
|
-
return self.model_class.model_validate(self.
|
|
68
|
+
return self.model_class.model_validate(self.collection.save(_validated(self.model_class, model)))
|
|
69
69
|
|
|
70
70
|
def get(self, id: str) -> M | None:
|
|
71
|
-
item = self.
|
|
71
|
+
item = self.collection.get(id)
|
|
72
72
|
if item is None:
|
|
73
73
|
return None
|
|
74
74
|
return self.model_class.model_validate(item)
|
|
75
75
|
|
|
76
76
|
def patch(self, id: str, data: Item | M) -> M:
|
|
77
|
-
item = _patched(self.model_class, self.item_type, id, self.
|
|
78
|
-
return self.model_class.model_validate(self.
|
|
77
|
+
item = _patched(self.model_class, self.item_type, id, self.collection.get(id), data)
|
|
78
|
+
return self.model_class.model_validate(self.collection.save(item))
|
|
79
79
|
|
|
80
80
|
def delete(self, id: str) -> bool:
|
|
81
|
-
return self.
|
|
81
|
+
return self.collection.delete(id)
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
class AsyncPydanticCollection[M: pydantic.BaseModel]:
|
|
@@ -89,34 +89,34 @@ class AsyncPydanticCollection[M: pydantic.BaseModel]:
|
|
|
89
89
|
|
|
90
90
|
def __init__(self, item_type: str, storage: AsyncStorage, model_class: type[M]):
|
|
91
91
|
self.model_class = model_class
|
|
92
|
-
self.
|
|
92
|
+
self.collection = AsyncCollection(item_type, storage)
|
|
93
93
|
|
|
94
94
|
@property
|
|
95
95
|
def item_type(self) -> str:
|
|
96
|
-
return self.
|
|
96
|
+
return self.collection.item_type
|
|
97
97
|
|
|
98
98
|
@property
|
|
99
99
|
def storage(self) -> AsyncStorage:
|
|
100
|
-
return self.
|
|
100
|
+
return self.collection.storage
|
|
101
101
|
|
|
102
102
|
async def keys(self) -> list[str]:
|
|
103
|
-
return await self.
|
|
103
|
+
return await self.collection.keys()
|
|
104
104
|
|
|
105
|
-
async def
|
|
106
|
-
return [self.model_class.model_validate(item) for item in await self.
|
|
105
|
+
async def items(self) -> list[M]:
|
|
106
|
+
return [self.model_class.model_validate(item) for item in await self.collection.items()]
|
|
107
107
|
|
|
108
108
|
async def save(self, model: M) -> M:
|
|
109
|
-
return self.model_class.model_validate(await self.
|
|
109
|
+
return self.model_class.model_validate(await self.collection.save(_validated(self.model_class, model)))
|
|
110
110
|
|
|
111
111
|
async def get(self, id: str) -> M | None:
|
|
112
|
-
item = await self.
|
|
112
|
+
item = await self.collection.get(id)
|
|
113
113
|
if item is None:
|
|
114
114
|
return None
|
|
115
115
|
return self.model_class.model_validate(item)
|
|
116
116
|
|
|
117
117
|
async def patch(self, id: str, data: Item | M) -> M:
|
|
118
|
-
item = _patched(self.model_class, self.item_type, id, await self.
|
|
119
|
-
return self.model_class.model_validate(await self.
|
|
118
|
+
item = _patched(self.model_class, self.item_type, id, await self.collection.get(id), data)
|
|
119
|
+
return self.model_class.model_validate(await self.collection.save(item))
|
|
120
120
|
|
|
121
121
|
async def delete(self, id: str) -> bool:
|
|
122
|
-
return await self.
|
|
122
|
+
return await self.collection.delete(id)
|
|
@@ -4,7 +4,7 @@ from objbase.interface import AsyncStorage, Item, Storage
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class InMemoryStorage(Storage, AsyncStorage):
|
|
7
|
-
"""In-memory storage implementation for
|
|
7
|
+
"""In-memory storage implementation for collection items.
|
|
8
8
|
|
|
9
9
|
Items are deep-copied on the way in and out, so callers never share
|
|
10
10
|
mutable state with the store.
|
|
@@ -62,7 +62,7 @@ def _contained_path(real_base: str, *parts: str) -> str:
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
class LocalFileStorage(Storage):
|
|
65
|
-
"""Simple file-based storage that saves all items of a given
|
|
65
|
+
"""Simple file-based storage that saves all items of a given collection type in a single JSON file.
|
|
66
66
|
|
|
67
67
|
Safe for concurrent use by multiple threads and processes on the same machine:
|
|
68
68
|
writes hold an exclusive lock on ``.{item_type}.json.lock`` for the whole
|
|
@@ -71,9 +71,9 @@ class LocalFileStorage(Storage):
|
|
|
71
71
|
"""
|
|
72
72
|
|
|
73
73
|
def __init__(self, base_dir: str):
|
|
74
|
-
self.
|
|
75
|
-
if not os.path.exists(self.
|
|
76
|
-
raise ValueError(f"Base directory {self.
|
|
74
|
+
self.collection_dir = base_dir
|
|
75
|
+
if not os.path.exists(self.collection_dir):
|
|
76
|
+
raise ValueError(f"Base directory {self.collection_dir} does not exist.")
|
|
77
77
|
self._real_base = os.path.realpath(base_dir)
|
|
78
78
|
|
|
79
79
|
def keys(self, item_type: str) -> list[str]:
|
|
@@ -136,7 +136,7 @@ class LocalFileStorage(Storage):
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
class LocalDirectoryStorage(Storage):
|
|
139
|
-
"""Alternative file-based storage that uses a directory per
|
|
139
|
+
"""Alternative file-based storage that uses a directory per collection type and individual files per item.
|
|
140
140
|
|
|
141
141
|
Each type directory also holds an index file (``.index``) listing the ids of
|
|
142
142
|
all items of that type, one per line, so ``keys()`` doesn't have to scan the
|
|
@@ -148,9 +148,9 @@ class LocalDirectoryStorage(Storage):
|
|
|
148
148
|
INDEX_FILE = ".index"
|
|
149
149
|
|
|
150
150
|
def __init__(self, base_dir: str):
|
|
151
|
-
self.
|
|
152
|
-
if not os.path.exists(self.
|
|
153
|
-
raise ValueError(f"Base directory {self.
|
|
151
|
+
self.collection_dir = base_dir
|
|
152
|
+
if not os.path.exists(self.collection_dir):
|
|
153
|
+
raise ValueError(f"Base directory {self.collection_dir} does not exist.")
|
|
154
154
|
self._real_base = os.path.realpath(base_dir)
|
|
155
155
|
|
|
156
156
|
def _type_dir(self, item_type: str) -> str:
|
|
@@ -7,15 +7,21 @@ if TYPE_CHECKING:
|
|
|
7
7
|
from pymongo import MongoClient
|
|
8
8
|
from pymongo.collection import Collection
|
|
9
9
|
|
|
10
|
+
DEFAULT_DB_NAME = "objbase"
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class MongoDBStorage(Storage):
|
|
12
|
-
"""MongoDB-based storage implementation for
|
|
14
|
+
"""MongoDB-based storage implementation for collection items.
|
|
15
|
+
|
|
16
|
+
Items are stored in the ``db_name`` database, one MongoDB collection per item type.
|
|
17
|
+
"""
|
|
13
18
|
|
|
14
|
-
def __init__(self, mongo_client: "MongoClient[Item]"):
|
|
19
|
+
def __init__(self, mongo_client: "MongoClient[Item]", db_name: str = DEFAULT_DB_NAME):
|
|
15
20
|
self.mongo_client = mongo_client
|
|
21
|
+
self.db_name = db_name
|
|
16
22
|
|
|
17
23
|
def get_mongo_collection(self, item_type: str) -> "Collection[Item]":
|
|
18
|
-
db = self.mongo_client[
|
|
24
|
+
db = self.mongo_client[self.db_name]
|
|
19
25
|
return db[item_type]
|
|
20
26
|
|
|
21
27
|
def keys(self, item_type: str) -> list[str]:
|
|
@@ -78,13 +78,13 @@ class TestAsyncCollectionGetFilter:
|
|
|
78
78
|
async def test_get_missing_returns_none(self, todos):
|
|
79
79
|
assert await todos.get("nope") is None
|
|
80
80
|
|
|
81
|
-
async def
|
|
81
|
+
async def test_items_returns_all_items(self, todos):
|
|
82
82
|
await todos.save({"id": "1"})
|
|
83
83
|
await todos.save({"id": "2"})
|
|
84
|
-
assert sorted(i["id"] for i in await todos.
|
|
84
|
+
assert sorted(i["id"] for i in await todos.items()) == ["1", "2"]
|
|
85
85
|
|
|
86
|
-
async def
|
|
87
|
-
assert await todos.
|
|
86
|
+
async def test_items_empty(self, todos):
|
|
87
|
+
assert await todos.items() == []
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
class TestAsyncCollectionPatch:
|
|
@@ -175,12 +175,12 @@ class TestAsyncPydanticCollection:
|
|
|
175
175
|
storage.write("todo", {"id": "1", "title": "a", "done": "not a bool"}) # written outside the model
|
|
176
176
|
assert await model_todos.keys() == ["1"]
|
|
177
177
|
with pytest.raises(pydantic.ValidationError):
|
|
178
|
-
await model_todos.
|
|
178
|
+
await model_todos.items()
|
|
179
179
|
|
|
180
|
-
async def
|
|
180
|
+
async def test_items_returns_models(self, model_todos):
|
|
181
181
|
await model_todos.save(Todo(id="1", title="a"))
|
|
182
182
|
await model_todos.save(Todo(id="2", title="b"))
|
|
183
|
-
result = await model_todos.
|
|
183
|
+
result = await model_todos.items()
|
|
184
184
|
assert all(isinstance(t, Todo) for t in result)
|
|
185
185
|
assert sorted(t.id for t in result) == ["1", "2"]
|
|
186
186
|
|
|
@@ -209,7 +209,7 @@ class TestAsyncPydanticCollection:
|
|
|
209
209
|
with pytest.raises(pydantic.ValidationError):
|
|
210
210
|
await model_todos.patch("1", {"done": "not a bool"})
|
|
211
211
|
assert storage.read("todo", "1") == {"id": "1", "title": "a", "done": False}
|
|
212
|
-
assert await model_todos.
|
|
212
|
+
assert await model_todos.items() == [Todo(id="1", title="a")]
|
|
213
213
|
|
|
214
214
|
async def test_invalid_model_is_not_stored(self, model_todos, storage):
|
|
215
215
|
todo = Todo(id="1", title="a")
|
|
@@ -53,8 +53,8 @@ class TestAsyncFileStorageInit:
|
|
|
53
53
|
with pytest.raises(ValueError, match="does not exist"):
|
|
54
54
|
async_cls(str(tmp_path / "missing"))
|
|
55
55
|
|
|
56
|
-
def
|
|
57
|
-
assert storage.
|
|
56
|
+
def test_exposes_collection_dir(self, storage, base_dir):
|
|
57
|
+
assert storage.collection_dir == base_dir
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
# ---------------------------------------------------------------------------
|
|
@@ -128,7 +128,7 @@ class TestAsyncFileStorageBehaviour:
|
|
|
128
128
|
assert sorted(await storage.akeys("todo"), key=int) == [str(i) for i in range(30)]
|
|
129
129
|
assert len(await storage.aitems("todo")) == 30
|
|
130
130
|
|
|
131
|
-
async def
|
|
131
|
+
async def test_works_with_async_collection(self, storage):
|
|
132
132
|
todos = AsyncCollection(item_type="todo", storage=storage)
|
|
133
133
|
await todos.save({"id": "1", "done": False})
|
|
134
134
|
assert await todos.patch("1", {"done": True}) == {"id": "1", "done": True}
|
|
@@ -8,6 +8,7 @@ import pytest
|
|
|
8
8
|
from testcontainers.community.mongodb import MongoDbContainer
|
|
9
9
|
|
|
10
10
|
from objbase.asyncio.storage.mongodb import AsyncMongoDBStorage
|
|
11
|
+
from objbase.storage.mongodb import DEFAULT_DB_NAME
|
|
11
12
|
|
|
12
13
|
# See tests/test_mongodb_storage.py for why mongo:latest is not used.
|
|
13
14
|
MONGO_IMAGE = os.getenv("OBJBASE_TEST_MONGO_IMAGE", "mongo:7.0")
|
|
@@ -26,9 +27,9 @@ def mongo_container():
|
|
|
26
27
|
|
|
27
28
|
@pytest.fixture()
|
|
28
29
|
async def mongo_client(mongo_container):
|
|
29
|
-
"""Return an AsyncMongoClient and drop the
|
|
30
|
+
"""Return an AsyncMongoClient and drop the collection DB before each test."""
|
|
30
31
|
client: pymongo.AsyncMongoClient[dict[str, Any]] = pymongo.AsyncMongoClient(mongo_container.get_connection_url())
|
|
31
|
-
await client.drop_database(
|
|
32
|
+
await client.drop_database(DEFAULT_DB_NAME)
|
|
32
33
|
yield client
|
|
33
34
|
await client.close()
|
|
34
35
|
|
|
@@ -190,3 +191,11 @@ class TestAsyncMongoDBStorageLayout:
|
|
|
190
191
|
|
|
191
192
|
MongoDBStorage(mongo_container.get_connection_client()).write("todo", {"id": "1", "n": 1})
|
|
192
193
|
assert await storage.aread("todo", "1") == {"id": "1", "n": 1}
|
|
194
|
+
|
|
195
|
+
async def test_custom_db_name_shares_data_with_sync_storage(self, mongo_container, mongo_client):
|
|
196
|
+
from objbase.storage.mongodb import MongoDBStorage
|
|
197
|
+
|
|
198
|
+
await mongo_client.drop_database("myapp")
|
|
199
|
+
MongoDBStorage(mongo_container.get_connection_client(), db_name="myapp").write("todo", {"id": "1"})
|
|
200
|
+
assert await AsyncMongoDBStorage(mongo_client, db_name="myapp").aread("todo", "1") == {"id": "1"}
|
|
201
|
+
assert await AsyncMongoDBStorage(mongo_client).aread("todo", "1") is None
|
|
@@ -47,7 +47,7 @@ class TestAsyncRedisStorageWrite:
|
|
|
47
47
|
|
|
48
48
|
async def test_write_stores_item_in_type_hash(self, storage, redis_client):
|
|
49
49
|
await storage.awrite("todo", {"id": "1", "title": "Buy milk"})
|
|
50
|
-
assert await redis_client.hexists("
|
|
50
|
+
assert await redis_client.hexists("objbase:todo", "1")
|
|
51
51
|
|
|
52
52
|
async def test_write_stores_all_fields(self, storage):
|
|
53
53
|
item = {"id": "1", "title": "Buy milk", "done": "false"}
|
|
@@ -151,7 +151,7 @@ class TestAsyncRedisStorageDelete:
|
|
|
151
151
|
async def test_delete_removes_item_from_redis(self, storage, redis_client):
|
|
152
152
|
await storage.awrite("todo", {"id": "1"})
|
|
153
153
|
await storage.adelete("todo", "1")
|
|
154
|
-
assert not await redis_client.hexists("
|
|
154
|
+
assert not await redis_client.hexists("objbase:todo", "1")
|
|
155
155
|
|
|
156
156
|
async def test_delete_item_no_longer_readable(self, storage):
|
|
157
157
|
await storage.awrite("todo", {"id": "1"})
|
|
@@ -139,7 +139,7 @@ class TestAsyncSQLiteStorageBehaviour:
|
|
|
139
139
|
await asyncio.gather(*(storage.awrite("todo", {"id": str(i)}) for i in range(20)))
|
|
140
140
|
assert sorted(await storage.akeys("todo"), key=int) == [str(i) for i in range(20)]
|
|
141
141
|
|
|
142
|
-
async def
|
|
142
|
+
async def test_works_with_async_collection(self, storage):
|
|
143
143
|
todos = AsyncCollection(item_type="todo", storage=storage)
|
|
144
144
|
await todos.save({"id": "1", "done": False})
|
|
145
145
|
assert await todos.patch("1", {"done": True}) == {"id": "1", "done": True}
|
|
@@ -76,12 +76,12 @@ class TestCollectionGetFilter:
|
|
|
76
76
|
def test_get_missing_returns_none(self, todos):
|
|
77
77
|
assert todos.get("nope") is None
|
|
78
78
|
|
|
79
|
-
def
|
|
79
|
+
def test_items_returns_all_items(self, todos):
|
|
80
80
|
todos.save({"id": "1"})
|
|
81
81
|
todos.save({"id": "2"})
|
|
82
82
|
assert sorted(i["id"] for i in todos.items()) == ["1", "2"]
|
|
83
83
|
|
|
84
|
-
def
|
|
84
|
+
def test_items_empty(self, todos):
|
|
85
85
|
assert todos.items() == []
|
|
86
86
|
|
|
87
87
|
|
|
@@ -178,12 +178,12 @@ class TestPydanticCollection:
|
|
|
178
178
|
storage.write("todo", {"id": "1", "title": "a", "done": "not a bool"}) # written outside the model
|
|
179
179
|
assert model_todos.keys() == ["1"]
|
|
180
180
|
with pytest.raises(pydantic.ValidationError):
|
|
181
|
-
model_todos.
|
|
181
|
+
model_todos.items()
|
|
182
182
|
|
|
183
|
-
def
|
|
183
|
+
def test_items_returns_models(self, model_todos):
|
|
184
184
|
model_todos.save(Todo(id="1", title="a"))
|
|
185
185
|
model_todos.save(Todo(id="2", title="b"))
|
|
186
|
-
result = model_todos.
|
|
186
|
+
result = model_todos.items()
|
|
187
187
|
assert all(isinstance(t, Todo) for t in result)
|
|
188
188
|
assert sorted(t.id for t in result) == ["1", "2"]
|
|
189
189
|
|
|
@@ -212,7 +212,7 @@ class TestPydanticCollection:
|
|
|
212
212
|
with pytest.raises(pydantic.ValidationError):
|
|
213
213
|
model_todos.patch("1", {"done": "not a bool"})
|
|
214
214
|
assert storage.read("todo", "1") == {"id": "1", "title": "a", "done": False}
|
|
215
|
-
assert model_todos.
|
|
215
|
+
assert model_todos.items() == [Todo(id="1", title="a")]
|
|
216
216
|
|
|
217
217
|
def test_invalid_model_is_not_stored(self, model_todos, storage):
|
|
218
218
|
todo = Todo(id="1", title="a")
|
|
@@ -55,7 +55,7 @@ class TestLocalFileStorageInit:
|
|
|
55
55
|
|
|
56
56
|
def test_init_succeeds_with_existing_dir(self, base_dir):
|
|
57
57
|
storage = LocalFileStorage(base_dir)
|
|
58
|
-
assert storage.
|
|
58
|
+
assert storage.collection_dir == base_dir
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
class TestLocalFileStorageSelect:
|
|
@@ -162,7 +162,7 @@ class TestLocalDirectoryStorageInit:
|
|
|
162
162
|
|
|
163
163
|
def test_init_succeeds_with_existing_dir(self, base_dir):
|
|
164
164
|
storage = LocalDirectoryStorage(base_dir)
|
|
165
|
-
assert storage.
|
|
165
|
+
assert storage.collection_dir == base_dir
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
class TestLocalDirectoryStorageSelect:
|
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
import pytest
|
|
6
6
|
from testcontainers.community.mongodb import MongoDbContainer
|
|
7
7
|
|
|
8
|
-
from objbase.storage.mongodb import MongoDBStorage
|
|
8
|
+
from objbase.storage.mongodb import DEFAULT_DB_NAME, MongoDBStorage
|
|
9
9
|
|
|
10
10
|
# mongo:latest (8.x) refuses to start on Linux kernels >= 6.19 (SERVER-121912),
|
|
11
11
|
# which recent Docker Desktop VMs ship. Pin a known-good image by default.
|
|
@@ -25,9 +25,9 @@ def mongo_container():
|
|
|
25
25
|
|
|
26
26
|
@pytest.fixture()
|
|
27
27
|
def mongo_client(mongo_container):
|
|
28
|
-
"""Return a MongoClient and drop the
|
|
28
|
+
"""Return a MongoClient and drop the collection DB before each test."""
|
|
29
29
|
client = mongo_container.get_connection_client()
|
|
30
|
-
client.drop_database(
|
|
30
|
+
client.drop_database(DEFAULT_DB_NAME)
|
|
31
31
|
return client
|
|
32
32
|
|
|
33
33
|
|
|
@@ -202,3 +202,30 @@ class TestMongoDBStorageDelete:
|
|
|
202
202
|
storage.write("todo", {"id": "2"})
|
|
203
203
|
storage.delete("todo", "1")
|
|
204
204
|
assert storage.read("todo", "2") == {"id": "2"}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# database name
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestMongoDBStorageDbName:
|
|
213
|
+
def test_default_db_name(self, storage, mongo_client):
|
|
214
|
+
storage.write("todo", {"id": "1"})
|
|
215
|
+
assert mongo_client["objbase"]["todo"].count_documents({"id": "1"}) == 1
|
|
216
|
+
|
|
217
|
+
def test_custom_db_name(self, mongo_client):
|
|
218
|
+
mongo_client.drop_database("myapp")
|
|
219
|
+
storage = MongoDBStorage(mongo_client, db_name="myapp")
|
|
220
|
+
storage.write("todo", {"id": "1"})
|
|
221
|
+
assert mongo_client["myapp"]["todo"].count_documents({"id": "1"}) == 1
|
|
222
|
+
assert "todo" not in mongo_client["objbase"].list_collection_names()
|
|
223
|
+
|
|
224
|
+
def test_different_db_names_are_isolated(self, mongo_client):
|
|
225
|
+
mongo_client.drop_database("a")
|
|
226
|
+
mongo_client.drop_database("b")
|
|
227
|
+
a = MongoDBStorage(mongo_client, db_name="a")
|
|
228
|
+
b = MongoDBStorage(mongo_client, db_name="b")
|
|
229
|
+
a.write("todo", {"id": "1"})
|
|
230
|
+
assert b.read("todo", "1") is None
|
|
231
|
+
assert b.items("todo") == []
|
|
@@ -43,7 +43,7 @@ class TestRedisStorageWrite:
|
|
|
43
43
|
|
|
44
44
|
def test_write_stores_item_as_json_in_type_hash(self, storage, redis_client):
|
|
45
45
|
storage.write("todo", {"id": "1", "title": "Buy milk"})
|
|
46
|
-
raw = redis_client.hget("
|
|
46
|
+
raw = redis_client.hget("objbase:todo", "1")
|
|
47
47
|
assert json.loads(raw) == {"id": "1", "title": "Buy milk"}
|
|
48
48
|
|
|
49
49
|
def test_write_preserves_value_types(self, storage):
|
|
@@ -149,7 +149,7 @@ class TestRedisStorageDelete:
|
|
|
149
149
|
def test_delete_removes_item_from_redis(self, storage, redis_client):
|
|
150
150
|
storage.write("todo", {"id": "1"})
|
|
151
151
|
storage.delete("todo", "1")
|
|
152
|
-
assert not redis_client.hexists("
|
|
152
|
+
assert not redis_client.hexists("objbase:todo", "1")
|
|
153
153
|
|
|
154
154
|
def test_delete_item_no_longer_readable(self, storage):
|
|
155
155
|
storage.write("todo", {"id": "1"})
|
|
@@ -203,7 +203,7 @@ class TestRedisStorageLayout:
|
|
|
203
203
|
storage = RedisStorage(redis_client, key_prefix="myapp:")
|
|
204
204
|
storage.write("todo", {"id": "1"})
|
|
205
205
|
assert redis_client.hexists("myapp:todo", "1")
|
|
206
|
-
assert not redis_client.exists("
|
|
206
|
+
assert not redis_client.exists("objbase:todo")
|
|
207
207
|
|
|
208
208
|
def test_different_prefixes_are_isolated(self, redis_client):
|
|
209
209
|
a = RedisStorage(redis_client, key_prefix="a:")
|
|
@@ -76,7 +76,7 @@ def _directory(request, tmp_path):
|
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def _sqlite(request, tmp_path):
|
|
79
|
-
return SQLiteStorage(str(tmp_path / "
|
|
79
|
+
return SQLiteStorage(str(tmp_path / "collection.db"))
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def _redis(request, tmp_path):
|
|
@@ -88,10 +88,10 @@ def _redis(request, tmp_path):
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def _mongodb(request, tmp_path):
|
|
91
|
-
from objbase.storage.mongodb import MongoDBStorage
|
|
91
|
+
from objbase.storage.mongodb import DEFAULT_DB_NAME, MongoDBStorage
|
|
92
92
|
|
|
93
93
|
client = request.getfixturevalue("mongo_container").get_connection_client()
|
|
94
|
-
client.drop_database(
|
|
94
|
+
client.drop_database(DEFAULT_DB_NAME)
|
|
95
95
|
return MongoDBStorage(client)
|
|
96
96
|
|
|
97
97
|
|
|
@@ -293,7 +293,7 @@ async def _async_mongodb(request):
|
|
|
293
293
|
|
|
294
294
|
url = request.getfixturevalue("mongo_container").get_connection_url()
|
|
295
295
|
client: pymongo.AsyncMongoClient[Item] = pymongo.AsyncMongoClient(url)
|
|
296
|
-
await client.drop_database("
|
|
296
|
+
await client.drop_database("collection")
|
|
297
297
|
return AsyncMongoDBStorage(client)
|
|
298
298
|
|
|
299
299
|
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Async to-do list stored in one JSON file per item type ({base_dir}/todo.json). No extra dependencies needed.
|
|
2
|
-
# Set INVENTORY_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("INVENTORY_DIR", "data")
|
|
12
|
-
os.makedirs(base_dir, exist_ok=True) # the base directory must exist
|
|
13
|
-
storage = AsyncLocalFileStorage(base_dir=base_dir)
|
|
14
|
-
todos_inventory = AsyncCollection(item_type="todo", storage=storage)
|
|
15
|
-
|
|
16
|
-
# Create some to-do items
|
|
17
|
-
await todos_inventory.save({"id": "1", "name": "Buy groceries", "status": "pending"})
|
|
18
|
-
await todos_inventory.save({"id": "2", "name": "Walk the dog", "status": "pending"})
|
|
19
|
-
print("All To-dos:", await todos_inventory.filter())
|
|
20
|
-
|
|
21
|
-
# Update a to-do item
|
|
22
|
-
updated_todo = await todos_inventory.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_inventory.keys():
|
|
27
|
-
print(f"Deleted To-do {todo_id}:", await todos_inventory.delete(todo_id))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
asyncio.run(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|