objbase 0.4.0__tar.gz → 0.5.1__tar.gz

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