typed-store 1.0.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.
- typed_store-1.0.0/.gitignore +10 -0
- typed_store-1.0.0/LICENSE +21 -0
- typed_store-1.0.0/PKG-INFO +206 -0
- typed_store-1.0.0/README.md +182 -0
- typed_store-1.0.0/docs/README.md +17 -0
- typed_store-1.0.0/docs/api-surface.md +268 -0
- typed_store-1.0.0/docs/design-spec.md +246 -0
- typed_store-1.0.0/docs/internal/progress.md +77 -0
- typed_store-1.0.0/docs/internal/templates/progress-template.md +31 -0
- typed_store-1.0.0/docs/internal/templates/spec-template.md +15 -0
- typed_store-1.0.0/docs/internal/workflow.md +232 -0
- typed_store-1.0.0/docs/publishing.md +137 -0
- typed_store-1.0.0/docs/superpowers/plans/2026-03-29-protocol-first-public-sdk-v0-2.md +733 -0
- typed_store-1.0.0/docs/superpowers/plans/2026-03-29-protocol-first-public-sdk-v0-3.md +717 -0
- typed_store-1.0.0/docs/superpowers/plans/2026-03-30-public-sdk-v1-0.md +1122 -0
- typed_store-1.0.0/docs/superpowers/specs/2026-03-29-public-sdk-protocol-first-design.md +390 -0
- typed_store-1.0.0/docs/superpowers/specs/2026-03-30-public-sdk-v1-design.md +304 -0
- typed_store-1.0.0/examples/async_basic.py +42 -0
- typed_store-1.0.0/examples/async_repository_pattern.py +83 -0
- typed_store-1.0.0/examples/bulk_operations.py +40 -0
- typed_store-1.0.0/examples/model_mixin.py +70 -0
- typed_store-1.0.0/examples/model_store_view.py +52 -0
- typed_store-1.0.0/examples/repository_pattern.py +74 -0
- typed_store-1.0.0/examples/sync_basic.py +33 -0
- typed_store-1.0.0/pyproject.toml +86 -0
- typed_store-1.0.0/tests/conftest.py +89 -0
- typed_store-1.0.0/tests/test_async_store.py +151 -0
- typed_store-1.0.0/tests/test_bound_model.py +102 -0
- typed_store-1.0.0/tests/test_error_boundaries.py +90 -0
- typed_store-1.0.0/tests/test_examples.py +85 -0
- typed_store-1.0.0/tests/test_protocols.py +78 -0
- typed_store-1.0.0/tests/test_public_api.py +56 -0
- typed_store-1.0.0/tests/test_shortcuts.py +376 -0
- typed_store-1.0.0/tests/test_specs.py +66 -0
- typed_store-1.0.0/tests/test_sync_store.py +145 -0
- typed_store-1.0.0/typed_store/__init__.py +90 -0
- typed_store-1.0.0/typed_store/async_store.py +346 -0
- typed_store-1.0.0/typed_store/bound_model.py +219 -0
- typed_store-1.0.0/typed_store/engine.py +90 -0
- typed_store-1.0.0/typed_store/errors.py +33 -0
- typed_store-1.0.0/typed_store/model.py +41 -0
- typed_store-1.0.0/typed_store/protocols.py +301 -0
- typed_store-1.0.0/typed_store/py.typed +1 -0
- typed_store-1.0.0/typed_store/results.py +15 -0
- typed_store-1.0.0/typed_store/session.py +50 -0
- typed_store-1.0.0/typed_store/specs.py +152 -0
- typed_store-1.0.0/typed_store/store.py +88 -0
- typed_store-1.0.0/typed_store/sync.py +346 -0
- typed_store-1.0.0/typed_store/uow.py +98 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ticoag
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typed-store
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Type-first data access SDK built on top of SQLAlchemy
|
|
5
|
+
Author: ticoag
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: data-access,orm,repository,sqlalchemy,typed
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Database
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: greenlet>=3.1.1
|
|
22
|
+
Requires-Dist: sqlalchemy>=2.0.36
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# TypedStore
|
|
26
|
+
|
|
27
|
+
[](https://www.python.org/) [](https://www.sqlalchemy.org/) [](LICENSE) []()
|
|
28
|
+
|
|
29
|
+
Type-first data access SDK built on top of SQLAlchemy 2.x.
|
|
30
|
+
|
|
31
|
+
TypedStore 把项目中重复出现的 session 管理、CRUD facade、分页与事务样板收敛为一组类型安全的 API——不替代 SQLAlchemy,只做它上层的薄 facade。
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Explicit sync / async** — `SyncTypedStore` 与 `AsyncTypedStore` 分离,无隐式双重语义
|
|
36
|
+
- **One-line init** — `SyncTypedStore.from_url("sqlite:///app.db")` 即可开始使用
|
|
37
|
+
- **Protocol-first queries** — `store.find_many(User, query=Query[User]().where(...))`
|
|
38
|
+
- **Bind-first models** — `User.bind(store).find_many()` 让模型天然具备显式绑定能力
|
|
39
|
+
- **Explicit request objects** — `Query`, `PageRequest`, `Patch`, `ProjectionQuery`
|
|
40
|
+
- **Bulk mutation APIs** — `bulk_update` / `bulk_delete` 与 object mutation 语义分离
|
|
41
|
+
- **UnitOfWork** — 明确的事务边界(sync / async)
|
|
42
|
+
- **TypedStoreModel** — 为模型提供纯函数式 `bind(store)` 入口
|
|
43
|
+
- **TypedStore composition root** — 通过 `.sync` / `.async_` 统一装配 sync / async store
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install typed-store
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
<details>
|
|
54
|
+
<summary>本地开发</summary>
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv sync --group dev
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
</details>
|
|
61
|
+
|
|
62
|
+
### Sync
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from typed_store import PageRequest, Query, SyncTypedStore
|
|
66
|
+
|
|
67
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
68
|
+
Base.metadata.create_all(store.engine)
|
|
69
|
+
|
|
70
|
+
store.insert(User(name="alice"))
|
|
71
|
+
query = Query[User]().where(User.role == "admin").order(User.id.asc())
|
|
72
|
+
page = PageRequest(limit=10, offset=0)
|
|
73
|
+
|
|
74
|
+
store.find_many(User, query=query)
|
|
75
|
+
store.paginate(User, query=query, page=page)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Async
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from typed_store import AsyncTypedStore, PageRequest, Query
|
|
82
|
+
|
|
83
|
+
store = AsyncTypedStore.from_url("sqlite+aiosqlite:///app.db")
|
|
84
|
+
|
|
85
|
+
await store.insert(User(name="alice"))
|
|
86
|
+
query = Query[User]().where(User.role == "admin")
|
|
87
|
+
await store.find_many(User, query=query)
|
|
88
|
+
await store.paginate(User, query=query, page=PageRequest(limit=10, offset=0))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### TypedStore (sync + async)
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from typed_store import Query, TypedStore
|
|
95
|
+
|
|
96
|
+
ts = TypedStore.from_url("sqlite:///app.db", async_url="sqlite+aiosqlite:///app.db")
|
|
97
|
+
|
|
98
|
+
# 显式使用 sync store
|
|
99
|
+
ts.sync.find_many(User, query=Query[User]())
|
|
100
|
+
ts.sync.insert(User(name="alice"))
|
|
101
|
+
|
|
102
|
+
# 异步明确走 .async_
|
|
103
|
+
await ts.async_.find_many(User, query=Query[User]())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Model Binding
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from typed_store import PageRequest, Query, SyncTypedStore, TypedStoreModel
|
|
110
|
+
|
|
111
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
112
|
+
|
|
113
|
+
class User(Base, TypedStoreModel):
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
users = User.bind(store)
|
|
117
|
+
users.insert(User(name="alice"))
|
|
118
|
+
users.find_many(query=Query[User]().where(User.role == "admin"))
|
|
119
|
+
users.get(1)
|
|
120
|
+
users.paginate(query=Query[User](), page=PageRequest(limit=10, offset=0))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Request Objects
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from typed_store import PageRequest, Patch, Query
|
|
127
|
+
|
|
128
|
+
query = Query[User]().where(User.role == "admin").order(User.id.asc())
|
|
129
|
+
page = PageRequest(limit=10, offset=0)
|
|
130
|
+
patch = Patch[User]({"role": "superadmin"})
|
|
131
|
+
|
|
132
|
+
store.find_many(User, query=query)
|
|
133
|
+
store.paginate(User, query=query, page=page)
|
|
134
|
+
store.update(User, query=Query[User]().where(User.name == "alice"), patch=patch)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Bulk Mutation
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from typed_store import Patch, Query
|
|
141
|
+
|
|
142
|
+
store.bulk_update(
|
|
143
|
+
User,
|
|
144
|
+
query=Query[User]().where(User.role == "member"),
|
|
145
|
+
patch=Patch[User]({"role": "staff"}),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
User.bind(store).bulk_delete(
|
|
149
|
+
query=Query[User]().where(User.role == "guest"),
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Model Mixin
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from typed_store import Query, SyncTypedStore, TypedStoreModel
|
|
157
|
+
|
|
158
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
159
|
+
|
|
160
|
+
class User(Base, TypedStoreModel):
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
users = User.bind(store)
|
|
164
|
+
users.insert(User(name="alice"))
|
|
165
|
+
items = users.find_many(query=Query[User]())
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Repository Pattern
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
172
|
+
repo = UserRepository(store)
|
|
173
|
+
service = UserService(store)
|
|
174
|
+
service.register_admin("alice@example.com")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> 完整示例见 [`examples/`](examples/),对应 smoke 测试见 [`tests/test_examples.py`](tests/test_examples.py)。
|
|
178
|
+
|
|
179
|
+
## Requirements
|
|
180
|
+
|
|
181
|
+
| Dependency | Version |
|
|
182
|
+
| ---------- | --------- |
|
|
183
|
+
| Python | >= 3.12 |
|
|
184
|
+
| SQLAlchemy | >= 2.0.36 |
|
|
185
|
+
|
|
186
|
+
## Documentation
|
|
187
|
+
|
|
188
|
+
| Document | Description |
|
|
189
|
+
| -------------------------------------------- | ------------- |
|
|
190
|
+
| [`docs/api-surface.md`](docs/api-surface.md) | 完整 API 说明 |
|
|
191
|
+
| [`docs/design-spec.md`](docs/design-spec.md) | 设计规格 |
|
|
192
|
+
| [`docs/publishing.md`](docs/publishing.md) | 发布配置清单 |
|
|
193
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | 版本变更记录 |
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv run pytest # tests
|
|
199
|
+
uv run ruff check . # lint
|
|
200
|
+
uv run ruff format --check . # format check
|
|
201
|
+
uv run ty check # type check
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# TypedStore
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/) [](https://www.sqlalchemy.org/) [](LICENSE) []()
|
|
4
|
+
|
|
5
|
+
Type-first data access SDK built on top of SQLAlchemy 2.x.
|
|
6
|
+
|
|
7
|
+
TypedStore 把项目中重复出现的 session 管理、CRUD facade、分页与事务样板收敛为一组类型安全的 API——不替代 SQLAlchemy,只做它上层的薄 facade。
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Explicit sync / async** — `SyncTypedStore` 与 `AsyncTypedStore` 分离,无隐式双重语义
|
|
12
|
+
- **One-line init** — `SyncTypedStore.from_url("sqlite:///app.db")` 即可开始使用
|
|
13
|
+
- **Protocol-first queries** — `store.find_many(User, query=Query[User]().where(...))`
|
|
14
|
+
- **Bind-first models** — `User.bind(store).find_many()` 让模型天然具备显式绑定能力
|
|
15
|
+
- **Explicit request objects** — `Query`, `PageRequest`, `Patch`, `ProjectionQuery`
|
|
16
|
+
- **Bulk mutation APIs** — `bulk_update` / `bulk_delete` 与 object mutation 语义分离
|
|
17
|
+
- **UnitOfWork** — 明确的事务边界(sync / async)
|
|
18
|
+
- **TypedStoreModel** — 为模型提供纯函数式 `bind(store)` 入口
|
|
19
|
+
- **TypedStore composition root** — 通过 `.sync` / `.async_` 统一装配 sync / async store
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install typed-store
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
<details>
|
|
30
|
+
<summary>本地开发</summary>
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv sync --group dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
</details>
|
|
37
|
+
|
|
38
|
+
### Sync
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from typed_store import PageRequest, Query, SyncTypedStore
|
|
42
|
+
|
|
43
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
44
|
+
Base.metadata.create_all(store.engine)
|
|
45
|
+
|
|
46
|
+
store.insert(User(name="alice"))
|
|
47
|
+
query = Query[User]().where(User.role == "admin").order(User.id.asc())
|
|
48
|
+
page = PageRequest(limit=10, offset=0)
|
|
49
|
+
|
|
50
|
+
store.find_many(User, query=query)
|
|
51
|
+
store.paginate(User, query=query, page=page)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Async
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from typed_store import AsyncTypedStore, PageRequest, Query
|
|
58
|
+
|
|
59
|
+
store = AsyncTypedStore.from_url("sqlite+aiosqlite:///app.db")
|
|
60
|
+
|
|
61
|
+
await store.insert(User(name="alice"))
|
|
62
|
+
query = Query[User]().where(User.role == "admin")
|
|
63
|
+
await store.find_many(User, query=query)
|
|
64
|
+
await store.paginate(User, query=query, page=PageRequest(limit=10, offset=0))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### TypedStore (sync + async)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from typed_store import Query, TypedStore
|
|
71
|
+
|
|
72
|
+
ts = TypedStore.from_url("sqlite:///app.db", async_url="sqlite+aiosqlite:///app.db")
|
|
73
|
+
|
|
74
|
+
# 显式使用 sync store
|
|
75
|
+
ts.sync.find_many(User, query=Query[User]())
|
|
76
|
+
ts.sync.insert(User(name="alice"))
|
|
77
|
+
|
|
78
|
+
# 异步明确走 .async_
|
|
79
|
+
await ts.async_.find_many(User, query=Query[User]())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Model Binding
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from typed_store import PageRequest, Query, SyncTypedStore, TypedStoreModel
|
|
86
|
+
|
|
87
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
88
|
+
|
|
89
|
+
class User(Base, TypedStoreModel):
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
users = User.bind(store)
|
|
93
|
+
users.insert(User(name="alice"))
|
|
94
|
+
users.find_many(query=Query[User]().where(User.role == "admin"))
|
|
95
|
+
users.get(1)
|
|
96
|
+
users.paginate(query=Query[User](), page=PageRequest(limit=10, offset=0))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Request Objects
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from typed_store import PageRequest, Patch, Query
|
|
103
|
+
|
|
104
|
+
query = Query[User]().where(User.role == "admin").order(User.id.asc())
|
|
105
|
+
page = PageRequest(limit=10, offset=0)
|
|
106
|
+
patch = Patch[User]({"role": "superadmin"})
|
|
107
|
+
|
|
108
|
+
store.find_many(User, query=query)
|
|
109
|
+
store.paginate(User, query=query, page=page)
|
|
110
|
+
store.update(User, query=Query[User]().where(User.name == "alice"), patch=patch)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Bulk Mutation
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from typed_store import Patch, Query
|
|
117
|
+
|
|
118
|
+
store.bulk_update(
|
|
119
|
+
User,
|
|
120
|
+
query=Query[User]().where(User.role == "member"),
|
|
121
|
+
patch=Patch[User]({"role": "staff"}),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
User.bind(store).bulk_delete(
|
|
125
|
+
query=Query[User]().where(User.role == "guest"),
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Model Mixin
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from typed_store import Query, SyncTypedStore, TypedStoreModel
|
|
133
|
+
|
|
134
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
135
|
+
|
|
136
|
+
class User(Base, TypedStoreModel):
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
users = User.bind(store)
|
|
140
|
+
users.insert(User(name="alice"))
|
|
141
|
+
items = users.find_many(query=Query[User]())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Repository Pattern
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
148
|
+
repo = UserRepository(store)
|
|
149
|
+
service = UserService(store)
|
|
150
|
+
service.register_admin("alice@example.com")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> 完整示例见 [`examples/`](examples/),对应 smoke 测试见 [`tests/test_examples.py`](tests/test_examples.py)。
|
|
154
|
+
|
|
155
|
+
## Requirements
|
|
156
|
+
|
|
157
|
+
| Dependency | Version |
|
|
158
|
+
| ---------- | --------- |
|
|
159
|
+
| Python | >= 3.12 |
|
|
160
|
+
| SQLAlchemy | >= 2.0.36 |
|
|
161
|
+
|
|
162
|
+
## Documentation
|
|
163
|
+
|
|
164
|
+
| Document | Description |
|
|
165
|
+
| -------------------------------------------- | ------------- |
|
|
166
|
+
| [`docs/api-surface.md`](docs/api-surface.md) | 完整 API 说明 |
|
|
167
|
+
| [`docs/design-spec.md`](docs/design-spec.md) | 设计规格 |
|
|
168
|
+
| [`docs/publishing.md`](docs/publishing.md) | 发布配置清单 |
|
|
169
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | 版本变更记录 |
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
uv run pytest # tests
|
|
175
|
+
uv run ruff check . # lint
|
|
176
|
+
uv run ruff format --check . # format check
|
|
177
|
+
uv run ty check # type check
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
## User Guide
|
|
4
|
+
|
|
5
|
+
| Document | Description |
|
|
6
|
+
|---|---|
|
|
7
|
+
| [api-surface.md](api-surface.md) | API reference — entry points, methods, usage scenarios |
|
|
8
|
+
| [design-spec.md](design-spec.md) | Design specification — goals, architecture, core concepts |
|
|
9
|
+
| [publishing.md](publishing.md) | Release & publishing guide for PyPI / TestPyPI |
|
|
10
|
+
|
|
11
|
+
## Internal (Development)
|
|
12
|
+
|
|
13
|
+
| Document | Description |
|
|
14
|
+
|---|---|
|
|
15
|
+
| [internal/workflow.md](internal/workflow.md) | Development workflow & task recovery procedure |
|
|
16
|
+
| [internal/progress.md](internal/progress.md) | Current progress tracking for TypedStore |
|
|
17
|
+
| [internal/templates/](internal/templates/) | Spec & progress document templates |
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# API Surface
|
|
2
|
+
|
|
3
|
+
本文档描述 `TypedStore` 当前对外推荐的 public API。目标是让使用者快速判断:
|
|
4
|
+
|
|
5
|
+
- 从哪里进入
|
|
6
|
+
- store capability protocol 和 concrete store 的关系
|
|
7
|
+
- request objects 应该如何驱动查询、分页、更新和投影
|
|
8
|
+
|
|
9
|
+
## 1. Primary Entry Points
|
|
10
|
+
|
|
11
|
+
当前推荐的主入口只有四类:
|
|
12
|
+
|
|
13
|
+
- `SyncTypedStore`:同步 SQLAlchemy 路径的正式实现
|
|
14
|
+
- `AsyncTypedStore`:异步 SQLAlchemy 路径的正式实现
|
|
15
|
+
- `TypedStoreModel.bind(store)`:模型一等能力的绑定入口
|
|
16
|
+
- `TypedStore`:只负责组合 `.sync` / `.async_` 的 composition root
|
|
17
|
+
|
|
18
|
+
## 2. Request Objects
|
|
19
|
+
|
|
20
|
+
`TypedStore` 的查询与变更都由显式 request objects 驱动,而不是内联 filter 参数。
|
|
21
|
+
|
|
22
|
+
### `Query[TModel]`
|
|
23
|
+
|
|
24
|
+
用于实体查询:
|
|
25
|
+
|
|
26
|
+
- `Query[TModel]()`:创建空查询
|
|
27
|
+
- `.where(*filters)`:追加过滤条件
|
|
28
|
+
- `.order(*clauses)`:追加排序
|
|
29
|
+
- `.limit_to(limit)`:设置 limit
|
|
30
|
+
- `.offset_by(offset)`:设置 offset
|
|
31
|
+
- `.with_options(*options)`:追加 SQLAlchemy loader options
|
|
32
|
+
|
|
33
|
+
### `PageRequest`
|
|
34
|
+
|
|
35
|
+
用于分页请求:
|
|
36
|
+
|
|
37
|
+
- `PageRequest(limit, offset=0)`
|
|
38
|
+
|
|
39
|
+
### `Patch[TModel]`
|
|
40
|
+
|
|
41
|
+
用于字段级更新:
|
|
42
|
+
|
|
43
|
+
- `Patch[TModel](values={"field": value})`
|
|
44
|
+
|
|
45
|
+
### `ProjectionQuery[TRow]`
|
|
46
|
+
|
|
47
|
+
用于显式列投影:
|
|
48
|
+
|
|
49
|
+
- `ProjectionQuery[TRow](*columns)`
|
|
50
|
+
- `.where(*filters)`
|
|
51
|
+
- `.order(*clauses)`
|
|
52
|
+
- `.with_options(*options)`
|
|
53
|
+
|
|
54
|
+
## 3. Sync Capability Protocols
|
|
55
|
+
|
|
56
|
+
同步 capability protocol 定义 public contract:
|
|
57
|
+
|
|
58
|
+
- `ReadableStoreProtocol[TModel]`
|
|
59
|
+
- `WritableStoreProtocol[TModel]`
|
|
60
|
+
- `PatchableStoreProtocol[TModel]`
|
|
61
|
+
- `BulkPatchableStoreProtocol[TModel]`
|
|
62
|
+
- `DeletableStoreProtocol[TModel]`
|
|
63
|
+
- `BulkDeletableStoreProtocol[TModel]`
|
|
64
|
+
- `StatementExecutorProtocol`
|
|
65
|
+
- `TransactionalStoreProtocol`
|
|
66
|
+
- `SyncModelBoundStoreProtocol[TModel]`
|
|
67
|
+
|
|
68
|
+
这些协议的意义是定义“这个对象能做什么”,而不是要求使用者必须直接实例化协议本身。
|
|
69
|
+
这些对象共同构成 `v1.0` 的稳定同步 public contract。
|
|
70
|
+
|
|
71
|
+
## 4. Async Capability Protocols
|
|
72
|
+
|
|
73
|
+
异步路径与同步路径保持明确分离,对应协议为:
|
|
74
|
+
|
|
75
|
+
- `AsyncReadableStoreProtocol[TModel]`
|
|
76
|
+
- `AsyncWritableStoreProtocol[TModel]`
|
|
77
|
+
- `AsyncPatchableStoreProtocol[TModel]`
|
|
78
|
+
- `AsyncBulkPatchableStoreProtocol[TModel]`
|
|
79
|
+
- `AsyncDeletableStoreProtocol[TModel]`
|
|
80
|
+
- `AsyncBulkDeletableStoreProtocol[TModel]`
|
|
81
|
+
- `AsyncStatementExecutorProtocol`
|
|
82
|
+
- `AsyncTransactionalStoreProtocol`
|
|
83
|
+
- `AsyncModelBoundStoreProtocol[TModel]`
|
|
84
|
+
|
|
85
|
+
这些对象共同构成 `v1.0` 的稳定异步 public contract。
|
|
86
|
+
|
|
87
|
+
## 5. `SyncTypedStore`
|
|
88
|
+
|
|
89
|
+
`SyncTypedStore` 是基于 SQLAlchemy `Session` 的同步实现。
|
|
90
|
+
|
|
91
|
+
### Factory And Lifecycle
|
|
92
|
+
|
|
93
|
+
- `SyncTypedStore.from_url(url, *, echo=False, **engine_options)`
|
|
94
|
+
- `.engine`
|
|
95
|
+
- `.close()`
|
|
96
|
+
- `.dispose()`
|
|
97
|
+
- `.unit_of_work(*, auto_commit=True)`
|
|
98
|
+
|
|
99
|
+
### CRUD And Query Methods
|
|
100
|
+
|
|
101
|
+
- `insert(entity, *, session=None, commit=True, refresh=False) -> TModel`
|
|
102
|
+
- `insert_many(entities, *, session=None, commit=True) -> list[TModel]`
|
|
103
|
+
- `get(model, ident, *, session=None) -> TModel | None`
|
|
104
|
+
- `find_one(model, *, query, session=None) -> TModel | None`
|
|
105
|
+
- `find_many(model, *, query, session=None) -> list[TModel]`
|
|
106
|
+
- `exists(model, *, query, session=None) -> bool`
|
|
107
|
+
- `count(model, *, query, session=None) -> int`
|
|
108
|
+
- `paginate(model, *, query, page, session=None) -> Page[TModel]`
|
|
109
|
+
- `update(model, *, query, patch, session=None, commit=True) -> int`
|
|
110
|
+
- `bulk_update(model, *, query, patch, session=None, commit=True) -> int`
|
|
111
|
+
- `delete(model, *, query, session=None, commit=True) -> int`
|
|
112
|
+
- `bulk_delete(model, *, query, session=None, commit=True) -> int`
|
|
113
|
+
- `select_rows(model, *, projection, session=None) -> list[TRow]`
|
|
114
|
+
- `select_scalars(statement, *, session=None) -> list[TScalar]`
|
|
115
|
+
|
|
116
|
+
### Example
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from typed_store import PageRequest, Query, SyncTypedStore
|
|
120
|
+
|
|
121
|
+
store = SyncTypedStore.from_url("sqlite:///app.db")
|
|
122
|
+
|
|
123
|
+
admins = store.find_many(
|
|
124
|
+
User,
|
|
125
|
+
query=Query[User]().where(User.role == "admin").order(User.id.asc()),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
page = store.paginate(
|
|
129
|
+
User,
|
|
130
|
+
query=Query[User]().where(User.role == "admin").order(User.id.asc()),
|
|
131
|
+
page=PageRequest(limit=20, offset=0),
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 6. `AsyncTypedStore`
|
|
136
|
+
|
|
137
|
+
`AsyncTypedStore` 是基于 SQLAlchemy `AsyncSession` 的异步实现。
|
|
138
|
+
|
|
139
|
+
### Factory And Lifecycle
|
|
140
|
+
|
|
141
|
+
- `AsyncTypedStore.from_url(url, *, echo=False, **engine_options)`
|
|
142
|
+
- `.engine`
|
|
143
|
+
- `.aclose()`
|
|
144
|
+
- `.dispose()`
|
|
145
|
+
- `.unit_of_work(*, auto_commit=True)`
|
|
146
|
+
|
|
147
|
+
### CRUD And Query Methods
|
|
148
|
+
|
|
149
|
+
签名与 `SyncTypedStore` 对称,但所有行为都是 `async def`。
|
|
150
|
+
|
|
151
|
+
### Example
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from typed_store import AsyncTypedStore, PageRequest, Query
|
|
155
|
+
|
|
156
|
+
store = AsyncTypedStore.from_url("sqlite+aiosqlite:///app.db")
|
|
157
|
+
|
|
158
|
+
items = await store.find_many(
|
|
159
|
+
User,
|
|
160
|
+
query=Query[User]().where(User.role == "admin"),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
page = await store.paginate(
|
|
164
|
+
User,
|
|
165
|
+
query=Query[User]().where(User.role == "admin").order(User.id.asc()),
|
|
166
|
+
page=PageRequest(limit=20, offset=0),
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 7. `TypedStore`
|
|
171
|
+
|
|
172
|
+
`TypedStore` 是 composition root,不是直接 CRUD facade。
|
|
173
|
+
|
|
174
|
+
### Surface
|
|
175
|
+
|
|
176
|
+
- `TypedStore.from_url(url=None, *, async_url=None, echo=False, **engine_options)`
|
|
177
|
+
- `.engine`
|
|
178
|
+
- `.async_engine`
|
|
179
|
+
- `.sync`
|
|
180
|
+
- `.async_`
|
|
181
|
+
- `.close()`
|
|
182
|
+
- `.dispose()`
|
|
183
|
+
- `.aclose()`
|
|
184
|
+
- `.unit_of_work(*, auto_commit=True)`
|
|
185
|
+
- `.async_unit_of_work(*, auto_commit=True)`
|
|
186
|
+
|
|
187
|
+
### Example
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from typed_store import Query, TypedStore
|
|
191
|
+
|
|
192
|
+
ts = TypedStore.from_url("sqlite:///app.db", async_url="sqlite+aiosqlite:///app.db")
|
|
193
|
+
|
|
194
|
+
ts.sync.find_many(User, query=Query[User]())
|
|
195
|
+
await ts.async_.find_many(User, query=Query[User]())
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 8. `TypedStoreModel.bind(store)`
|
|
199
|
+
|
|
200
|
+
模型能力是一等 public API,但只有绑定后才可操作。
|
|
201
|
+
|
|
202
|
+
### Rules
|
|
203
|
+
|
|
204
|
+
- 只有 `bind(store)`,没有隐式默认 store
|
|
205
|
+
- `bind()` 是纯函数式绑定,不会把 store 写回模型类
|
|
206
|
+
- sync model view 依赖 sync capability protocols
|
|
207
|
+
- async model view 依赖 async capability protocols
|
|
208
|
+
|
|
209
|
+
### Bound Model Methods
|
|
210
|
+
|
|
211
|
+
`Model.bind(store)` 返回的 bound model view 提供:
|
|
212
|
+
|
|
213
|
+
- `insert`
|
|
214
|
+
- `insert_many`
|
|
215
|
+
- `get`
|
|
216
|
+
- `find_one`
|
|
217
|
+
- `find_many`
|
|
218
|
+
- `exists`
|
|
219
|
+
- `count`
|
|
220
|
+
- `paginate`
|
|
221
|
+
- `update`
|
|
222
|
+
- `bulk_update`
|
|
223
|
+
- `delete`
|
|
224
|
+
- `bulk_delete`
|
|
225
|
+
|
|
226
|
+
### Example
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from typed_store import PageRequest, Patch, Query
|
|
230
|
+
|
|
231
|
+
users = User.bind(store)
|
|
232
|
+
|
|
233
|
+
items = users.find_many(
|
|
234
|
+
query=Query[User]().where(User.role == "admin").order(User.id.asc()),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
updated = users.update(
|
|
238
|
+
query=Query[User]().where(User.email == "alice@example.com"),
|
|
239
|
+
patch=Patch[User]({"role": "staff"}),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
page = users.paginate(
|
|
243
|
+
query=Query[User]().where(User.role == "staff"),
|
|
244
|
+
page=PageRequest(limit=10, offset=0),
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## 9. Error Boundary
|
|
249
|
+
|
|
250
|
+
当前 public API 的运行时保护只保留 contract/configuration 级错误:
|
|
251
|
+
|
|
252
|
+
- 缺失 sync / async session factory
|
|
253
|
+
- `bind()` 传入不满足协议的 store
|
|
254
|
+
- bulk mutation 使用了不支持的 `Query` 形态
|
|
255
|
+
- 调用签名明显错误,例如遗漏 `projection=` 这样的关键字参数
|
|
256
|
+
|
|
257
|
+
数据本身的业务校验不由 `TypedStore` 重复承担,默认依赖边界层与静态类型系统。
|
|
258
|
+
|
|
259
|
+
## 10. Stable v1.0 Surface
|
|
260
|
+
|
|
261
|
+
以下对象属于 `v1.0` 稳定 public API:
|
|
262
|
+
|
|
263
|
+
- stores: `SyncTypedStore`, `AsyncTypedStore`, `TypedStore`
|
|
264
|
+
- models: `TypedStoreModel`, `SyncBoundModelView`, `AsyncBoundModelView`
|
|
265
|
+
- request objects: `Query`, `PageRequest`, `Patch`, `ProjectionQuery`
|
|
266
|
+
- results: `Page`
|
|
267
|
+
- protocols: readable / writable / patchable / bulk / deletable / statement / transactional variants
|
|
268
|
+
- support objects: `UnitOfWork`, `AsyncUnitOfWork`, `SessionProvider`
|