sqla-fancy-core 1.0.0__py3-none-any.whl → 1.2.2__py3-none-any.whl
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/__init__.py +9 -157
- sqla_fancy_core/decorators.py +196 -0
- sqla_fancy_core/factories.py +156 -0
- sqla_fancy_core/wrappers.py +439 -0
- sqla_fancy_core-1.2.2.dist-info/METADATA +486 -0
- sqla_fancy_core-1.2.2.dist-info/RECORD +8 -0
- sqla_fancy_core-1.0.0.dist-info/METADATA +0 -192
- sqla_fancy_core-1.0.0.dist-info/RECORD +0 -5
- {sqla_fancy_core-1.0.0.dist-info → sqla_fancy_core-1.2.2.dist-info}/WHEEL +0 -0
- {sqla_fancy_core-1.0.0.dist-info → sqla_fancy_core-1.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqla-fancy-core
|
|
3
|
+
Version: 1.2.2
|
|
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
|
+
A collection of type-safe, async friendly, and un-opinionated enhancements to SQLAlchemy Core that works well with mordern web servers.
|
|
56
|
+
|
|
57
|
+
**Why?**
|
|
58
|
+
|
|
59
|
+
- ORMs are magical, but it's not always a feature. Sometimes, we crave for familiar.
|
|
60
|
+
- SQLAlchemy Core is powerful but `table.c.column` breaks static type checking and has runtime overhead. This library provides a better way to define tables while keeping all of SQLAlchemy's flexibility. See [Table Factory](#table-factory).
|
|
61
|
+
- The idea of sessions can get feel too magical and opinionated. This library removes the magic and opinions and takes you to back to familiar transactions's territory, providing multiple un-opinionated APIs to deal with it. See [Wrappers](#fancy-engine-wrappers) and [Decorators](#decorators-inject-connect-transact).
|
|
62
|
+
|
|
63
|
+
**Demos:**
|
|
64
|
+
|
|
65
|
+
- [FastAPI - sqla-fancy-core example app](https://github.com/sayanarijit/fastapi-sqla-fancy-core-example-app).
|
|
66
|
+
|
|
67
|
+
## Table factory
|
|
68
|
+
|
|
69
|
+
Define tables with static column references## Target audience
|
|
70
|
+
|
|
71
|
+
For production use by developers who prefer query builders over ORMs, need robust sync/async support, and want type-safe, readable code.
|
|
72
|
+
|
|
73
|
+
**Example:**
|
|
74
|
+
|
|
75
|
+
Define tables:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import sqlalchemy as sa
|
|
79
|
+
from sqla_fancy_core import TableFactory
|
|
80
|
+
|
|
81
|
+
tf = TableFactory()
|
|
82
|
+
|
|
83
|
+
class Author:
|
|
84
|
+
id = tf.auto_id()
|
|
85
|
+
name = tf.string("name")
|
|
86
|
+
created_at = tf.created_at()
|
|
87
|
+
updated_at = tf.updated_at()
|
|
88
|
+
|
|
89
|
+
Table = tf("author")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For complex scenarios, define columns explicitly:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
class Book:
|
|
96
|
+
id = tf(sa.Column("id", sa.Integer, primary_key=True, autoincrement=True))
|
|
97
|
+
title = tf(sa.Column("title", sa.String(255), nullable=False))
|
|
98
|
+
author_id = tf(sa.Column("author_id", sa.Integer, sa.ForeignKey(Author.id)))
|
|
99
|
+
created_at = tf(
|
|
100
|
+
sa.Column(
|
|
101
|
+
"created_at",
|
|
102
|
+
sa.DateTime,
|
|
103
|
+
nullable=False,
|
|
104
|
+
server_default=sa.func.now(),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
updated_at = tf(
|
|
108
|
+
sa.Column(
|
|
109
|
+
"updated_at",
|
|
110
|
+
sa.DateTime,
|
|
111
|
+
nullable=False,
|
|
112
|
+
server_default=sa.func.now(),
|
|
113
|
+
onupdate=sa.func.now(),
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
Table = tf(sa.Table("book", sa.MetaData()))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Create tables:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
124
|
+
|
|
125
|
+
# Create the engine
|
|
126
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
127
|
+
|
|
128
|
+
# Create the tables
|
|
129
|
+
async with engine.begin() as conn:
|
|
130
|
+
await conn.run_sync(tf.metadata.create_all)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Perform CRUD operations:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
async with engine.begin() as txn:
|
|
137
|
+
# Insert author
|
|
138
|
+
qry = (
|
|
139
|
+
sa.insert(Author.Table)
|
|
140
|
+
.values({Author.name: "John Doe"})
|
|
141
|
+
.returning(Author.id)
|
|
142
|
+
)
|
|
143
|
+
author = (await txn.execute(qry)).mappings().one()
|
|
144
|
+
author_id = author[Author.id]
|
|
145
|
+
assert author_id == 1
|
|
146
|
+
|
|
147
|
+
# Insert book
|
|
148
|
+
qry = (
|
|
149
|
+
sa.insert(Book.Table)
|
|
150
|
+
.values({Book.title: "My Book", Book.author_id: author_id})
|
|
151
|
+
.returning(Book.id)
|
|
152
|
+
)
|
|
153
|
+
book = (await txn.execute(qry)).mappings().one()
|
|
154
|
+
assert book[Book.id] == 1
|
|
155
|
+
|
|
156
|
+
# Query the data
|
|
157
|
+
qry = sa.select(Author.name, Book.title).join(
|
|
158
|
+
Book.Table,
|
|
159
|
+
Book.author_id == Author.id,
|
|
160
|
+
)
|
|
161
|
+
result = (await txn.execute(qry)).all()
|
|
162
|
+
assert result == [("John Doe", "My Book")], result
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Fancy Engine Wrappers
|
|
166
|
+
|
|
167
|
+
Simplify connection and transaction management. The `fancy()` function wraps a SQLAlchemy engine and provides:
|
|
168
|
+
|
|
169
|
+
- `x(conn, query)`: Execute query with optional connection
|
|
170
|
+
- `tx(conn, query)`: Execute query in transaction
|
|
171
|
+
- `atomic()`: Context manager for transaction scope
|
|
172
|
+
- `ax(query)`: Execute inside `atomic()` context (raises `AtomicContextError` outside)
|
|
173
|
+
- `atx(query)`: Auto-transactional (reuses `atomic()` if present, or creates new transaction)
|
|
174
|
+
|
|
175
|
+
### Basic Examples
|
|
176
|
+
|
|
177
|
+
**Sync Example:**
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import sqlalchemy as sa
|
|
181
|
+
from sqla_fancy_core import fancy
|
|
182
|
+
|
|
183
|
+
engine = sa.create_engine("sqlite:///:memory:")
|
|
184
|
+
fancy_engine = fancy(engine)
|
|
185
|
+
|
|
186
|
+
def get_data(conn: sa.Connection | None = None):
|
|
187
|
+
return fancy_engine.tx(conn, sa.select(sa.literal(1))).scalar_one()
|
|
188
|
+
|
|
189
|
+
# Without an explicit transaction
|
|
190
|
+
assert get_data() == 1
|
|
191
|
+
|
|
192
|
+
# With an explicit transaction
|
|
193
|
+
with engine.begin() as conn:
|
|
194
|
+
assert get_data(conn) == 1
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Async Example:**
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
import sqlalchemy as sa
|
|
201
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
202
|
+
from sqla_fancy_core import fancy
|
|
203
|
+
|
|
204
|
+
async def main():
|
|
205
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
206
|
+
fancy_engine = fancy(engine)
|
|
207
|
+
|
|
208
|
+
async def get_data(conn: sa.AsyncConnection | None = None):
|
|
209
|
+
result = await fancy_engine.x(conn, sa.select(sa.literal(1)))
|
|
210
|
+
return result.scalar_one()
|
|
211
|
+
|
|
212
|
+
# Without an explicit transaction
|
|
213
|
+
assert await get_data() == 1
|
|
214
|
+
|
|
215
|
+
# With an explicit transaction
|
|
216
|
+
async with engine.connect() as conn:
|
|
217
|
+
assert await get_data(conn) == 1
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Using the atomic() Context Manager
|
|
221
|
+
|
|
222
|
+
Group operations in a single transaction. Nested `atomic()` contexts share the outer connection.
|
|
223
|
+
|
|
224
|
+
**Sync Example:**
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
import sqlalchemy as sa
|
|
228
|
+
from sqla_fancy_core import fancy, TableFactory
|
|
229
|
+
|
|
230
|
+
tf = TableFactory()
|
|
231
|
+
|
|
232
|
+
class User:
|
|
233
|
+
id = tf.auto_id()
|
|
234
|
+
name = tf.string("name")
|
|
235
|
+
Table = tf("users")
|
|
236
|
+
|
|
237
|
+
engine = sa.create_engine("sqlite:///:memory:")
|
|
238
|
+
tf.metadata.create_all(engine)
|
|
239
|
+
fancy_engine = fancy(engine)
|
|
240
|
+
|
|
241
|
+
# Group operations in one transaction
|
|
242
|
+
with fancy_engine.atomic():
|
|
243
|
+
fancy_engine.ax(sa.insert(User.Table).values(name="Alice"))
|
|
244
|
+
fancy_engine.ax(sa.insert(User.Table).values(name="Bob"))
|
|
245
|
+
result = fancy_engine.ax(sa.select(sa.func.count()).select_from(User.Table))
|
|
246
|
+
count = result.scalar_one()
|
|
247
|
+
assert count == 2
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Async Example:**
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
import sqlalchemy as sa
|
|
254
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
255
|
+
from sqla_fancy_core import fancy, TableFactory
|
|
256
|
+
|
|
257
|
+
tf = TableFactory()
|
|
258
|
+
|
|
259
|
+
class User:
|
|
260
|
+
id = tf.auto_id()
|
|
261
|
+
name = tf.string("name")
|
|
262
|
+
Table = tf("users")
|
|
263
|
+
|
|
264
|
+
async def run_example():
|
|
265
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
266
|
+
async with engine.begin() as conn:
|
|
267
|
+
await conn.run_sync(tf.metadata.create_all)
|
|
268
|
+
|
|
269
|
+
fancy_engine = fancy(engine)
|
|
270
|
+
|
|
271
|
+
async with fancy_engine.atomic():
|
|
272
|
+
await fancy_engine.ax(sa.insert(User.Table).values(name="Alice"))
|
|
273
|
+
await fancy_engine.ax(sa.insert(User.Table).values(name="Bob"))
|
|
274
|
+
result = await fancy_engine.ax(sa.select(sa.func.count()).select_from(User.Table))
|
|
275
|
+
count = result.scalar_one()
|
|
276
|
+
assert count == 2
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Key Points:**
|
|
280
|
+
|
|
281
|
+
- `ax()` requires `atomic()` context
|
|
282
|
+
- `atx()` auto-manages transactions (safe inside or outside `atomic()`)
|
|
283
|
+
- Nested `atomic()` contexts share connections
|
|
284
|
+
- Auto-commit on success, auto-rollback on exception
|
|
285
|
+
|
|
286
|
+
### ax vs atx vs tx
|
|
287
|
+
|
|
288
|
+
- `ax(q)`: Requires `atomic()` context. For batch operations.
|
|
289
|
+
- `atx(q)`: Fire-and-forget with transaction. Reuses outer `atomic()` context if any, or creates new transaction.
|
|
290
|
+
- `tx(conn, q)`: Low-level. Uses provided `conn`, or outer `atomic()` context if any, or creates new transactional connection.
|
|
291
|
+
|
|
292
|
+
## Decorators: Inject, connect, transact
|
|
293
|
+
|
|
294
|
+
Keep functions connection-agnostic with decorator-based injection.
|
|
295
|
+
|
|
296
|
+
**Components:**
|
|
297
|
+
|
|
298
|
+
- `Inject(engine)`: Marks parameter for connection injection
|
|
299
|
+
- `@connect`: Ensures live connection (no transaction by default)
|
|
300
|
+
- `@transact`: Ensures transactional connection
|
|
301
|
+
|
|
302
|
+
Use `@connect` for read-only operations. Use `@transact` for writes.
|
|
303
|
+
|
|
304
|
+
### Sync examples
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
import sqlalchemy as sa
|
|
308
|
+
from sqla_fancy_core.decorators import Inject, connect, transact
|
|
309
|
+
|
|
310
|
+
engine = sa.create_engine("sqlite:///:memory:")
|
|
311
|
+
metadata = sa.MetaData()
|
|
312
|
+
users = sa.Table(
|
|
313
|
+
"users",
|
|
314
|
+
metadata,
|
|
315
|
+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
|
316
|
+
sa.Column("name", sa.String),
|
|
317
|
+
)
|
|
318
|
+
metadata.create_all(engine)
|
|
319
|
+
|
|
320
|
+
@connect
|
|
321
|
+
def get_user_count(conn=Inject(engine)):
|
|
322
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
323
|
+
|
|
324
|
+
assert get_user_count() == 0
|
|
325
|
+
|
|
326
|
+
@transact
|
|
327
|
+
def create_user(name: str, conn=Inject(engine)):
|
|
328
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
329
|
+
|
|
330
|
+
# Without an explicit transaction
|
|
331
|
+
create_user("alice")
|
|
332
|
+
assert get_user_count() == 1
|
|
333
|
+
|
|
334
|
+
# With an explicit transaction
|
|
335
|
+
with engine.begin() as txn:
|
|
336
|
+
create_user("bob", conn=txn)
|
|
337
|
+
assert get_user_count(conn=txn) == 2
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Async examples
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
import sqlalchemy as sa
|
|
344
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncConnection
|
|
345
|
+
from sqla_fancy_core.decorators import Inject, connect, transact
|
|
346
|
+
|
|
347
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
348
|
+
metadata = sa.MetaData()
|
|
349
|
+
users = sa.Table(
|
|
350
|
+
"users",
|
|
351
|
+
metadata,
|
|
352
|
+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
|
353
|
+
sa.Column("name", sa.String),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
async with engine.begin() as conn:
|
|
357
|
+
await conn.run_sync(metadata.create_all)
|
|
358
|
+
|
|
359
|
+
@connect
|
|
360
|
+
async def get_user_count(conn=Inject(engine)):
|
|
361
|
+
result = await conn.execute(sa.select(sa.func.count()).select_from(users))
|
|
362
|
+
return result.scalar_one()
|
|
363
|
+
|
|
364
|
+
@transact
|
|
365
|
+
async def create_user(name: str, conn=Inject(engine)):
|
|
366
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
367
|
+
|
|
368
|
+
# Without an explicit transaction
|
|
369
|
+
assert await get_user_count() == 0
|
|
370
|
+
await create_user("carol")
|
|
371
|
+
assert await get_user_count() == 1
|
|
372
|
+
|
|
373
|
+
# With an explicit transaction
|
|
374
|
+
async with engine.connect() as conn:
|
|
375
|
+
await create_user("dave", conn=conn)
|
|
376
|
+
assert await get_user_count(conn=conn) == 2
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Works with dependency injection frameworks like FastAPI:
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
from typing import Annotated
|
|
383
|
+
from fastapi import Depends, FastAPI, Form
|
|
384
|
+
import sqlalchemy as sa
|
|
385
|
+
from sqla_fancy_core.decorators import Inject, transact
|
|
386
|
+
|
|
387
|
+
app = FastAPI()
|
|
388
|
+
|
|
389
|
+
def get_transaction():
|
|
390
|
+
with engine.begin() as conn:
|
|
391
|
+
yield conn
|
|
392
|
+
|
|
393
|
+
@transact
|
|
394
|
+
@app.post("/create-user")
|
|
395
|
+
def create_user(
|
|
396
|
+
name: Annotated[str, Form(...)],
|
|
397
|
+
conn: Annotated[sa.Connection, Depends(get_transaction)] = Inject(engine),
|
|
398
|
+
):
|
|
399
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
400
|
+
|
|
401
|
+
# Works outside FastAPI too — starts its own transaction
|
|
402
|
+
create_user(name="outside fastapi")
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Async with FastAPI:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from typing import Annotated
|
|
409
|
+
from fastapi import Depends, FastAPI, Form
|
|
410
|
+
from sqlalchemy.ext.asyncio import AsyncConnection
|
|
411
|
+
import sqlalchemy as sa
|
|
412
|
+
from sqla_fancy_core.decorators import Inject, transact
|
|
413
|
+
|
|
414
|
+
app = FastAPI()
|
|
415
|
+
|
|
416
|
+
async def get_transaction():
|
|
417
|
+
async with engine.begin() as conn:
|
|
418
|
+
yield conn
|
|
419
|
+
|
|
420
|
+
@transact
|
|
421
|
+
@app.post("/create-user")
|
|
422
|
+
async def create_user(
|
|
423
|
+
name: Annotated[str, Form(...)],
|
|
424
|
+
conn: Annotated[AsyncConnection, Depends(get_transaction)] = Inject(engine),
|
|
425
|
+
):
|
|
426
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## With Pydantic Validation
|
|
430
|
+
|
|
431
|
+
Integrate with Pydantic for validation:
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from typing import Any
|
|
435
|
+
import sqlalchemy as sa
|
|
436
|
+
from pydantic import BaseModel, Field
|
|
437
|
+
import pytest
|
|
438
|
+
|
|
439
|
+
from sqla_fancy_core import TableFactory
|
|
440
|
+
|
|
441
|
+
tf = TableFactory()
|
|
442
|
+
|
|
443
|
+
def field(col, default: Any = ...) -> Field:
|
|
444
|
+
return col.info["kwargs"]["field"](default)
|
|
445
|
+
|
|
446
|
+
# Define a table
|
|
447
|
+
class User:
|
|
448
|
+
name = tf(
|
|
449
|
+
sa.Column("name", sa.String),
|
|
450
|
+
field=lambda default: Field(default, max_length=5),
|
|
451
|
+
)
|
|
452
|
+
Table = tf("author")
|
|
453
|
+
|
|
454
|
+
# Define a pydantic schema
|
|
455
|
+
class CreateUser(BaseModel):
|
|
456
|
+
name: str = field(User.name)
|
|
457
|
+
|
|
458
|
+
# Define a pydantic schema
|
|
459
|
+
class UpdateUser(BaseModel):
|
|
460
|
+
name: str | None = field(User.name, None)
|
|
461
|
+
|
|
462
|
+
assert CreateUser(name="John").model_dump() == {"name": "John"}
|
|
463
|
+
assert UpdateUser(name="John").model_dump() == {"name": "John"}
|
|
464
|
+
assert UpdateUser().model_dump(exclude_unset=True) == {}
|
|
465
|
+
|
|
466
|
+
with pytest.raises(ValueError):
|
|
467
|
+
CreateUser()
|
|
468
|
+
with pytest.raises(ValueError):
|
|
469
|
+
UpdateUser(name="John Doe")
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Target audience
|
|
473
|
+
|
|
474
|
+
Production. For folks who prefer query maker over ORM, looking for a robust sync/async driver integration, wanting to keep code readable and secure.
|
|
475
|
+
|
|
476
|
+
## Comparison with other projects:
|
|
477
|
+
|
|
478
|
+
**Peewee**: No type hints. Also, no official async support.
|
|
479
|
+
|
|
480
|
+
**Piccolo**: Tight integration with drivers. Very opinionated. Not as flexible or mature as sqlalchemy core.
|
|
481
|
+
|
|
482
|
+
**Pypika**: Doesn’t prevent sql injection by default. Hence can be considered insecure.
|
|
483
|
+
|
|
484
|
+
**Raw string queries with placeholders**: sacrifices code readability, and prone to sql injection if one forgets to use placeholders.
|
|
485
|
+
|
|
486
|
+
**Other ORMs**: They are full blown ORMs, not query makers.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sqla_fancy_core/__init__.py,sha256=2Zxz2oM30Vv0_JJOMulUmts1nfag8mPyRAT7o8MW-pY,313
|
|
2
|
+
sqla_fancy_core/decorators.py,sha256=VhkYf5x6qwcQnc8QCuUzKPQxMI3tNNGM7nVSPUqMkPw,6649
|
|
3
|
+
sqla_fancy_core/factories.py,sha256=EgOhc15rCo9GyIuSNhuoB1pJ6lXx_UtRR5y9hh2lEtM,6326
|
|
4
|
+
sqla_fancy_core/wrappers.py,sha256=tQXyo84-7-8Mqc-tqDUwQsFpnNm4AeEeiGCLpLDWI_c,15090
|
|
5
|
+
sqla_fancy_core-1.2.2.dist-info/METADATA,sha256=-Ypz729sAc655XVtrB68TPbbnRiX8cT1NxmHCBCD1EM,14635
|
|
6
|
+
sqla_fancy_core-1.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
sqla_fancy_core-1.2.2.dist-info/licenses/LICENSE,sha256=XcYXJ0ipvwOn-nzko6p_xoCCbke8tAhmlIN04rUZDLk,1068
|
|
8
|
+
sqla_fancy_core-1.2.2.dist-info/RECORD,,
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sqla-fancy-core
|
|
3
|
-
Version: 1.0.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: test
|
|
37
|
-
Requires-Dist: flake8; extra == 'test'
|
|
38
|
-
Requires-Dist: pydantic; extra == 'test'
|
|
39
|
-
Requires-Dist: pytest; extra == 'test'
|
|
40
|
-
Description-Content-Type: text/markdown
|
|
41
|
-
|
|
42
|
-
# sqla-fancy-core
|
|
43
|
-
|
|
44
|
-
SQLAlchemy core, but fancier.
|
|
45
|
-
|
|
46
|
-
### Basic Usage
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
import sqlalchemy as sa
|
|
50
|
-
from sqla_fancy_core import TableFactory
|
|
51
|
-
|
|
52
|
-
tf = TableFactory()
|
|
53
|
-
|
|
54
|
-
# Define a table
|
|
55
|
-
class Author:
|
|
56
|
-
|
|
57
|
-
id = tf.auto_id()
|
|
58
|
-
name = tf.string("name")
|
|
59
|
-
created_at = tf.created_at()
|
|
60
|
-
updated_at = tf.updated_at()
|
|
61
|
-
|
|
62
|
-
Table = tf("author")
|
|
63
|
-
|
|
64
|
-
# Or define it without losing type hints
|
|
65
|
-
class Book:
|
|
66
|
-
id = tf(sa.Column("id", sa.Integer, primary_key=True, autoincrement=True))
|
|
67
|
-
title = tf(sa.Column("title", sa.String(255), nullable=False))
|
|
68
|
-
author_id = tf(sa.Column("author_id", sa.Integer, sa.ForeignKey(Author.id)))
|
|
69
|
-
created_at = tf(
|
|
70
|
-
sa.Column(
|
|
71
|
-
"created_at",
|
|
72
|
-
sa.DateTime,
|
|
73
|
-
nullable=False,
|
|
74
|
-
server_default=sa.func.now(),
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
updated_at = tf(
|
|
78
|
-
sa.Column(
|
|
79
|
-
"updated_at",
|
|
80
|
-
sa.DateTime,
|
|
81
|
-
nullable=False,
|
|
82
|
-
server_default=sa.func.now(),
|
|
83
|
-
onupdate=sa.func.now(),
|
|
84
|
-
)
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
Table = tf(sa.Table("book", sa.MetaData()))
|
|
88
|
-
|
|
89
|
-
# Create the tables
|
|
90
|
-
engine = sa.create_engine("sqlite:///:memory:")
|
|
91
|
-
tf.metadata.create_all(engine)
|
|
92
|
-
|
|
93
|
-
with engine.connect() as conn:
|
|
94
|
-
# Insert author
|
|
95
|
-
qry = (
|
|
96
|
-
sa.insert(Author.Table)
|
|
97
|
-
.values({Author.name: "John Doe"})
|
|
98
|
-
.returning(Author.id)
|
|
99
|
-
)
|
|
100
|
-
author = next(conn.execute(qry).mappings())
|
|
101
|
-
author_id = author[Author.id]
|
|
102
|
-
assert author_id == 1
|
|
103
|
-
|
|
104
|
-
# Insert book
|
|
105
|
-
qry = (
|
|
106
|
-
sa.insert(Book.Table)
|
|
107
|
-
.values({Book.title: "My Book", Book.author_id: author_id})
|
|
108
|
-
.returning(Book.id)
|
|
109
|
-
)
|
|
110
|
-
book = next(conn.execute(qry).mappings())
|
|
111
|
-
assert book[Book.id] == 1
|
|
112
|
-
|
|
113
|
-
# Query the data
|
|
114
|
-
qry = sa.select(Author.name, Book.title).join(
|
|
115
|
-
Book.Table,
|
|
116
|
-
Book.author_id == Author.id,
|
|
117
|
-
)
|
|
118
|
-
result = conn.execute(qry).fetchall()
|
|
119
|
-
assert result == [("John Doe", "My Book")], result
|
|
120
|
-
|
|
121
|
-
# Create the tables
|
|
122
|
-
engine = sa.create_engine("sqlite:///:memory:")
|
|
123
|
-
tf.metadata.create_all(engine)
|
|
124
|
-
|
|
125
|
-
with engine.connect() as conn:
|
|
126
|
-
# Insert author
|
|
127
|
-
qry = (
|
|
128
|
-
sa.insert(Author.Table)
|
|
129
|
-
.values({Author.name: "John Doe"})
|
|
130
|
-
.returning(Author.id)
|
|
131
|
-
)
|
|
132
|
-
author = next(conn.execute(qry).mappings())
|
|
133
|
-
author_id = author[Author.id]
|
|
134
|
-
assert author_id == 1
|
|
135
|
-
|
|
136
|
-
# Insert book
|
|
137
|
-
qry = (
|
|
138
|
-
sa.insert(Book.Table)
|
|
139
|
-
.values({Book.title: "My Book", Book.author_id: author_id})
|
|
140
|
-
.returning(Book.id)
|
|
141
|
-
)
|
|
142
|
-
book = next(conn.execute(qry).mappings())
|
|
143
|
-
assert book[Book.id] == 1
|
|
144
|
-
|
|
145
|
-
# Query the data
|
|
146
|
-
qry = sa.select(Author.name, Book.title).join(
|
|
147
|
-
Book.Table,
|
|
148
|
-
Book.author_id == Author.id,
|
|
149
|
-
)
|
|
150
|
-
result = conn.execute(qry).fetchall()
|
|
151
|
-
assert result == [("John Doe", "My Book")], result
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### With Pydantic Validation
|
|
155
|
-
|
|
156
|
-
```python
|
|
157
|
-
from typing import Any
|
|
158
|
-
import sqlalchemy as sa
|
|
159
|
-
from pydantic import BaseModel, Field
|
|
160
|
-
|
|
161
|
-
from sqla_fancy_core import TableFactory
|
|
162
|
-
|
|
163
|
-
tf = TableFactory()
|
|
164
|
-
|
|
165
|
-
def field(col, default: Any = ...) -> Field:
|
|
166
|
-
return col.info["kwargs"]["field"](default)
|
|
167
|
-
|
|
168
|
-
# Define a table
|
|
169
|
-
class User:
|
|
170
|
-
name = tf(
|
|
171
|
-
sa.Column("name", sa.String),
|
|
172
|
-
field=lambda default: Field(default, max_length=5),
|
|
173
|
-
)
|
|
174
|
-
Table = tf("author")
|
|
175
|
-
|
|
176
|
-
# Define a pydantic schema
|
|
177
|
-
class CreateUser(BaseModel):
|
|
178
|
-
name: str = field(User.name)
|
|
179
|
-
|
|
180
|
-
# Define a pydantic schema
|
|
181
|
-
class UpdateUser(BaseModel):
|
|
182
|
-
name: str | None = field(User.name, None)
|
|
183
|
-
|
|
184
|
-
assert CreateUser(name="John").model_dump() == {"name": "John"}
|
|
185
|
-
assert UpdateUser(name="John").model_dump() == {"name": "John"}
|
|
186
|
-
assert UpdateUser().model_dump(exclude_unset=True) == {}
|
|
187
|
-
|
|
188
|
-
with pytest.raises(ValueError):
|
|
189
|
-
CreateUser()
|
|
190
|
-
with pytest.raises(ValueError):
|
|
191
|
-
UpdateUser(name="John Doe")
|
|
192
|
-
```
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
sqla_fancy_core/__init__.py,sha256=GwPbjEPhM-mxFcsjZizCQ9dnUNgqBswCfwrI_u-JVfc,6376
|
|
2
|
-
sqla_fancy_core-1.0.0.dist-info/METADATA,sha256=1UBn6tJ7EK6TXKBdq6gUWtzStIqeHS3lsKDb9pkZBCE,5671
|
|
3
|
-
sqla_fancy_core-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
-
sqla_fancy_core-1.0.0.dist-info/licenses/LICENSE,sha256=XcYXJ0ipvwOn-nzko6p_xoCCbke8tAhmlIN04rUZDLk,1068
|
|
5
|
-
sqla_fancy_core-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|