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.

@@ -1,159 +1,5 @@
1
1
  """SQLAlchemy core, but fancier."""
2
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, 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,,