sqla-fancy-core 1.0.2__py3-none-any.whl → 1.1.0__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 +3 -157
- sqla_fancy_core/decorators.py +160 -0
- sqla_fancy_core/factories.py +156 -0
- sqla_fancy_core/wrappers.py +239 -0
- sqla_fancy_core-1.1.0.dist-info/METADATA +422 -0
- sqla_fancy_core-1.1.0.dist-info/RECORD +8 -0
- sqla_fancy_core-1.0.2.dist-info/METADATA +0 -160
- sqla_fancy_core-1.0.2.dist-info/RECORD +0 -5
- {sqla_fancy_core-1.0.2.dist-info → sqla_fancy_core-1.1.0.dist-info}/WHEEL +0 -0
- {sqla_fancy_core-1.0.2.dist-info → sqla_fancy_core-1.1.0.dist-info}/licenses/LICENSE +0 -0
sqla_fancy_core/__init__.py
CHANGED
|
@@ -1,159 +1,5 @@
|
|
|
1
1
|
"""SQLAlchemy core, but fancier."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TableFactory:
|
|
9
|
-
"""A factory for creating SQLAlchemy columns with default values."""
|
|
10
|
-
|
|
11
|
-
def __init__(self, metadata: Optional[sa.MetaData] = None):
|
|
12
|
-
"""Initialize the factory with default values."""
|
|
13
|
-
if metadata is None:
|
|
14
|
-
self.metadata = sa.MetaData()
|
|
15
|
-
else:
|
|
16
|
-
self.metadata = metadata
|
|
17
|
-
self.c = []
|
|
18
|
-
|
|
19
|
-
def col(self, *args, **kwargs) -> sa.Column:
|
|
20
|
-
col = sa.Column(*args, **kwargs)
|
|
21
|
-
return self(col)
|
|
22
|
-
|
|
23
|
-
def integer(self, name: str, *args, **kwargs) -> sa.Column:
|
|
24
|
-
return self.col(name, sa.Integer, *args, **kwargs)
|
|
25
|
-
|
|
26
|
-
def string(self, name: str, *args, **kwargs) -> sa.Column:
|
|
27
|
-
return self.col(name, sa.String, *args, **kwargs)
|
|
28
|
-
|
|
29
|
-
def text(self, name: str, *args, **kwargs) -> sa.Column:
|
|
30
|
-
return self.col(name, sa.Text, *args, **kwargs)
|
|
31
|
-
|
|
32
|
-
def float(self, name: str, *args, **kwargs) -> sa.Column:
|
|
33
|
-
return self.col(name, sa.Float, *args, **kwargs)
|
|
34
|
-
|
|
35
|
-
def numeric(self, name: str, *args, **kwargs) -> sa.Column:
|
|
36
|
-
return self.col(name, sa.Numeric, *args, **kwargs)
|
|
37
|
-
|
|
38
|
-
def bigint(self, name: str, *args, **kwargs) -> sa.Column:
|
|
39
|
-
return self.col(name, sa.BigInteger, *args, **kwargs)
|
|
40
|
-
|
|
41
|
-
def smallint(self, name: str, *args, **kwargs) -> sa.Column:
|
|
42
|
-
return self.col(name, sa.SmallInteger, *args, **kwargs)
|
|
43
|
-
|
|
44
|
-
def timestamp(self, name: str, *args, **kwargs) -> sa.Column:
|
|
45
|
-
return self.col(name, sa.TIMESTAMP, *args, **kwargs)
|
|
46
|
-
|
|
47
|
-
def date(self, name: str, *args, **kwargs) -> sa.Column:
|
|
48
|
-
return self.col(name, sa.Date, *args, **kwargs)
|
|
49
|
-
|
|
50
|
-
def datetime(self, name: str, *args, **kwargs) -> sa.Column:
|
|
51
|
-
return self.col(name, sa.DateTime, *args, **kwargs)
|
|
52
|
-
|
|
53
|
-
def today(self, name: str, *args, **kwargs) -> sa.Column:
|
|
54
|
-
return self.date(name, default=sa.func.now(), *args, **kwargs)
|
|
55
|
-
|
|
56
|
-
def time(self, name: str, *args, **kwargs) -> sa.Column:
|
|
57
|
-
return self.col(name, sa.Time, *args, **kwargs)
|
|
58
|
-
|
|
59
|
-
def timenow(self, name: str, *args, **kwargs) -> sa.Column:
|
|
60
|
-
return self.time(name, default=sa.func.now(), *args, **kwargs)
|
|
61
|
-
|
|
62
|
-
def now(self, name: str, *args, **kwargs) -> sa.Column:
|
|
63
|
-
return self.datetime(name, default=sa.func.now(), *args, **kwargs)
|
|
64
|
-
|
|
65
|
-
def boolean(self, name: str, *args, **kwargs) -> sa.Column:
|
|
66
|
-
return self.col(name, sa.Boolean, *args, **kwargs)
|
|
67
|
-
|
|
68
|
-
def true(self, name: str, *args, **kwargs):
|
|
69
|
-
return self.boolean(name, default=True, *args, **kwargs)
|
|
70
|
-
|
|
71
|
-
def false(self, name: str, *args, **kwargs):
|
|
72
|
-
return self.boolean(name, default=False, *args, **kwargs)
|
|
73
|
-
|
|
74
|
-
def foreign_key(self, name: str, ref: Union[str, sa.Column], *args, **kwargs):
|
|
75
|
-
return self.col(name, sa.ForeignKey(ref), *args, **kwargs)
|
|
76
|
-
|
|
77
|
-
def enum(self, name: str, enum: type, *args, **kwargs) -> sa.Column:
|
|
78
|
-
return self.col(name, sa.Enum(enum), *args, **kwargs)
|
|
79
|
-
|
|
80
|
-
def json(self, name: str, *args, **kwargs) -> sa.Column:
|
|
81
|
-
return self.col(name, sa.JSON, *args, **kwargs)
|
|
82
|
-
|
|
83
|
-
def array(self, name: str, *args, **kwargs) -> sa.Column:
|
|
84
|
-
return self.col(name, sa.ARRAY, *args, **kwargs)
|
|
85
|
-
|
|
86
|
-
def array_int(self, name: str, *args, **kwargs) -> sa.Column:
|
|
87
|
-
return self.array(name, sa.Integer, *args, **kwargs)
|
|
88
|
-
|
|
89
|
-
def array_str(self, name: str, *args, **kwargs) -> sa.Column:
|
|
90
|
-
return self.array(name, sa.String, *args, **kwargs)
|
|
91
|
-
|
|
92
|
-
def array_text(self, name: str, *args, **kwargs) -> sa.Column:
|
|
93
|
-
return self.array(name, sa.Text, *args, **kwargs)
|
|
94
|
-
|
|
95
|
-
def array_float(self, name: str, *args, **kwargs) -> sa.Column:
|
|
96
|
-
return self.array(name, sa.Float, *args, **kwargs)
|
|
97
|
-
|
|
98
|
-
def array_numeric(self, name: str, *args, **kwargs) -> sa.Column:
|
|
99
|
-
return self.array(name, sa.Numeric, *args, **kwargs)
|
|
100
|
-
|
|
101
|
-
def array_bigint(self, name: str, *args, **kwargs) -> sa.Column:
|
|
102
|
-
return self.array(name, sa.BigInteger, *args, **kwargs)
|
|
103
|
-
|
|
104
|
-
def array_smallint(self, name: str, *args, **kwargs) -> sa.Column:
|
|
105
|
-
return self.array(name, sa.SmallInteger, *args, **kwargs)
|
|
106
|
-
|
|
107
|
-
def array_timestamp(self, name: str, *args, **kwargs) -> sa.Column:
|
|
108
|
-
return self.array(name, sa.TIMESTAMP, *args, **kwargs)
|
|
109
|
-
|
|
110
|
-
def array_date(self, name: str, *args, **kwargs) -> sa.Column:
|
|
111
|
-
return self.array(name, sa.Date, *args, **kwargs)
|
|
112
|
-
|
|
113
|
-
def array_datetime(self, name: str, *args, **kwargs) -> sa.Column:
|
|
114
|
-
return self.array(name, sa.DateTime, *args, **kwargs)
|
|
115
|
-
|
|
116
|
-
def array_time(self, name: str, *args, **kwargs) -> sa.Column:
|
|
117
|
-
return self.array(name, sa.Time, *args, **kwargs)
|
|
118
|
-
|
|
119
|
-
def array_boolean(self, name: str, *args, **kwargs) -> sa.Column:
|
|
120
|
-
return self.array(name, sa.Boolean, *args, **kwargs)
|
|
121
|
-
|
|
122
|
-
def array_enum(self, name: str, enum: type, *args, **kwargs) -> sa.Column:
|
|
123
|
-
return self.array(name, sa.Enum(enum), *args, **kwargs)
|
|
124
|
-
|
|
125
|
-
def auto_id(self, name="id", *args, **kwargs) -> sa.Column:
|
|
126
|
-
return self.integer(
|
|
127
|
-
name, primary_key=True, index=True, autoincrement=True, *args, **kwargs
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
def updated_at(self, name="updated_at", *args, **kwargs) -> sa.Column:
|
|
131
|
-
return self.datetime(
|
|
132
|
-
name, default=sa.func.now(), onupdate=sa.func.now(), *args, **kwargs
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
def created_at(self, name="created_at", *args, **kwargs) -> sa.Column:
|
|
136
|
-
return self.datetime(name, default=sa.func.now(), *args, **kwargs)
|
|
137
|
-
|
|
138
|
-
@overload
|
|
139
|
-
def __call__(self, arg1: str, *args, **kwargs) -> sa.Table: ...
|
|
140
|
-
@overload
|
|
141
|
-
def __call__(self, arg1: sa.Column, *args, **kwargs) -> sa.Column: ...
|
|
142
|
-
@overload
|
|
143
|
-
def __call__(self, arg1: sa.Table, *args, **kwargs) -> sa.Table: ...
|
|
144
|
-
def __call__(self, arg1, *args, **kwargs):
|
|
145
|
-
if isinstance(arg1, str):
|
|
146
|
-
cols = self.c
|
|
147
|
-
self.c = []
|
|
148
|
-
return sa.Table(arg1, self.metadata, *args, *cols, **kwargs)
|
|
149
|
-
elif isinstance(arg1, sa.Column):
|
|
150
|
-
arg1.info["args"] = args
|
|
151
|
-
arg1.info["kwargs"] = kwargs
|
|
152
|
-
self.c.append(arg1)
|
|
153
|
-
return arg1
|
|
154
|
-
elif isinstance(arg1, sa.Table):
|
|
155
|
-
cols = self.c
|
|
156
|
-
self.c = []
|
|
157
|
-
return sa.Table(arg1.name, self.metadata, *args, *cols, **kwargs)
|
|
158
|
-
else:
|
|
159
|
-
raise TypeError(f"Expected a string or Column, got {type(arg1).__name__}")
|
|
3
|
+
from sqla_fancy_core.factories import TableFactory # noqa
|
|
4
|
+
from sqla_fancy_core.wrappers import FancyEngineWrapper, AsyncFancyEngineWrapper, fancy # noqa
|
|
5
|
+
from sqla_fancy_core.decorators import transact, Inject # noqa
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Some decorators for fun times with SQLAlchemy core."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Union, overload
|
|
6
|
+
|
|
7
|
+
import sqlalchemy as sa
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
|
|
9
|
+
|
|
10
|
+
EngineType = Union[sa.engine.Engine, AsyncEngine]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Injectable:
|
|
14
|
+
def __init__(self, engine: EngineType):
|
|
15
|
+
self.engine = engine
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
def Inject(engine: sa.Engine) -> sa.Connection: ...
|
|
20
|
+
@overload
|
|
21
|
+
def Inject(engine: AsyncEngine) -> AsyncConnection: ...
|
|
22
|
+
def Inject(engine: EngineType): # type: ignore
|
|
23
|
+
"""A marker class for dependency injection."""
|
|
24
|
+
return _Injectable(engine)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def transact(func):
|
|
28
|
+
"""A decorator that provides a transactional context.
|
|
29
|
+
|
|
30
|
+
If the decorated function is called with a connection object, that
|
|
31
|
+
connection is used. Otherwise, a new transaction is started from the
|
|
32
|
+
engine, and the new connection is injected to the function.
|
|
33
|
+
|
|
34
|
+
Example: ::
|
|
35
|
+
@transact
|
|
36
|
+
def create_user(name: str, conn: sa.Connection = Inject(engine)):
|
|
37
|
+
conn.execute(...)
|
|
38
|
+
|
|
39
|
+
# This will create a new transaction
|
|
40
|
+
create_user("test")
|
|
41
|
+
|
|
42
|
+
# This will use the existing connection
|
|
43
|
+
with engine.connect() as conn:
|
|
44
|
+
create_user(name="existing", conn=conn)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Find the parameter with value Inject
|
|
48
|
+
sig = inspect.signature(func)
|
|
49
|
+
inject_param_name = None
|
|
50
|
+
for name, param in sig.parameters.items():
|
|
51
|
+
if param.default is not inspect.Parameter.empty and isinstance(
|
|
52
|
+
param.default, _Injectable
|
|
53
|
+
):
|
|
54
|
+
inject_param_name = name
|
|
55
|
+
break
|
|
56
|
+
if inject_param_name is None:
|
|
57
|
+
return func # No injection needed
|
|
58
|
+
|
|
59
|
+
engine = sig.parameters[inject_param_name].default.engine
|
|
60
|
+
is_async = isinstance(engine, AsyncEngine)
|
|
61
|
+
|
|
62
|
+
if is_async:
|
|
63
|
+
|
|
64
|
+
@functools.wraps(func)
|
|
65
|
+
async def async_wrapper(*args, **kwargs):
|
|
66
|
+
conn = kwargs.get(inject_param_name)
|
|
67
|
+
if isinstance(conn, AsyncConnection):
|
|
68
|
+
if conn.in_transaction():
|
|
69
|
+
return await func(*args, **kwargs)
|
|
70
|
+
else:
|
|
71
|
+
async with conn.begin():
|
|
72
|
+
return await func(*args, **kwargs)
|
|
73
|
+
else:
|
|
74
|
+
async with engine.begin() as conn:
|
|
75
|
+
kwargs[inject_param_name] = conn
|
|
76
|
+
return await func(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
return async_wrapper
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
|
|
82
|
+
@functools.wraps(func)
|
|
83
|
+
def sync_wrapper(*args, **kwargs):
|
|
84
|
+
conn = kwargs.get(inject_param_name)
|
|
85
|
+
if isinstance(conn, sa.Connection):
|
|
86
|
+
if conn.in_transaction():
|
|
87
|
+
return func(*args, **kwargs)
|
|
88
|
+
else:
|
|
89
|
+
with conn.begin():
|
|
90
|
+
return func(*args, **kwargs)
|
|
91
|
+
else:
|
|
92
|
+
with engine.begin() as conn:
|
|
93
|
+
kwargs[inject_param_name] = conn
|
|
94
|
+
return func(*args, **kwargs)
|
|
95
|
+
|
|
96
|
+
return sync_wrapper
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def connect(func):
|
|
100
|
+
"""A decorator that provides a connection context.
|
|
101
|
+
|
|
102
|
+
If the decorated function is called with a connection object, that
|
|
103
|
+
connection is used. Otherwise, a new connection is created from the
|
|
104
|
+
engine, and the new connection is injected to the function.
|
|
105
|
+
|
|
106
|
+
Example: ::
|
|
107
|
+
@connect
|
|
108
|
+
def get_user_count(conn: sa.Connection = Inject(engine)) -> int:
|
|
109
|
+
return conn.execute(...).scalar_one()
|
|
110
|
+
|
|
111
|
+
# This will create a new connection
|
|
112
|
+
count = get_user_count()
|
|
113
|
+
|
|
114
|
+
# This will use the existing connection
|
|
115
|
+
with engine.connect() as conn:
|
|
116
|
+
count = get_user_count(conn)
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
# Find the parameter with value Inject
|
|
120
|
+
sig = inspect.signature(func)
|
|
121
|
+
inject_param_name = None
|
|
122
|
+
for name, param in sig.parameters.items():
|
|
123
|
+
if param.default is not inspect.Parameter.empty and isinstance(
|
|
124
|
+
param.default, _Injectable
|
|
125
|
+
):
|
|
126
|
+
inject_param_name = name
|
|
127
|
+
break
|
|
128
|
+
if inject_param_name is None:
|
|
129
|
+
return func # No injection needed
|
|
130
|
+
|
|
131
|
+
engine = sig.parameters[inject_param_name].default.engine
|
|
132
|
+
is_async = isinstance(engine, AsyncEngine)
|
|
133
|
+
|
|
134
|
+
if is_async:
|
|
135
|
+
|
|
136
|
+
@functools.wraps(func)
|
|
137
|
+
async def async_wrapper(*args, **kwargs):
|
|
138
|
+
conn = kwargs.get(inject_param_name)
|
|
139
|
+
if isinstance(conn, AsyncConnection):
|
|
140
|
+
return await func(*args, **kwargs)
|
|
141
|
+
else:
|
|
142
|
+
async with engine.connect() as conn:
|
|
143
|
+
kwargs[inject_param_name] = conn
|
|
144
|
+
return await func(*args, **kwargs)
|
|
145
|
+
|
|
146
|
+
return async_wrapper
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
|
|
150
|
+
@functools.wraps(func)
|
|
151
|
+
def sync_wrapper(*args, **kwargs):
|
|
152
|
+
conn = kwargs.get(inject_param_name)
|
|
153
|
+
if isinstance(conn, sa.Connection):
|
|
154
|
+
return func(*args, **kwargs)
|
|
155
|
+
else:
|
|
156
|
+
with engine.connect() as conn:
|
|
157
|
+
kwargs[inject_param_name] = conn
|
|
158
|
+
return func(*args, **kwargs)
|
|
159
|
+
|
|
160
|
+
return sync_wrapper
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Some factories for fun times with SQLAlchemy core."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union, overload
|
|
4
|
+
|
|
5
|
+
import sqlalchemy as sa
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TableFactory:
|
|
9
|
+
"""A factory for creating SQLAlchemy columns with default values."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, metadata: Optional[sa.MetaData] = None):
|
|
12
|
+
"""Initialize the factory with default values."""
|
|
13
|
+
if metadata is None:
|
|
14
|
+
self.metadata = sa.MetaData()
|
|
15
|
+
else:
|
|
16
|
+
self.metadata = metadata
|
|
17
|
+
self.c = []
|
|
18
|
+
|
|
19
|
+
def col(self, *args, **kwargs) -> sa.Column:
|
|
20
|
+
col = sa.Column(*args, **kwargs)
|
|
21
|
+
return self(col)
|
|
22
|
+
|
|
23
|
+
def integer(self, name: str, *args, **kwargs) -> sa.Column:
|
|
24
|
+
return self.col(name, sa.Integer, *args, **kwargs)
|
|
25
|
+
|
|
26
|
+
def string(self, name: str, *args, **kwargs) -> sa.Column:
|
|
27
|
+
return self.col(name, sa.String, *args, **kwargs)
|
|
28
|
+
|
|
29
|
+
def text(self, name: str, *args, **kwargs) -> sa.Column:
|
|
30
|
+
return self.col(name, sa.Text, *args, **kwargs)
|
|
31
|
+
|
|
32
|
+
def float(self, name: str, *args, **kwargs) -> sa.Column:
|
|
33
|
+
return self.col(name, sa.Float, *args, **kwargs)
|
|
34
|
+
|
|
35
|
+
def numeric(self, name: str, *args, **kwargs) -> sa.Column:
|
|
36
|
+
return self.col(name, sa.Numeric, *args, **kwargs)
|
|
37
|
+
|
|
38
|
+
def bigint(self, name: str, *args, **kwargs) -> sa.Column:
|
|
39
|
+
return self.col(name, sa.BigInteger, *args, **kwargs)
|
|
40
|
+
|
|
41
|
+
def smallint(self, name: str, *args, **kwargs) -> sa.Column:
|
|
42
|
+
return self.col(name, sa.SmallInteger, *args, **kwargs)
|
|
43
|
+
|
|
44
|
+
def timestamp(self, name: str, *args, **kwargs) -> sa.Column:
|
|
45
|
+
return self.col(name, sa.TIMESTAMP, *args, **kwargs)
|
|
46
|
+
|
|
47
|
+
def date(self, name: str, *args, **kwargs) -> sa.Column:
|
|
48
|
+
return self.col(name, sa.Date, *args, **kwargs)
|
|
49
|
+
|
|
50
|
+
def datetime(self, name: str, *args, **kwargs) -> sa.Column:
|
|
51
|
+
return self.col(name, sa.DateTime, *args, **kwargs)
|
|
52
|
+
|
|
53
|
+
def today(self, name: str, *args, **kwargs) -> sa.Column:
|
|
54
|
+
return self.date(name, default=sa.func.now(), *args, **kwargs)
|
|
55
|
+
|
|
56
|
+
def time(self, name: str, *args, **kwargs) -> sa.Column:
|
|
57
|
+
return self.col(name, sa.Time, *args, **kwargs)
|
|
58
|
+
|
|
59
|
+
def timenow(self, name: str, *args, **kwargs) -> sa.Column:
|
|
60
|
+
return self.time(name, default=sa.func.now(), *args, **kwargs)
|
|
61
|
+
|
|
62
|
+
def now(self, name: str, *args, **kwargs) -> sa.Column:
|
|
63
|
+
return self.datetime(name, default=sa.func.now(), *args, **kwargs)
|
|
64
|
+
|
|
65
|
+
def boolean(self, name: str, *args, **kwargs) -> sa.Column:
|
|
66
|
+
return self.col(name, sa.Boolean, *args, **kwargs)
|
|
67
|
+
|
|
68
|
+
def true(self, name: str, *args, **kwargs):
|
|
69
|
+
return self.boolean(name, default=True, *args, **kwargs)
|
|
70
|
+
|
|
71
|
+
def false(self, name: str, *args, **kwargs):
|
|
72
|
+
return self.boolean(name, default=False, *args, **kwargs)
|
|
73
|
+
|
|
74
|
+
def foreign_key(self, name: str, ref: Union[str, sa.Column], *args, **kwargs):
|
|
75
|
+
return self.col(name, sa.ForeignKey(ref), *args, **kwargs)
|
|
76
|
+
|
|
77
|
+
def enum(self, name: str, enum: type, *args, **kwargs) -> sa.Column:
|
|
78
|
+
return self.col(name, sa.Enum(enum), *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
def json(self, name: str, *args, **kwargs) -> sa.Column:
|
|
81
|
+
return self.col(name, sa.JSON, *args, **kwargs)
|
|
82
|
+
|
|
83
|
+
def array(self, name: str, *args, **kwargs) -> sa.Column:
|
|
84
|
+
return self.col(name, sa.ARRAY, *args, **kwargs)
|
|
85
|
+
|
|
86
|
+
def array_int(self, name: str, *args, **kwargs) -> sa.Column:
|
|
87
|
+
return self.array(name, sa.Integer, *args, **kwargs)
|
|
88
|
+
|
|
89
|
+
def array_str(self, name: str, *args, **kwargs) -> sa.Column:
|
|
90
|
+
return self.array(name, sa.String, *args, **kwargs)
|
|
91
|
+
|
|
92
|
+
def array_text(self, name: str, *args, **kwargs) -> sa.Column:
|
|
93
|
+
return self.array(name, sa.Text, *args, **kwargs)
|
|
94
|
+
|
|
95
|
+
def array_float(self, name: str, *args, **kwargs) -> sa.Column:
|
|
96
|
+
return self.array(name, sa.Float, *args, **kwargs)
|
|
97
|
+
|
|
98
|
+
def array_numeric(self, name: str, *args, **kwargs) -> sa.Column:
|
|
99
|
+
return self.array(name, sa.Numeric, *args, **kwargs)
|
|
100
|
+
|
|
101
|
+
def array_bigint(self, name: str, *args, **kwargs) -> sa.Column:
|
|
102
|
+
return self.array(name, sa.BigInteger, *args, **kwargs)
|
|
103
|
+
|
|
104
|
+
def array_smallint(self, name: str, *args, **kwargs) -> sa.Column:
|
|
105
|
+
return self.array(name, sa.SmallInteger, *args, **kwargs)
|
|
106
|
+
|
|
107
|
+
def array_timestamp(self, name: str, *args, **kwargs) -> sa.Column:
|
|
108
|
+
return self.array(name, sa.TIMESTAMP, *args, **kwargs)
|
|
109
|
+
|
|
110
|
+
def array_date(self, name: str, *args, **kwargs) -> sa.Column:
|
|
111
|
+
return self.array(name, sa.Date, *args, **kwargs)
|
|
112
|
+
|
|
113
|
+
def array_datetime(self, name: str, *args, **kwargs) -> sa.Column:
|
|
114
|
+
return self.array(name, sa.DateTime, *args, **kwargs)
|
|
115
|
+
|
|
116
|
+
def array_time(self, name: str, *args, **kwargs) -> sa.Column:
|
|
117
|
+
return self.array(name, sa.Time, *args, **kwargs)
|
|
118
|
+
|
|
119
|
+
def array_boolean(self, name: str, *args, **kwargs) -> sa.Column:
|
|
120
|
+
return self.array(name, sa.Boolean, *args, **kwargs)
|
|
121
|
+
|
|
122
|
+
def array_enum(self, name: str, enum: type, *args, **kwargs) -> sa.Column:
|
|
123
|
+
return self.array(name, sa.Enum(enum), *args, **kwargs)
|
|
124
|
+
|
|
125
|
+
def auto_id(self, name="id", *args, **kwargs) -> sa.Column:
|
|
126
|
+
return self.integer(
|
|
127
|
+
name, primary_key=True, index=True, autoincrement=True, *args, **kwargs
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def updated_at(self, name="updated_at", *args, **kwargs) -> sa.Column:
|
|
131
|
+
return self.datetime(
|
|
132
|
+
name, default=sa.func.now(), onupdate=sa.func.now(), *args, **kwargs
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def created_at(self, name="created_at", *args, **kwargs) -> sa.Column:
|
|
136
|
+
return self.datetime(name, default=sa.func.now(), *args, **kwargs)
|
|
137
|
+
|
|
138
|
+
@overload
|
|
139
|
+
def __call__(self, arg1: str, *args, **kwargs) -> sa.Table: ...
|
|
140
|
+
@overload
|
|
141
|
+
def __call__(self, arg1: sa.Column, *args, **kwargs) -> sa.Column: ...
|
|
142
|
+
@overload
|
|
143
|
+
def __call__(self, arg1: sa.Table, *args, **kwargs) -> sa.Table: ...
|
|
144
|
+
def __call__(self, arg1, *args, **kwargs):
|
|
145
|
+
if isinstance(arg1, sa.Column):
|
|
146
|
+
arg1.info["args"] = args
|
|
147
|
+
arg1.info["kwargs"] = kwargs
|
|
148
|
+
self.c.append(arg1)
|
|
149
|
+
return arg1
|
|
150
|
+
elif isinstance(arg1, Union[str, sa.Table]):
|
|
151
|
+
cols = self.c
|
|
152
|
+
tablename = arg1.name if isinstance(arg1, sa.Table) else arg1
|
|
153
|
+
self.c = []
|
|
154
|
+
return sa.Table(tablename, self.metadata, *args, *cols, **kwargs)
|
|
155
|
+
else:
|
|
156
|
+
raise TypeError(f"Expected a string or Column, got {type(arg1).__name__}")
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Some wrappers for fun times with SQLAlchemy core."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, TypeVar, overload
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Connection, CursorResult, Engine, Executable
|
|
6
|
+
from sqlalchemy.engine.interfaces import (
|
|
7
|
+
CoreExecuteOptionsParameter,
|
|
8
|
+
_CoreAnyExecuteParams,
|
|
9
|
+
)
|
|
10
|
+
from sqlalchemy.ext.asyncio import (
|
|
11
|
+
AsyncConnection,
|
|
12
|
+
AsyncEngine,
|
|
13
|
+
)
|
|
14
|
+
from sqlalchemy.sql.selectable import TypedReturnsRows
|
|
15
|
+
|
|
16
|
+
_T = TypeVar("_T", bound=Any)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FancyEngineWrapper:
|
|
20
|
+
"""A wrapper around SQLAlchemy Engine with additional features."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, engine: Engine) -> None:
|
|
23
|
+
self.engine = engine
|
|
24
|
+
|
|
25
|
+
@overload
|
|
26
|
+
def x(
|
|
27
|
+
self,
|
|
28
|
+
connection: Optional[Connection],
|
|
29
|
+
statement: TypedReturnsRows[_T],
|
|
30
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
31
|
+
*,
|
|
32
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
33
|
+
) -> CursorResult[_T]: ...
|
|
34
|
+
@overload
|
|
35
|
+
def x(
|
|
36
|
+
self,
|
|
37
|
+
connection: Optional[Connection],
|
|
38
|
+
statement: Executable,
|
|
39
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
40
|
+
*,
|
|
41
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
42
|
+
) -> CursorResult[Any]: ...
|
|
43
|
+
def x(
|
|
44
|
+
self,
|
|
45
|
+
connection: Optional[Connection],
|
|
46
|
+
statement: Executable,
|
|
47
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
48
|
+
*,
|
|
49
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
50
|
+
) -> CursorResult[Any]:
|
|
51
|
+
"""Connect to the database, execute the query, and return the result.
|
|
52
|
+
|
|
53
|
+
If a connection is provided, use it; otherwise, create a new one.
|
|
54
|
+
"""
|
|
55
|
+
if connection:
|
|
56
|
+
return connection.execute(
|
|
57
|
+
statement, parameters, execution_options=execution_options
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
with self.engine.connect() as connection:
|
|
61
|
+
return connection.execute(
|
|
62
|
+
statement, parameters, execution_options=execution_options
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@overload
|
|
66
|
+
def tx(
|
|
67
|
+
self,
|
|
68
|
+
connection: Optional[Connection],
|
|
69
|
+
statement: TypedReturnsRows[_T],
|
|
70
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
71
|
+
*,
|
|
72
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
73
|
+
) -> CursorResult[_T]: ...
|
|
74
|
+
@overload
|
|
75
|
+
def tx(
|
|
76
|
+
self,
|
|
77
|
+
connection: Optional[Connection],
|
|
78
|
+
statement: Executable,
|
|
79
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
80
|
+
*,
|
|
81
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
82
|
+
) -> CursorResult[Any]: ...
|
|
83
|
+
def tx(
|
|
84
|
+
self,
|
|
85
|
+
connection: Optional[Connection],
|
|
86
|
+
statement: Executable,
|
|
87
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
88
|
+
*,
|
|
89
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
90
|
+
) -> CursorResult[Any]:
|
|
91
|
+
"""Begin a transaction, execute the query, and return the result.
|
|
92
|
+
|
|
93
|
+
If a connection is provided, use it; otherwise, create a new one.
|
|
94
|
+
"""
|
|
95
|
+
if connection:
|
|
96
|
+
if connection.in_transaction():
|
|
97
|
+
# Transaction is already active
|
|
98
|
+
return connection.execute(
|
|
99
|
+
statement, parameters, execution_options=execution_options
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
with connection.begin():
|
|
103
|
+
return connection.execute(
|
|
104
|
+
statement, parameters, execution_options=execution_options
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
with self.engine.begin() as connection:
|
|
108
|
+
return connection.execute(
|
|
109
|
+
statement, parameters, execution_options=execution_options
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AsyncFancyEngineWrapper:
|
|
114
|
+
"""A wrapper around SQLAlchemy AsyncEngine with additional features."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, engine: AsyncEngine) -> None:
|
|
117
|
+
self.engine = engine
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
async def x(
|
|
121
|
+
self,
|
|
122
|
+
connection: Optional[AsyncConnection],
|
|
123
|
+
statement: TypedReturnsRows[_T],
|
|
124
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
125
|
+
*,
|
|
126
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
127
|
+
) -> CursorResult[_T]: ...
|
|
128
|
+
@overload
|
|
129
|
+
async def x(
|
|
130
|
+
self,
|
|
131
|
+
connection: Optional[AsyncConnection],
|
|
132
|
+
statement: Executable,
|
|
133
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
134
|
+
*,
|
|
135
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
136
|
+
) -> CursorResult[Any]: ...
|
|
137
|
+
async def x(
|
|
138
|
+
self,
|
|
139
|
+
connection: Optional[AsyncConnection],
|
|
140
|
+
statement: Executable,
|
|
141
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
142
|
+
*,
|
|
143
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
144
|
+
) -> CursorResult[Any]:
|
|
145
|
+
"""Connect to the database, execute the query, and return the result.
|
|
146
|
+
|
|
147
|
+
If a connection is provided, use it; otherwise, create a new one.
|
|
148
|
+
"""
|
|
149
|
+
if connection:
|
|
150
|
+
return await connection.execute(
|
|
151
|
+
statement, parameters, execution_options=execution_options
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
async with self.engine.connect() as connection:
|
|
155
|
+
return await connection.execute(
|
|
156
|
+
statement, parameters, execution_options=execution_options
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@overload
|
|
160
|
+
async def tx(
|
|
161
|
+
self,
|
|
162
|
+
connection: Optional[AsyncConnection],
|
|
163
|
+
statement: TypedReturnsRows[_T],
|
|
164
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
165
|
+
*,
|
|
166
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
167
|
+
) -> CursorResult[_T]: ...
|
|
168
|
+
@overload
|
|
169
|
+
async def tx(
|
|
170
|
+
self,
|
|
171
|
+
connection: Optional[AsyncConnection],
|
|
172
|
+
statement: Executable,
|
|
173
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
174
|
+
*,
|
|
175
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
176
|
+
) -> CursorResult[Any]: ...
|
|
177
|
+
async def tx(
|
|
178
|
+
self,
|
|
179
|
+
connection: Optional[AsyncConnection],
|
|
180
|
+
statement: Executable,
|
|
181
|
+
parameters: Optional[_CoreAnyExecuteParams] = None,
|
|
182
|
+
*,
|
|
183
|
+
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
184
|
+
) -> CursorResult[Any]:
|
|
185
|
+
"""Execute the query within a transaction and return the result.
|
|
186
|
+
|
|
187
|
+
If a connection is provided, use it; otherwise, create a new one.
|
|
188
|
+
"""
|
|
189
|
+
if connection:
|
|
190
|
+
if connection.in_transaction():
|
|
191
|
+
return await connection.execute(
|
|
192
|
+
statement, parameters, execution_options=execution_options
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
async with connection.begin():
|
|
196
|
+
return await connection.execute(
|
|
197
|
+
statement, parameters, execution_options=execution_options
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
async with self.engine.begin() as connection:
|
|
201
|
+
return await connection.execute(
|
|
202
|
+
statement, parameters, execution_options=execution_options
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@overload
|
|
207
|
+
def fancy(obj: Engine, /) -> FancyEngineWrapper: ...
|
|
208
|
+
@overload
|
|
209
|
+
def fancy(obj: AsyncEngine, /) -> AsyncFancyEngineWrapper: ...
|
|
210
|
+
def fancy(obj, /):
|
|
211
|
+
"""Fancy engine wrapper makes the following syntax possible: ::
|
|
212
|
+
|
|
213
|
+
import sqlalchemy as sa
|
|
214
|
+
|
|
215
|
+
fancy_engine = fancy(sa.create_engine("sqlite:///:memory:"))
|
|
216
|
+
|
|
217
|
+
def handler(conn: sa.Connection | None = None):
|
|
218
|
+
# Execute a query outside of a transaction
|
|
219
|
+
result = fancy_engine.x(conn, sa.select(...))
|
|
220
|
+
|
|
221
|
+
# Execute a query within a transaction
|
|
222
|
+
result = fancy_engine.tx(conn, sa.insert(...))
|
|
223
|
+
|
|
224
|
+
# Using an explicit connection:
|
|
225
|
+
with fancy_engine.engine.connect() as conn:
|
|
226
|
+
handler(conn=conn)
|
|
227
|
+
|
|
228
|
+
# Using a dependency injection system:
|
|
229
|
+
handler(conn=dependency(transaction)) # Uses the provided transaction connection
|
|
230
|
+
|
|
231
|
+
# Or without a given connection (e.g. in IPython shell):
|
|
232
|
+
handler()
|
|
233
|
+
"""
|
|
234
|
+
if isinstance(obj, Engine):
|
|
235
|
+
return FancyEngineWrapper(obj)
|
|
236
|
+
elif isinstance(obj, AsyncEngine):
|
|
237
|
+
return AsyncFancyEngineWrapper(obj)
|
|
238
|
+
else:
|
|
239
|
+
raise TypeError("Unsupported input type for fancy()")
|
|
@@ -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.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sqla_fancy_core/__init__.py,sha256=iCfZrKbdoAG65CeHGducjiA1p4zYLgFSrxGDzQEhM8U,256
|
|
2
|
+
sqla_fancy_core/decorators.py,sha256=0UlOL2MUAJq3Rv3hE7mLspvrZDX16f2pm_yh1mLgQWQ,5056
|
|
3
|
+
sqla_fancy_core/factories.py,sha256=EgOhc15rCo9GyIuSNhuoB1pJ6lXx_UtRR5y9hh2lEtM,6326
|
|
4
|
+
sqla_fancy_core/wrappers.py,sha256=TesGKm9ddsd7dJwbkonZPl46Huvqeh7AfUwB623qRwA,8165
|
|
5
|
+
sqla_fancy_core-1.1.0.dist-info/METADATA,sha256=1imY4gioOlMMTxS3GBit_-eUSGz71_ArMl9D7WWMxxU,14473
|
|
6
|
+
sqla_fancy_core-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
sqla_fancy_core-1.1.0.dist-info/licenses/LICENSE,sha256=XcYXJ0ipvwOn-nzko6p_xoCCbke8tAhmlIN04rUZDLk,1068
|
|
8
|
+
sqla_fancy_core-1.1.0.dist-info/RECORD,,
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sqla-fancy-core
|
|
3
|
-
Version: 1.0.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: 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
|
-
|
|
51
|
-
from sqla_fancy_core import TableFactory
|
|
52
|
-
|
|
53
|
-
tf = TableFactory()
|
|
54
|
-
|
|
55
|
-
# Define a table
|
|
56
|
-
class Author:
|
|
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 = conn.execute(qry).mappings().first()
|
|
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 = conn.execute(qry).mappings().first()
|
|
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).all()
|
|
119
|
-
assert result == [("John Doe", "My Book")], result
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### With Pydantic Validation
|
|
123
|
-
|
|
124
|
-
```python
|
|
125
|
-
from typing import Any
|
|
126
|
-
import sqlalchemy as sa
|
|
127
|
-
from pydantic import BaseModel, Field
|
|
128
|
-
|
|
129
|
-
from sqla_fancy_core import TableFactory
|
|
130
|
-
|
|
131
|
-
tf = TableFactory()
|
|
132
|
-
|
|
133
|
-
def field(col, default: Any = ...) -> Field:
|
|
134
|
-
return col.info["kwargs"]["field"](default)
|
|
135
|
-
|
|
136
|
-
# Define a table
|
|
137
|
-
class User:
|
|
138
|
-
name = tf(
|
|
139
|
-
sa.Column("name", sa.String),
|
|
140
|
-
field=lambda default: Field(default, max_length=5),
|
|
141
|
-
)
|
|
142
|
-
Table = tf("author")
|
|
143
|
-
|
|
144
|
-
# Define a pydantic schema
|
|
145
|
-
class CreateUser(BaseModel):
|
|
146
|
-
name: str = field(User.name)
|
|
147
|
-
|
|
148
|
-
# Define a pydantic schema
|
|
149
|
-
class UpdateUser(BaseModel):
|
|
150
|
-
name: str | None = field(User.name, None)
|
|
151
|
-
|
|
152
|
-
assert CreateUser(name="John").model_dump() == {"name": "John"}
|
|
153
|
-
assert UpdateUser(name="John").model_dump() == {"name": "John"}
|
|
154
|
-
assert UpdateUser().model_dump(exclude_unset=True) == {}
|
|
155
|
-
|
|
156
|
-
with pytest.raises(ValueError):
|
|
157
|
-
CreateUser()
|
|
158
|
-
with pytest.raises(ValueError):
|
|
159
|
-
UpdateUser(name="John Doe")
|
|
160
|
-
```
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
sqla_fancy_core/__init__.py,sha256=BbhW9HwA-YvLUh_r9AIigqN4vAkdDBLf0b9rO5VnuhA,6378
|
|
2
|
-
sqla_fancy_core-1.0.2.dist-info/METADATA,sha256=qkwA6GOVIS0rr0zeelQkV6AMV6AjPOSxsz7M9yQdFUY,4815
|
|
3
|
-
sqla_fancy_core-1.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
-
sqla_fancy_core-1.0.2.dist-info/licenses/LICENSE,sha256=XcYXJ0ipvwOn-nzko6p_xoCCbke8tAhmlIN04rUZDLk,1068
|
|
5
|
-
sqla_fancy_core-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|