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.

@@ -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,,