sqla-fancy-core 1.0.2__tar.gz → 1.1.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.
Potentially problematic release.
This version of sqla-fancy-core might be problematic. Click here for more details.
- {sqla_fancy_core-1.0.2 → sqla_fancy_core-1.1.0}/.github/workflows/ci.yaml +1 -1
- sqla_fancy_core-1.1.0/PKG-INFO +422 -0
- sqla_fancy_core-1.1.0/README.md +370 -0
- {sqla_fancy_core-1.0.2 → sqla_fancy_core-1.1.0}/pyproject.toml +13 -2
- sqla_fancy_core-1.1.0/sqla_fancy_core/__init__.py +5 -0
- sqla_fancy_core-1.1.0/sqla_fancy_core/decorators.py +160 -0
- sqla_fancy_core-1.0.2/sqla_fancy_core/__init__.py → sqla_fancy_core-1.1.0/sqla_fancy_core/factories.py +5 -8
- sqla_fancy_core-1.1.0/sqla_fancy_core/wrappers.py +239 -0
- sqla_fancy_core-1.1.0/tests/test_async_fancy_engine.py +97 -0
- sqla_fancy_core-1.1.0/tests/test_connect.py +166 -0
- sqla_fancy_core-1.1.0/tests/test_fancy_engine.py +87 -0
- {sqla_fancy_core-1.0.2 → sqla_fancy_core-1.1.0}/tests/test_table_factory.py +7 -5
- sqla_fancy_core-1.1.0/tests/test_table_factory_async.py +80 -0
- sqla_fancy_core-1.1.0/tests/test_transact.py +261 -0
- sqla_fancy_core-1.1.0/uv.lock +3503 -0
- sqla_fancy_core-1.0.2/PKG-INFO +0 -160
- sqla_fancy_core-1.0.2/README.md +0 -119
- sqla_fancy_core-1.0.2/uv.lock +0 -467
- {sqla_fancy_core-1.0.2 → sqla_fancy_core-1.1.0}/.gitignore +0 -0
- {sqla_fancy_core-1.0.2 → sqla_fancy_core-1.1.0}/LICENSE +0 -0
- {sqla_fancy_core-1.0.2 → sqla_fancy_core-1.1.0}/tests/__init__.py +0 -0
- {sqla_fancy_core-1.0.2 → sqla_fancy_core-1.1.0}/tests/test_field.py +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqla-fancy-core
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: SQLAlchemy core, but fancier
|
|
5
|
+
Project-URL: Homepage, https://github.com/sayanarijit/sqla-fancy-core
|
|
6
|
+
Author-email: Arijit Basu <sayanarijit@gmail.com>
|
|
7
|
+
Maintainer-email: Arijit Basu <sayanarijit@gmail.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2023 Arijit Basu
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: sql,sqlalchemy,sqlalchemy-core
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Requires-Python: >=3.7
|
|
35
|
+
Requires-Dist: sqlalchemy
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: build; extra == 'dev'
|
|
38
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
39
|
+
Requires-Dist: ipython; extra == 'dev'
|
|
40
|
+
Requires-Dist: twine; extra == 'dev'
|
|
41
|
+
Provides-Extra: test
|
|
42
|
+
Requires-Dist: aiosqlite; extra == 'test'
|
|
43
|
+
Requires-Dist: fastapi; extra == 'test'
|
|
44
|
+
Requires-Dist: flake8; extra == 'test'
|
|
45
|
+
Requires-Dist: httpx; extra == 'test'
|
|
46
|
+
Requires-Dist: pydantic; extra == 'test'
|
|
47
|
+
Requires-Dist: pytest; extra == 'test'
|
|
48
|
+
Requires-Dist: pytest-asyncio; extra == 'test'
|
|
49
|
+
Requires-Dist: python-multipart; extra == 'test'
|
|
50
|
+
Requires-Dist: sqlalchemy[asyncio]; extra == 'test'
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
|
|
53
|
+
# sqla-fancy-core
|
|
54
|
+
|
|
55
|
+
There are plenty of ORMs to choose from in Python world, but not many sql query makers for folks who prefer to stay close to the original SQL syntax, without sacrificing security and code readability. The closest, most mature and most flexible query maker you can find is SQLAlchemy core.
|
|
56
|
+
|
|
57
|
+
But the syntax of defining tables and making queries has a lot of scope for improvement. For example, the `table.c.column` syntax is too dynamic, unreadable, and probably has performance impact too. It also doesn’t play along with static type checkers and linting tools.
|
|
58
|
+
|
|
59
|
+
So here I present one attempt at getting the best out of SQLAlchemy core by changing the way we define tables.
|
|
60
|
+
|
|
61
|
+
The table factory class it exposes, helps define tables in a way that eliminates the above drawbacks. Moreover, you can subclass it to add your preferred global defaults for columns (e.g. not null as default). Or specify custom column types with consistent naming (e.g. created_at).
|
|
62
|
+
|
|
63
|
+
## Basic Usage
|
|
64
|
+
|
|
65
|
+
First, let's define a table using the `TableFactory`.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import sqlalchemy as sa
|
|
69
|
+
from sqla_fancy_core import TableFactory
|
|
70
|
+
|
|
71
|
+
tf = TableFactory()
|
|
72
|
+
|
|
73
|
+
class Author:
|
|
74
|
+
id = tf.auto_id()
|
|
75
|
+
name = tf.string("name")
|
|
76
|
+
created_at = tf.created_at()
|
|
77
|
+
updated_at = tf.updated_at()
|
|
78
|
+
|
|
79
|
+
Table = tf("author")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The `TableFactory` provides a convenient way to define columns with common attributes. For more complex scenarios, you can define tables without losing type hints:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
class Book:
|
|
86
|
+
id = tf(sa.Column("id", sa.Integer, primary_key=True, autoincrement=True))
|
|
87
|
+
title = tf(sa.Column("title", sa.String(255), nullable=False))
|
|
88
|
+
author_id = tf(sa.Column("author_id", sa.Integer, sa.ForeignKey(Author.id)))
|
|
89
|
+
created_at = tf(
|
|
90
|
+
sa.Column(
|
|
91
|
+
"created_at",
|
|
92
|
+
sa.DateTime,
|
|
93
|
+
nullable=False,
|
|
94
|
+
server_default=sa.func.now(),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
updated_at = tf(
|
|
98
|
+
sa.Column(
|
|
99
|
+
"updated_at",
|
|
100
|
+
sa.DateTime,
|
|
101
|
+
nullable=False,
|
|
102
|
+
server_default=sa.func.now(),
|
|
103
|
+
onupdate=sa.func.now(),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
Table = tf(sa.Table("book", sa.MetaData()))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Now, let's create an engine and the tables.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
114
|
+
|
|
115
|
+
# Create the engine
|
|
116
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
117
|
+
|
|
118
|
+
# Create the tables
|
|
119
|
+
async with engine.begin() as conn:
|
|
120
|
+
await conn.run_sync(tf.metadata.create_all)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
With the tables created, you can perform CRUD operations.
|
|
124
|
+
|
|
125
|
+
### CRUD Operations
|
|
126
|
+
|
|
127
|
+
Here's how you can interact with the database using the defined tables.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
async with engine.begin() as txn:
|
|
131
|
+
# Insert author
|
|
132
|
+
qry = (
|
|
133
|
+
sa.insert(Author.Table)
|
|
134
|
+
.values({Author.name: "John Doe"})
|
|
135
|
+
.returning(Author.id)
|
|
136
|
+
)
|
|
137
|
+
author = (await txn.execute(qry)).mappings().one()
|
|
138
|
+
author_id = author[Author.id]
|
|
139
|
+
assert author_id == 1
|
|
140
|
+
|
|
141
|
+
# Insert book
|
|
142
|
+
qry = (
|
|
143
|
+
sa.insert(Book.Table)
|
|
144
|
+
.values({Book.title: "My Book", Book.author_id: author_id})
|
|
145
|
+
.returning(Book.id)
|
|
146
|
+
)
|
|
147
|
+
book = (await txn.execute(qry)).mappings().one()
|
|
148
|
+
assert book[Book.id] == 1
|
|
149
|
+
|
|
150
|
+
# Query the data
|
|
151
|
+
qry = sa.select(Author.name, Book.title).join(
|
|
152
|
+
Book.Table,
|
|
153
|
+
Book.author_id == Author.id,
|
|
154
|
+
)
|
|
155
|
+
result = (await txn.execute(qry)).all()
|
|
156
|
+
assert result == [("John Doe", "My Book")], result
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Fancy Engine Wrappers
|
|
160
|
+
|
|
161
|
+
`sqla-fancy-core` provides `fancy` engine wrappers that simplify database interactions by automatically managing connections and transactions. The `fancy` function wraps a SQLAlchemy `Engine` or `AsyncEngine` and returns a wrapper object with two primary methods:
|
|
162
|
+
|
|
163
|
+
- `x(conn, query)`: Executes a query. It uses the provided `conn` if available, otherwise it creates a new connection.
|
|
164
|
+
- `tx(conn, query)`: Executes a query within a transaction. It uses the provided `conn` if available, otherwise it creates a new connection and begins a transaction.
|
|
165
|
+
|
|
166
|
+
This is particularly useful for writing connection-agnostic query functions.
|
|
167
|
+
|
|
168
|
+
**Sync Example:**
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
import sqlalchemy as sa
|
|
172
|
+
from sqla_fancy_core import fancy
|
|
173
|
+
|
|
174
|
+
engine = sa.create_engine("sqlite:///:memory:")
|
|
175
|
+
fancy_engine = fancy(engine)
|
|
176
|
+
|
|
177
|
+
def get_data(conn: sa.Connection | None = None):
|
|
178
|
+
return fancy_engine.tx(conn, sa.select(sa.literal(1))).scalar_one()
|
|
179
|
+
|
|
180
|
+
# Without an explicit transaction
|
|
181
|
+
assert get_data() == 1
|
|
182
|
+
|
|
183
|
+
# With an explicit transaction
|
|
184
|
+
with engine.begin() as conn:
|
|
185
|
+
assert get_data(conn) == 1
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Async Example:**
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import sqlalchemy as sa
|
|
192
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
193
|
+
from sqla_fancy_core import fancy
|
|
194
|
+
|
|
195
|
+
async def main():
|
|
196
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
197
|
+
fancy_engine = fancy(engine)
|
|
198
|
+
|
|
199
|
+
async def get_data(conn: sa.AsyncConnection | None = None):
|
|
200
|
+
result = await fancy_engine.x(conn, sa.select(sa.literal(1)))
|
|
201
|
+
return result.scalar_one()
|
|
202
|
+
|
|
203
|
+
# Without an explicit transaction
|
|
204
|
+
assert await get_data() == 1
|
|
205
|
+
|
|
206
|
+
# With an explicit transaction
|
|
207
|
+
async with engine.connect() as conn:
|
|
208
|
+
assert await get_data(conn) == 1
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Decorators: Inject, connect, transact
|
|
212
|
+
|
|
213
|
+
When writing plain SQLAlchemy Core code, you often pass connections around and manage transactions manually. The decorators in `sqla-fancy-core` help you keep functions connection-agnostic and composable, while remaining explicit and safe.
|
|
214
|
+
|
|
215
|
+
At the heart of it is `Inject(engine)`, a tiny marker used as a default parameter value to tell decorators where to inject a connection.
|
|
216
|
+
|
|
217
|
+
- `Inject(engine)`: marks which parameter should receive a connection derived from the given engine.
|
|
218
|
+
- `@connect`: ensures the injected parameter is a live connection. If you passed a connection explicitly, it will use that one as-is. Otherwise, it will open a new connection for the call and close it afterwards. No transaction is created by default.
|
|
219
|
+
- `@transact`: ensures the injected parameter is inside a transaction. If you pass a connection already in a transaction, it reuses it; if you pass a connection outside a transaction, it starts one; if you pass nothing, it opens a new connection and begins a transaction for the duration of the call.
|
|
220
|
+
|
|
221
|
+
All three work both for sync and async engines. The signatures remain the same — you only change the default value to `Inject(engine)`.
|
|
222
|
+
|
|
223
|
+
### Quick reference
|
|
224
|
+
|
|
225
|
+
- Prefer `@connect` for read-only operations or when you want to control commit/rollback yourself.
|
|
226
|
+
- Prefer `@transact` to wrap a function in a transaction automatically and consistently.
|
|
227
|
+
- You can still pass `conn=...` explicitly to either decorator to reuse an existing connection/transaction.
|
|
228
|
+
|
|
229
|
+
### Sync examples
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
import sqlalchemy as sa
|
|
233
|
+
from sqla_fancy_core.decorators import Inject, connect, transact
|
|
234
|
+
|
|
235
|
+
engine = sa.create_engine("sqlite:///:memory:")
|
|
236
|
+
metadata = sa.MetaData()
|
|
237
|
+
users = sa.Table(
|
|
238
|
+
"users",
|
|
239
|
+
metadata,
|
|
240
|
+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
|
241
|
+
sa.Column("name", sa.String),
|
|
242
|
+
)
|
|
243
|
+
metadata.create_all(engine)
|
|
244
|
+
|
|
245
|
+
# 1) Ensure a connection is available (no implicit transaction)
|
|
246
|
+
@connect
|
|
247
|
+
def get_user_count(conn=Inject(engine)):
|
|
248
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
249
|
+
|
|
250
|
+
assert get_user_count() == 0
|
|
251
|
+
|
|
252
|
+
# 2) Wrap in a transaction automatically
|
|
253
|
+
@transact
|
|
254
|
+
def create_user(name: str, conn=Inject(engine)):
|
|
255
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
256
|
+
|
|
257
|
+
create_user("alice")
|
|
258
|
+
assert get_user_count() == 1
|
|
259
|
+
|
|
260
|
+
# 3) Reuse an explicit connection or transaction
|
|
261
|
+
with engine.begin() as txn:
|
|
262
|
+
create_user("bob", conn=txn)
|
|
263
|
+
assert get_user_count(conn=txn) == 2
|
|
264
|
+
|
|
265
|
+
assert get_user_count() == 2
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Async examples
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
import sqlalchemy as sa
|
|
272
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncConnection
|
|
273
|
+
from sqla_fancy_core.decorators import Inject, connect, transact
|
|
274
|
+
|
|
275
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
276
|
+
metadata = sa.MetaData()
|
|
277
|
+
users = sa.Table(
|
|
278
|
+
"users",
|
|
279
|
+
metadata,
|
|
280
|
+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
|
281
|
+
sa.Column("name", sa.String),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
async with engine.begin() as conn:
|
|
285
|
+
await conn.run_sync(metadata.create_all)
|
|
286
|
+
|
|
287
|
+
@connect
|
|
288
|
+
async def get_user_count(conn=Inject(engine)):
|
|
289
|
+
result = await conn.execute(sa.select(sa.func.count()).select_from(users))
|
|
290
|
+
return result.scalar_one()
|
|
291
|
+
|
|
292
|
+
@transact
|
|
293
|
+
async def create_user(name: str, conn=Inject(engine)):
|
|
294
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
295
|
+
|
|
296
|
+
assert await get_user_count() == 0
|
|
297
|
+
await create_user("carol")
|
|
298
|
+
assert await get_user_count() == 1
|
|
299
|
+
|
|
300
|
+
async with engine.connect() as conn:
|
|
301
|
+
await create_user("dave", conn=conn)
|
|
302
|
+
assert await get_user_count(conn=conn) == 2
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Works with dependency injection frameworks
|
|
306
|
+
|
|
307
|
+
These decorators pair nicely with frameworks like FastAPI. You can keep a single function that works both inside DI (with an injected connection) and outside it (self-managed).
|
|
308
|
+
|
|
309
|
+
Sync example with FastAPI:
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from typing import Annotated
|
|
313
|
+
from fastapi import Depends, FastAPI, Form
|
|
314
|
+
import sqlalchemy as sa
|
|
315
|
+
from sqla_fancy_core.decorators import Inject, transact
|
|
316
|
+
|
|
317
|
+
app = FastAPI()
|
|
318
|
+
|
|
319
|
+
def get_transaction():
|
|
320
|
+
with engine.begin() as conn:
|
|
321
|
+
yield conn
|
|
322
|
+
|
|
323
|
+
@transact
|
|
324
|
+
@app.post("/create-user")
|
|
325
|
+
def create_user(
|
|
326
|
+
name: Annotated[str, Form(...)],
|
|
327
|
+
conn: Annotated[sa.Connection, Depends(get_transaction)] = Inject(engine),
|
|
328
|
+
):
|
|
329
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
330
|
+
|
|
331
|
+
# Works outside FastAPI too — starts its own transaction
|
|
332
|
+
create_user(name="outside fastapi")
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Async example with FastAPI:
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
from typing import Annotated
|
|
339
|
+
from fastapi import Depends, FastAPI, Form
|
|
340
|
+
from sqlalchemy.ext.asyncio import AsyncConnection
|
|
341
|
+
import sqlalchemy as sa
|
|
342
|
+
from sqla_fancy_core.decorators import Inject, transact
|
|
343
|
+
|
|
344
|
+
app = FastAPI()
|
|
345
|
+
|
|
346
|
+
async def get_transaction():
|
|
347
|
+
async with engine.begin() as conn:
|
|
348
|
+
yield conn
|
|
349
|
+
|
|
350
|
+
@transact
|
|
351
|
+
@app.post("/create-user")
|
|
352
|
+
async def create_user(
|
|
353
|
+
name: Annotated[str, Form(...)],
|
|
354
|
+
conn: Annotated[AsyncConnection, Depends(get_transaction)] = Inject(engine),
|
|
355
|
+
):
|
|
356
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Notes:
|
|
360
|
+
|
|
361
|
+
- `@connect` never starts a transaction by itself; `@transact` ensures one.
|
|
362
|
+
- Passing an explicit `conn` always wins — the decorators simply adapt to what you give them.
|
|
363
|
+
- The injection marker keeps your function signatures clean and type-checker friendly.
|
|
364
|
+
|
|
365
|
+
## With Pydantic Validation
|
|
366
|
+
|
|
367
|
+
You can integrate `sqla-fancy-core` with Pydantic for data validation.
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
from typing import Any
|
|
371
|
+
import sqlalchemy as sa
|
|
372
|
+
from pydantic import BaseModel, Field
|
|
373
|
+
import pytest
|
|
374
|
+
|
|
375
|
+
from sqla_fancy_core import TableFactory
|
|
376
|
+
|
|
377
|
+
tf = TableFactory()
|
|
378
|
+
|
|
379
|
+
def field(col, default: Any = ...) -> Field:
|
|
380
|
+
return col.info["kwargs"]["field"](default)
|
|
381
|
+
|
|
382
|
+
# Define a table
|
|
383
|
+
class User:
|
|
384
|
+
name = tf(
|
|
385
|
+
sa.Column("name", sa.String),
|
|
386
|
+
field=lambda default: Field(default, max_length=5),
|
|
387
|
+
)
|
|
388
|
+
Table = tf("author")
|
|
389
|
+
|
|
390
|
+
# Define a pydantic schema
|
|
391
|
+
class CreateUser(BaseModel):
|
|
392
|
+
name: str = field(User.name)
|
|
393
|
+
|
|
394
|
+
# Define a pydantic schema
|
|
395
|
+
class UpdateUser(BaseModel):
|
|
396
|
+
name: str | None = field(User.name, None)
|
|
397
|
+
|
|
398
|
+
assert CreateUser(name="John").model_dump() == {"name": "John"}
|
|
399
|
+
assert UpdateUser(name="John").model_dump() == {"name": "John"}
|
|
400
|
+
assert UpdateUser().model_dump(exclude_unset=True) == {}
|
|
401
|
+
|
|
402
|
+
with pytest.raises(ValueError):
|
|
403
|
+
CreateUser()
|
|
404
|
+
with pytest.raises(ValueError):
|
|
405
|
+
UpdateUser(name="John Doe")
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Target audience
|
|
409
|
+
|
|
410
|
+
Production. For folks who prefer query maker over ORM, looking for a robust sync/async driver integration, wanting to keep code readable and secure.
|
|
411
|
+
|
|
412
|
+
## Comparison with other projects:
|
|
413
|
+
|
|
414
|
+
**Peewee**: No type hints. Also, no official async support.
|
|
415
|
+
|
|
416
|
+
**Piccolo**: Tight integration with drivers. Very opinionated. Not as flexible or mature as sqlalchemy core.
|
|
417
|
+
|
|
418
|
+
**Pypika**: Doesn’t prevent sql injection by default. Hence can be considered insecure.
|
|
419
|
+
|
|
420
|
+
**Raw string queries with placeholders**: sacrifices code readability, and prone to sql injection if one forgets to use placeholders.
|
|
421
|
+
|
|
422
|
+
**Other ORMs**: They are full blown ORMs, not query makers.
|