objbase 0.4.0__tar.gz → 0.5.0__tar.gz

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