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.
Files changed (58) hide show
  1. {objbase-0.5.0 → objbase-0.5.1}/PKG-INFO +17 -16
  2. {objbase-0.5.0 → objbase-0.5.1}/README.md +16 -15
  3. {objbase-0.5.0 → objbase-0.5.1}/examples/async_directory_example.py +9 -9
  4. {objbase-0.5.0 → objbase-0.5.1}/examples/async_example.py +5 -5
  5. objbase-0.5.1/examples/async_file_example.py +30 -0
  6. {objbase-0.5.0 → objbase-0.5.1}/examples/async_mongodb_example.py +7 -7
  7. {objbase-0.5.0 → objbase-0.5.1}/examples/async_pydantic_example.py +7 -7
  8. {objbase-0.5.0 → objbase-0.5.1}/examples/async_sqlite_example.py +7 -7
  9. {objbase-0.5.0 → objbase-0.5.1}/examples/dict_example.py +5 -5
  10. {objbase-0.5.0 → objbase-0.5.1}/examples/mongodb_example.py +7 -7
  11. {objbase-0.5.0 → objbase-0.5.1}/examples/pydantic_example.py +5 -5
  12. {objbase-0.5.0 → objbase-0.5.1}/pyproject.toml +1 -1
  13. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/collection.py +1 -1
  14. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/local.py +4 -4
  15. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/mongodb.py +4 -2
  16. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/pydantic.py +22 -22
  17. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/inmemory.py +1 -1
  18. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/local.py +8 -8
  19. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/mongodb.py +9 -3
  20. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/redis.py +1 -1
  21. {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_collection.py +8 -8
  22. {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_file_storage.py +3 -3
  23. {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_mongodb_storage.py +11 -2
  24. {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_redis_storage.py +2 -2
  25. {objbase-0.5.0 → objbase-0.5.1}/tests/test_async_sqlite_storage.py +1 -1
  26. {objbase-0.5.0 → objbase-0.5.1}/tests/test_collection.py +6 -6
  27. {objbase-0.5.0 → objbase-0.5.1}/tests/test_file_storage.py +2 -2
  28. {objbase-0.5.0 → objbase-0.5.1}/tests/test_mongodb_storage.py +30 -3
  29. {objbase-0.5.0 → objbase-0.5.1}/tests/test_redis_storage.py +3 -3
  30. {objbase-0.5.0 → objbase-0.5.1}/tests/test_storage_contract.py +4 -4
  31. {objbase-0.5.0 → objbase-0.5.1}/uv.lock +1 -1
  32. objbase-0.5.0/examples/async_file_example.py +0 -30
  33. {objbase-0.5.0 → objbase-0.5.1}/.github/dependabot.yml +0 -0
  34. {objbase-0.5.0 → objbase-0.5.1}/.github/workflows/ci.yml +0 -0
  35. {objbase-0.5.0 → objbase-0.5.1}/.github/workflows/release.yml +0 -0
  36. {objbase-0.5.0 → objbase-0.5.1}/.gitignore +0 -0
  37. {objbase-0.5.0 → objbase-0.5.1}/DEVELOPER.md +0 -0
  38. {objbase-0.5.0 → objbase-0.5.1}/LICENSE +0 -0
  39. {objbase-0.5.0 → objbase-0.5.1}/release.sh +0 -0
  40. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/__init__.py +0 -0
  41. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/__init__.py +0 -0
  42. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/__init__.py +0 -0
  43. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/redis.py +0 -0
  44. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/sqlite.py +0 -0
  45. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/asyncio/storage/threaded.py +0 -0
  46. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/collection.py +0 -0
  47. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/errors.py +0 -0
  48. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/interface.py +0 -0
  49. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/py.typed +0 -0
  50. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/__init__.py +0 -0
  51. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/storage/sqlite.py +0 -0
  52. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/util/__init__.py +0 -0
  53. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/util/file_util.py +0 -0
  54. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/util/mongodb_util.py +0 -0
  55. {objbase-0.5.0 → objbase-0.5.1}/src/objbase/util/redis_util.py +0 -0
  56. {objbase-0.5.0 → objbase-0.5.1}/tests/test_inmemory_storage.py +0 -0
  57. {objbase-0.5.0 → objbase-0.5.1}/tests/test_package.py +0 -0
  58. {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.0
3
+ Version: 0.5.1
4
4
  Summary: Damn simple object store for Python dicts and Pydantic models across multiple backends
5
5
  Project-URL: Homepage, https://github.com/fm-labs/objbase
6
6
  Project-URL: Issues, https://github.com/fm-labs/objbase/issues
@@ -41,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`, `filter`, `keys`, `patch`, `delete`
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.inventory` | `Collection` |
100
+ | `objbase.collection` | `Collection` |
101
101
  | `objbase.errors` | `CollectionError`, `ItemNotFoundError` |
102
102
  | `objbase.pydantic` | `PydanticCollection`, `AsyncPydanticCollection` (needs `objbase[pydantic]`) |
103
- | `objbase.storage.{inmemory,file,sqlite,redis,mongodb}_storage` | Sync storage adapters |
104
- | `objbase.asyncio.inventory` | `AsyncCollection` |
105
- | `objbase.asyncio.storage.{file,sqlite,redis,mongodb}_storage` | Async storage adapters |
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; `filter` and `keys` return `[]` for an empty type.
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` (an `CollectionError` and a `LookupError`) |
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, `inventory:{item_type}`, mapping item ids to
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 `inventory:`.
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.py` database, one collection per `item_type`.
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.filter()` returns `list[Todo]`.
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.filter() # → [{"id": "1", ...}]
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 `INVENTORY_DIR` or `SQLITE_DB_PATH` to change that. The MongoDB
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.filter()
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`, `filter`, `keys`, `patch`, `delete`
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.inventory` | `Collection` |
68
+ | `objbase.collection` | `Collection` |
69
69
  | `objbase.errors` | `CollectionError`, `ItemNotFoundError` |
70
70
  | `objbase.pydantic` | `PydanticCollection`, `AsyncPydanticCollection` (needs `objbase[pydantic]`) |
71
- | `objbase.storage.{inmemory,file,sqlite,redis,mongodb}_storage` | Sync storage adapters |
72
- | `objbase.asyncio.inventory` | `AsyncCollection` |
73
- | `objbase.asyncio.storage.{file,sqlite,redis,mongodb}_storage` | Async storage adapters |
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; `filter` and `keys` return `[]` for an empty type.
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` (an `CollectionError` and a `LookupError`) |
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, `inventory:{item_type}`, mapping item ids to
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 `inventory:`.
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.py` database, one collection per `item_type`.
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.filter()` returns `list[Todo]`.
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.filter() # → [{"id": "1", ...}]
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 `INVENTORY_DIR` or `SQLITE_DB_PATH` to change that. The MongoDB
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.filter()
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 INVENTORY_DIR to use a different directory.
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("INVENTORY_DIR", "data")
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
- todos_inventory = AsyncCollection(item_type="todo", storage=storage)
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
- todos_inventory.save({"id": "1", "name": "Buy groceries", "status": "pending"}),
19
- todos_inventory.save({"id": "2", "name": "Walk the dog", "status": "pending"}),
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 todos_inventory.keys()))
21
+ print("To-do ids:", sorted(await todos_collection.keys()))
22
22
 
23
23
  # Update a to-do item
24
- updated_todo = await todos_inventory.patch("1", {"status": "completed"})
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 todos_inventory.keys():
32
- print(f"Deleted To-do {todo_id}:", await todos_inventory.delete(todo_id))
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
- todos_inventory = AsyncCollection(item_type="todo", storage=InMemoryStorage())
9
+ todos_collection = AsyncCollection(item_type="todo", storage=InMemoryStorage())
10
10
 
11
11
  # Create a new to-do item
12
- created_todo = await todos_inventory.save({"id": "1", "name": "Buy groceries", "status": "pending"})
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 todos_inventory.get("1")
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 todos_inventory.patch("1", {"status": "completed"})
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 todos_inventory.delete("1")
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
- todos_inventory = AsyncCollection(item_type="todo", storage=storage)
18
+ todos_collection = AsyncCollection(item_type="todo", storage=storage)
19
19
 
20
20
  # Create some to-do items
21
- await todos_inventory.save({"id": "1", "name": "Buy groceries", "status": "pending"})
22
- await todos_inventory.save({"id": "2", "name": "Walk the dog", "status": "pending"})
23
- print("All To-dos:", await todos_inventory.filter())
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 todos_inventory.patch("1", {"status": "completed"})
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 todos_inventory.keys():
34
- print(f"Deleted To-do {todo_id}:", await todos_inventory.delete(todo_id))
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
- todos_inventory = AsyncPydanticCollection(item_type="todos", storage=InMemoryStorage(), model_class=Todo)
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 todos_inventory.save(Todo(id="1", title="Buy milk"))
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 todos_inventory.get("1")
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 todos_inventory.patch("1", fetched_todo)
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 todos_inventory.patch("1", {"completed": "not a bool"})
35
+ await todos_collection.patch("1", {"completed": "not a bool"})
36
36
  except pydantic.ValidationError:
37
- print("Rejected invalid patch; stored item unchanged:", await todos_inventory.get("1"))
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 todos_inventory.delete("1")
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
- todos_inventory = AsyncCollection(item_type="todo", storage=storage)
14
+ todos_collection = AsyncCollection(item_type="todo", storage=storage)
15
15
 
16
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())
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 todos_inventory.patch("1", {"status": "completed"})
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 todos_inventory.keys():
27
- print(f"Deleted To-do {todo_id}:", await todos_inventory.delete(todo_id))
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
- todos_inventory = Collection(item_type="todo", storage=InMemoryStorage())
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 = todos_inventory.save(new_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 = todos_inventory.get("1")
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 = todos_inventory.patch("1", {"status": "completed"})
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 = todos_inventory.delete("1")
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
- todos_inventory = Collection(item_type="todo", storage=storage)
13
+ todos_collection = Collection(item_type="todo", storage=storage)
14
14
 
15
15
  # Create some to-do items
16
- todos_inventory.save({"id": "1", "name": "Buy groceries", "status": "pending"})
17
- todos_inventory.save({"id": "2", "name": "Walk the dog", "status": "pending"})
18
- print("All To-dos:", todos_inventory.items())
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 = todos_inventory.patch("1", {"status": "completed"})
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 todos_inventory.keys():
29
- print(f"Deleted To-do {todo_id}:", todos_inventory.delete(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
- todos_inventory = PydanticCollection(item_type="todos", model_class=Todo, storage=model_storage)
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 = todos_inventory.save(Todo(id="1", title="Buy milk", completed=False))
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 = todos_inventory.get("1")
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 = todos_inventory.patch("1", fetched_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 = todos_inventory.delete("1")
31
+ delete_result = todos_collection.delete("1")
32
32
  print("Deleted To-do:", delete_result)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "objbase"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "Damn simple object store for Python dicts and Pydantic models across multiple backends"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = []
@@ -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 filter(self) -> list[Item]:
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 inventory_dir(self) -> str:
20
- return self.sync_storage.inventory_dir
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 inventory_dir(self) -> str:
36
- return self.sync_storage.inventory_dir
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["inventory"]
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.inventory = Collection(item_type, storage)
51
+ self.collection = Collection(item_type, storage)
52
52
 
53
53
  @property
54
54
  def item_type(self) -> str:
55
- return self.inventory.item_type
55
+ return self.collection.item_type
56
56
 
57
57
  @property
58
58
  def storage(self) -> Storage:
59
- return self.inventory.storage
59
+ return self.collection.storage
60
60
 
61
61
  def keys(self) -> list[str]:
62
- return self.inventory.keys()
62
+ return self.collection.keys()
63
63
 
64
- def filter(self) -> list[M]:
65
- return [self.model_class.model_validate(item) for item in self.inventory.items()]
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.inventory.save(_validated(self.model_class, model)))
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.inventory.get(id)
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.inventory.get(id), data)
78
- return self.model_class.model_validate(self.inventory.save(item))
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.inventory.delete(id)
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.inventory = AsyncCollection(item_type, storage)
92
+ self.collection = AsyncCollection(item_type, storage)
93
93
 
94
94
  @property
95
95
  def item_type(self) -> str:
96
- return self.inventory.item_type
96
+ return self.collection.item_type
97
97
 
98
98
  @property
99
99
  def storage(self) -> AsyncStorage:
100
- return self.inventory.storage
100
+ return self.collection.storage
101
101
 
102
102
  async def keys(self) -> list[str]:
103
- return await self.inventory.keys()
103
+ return await self.collection.keys()
104
104
 
105
- async def filter(self) -> list[M]:
106
- return [self.model_class.model_validate(item) for item in await self.inventory.filter()]
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.inventory.save(_validated(self.model_class, model)))
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.inventory.get(id)
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.inventory.get(id), data)
119
- return self.model_class.model_validate(await self.inventory.save(item))
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.inventory.delete(id)
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 inventory items.
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 inventory type in a single JSON file.
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.inventory_dir = base_dir
75
- if not os.path.exists(self.inventory_dir):
76
- raise ValueError(f"Base directory {self.inventory_dir} does not exist.")
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 inventory type and individual files per item.
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.inventory_dir = base_dir
152
- if not os.path.exists(self.inventory_dir):
153
- raise ValueError(f"Base directory {self.inventory_dir} does not exist.")
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 inventory items."""
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["inventory"]
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]:
@@ -3,7 +3,7 @@ from typing import Any, Protocol
3
3
 
4
4
  from objbase.interface import Item, Storage
5
5
 
6
- DEFAULT_KEY_PREFIX = "inventory:"
6
+ DEFAULT_KEY_PREFIX = "objbase:"
7
7
 
8
8
 
9
9
  class RedisHashClient(Protocol):
@@ -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 test_filter_returns_all_items(self, todos):
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.filter()) == ["1", "2"]
84
+ assert sorted(i["id"] for i in await todos.items()) == ["1", "2"]
85
85
 
86
- async def test_filter_empty(self, todos):
87
- assert await todos.filter() == []
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.filter()
178
+ await model_todos.items()
179
179
 
180
- async def test_filter_returns_models(self, model_todos):
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.filter()
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.filter() == [Todo(id="1", title="a")]
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 test_exposes_inventory_dir(self, storage, base_dir):
57
- assert storage.inventory_dir == base_dir
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 test_works_with_async_inventory(self, storage):
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 inventory DB before each test."""
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("inventory")
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("inventory:todo", "1")
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("inventory:todo", "1")
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 test_works_with_async_inventory(self, storage):
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 test_filter_returns_all_items(self, todos):
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 test_filter_empty(self, todos):
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.filter()
181
+ model_todos.items()
182
182
 
183
- def test_filter_returns_models(self, model_todos):
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.filter()
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.filter() == [Todo(id="1", title="a")]
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.inventory_dir == base_dir
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.inventory_dir == base_dir
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 inventory DB before each test."""
28
+ """Return a MongoClient and drop the collection DB before each test."""
29
29
  client = mongo_container.get_connection_client()
30
- client.drop_database("inventory")
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("inventory:todo", "1")
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("inventory:todo", "1")
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("inventory:todo")
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 / "inventory.db"))
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("inventory")
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("inventory")
296
+ await client.drop_database("collection")
297
297
  return AsyncMongoDBStorage(client)
298
298
 
299
299
 
@@ -414,7 +414,7 @@ wheels = [
414
414
 
415
415
  [[package]]
416
416
  name = "objbase"
417
- version = "0.5.0"
417
+ version = "0.5.1"
418
418
  source = { editable = "." }
419
419
 
420
420
  [package.optional-dependencies]
@@ -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