sqlqb 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sqlqb-0.1.4/.envrc ADDED
@@ -0,0 +1,3 @@
1
+ layout_ide
2
+
3
+ export PYTHONSTARTUP=.pythonrc.py
@@ -0,0 +1 @@
1
+ from sqlqb import * # noqa: F403
@@ -1,5 +1,5 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlqb
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A simple sql query builder
5
5
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlqb"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "A simple sql query builder"
5
5
  readme = "README.md"
6
6
  authors = []
@@ -13,6 +13,12 @@ dev = [
13
13
  "pytest-cov>=7.1.0",
14
14
  "ruff>=0.15.17",
15
15
  ]
16
+ pymysql = [
17
+ "pymysql>=1.2.0",
18
+ ]
19
+ sqlalchemy = [
20
+ "sqlalchemy>=2.0.51",
21
+ ]
16
22
 
17
23
  [build-system]
18
24
  requires = ["hatchling", "hatch-vcs"]
@@ -6,6 +6,7 @@ class Delete:
6
6
  self.__table: str | None = None
7
7
  self.__wheres: list[str] = []
8
8
  self.__params: list = []
9
+ self.__named_params: dict = {}
9
10
  self.__limit: int | None = None
10
11
 
11
12
  def From(self, table: str) -> "Delete":
@@ -14,9 +15,18 @@ class Delete:
14
15
  self.__table = table
15
16
  return self
16
17
 
17
- def Where(self, condition: str, *args) -> "Delete":
18
+ def Where(self, condition: str, *args, **kwargs) -> "Delete":
19
+ if args and kwargs:
20
+ raise ValueError("error: cannot mix positional and named params")
21
+ if args and self.__named_params:
22
+ raise ValueError("error: cannot mix positional and named params")
23
+ if kwargs and self.__params:
24
+ raise ValueError("error: cannot mix positional and named params")
18
25
  self.__wheres.append(condition)
19
- self.__params += args
26
+ if kwargs:
27
+ self.__named_params.update(kwargs)
28
+ else:
29
+ self.__params += list(args)
20
30
  return self
21
31
 
22
32
  def Limit(self, limit: int) -> "Delete":
@@ -24,7 +34,9 @@ class Delete:
24
34
  return self
25
35
 
26
36
  @property
27
- def params(self) -> list:
37
+ def params(self) -> list | dict:
38
+ if self.__named_params:
39
+ return self.__named_params
28
40
  return self.__params
29
41
 
30
42
  @property
@@ -1,3 +1,5 @@
1
+ import re
2
+
1
3
  from pymysql.connections import Connection as _Connection
2
4
  from pymysql.cursors import DictCursor
3
5
 
@@ -7,59 +9,72 @@ from sqlqb import Update as _Update
7
9
  from sqlqb import Delete as _Delete
8
10
 
9
11
 
12
+ def _to_pymysql(sql: str, params: list | dict) -> tuple[str, list | dict]:
13
+ """Convert :name placeholders to %(name)s style for PyMySQL when params is a dict."""
14
+ if isinstance(params, dict):
15
+ sql = re.sub(r":([a-zA-Z_][a-zA-Z0-9_]*)", r"%(\1)s", sql)
16
+ return sql, params
17
+
18
+
10
19
  class Select(_Select):
11
- def __init__(self, connection, *args):
20
+ def __init__(self, connection: _Connection, *args):
12
21
  super().__init__(*args)
13
22
  self.__connection = connection
14
23
 
15
24
  def execute(self):
25
+ sql, params = _to_pymysql(self.sql, self.params)
16
26
  cursor = self.__connection.cursor()
17
- cursor.execute(self.sql, self.params)
27
+ cursor.execute(sql, params)
18
28
  return cursor
19
29
 
20
30
  def fetchone(self):
31
+ sql, params = _to_pymysql(self.sql, self.params)
21
32
  with self.__connection.cursor() as cursor:
22
- cursor.execute(self.sql, self.params)
33
+ cursor.execute(sql, params)
23
34
  result = cursor.fetchone()
24
35
  return result
25
36
 
26
37
  def fetchall(self):
38
+ sql, params = _to_pymysql(self.sql, self.params)
27
39
  with self.__connection.cursor() as cursor:
28
- cursor.execute(self.sql, self.params)
40
+ cursor.execute(sql, params)
29
41
  result = cursor.fetchall()
30
42
  return result
31
43
 
32
44
 
33
45
  class Insert(_Insert):
34
- def __init__(self, connection):
46
+ def __init__(self, connection: _Connection):
35
47
  super().__init__()
36
48
  self.__connection = connection
37
49
 
38
50
  def execute(self) -> int:
51
+ sql, params = _to_pymysql(self.sql, self.params)
39
52
  with self.__connection.cursor() as cursor:
40
- cursor.execute(self.sql, self.params)
53
+ cursor.execute(sql, params)
41
54
  return cursor.rowcount
42
55
 
43
56
 
44
57
  class Update(_Update):
45
- def __init__(self, connection, table: str):
58
+ def __init__(self, connection: _Connection, table: str):
46
59
  super().__init__(table)
47
60
  self.__connection = connection
48
61
 
49
62
  def execute(self) -> int:
63
+ sql, params = _to_pymysql(self.sql, self.params)
50
64
  with self.__connection.cursor() as cursor:
51
- cursor.execute(self.sql, self.params)
65
+ cursor.execute(sql, params)
52
66
  return cursor.rowcount
53
67
 
54
68
 
55
69
  class Delete(_Delete):
56
- def __init__(self, connection):
70
+ def __init__(self, connection: _Connection):
57
71
  super().__init__()
58
72
  self.__connection = connection
59
73
 
60
74
  def execute(self) -> int:
75
+ sql, params = _to_pymysql(self.sql, self.params)
61
76
  with self.__connection.cursor() as cursor:
62
- cursor.execute(self.sql, self.params)
77
+ cursor.execute(sql, params)
63
78
  return cursor.rowcount
64
79
 
65
80
 
@@ -75,8 +90,11 @@ class Connection(_Connection):
75
90
  def Insert(self) -> Insert:
76
91
  return Insert(self)
77
92
 
93
+ def Update(self, table: str) -> Update:
94
+ return Update(self, table)
95
+
78
96
  def Delete(self) -> Delete:
79
97
  return Delete(self)
80
98
 
81
- def Update(self, table: str) -> Update:
82
- return Update(self, table)
99
+
100
+ connect = Connection
@@ -7,6 +7,7 @@ class Select:
7
7
  self.__table: str | None = None
8
8
  self.__wheres: list[str] = []
9
9
  self.__params: list = []
10
+ self.__named_params: dict = {}
10
11
  self.__joins: list[str] = []
11
12
  self.__orderby: list[tuple[str, str]] = []
12
13
  self.__groupby: list[str] = []
@@ -25,14 +26,32 @@ class Select:
25
26
  self.__table = table
26
27
  return self
27
28
 
28
- def Join(self, join: str, *args) -> "Select":
29
+ def Join(self, join: str, *args, **kwargs) -> "Select":
30
+ if args and kwargs:
31
+ raise ValueError("error: cannot mix positional and named params")
32
+ if args and self.__named_params:
33
+ raise ValueError("error: cannot mix positional and named params")
34
+ if kwargs and self.__params:
35
+ raise ValueError("error: cannot mix positional and named params")
29
36
  self.__joins.append(join)
30
- self.__params += args
37
+ if kwargs:
38
+ self.__named_params.update(kwargs)
39
+ else:
40
+ self.__params += list(args)
31
41
  return self
32
42
 
33
- def Where(self, condition: str, *args) -> "Select":
43
+ def Where(self, condition: str, *args, **kwargs) -> "Select":
44
+ if args and kwargs:
45
+ raise ValueError("error: cannot mix positional and named params")
46
+ if args and self.__named_params:
47
+ raise ValueError("error: cannot mix positional and named params")
48
+ if kwargs and self.__params:
49
+ raise ValueError("error: cannot mix positional and named params")
34
50
  self.__wheres.append(condition)
35
- self.__params += args
51
+ if kwargs:
52
+ self.__named_params.update(kwargs)
53
+ else:
54
+ self.__params += list(args)
36
55
  return self
37
56
 
38
57
  def OrderBy(self, column: str, direction: str = "ASC") -> "Select":
@@ -54,7 +73,9 @@ class Select:
54
73
  return self
55
74
 
56
75
  @property
57
- def params(self) -> list:
76
+ def params(self) -> list | dict:
77
+ if self.__named_params:
78
+ return self.__named_params
58
79
  return self.__params
59
80
 
60
81
  @property
@@ -62,8 +83,9 @@ class Select:
62
83
  sql = io.StringIO()
63
84
  sql.write("SELECT ")
64
85
  sql.write(", ".join(self.__columns or "*"))
65
- sql.write(" FROM ")
66
- sql.write(self.__table)
86
+ if self.__table:
87
+ sql.write(" FROM ")
88
+ sql.write(self.__table)
67
89
  if self.__joins:
68
90
  sql.write(" ")
69
91
  sql.write(" ".join(self.__joins))
@@ -0,0 +1,97 @@
1
+ from sqlalchemy import create_engine as _create_engine
2
+ from sqlalchemy import text
3
+ from sqlalchemy.engine import Connection as _Connection
4
+ from sqlalchemy.engine import Engine as _Engine
5
+
6
+ from sqlqb import Select as _Select
7
+ from sqlqb import Insert as _Insert
8
+ from sqlqb import Update as _Update
9
+ from sqlqb import Delete as _Delete
10
+
11
+
12
+ def _bind(sql: str, params: list | dict) -> tuple[str, dict]:
13
+ """Convert params for SQLAlchemy text(): pass dicts through, replace ? with :p0/:p1/... for lists."""
14
+ if isinstance(params, dict):
15
+ return sql, params
16
+ named: dict = {}
17
+ for i, val in enumerate(params):
18
+ key = f"p{i}"
19
+ sql = sql.replace("?", f":{key}", 1)
20
+ named[key] = val
21
+ return sql, named
22
+
23
+
24
+ class Select(_Select):
25
+ def __init__(self, connection: _Connection, *args):
26
+ super().__init__(*args)
27
+ self.__connection = connection
28
+
29
+ def execute(self):
30
+ sql, params = _bind(self.sql, self.params)
31
+ return self.__connection.execute(text(sql), params)
32
+
33
+ def fetchone(self):
34
+ sql, params = _bind(self.sql, self.params)
35
+ row = self.execute().mappings().fetchone()
36
+ return dict(row) if row is not None else None
37
+
38
+ def fetchall(self) -> list:
39
+ sql, params = _bind(self.sql, self.params)
40
+ return [dict(row) for row in self.execute().mappings()]
41
+
42
+
43
+ class Insert(_Insert):
44
+ def __init__(self, connection: _Connection):
45
+ super().__init__()
46
+ self.__connection = connection
47
+
48
+ def execute(self) -> int:
49
+ sql, params = _bind(self.sql, self.params)
50
+ return self.__connection.execute(text(sql), params).rowcount
51
+
52
+
53
+ class Update(_Update):
54
+ def __init__(self, connection: _Connection, table: str):
55
+ super().__init__(table)
56
+ self.__connection = connection
57
+
58
+ def execute(self) -> int:
59
+ sql, params = _bind(self.sql, self.params)
60
+ return self.__connection.execute(text(sql), params).rowcount
61
+
62
+
63
+ class Delete(_Delete):
64
+ def __init__(self, connection: _Connection):
65
+ super().__init__()
66
+ self.__connection = connection
67
+
68
+ def execute(self) -> int:
69
+ sql, params = _bind(self.sql, self.params)
70
+ return self.__connection.execute(text(sql), params).rowcount
71
+
72
+
73
+ class Connection(_Connection):
74
+ def Select(self, *args) -> Select:
75
+ return Select(self, *args)
76
+
77
+ def Insert(self) -> Insert:
78
+ return Insert(self)
79
+
80
+ def Update(self, table: str) -> Update:
81
+ return Update(self, table)
82
+
83
+ def Delete(self) -> Delete:
84
+ return Delete(self)
85
+
86
+
87
+ class Engine(_Engine):
88
+ def connect(self) -> Connection:
89
+ conn = super().connect()
90
+ conn.__class__ = Connection
91
+ return conn
92
+
93
+
94
+ def create_engine(url: str, **kwargs) -> Engine:
95
+ engine = _create_engine(url, **kwargs)
96
+ engine.__class__ = Engine
97
+ return engine
@@ -1,4 +1,6 @@
1
- import sqlite3
1
+ from sqlite3 import Connection as _Connection
2
+ from sqlite3 import Cursor as _Cursor
3
+ from sqlite3 import connect as _connect
2
4
 
3
5
  from sqlqb import Select as _Select
4
6
  from sqlqb import Insert as _Insert
@@ -7,11 +9,11 @@ from sqlqb import Delete as _Delete
7
9
 
8
10
 
9
11
  class Select(_Select):
10
- def __init__(self, connection, *args):
12
+ def __init__(self, connection: _Connection, *args):
11
13
  super().__init__(*args)
12
14
  self.__connection = connection
13
15
 
14
- def execute(self) -> sqlite3.Cursor:
16
+ def execute(self) -> _Cursor:
15
17
  return self.__connection.execute(self.sql, self.params)
16
18
 
17
19
  def fetchone(self):
@@ -22,7 +24,7 @@ class Select(_Select):
22
24
 
23
25
 
24
26
  class Insert(_Insert):
25
- def __init__(self, connection):
27
+ def __init__(self, connection: _Connection):
26
28
  super().__init__()
27
29
  self.__connection = connection
28
30
 
@@ -31,7 +33,7 @@ class Insert(_Insert):
31
33
 
32
34
 
33
35
  class Update(_Update):
34
- def __init__(self, connection, table: str):
36
+ def __init__(self, connection: _Connection, table: str):
35
37
  super().__init__(table)
36
38
  self.__connection = connection
37
39
 
@@ -40,7 +42,7 @@ class Update(_Update):
40
42
 
41
43
 
42
44
  class Delete(_Delete):
43
- def __init__(self, connection):
45
+ def __init__(self, connection: _Connection):
44
46
  super().__init__()
45
47
  self.__connection = connection
46
48
 
@@ -48,7 +50,7 @@ class Delete(_Delete):
48
50
  return self.__connection.execute(self.sql, self.params).rowcount
49
51
 
50
52
 
51
- class Connection(sqlite3.Connection):
53
+ class Connection(_Connection):
52
54
  def __init__(self, *args, **kwargs):
53
55
  super().__init__(*args, **kwargs)
54
56
  self.row_factory = lambda cursor, row: dict(zip([col[0] for col in cursor.description], row))
@@ -59,12 +61,12 @@ class Connection(sqlite3.Connection):
59
61
  def Insert(self) -> Insert:
60
62
  return Insert(self)
61
63
 
62
- def Delete(self) -> Delete:
63
- return Delete(self)
64
-
65
64
  def Update(self, table: str) -> Update:
66
65
  return Update(self, table)
67
66
 
67
+ def Delete(self) -> Delete:
68
+ return Delete(self)
69
+
68
70
 
69
71
  def connect(database, **kwargs) -> Connection:
70
- return sqlite3.connect(database, factory=Connection, **kwargs)
72
+ return _connect(database, factory=Connection, **kwargs)
@@ -9,6 +9,7 @@ class Update:
9
9
  self.__sets: dict[str, object] = {}
10
10
  self.__wheres: list[str] = []
11
11
  self.__where_params: list = []
12
+ self.__where_named_params: dict = {}
12
13
  self.__limit: int | None = None
13
14
 
14
15
  def Set(self, **kwargs) -> "Update":
@@ -17,9 +18,18 @@ class Update:
17
18
  self.__sets.update(kwargs)
18
19
  return self
19
20
 
20
- def Where(self, condition: str, *args) -> "Update":
21
+ def Where(self, condition: str, *args, **kwargs) -> "Update":
22
+ if args and kwargs:
23
+ raise ValueError("error: cannot mix positional and named params")
24
+ if args and self.__where_named_params:
25
+ raise ValueError("error: cannot mix positional and named params")
26
+ if kwargs and self.__where_params:
27
+ raise ValueError("error: cannot mix positional and named params")
21
28
  self.__wheres.append(condition)
22
- self.__where_params += args
29
+ if kwargs:
30
+ self.__where_named_params.update(kwargs)
31
+ else:
32
+ self.__where_params += list(args)
23
33
  return self
24
34
 
25
35
  def Limit(self, limit: int) -> "Update":
@@ -27,9 +37,13 @@ class Update:
27
37
  return self
28
38
 
29
39
  @property
30
- def params(self) -> list:
40
+ def params(self) -> list | dict:
31
41
  if not self.__sets:
32
42
  return []
43
+ if self.__where_named_params:
44
+ named = dict(self.__sets)
45
+ named.update(self.__where_named_params)
46
+ return named
33
47
  return list(self.__sets.values()) + self.__where_params
34
48
 
35
49
  @property
@@ -40,7 +54,10 @@ class Update:
40
54
  sql.write("UPDATE ")
41
55
  sql.write(self.__table)
42
56
  sql.write(" SET ")
43
- sql.write(", ".join([f"{col} = ?" for col in self.__sets]))
57
+ if self.__where_named_params:
58
+ sql.write(", ".join([f"{col} = :{col}" for col in self.__sets]))
59
+ else:
60
+ sql.write(", ".join([f"{col} = ?" for col in self.__sets]))
44
61
  if self.__wheres:
45
62
  sql.write(" WHERE ")
46
63
  sql.write(" AND ".join(self.__wheres))
@@ -0,0 +1,223 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ from sqlqb import pymysql
5
+ from sqlqb.pymysql import Select, Insert, Update, Delete, Connection
6
+ from pymysql.cursors import DictCursor
7
+
8
+
9
+ @pytest.fixture
10
+ def mock_conn():
11
+ """Mock pymysql connection whose cursor supports both direct use and context manager."""
12
+ conn = MagicMock()
13
+ cursor = MagicMock()
14
+ conn.cursor.return_value = cursor
15
+ cursor.__enter__.return_value = cursor
16
+ cursor.__exit__.return_value = False
17
+ return conn, cursor
18
+
19
+
20
+ @pytest.fixture
21
+ def connection():
22
+ """pymysql.Connection instance with the pymysql parent __init__ patched out."""
23
+ with patch.object(pymysql._Connection, "__init__", return_value=None):
24
+ conn = pymysql.Connection.__new__(pymysql.Connection)
25
+ yield conn
26
+
27
+
28
+ class TestSelect:
29
+ def test_execute_returns_cursor(self, mock_conn):
30
+ conn, cursor = mock_conn
31
+ result = Select(conn, "id", "name").From("users").execute()
32
+ assert result is cursor
33
+
34
+ def test_execute_calls_cursor_execute_with_sql_and_params(self, mock_conn):
35
+ conn, cursor = mock_conn
36
+ q = Select(conn, "name").From("users").Where("id = ?", 1)
37
+ q.execute()
38
+ cursor.execute.assert_called_once_with(q.sql, q.params)
39
+
40
+ def test_execute_does_not_use_context_manager(self, mock_conn):
41
+ conn, cursor = mock_conn
42
+ Select(conn, "id").From("users").execute()
43
+ cursor.__enter__.assert_not_called()
44
+
45
+ def test_fetchone_returns_result(self, mock_conn):
46
+ conn, cursor = mock_conn
47
+ cursor.fetchone.return_value = {"id": 1, "name": "Alice"}
48
+ result = Select(conn, "id", "name").From("users").Where("id = ?", 1).fetchone()
49
+ assert result == {"id": 1, "name": "Alice"}
50
+
51
+ def test_fetchone_returns_none_when_no_row(self, mock_conn):
52
+ conn, cursor = mock_conn
53
+ cursor.fetchone.return_value = None
54
+ result = Select(conn, "id").From("users").Where("id = ?", 999).fetchone()
55
+ assert result is None
56
+
57
+ def test_fetchone_calls_cursor_execute(self, mock_conn):
58
+ conn, cursor = mock_conn
59
+ q = Select(conn, "name").From("users").Where("id = ?", 1)
60
+ q.fetchone()
61
+ cursor.execute.assert_called_once_with(q.sql, q.params)
62
+
63
+ def test_fetchone_uses_context_manager(self, mock_conn):
64
+ conn, cursor = mock_conn
65
+ Select(conn, "name").From("users").fetchone()
66
+ cursor.__enter__.assert_called_once()
67
+
68
+ def test_fetchall_returns_result(self, mock_conn):
69
+ conn, cursor = mock_conn
70
+ cursor.fetchall.return_value = [{"id": 1}, {"id": 2}]
71
+ result = Select(conn, "id").From("users").fetchall()
72
+ assert result == [{"id": 1}, {"id": 2}]
73
+
74
+ def test_fetchall_returns_empty_list(self, mock_conn):
75
+ conn, cursor = mock_conn
76
+ cursor.fetchall.return_value = []
77
+ result = Select(conn, "id").From("users").fetchall()
78
+ assert result == []
79
+
80
+ def test_fetchall_calls_cursor_execute(self, mock_conn):
81
+ conn, cursor = mock_conn
82
+ q = Select(conn, "id").From("users")
83
+ q.fetchall()
84
+ cursor.execute.assert_called_once_with(q.sql, q.params)
85
+
86
+ def test_fetchall_uses_context_manager(self, mock_conn):
87
+ conn, cursor = mock_conn
88
+ Select(conn, "id").From("users").fetchall()
89
+ cursor.__enter__.assert_called_once()
90
+
91
+ def test_multiple_where_params_passed_correctly(self, mock_conn):
92
+ conn, cursor = mock_conn
93
+ q = Select(conn, "name").From("users").Where("age > ?", 18).Where("active = ?", True)
94
+ q.fetchall()
95
+ cursor.execute.assert_called_once_with(q.sql, [18, True])
96
+
97
+ def test_chaining_with_order_and_limit(self, mock_conn):
98
+ conn, cursor = mock_conn
99
+ cursor.fetchall.return_value = [{"name": "Alice"}]
100
+ result = Select(conn, "name").From("users").Where("age >= ?", 18).OrderBy("name").Limit(1).fetchall()
101
+ assert result == [{"name": "Alice"}]
102
+
103
+
104
+ class TestInsert:
105
+ def test_execute_returns_rowcount(self, mock_conn):
106
+ conn, cursor = mock_conn
107
+ cursor.rowcount = 1
108
+ count = Insert(conn).Into("users").Values(id=1, name="Alice").execute()
109
+ assert count == 1
110
+
111
+ def test_execute_calls_cursor_execute(self, mock_conn):
112
+ conn, cursor = mock_conn
113
+ q = Insert(conn).Into("users").Values(id=1, name="Alice")
114
+ q.execute()
115
+ cursor.execute.assert_called_once_with(q.sql, q.params)
116
+
117
+ def test_execute_uses_context_manager(self, mock_conn):
118
+ conn, cursor = mock_conn
119
+ Insert(conn).Into("users").Values(id=1, name="Alice").execute()
120
+ cursor.__enter__.assert_called_once()
121
+
122
+ def test_execute_multiple_rows_rowcount(self, mock_conn):
123
+ conn, cursor = mock_conn
124
+ cursor.rowcount = 2
125
+ count = Insert(conn).Into("users").Values(id=1, name="Alice").Values(id=2, name="Bob").execute()
126
+ assert count == 2
127
+
128
+
129
+ class TestUpdate:
130
+ def test_execute_returns_rowcount(self, mock_conn):
131
+ conn, cursor = mock_conn
132
+ cursor.rowcount = 1
133
+ count = Update(conn, "users").Set(name="Alice").Where("id = ?", 1).execute()
134
+ assert count == 1
135
+
136
+ def test_execute_calls_cursor_execute(self, mock_conn):
137
+ conn, cursor = mock_conn
138
+ q = Update(conn, "users").Set(name="Alice").Where("id = ?", 1)
139
+ q.execute()
140
+ cursor.execute.assert_called_once_with(q.sql, q.params)
141
+
142
+ def test_execute_uses_context_manager(self, mock_conn):
143
+ conn, cursor = mock_conn
144
+ Update(conn, "users").Set(name="Alice").execute()
145
+ cursor.__enter__.assert_called_once()
146
+
147
+ def test_execute_no_where_returns_affected_rowcount(self, mock_conn):
148
+ conn, cursor = mock_conn
149
+ cursor.rowcount = 5
150
+ count = Update(conn, "users").Set(active=False).execute()
151
+ assert count == 5
152
+
153
+ def test_execute_multiple_set_columns(self, mock_conn):
154
+ conn, cursor = mock_conn
155
+ cursor.rowcount = 1
156
+ q = Update(conn, "users").Set(name="Bob", age=20).Where("id = ?", 2)
157
+ count = q.execute()
158
+ cursor.execute.assert_called_once_with(q.sql, q.params)
159
+ assert count == 1
160
+
161
+
162
+ class TestDelete:
163
+ def test_execute_returns_rowcount(self, mock_conn):
164
+ conn, cursor = mock_conn
165
+ cursor.rowcount = 1
166
+ count = Delete(conn).From("users").Where("id = ?", 1).execute()
167
+ assert count == 1
168
+
169
+ def test_execute_calls_cursor_execute(self, mock_conn):
170
+ conn, cursor = mock_conn
171
+ q = Delete(conn).From("users").Where("id = ?", 1)
172
+ q.execute()
173
+ cursor.execute.assert_called_once_with(q.sql, q.params)
174
+
175
+ def test_execute_uses_context_manager(self, mock_conn):
176
+ conn, cursor = mock_conn
177
+ Delete(conn).From("users").Where("id = ?", 1).execute()
178
+ cursor.__enter__.assert_called_once()
179
+
180
+ def test_execute_no_where_returns_all_rowcount(self, mock_conn):
181
+ conn, cursor = mock_conn
182
+ cursor.rowcount = 3
183
+ count = Delete(conn).From("users").execute()
184
+ assert count == 3
185
+
186
+ def test_execute_with_limit(self, mock_conn):
187
+ conn, cursor = mock_conn
188
+ cursor.rowcount = 2
189
+ q = Delete(conn).From("users").Limit(2)
190
+ count = q.execute()
191
+ cursor.execute.assert_called_once_with(q.sql, q.params)
192
+ assert count == 2
193
+
194
+
195
+ class TestConnection:
196
+ def test_default_cursorclass_is_dictcursor(self):
197
+ with patch.object(pymysql._Connection, "__init__", return_value=None) as mock_init:
198
+ Connection(host="localhost", user="root", password="secret", db="mydb")
199
+ kwargs = mock_init.call_args[1]
200
+ assert kwargs["cursorclass"] is DictCursor
201
+
202
+ def test_custom_cursorclass_is_not_overridden(self):
203
+ custom_cursor = MagicMock()
204
+ with patch.object(pymysql._Connection, "__init__", return_value=None) as mock_init:
205
+ Connection(host="localhost", cursorclass=custom_cursor)
206
+ kwargs = mock_init.call_args[1]
207
+ assert kwargs["cursorclass"] is custom_cursor
208
+
209
+ def test_select_returns_select_instance(self, connection):
210
+ result = connection.Select("id", "name")
211
+ assert isinstance(result, Select)
212
+
213
+ def test_insert_returns_insert_instance(self, connection):
214
+ result = connection.Insert()
215
+ assert isinstance(result, Insert)
216
+
217
+ def test_delete_returns_delete_instance(self, connection):
218
+ result = connection.Delete()
219
+ assert isinstance(result, Delete)
220
+
221
+ def test_update_returns_update_instance(self, connection):
222
+ result = connection.Update("users")
223
+ assert isinstance(result, Update)
@@ -0,0 +1,226 @@
1
+ import pytest
2
+ from sqlalchemy import text
3
+ from sqlalchemy.engine import Connection as _SAConnection, Engine as _SAEngine
4
+ from sqlalchemy.pool import StaticPool
5
+
6
+ from sqlqb.sqlalchemy import Connection, Engine, create_engine
7
+
8
+
9
+ @pytest.fixture(scope="module")
10
+ def engine():
11
+ e = create_engine(
12
+ "sqlite:///:memory:",
13
+ connect_args={"check_same_thread": False},
14
+ poolclass=StaticPool,
15
+ )
16
+ with e.connect() as conn:
17
+ conn.execute(text("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"))
18
+ conn.Insert().Into("users").Values(id=1, name="Alice", age=30).execute()
19
+ conn.Insert().Into("users").Values(id=2, name="Bob", age=17).execute()
20
+ conn.Insert().Into("users").Values(id=3, name="Carol", age=25).execute()
21
+ conn.commit()
22
+ return e
23
+
24
+
25
+ @pytest.fixture
26
+ def conn(engine):
27
+ with engine.connect() as c:
28
+ yield c
29
+ c.rollback()
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Engine
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ class TestEngine:
38
+ def test_create_engine_returns_engine_subclass(self):
39
+ e = create_engine("sqlite:///:memory:")
40
+ assert isinstance(e, Engine)
41
+ assert isinstance(e, _SAEngine)
42
+
43
+ def test_connect_returns_connection_subclass(self, engine):
44
+ with engine.connect() as conn:
45
+ assert isinstance(conn, Connection)
46
+ assert isinstance(conn, _SAConnection)
47
+
48
+ def test_connect_as_context_manager(self, engine):
49
+ with engine.connect() as conn:
50
+ assert conn is not None
51
+
52
+ def test_connect_without_context_manager(self, engine):
53
+ conn = engine.connect()
54
+ assert isinstance(conn, Connection)
55
+ conn.close()
56
+
57
+ def test_engine_dialect_accessible(self):
58
+ e = create_engine("sqlite:///:memory:")
59
+ assert e.dialect.name == "sqlite"
60
+
61
+ def test_engine_dispose(self):
62
+ e = create_engine("sqlite:///:memory:")
63
+ e.dispose()
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Connection
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ class TestConnection:
72
+ def test_sa_methods_are_accessible(self, conn):
73
+ assert callable(conn.commit)
74
+ assert callable(conn.rollback)
75
+ assert callable(conn.close)
76
+ assert callable(conn.execute)
77
+
78
+ def test_select_returns_select_instance(self, conn):
79
+ from sqlqb.sqlalchemy import Select
80
+
81
+ assert isinstance(conn.Select("id"), Select)
82
+
83
+ def test_insert_returns_insert_instance(self, conn):
84
+ from sqlqb.sqlalchemy import Insert
85
+
86
+ assert isinstance(conn.Insert(), Insert)
87
+
88
+ def test_update_returns_update_instance(self, conn):
89
+ from sqlqb.sqlalchemy import Update
90
+
91
+ assert isinstance(conn.Update("users"), Update)
92
+
93
+ def test_delete_returns_delete_instance(self, conn):
94
+ from sqlqb.sqlalchemy import Delete
95
+
96
+ assert isinstance(conn.Delete(), Delete)
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Select
101
+ # ---------------------------------------------------------------------------
102
+
103
+
104
+ class TestSelect:
105
+ def test_fetchall(self, conn):
106
+ rows = conn.Select("id", "name").From("users").fetchall()
107
+ assert rows == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Carol"}]
108
+
109
+ def test_fetchone(self, conn):
110
+ row = conn.Select("name").From("users").Where("id = ?", 1).fetchone()
111
+ assert row == {"name": "Alice"}
112
+
113
+ def test_fetchone_returns_none_when_no_row(self, conn):
114
+ row = conn.Select("name").From("users").Where("id = ?", 999).fetchone()
115
+ assert row is None
116
+
117
+ def test_fetchall_returns_empty_list(self, conn):
118
+ rows = conn.Select("name").From("users").Where("id = ?", 999).fetchall()
119
+ assert rows == []
120
+
121
+ def test_where_single_param(self, conn):
122
+ rows = conn.Select("name").From("users").Where("age >= ?", 18).fetchall()
123
+ assert rows == [{"name": "Alice"}, {"name": "Carol"}]
124
+
125
+ def test_where_multiple_params(self, conn):
126
+ rows = conn.Select("name").From("users").Where("age >= ?", 18).Where("age <= ?", 29).fetchall()
127
+ assert rows == [{"name": "Carol"}]
128
+
129
+ def test_order_by_asc(self, conn):
130
+ rows = conn.Select("name").From("users").OrderBy("name").fetchall()
131
+ assert rows == [{"name": "Alice"}, {"name": "Bob"}, {"name": "Carol"}]
132
+
133
+ def test_order_by_desc(self, conn):
134
+ rows = conn.Select("name").From("users").OrderBy("age", "DESC").fetchall()
135
+ assert rows == [{"name": "Alice"}, {"name": "Carol"}, {"name": "Bob"}]
136
+
137
+ def test_limit(self, conn):
138
+ rows = conn.Select("id").From("users").Limit(2).fetchall()
139
+ assert rows == [{"id": 1}, {"id": 2}]
140
+
141
+ def test_execute_returns_sa_result(self, conn):
142
+ result = conn.Select("id", "name").From("users").execute()
143
+ rows = [dict(r) for r in result.mappings()]
144
+ assert rows == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Carol"}]
145
+
146
+ def test_fetchall_returns_plain_dicts(self, conn):
147
+ rows = conn.Select("id").From("users").Limit(1).fetchall()
148
+ assert type(rows[0]) is dict
149
+
150
+ def test_fetchone_returns_plain_dict(self, conn):
151
+ row = conn.Select("id").From("users").Where("id = ?", 1).fetchone()
152
+ assert type(row) is dict
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Insert
157
+ # ---------------------------------------------------------------------------
158
+
159
+
160
+ class TestInsert:
161
+ def test_execute_returns_rowcount(self, conn):
162
+ count = conn.Insert().Into("users").Values(id=4, name="Dave", age=40).execute()
163
+ assert count == 1
164
+
165
+ def test_inserted_row_is_queryable(self, conn):
166
+ conn.Insert().Into("users").Values(id=4, name="Dave", age=40).execute()
167
+ row = conn.Select("name").From("users").Where("id = ?", 4).fetchone()
168
+ assert row == {"name": "Dave"}
169
+
170
+ def test_insert_multiple_rows(self, conn):
171
+ count = conn.Insert().Into("users").Values(id=4, name="Dave", age=40).Values(id=5, name="Eve", age=22).execute()
172
+ assert count == 2
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Update
177
+ # ---------------------------------------------------------------------------
178
+
179
+
180
+ class TestUpdate:
181
+ def test_execute_returns_rowcount(self, conn):
182
+ count = conn.Update("users").Set(name="Alicia").Where("id = ?", 1).execute()
183
+ assert count == 1
184
+
185
+ def test_update_is_reflected(self, conn):
186
+ conn.Update("users").Set(name="Alicia").Where("id = ?", 1).execute()
187
+ row = conn.Select("name").From("users").Where("id = ?", 1).fetchone()
188
+ assert row == {"name": "Alicia"}
189
+
190
+ def test_update_multiple_columns(self, conn):
191
+ conn.Update("users").Set(name="Bobby", age=18).Where("id = ?", 2).execute()
192
+ row = conn.Select("name", "age").From("users").Where("id = ?", 2).fetchone()
193
+ assert row == {"name": "Bobby", "age": 18}
194
+
195
+ def test_update_no_where_affects_all(self, conn):
196
+ count = conn.Update("users").Set(age=0).execute()
197
+ assert count == 3
198
+
199
+ def test_update_affects_correct_rows_only(self, conn):
200
+ conn.Update("users").Set(age=99).Where("id = ?", 1).execute()
201
+ rows = conn.Select("id", "age").From("users").fetchall()
202
+ assert rows == [{"id": 1, "age": 99}, {"id": 2, "age": 17}, {"id": 3, "age": 25}]
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Delete
207
+ # ---------------------------------------------------------------------------
208
+
209
+
210
+ class TestDelete:
211
+ def test_execute_returns_rowcount(self, conn):
212
+ conn.Insert().Into("users").Values(id=4, name="Temp", age=1).execute()
213
+ count = conn.Delete().From("users").Where("id = ?", 4).execute()
214
+ assert count == 1
215
+
216
+ def test_deleted_row_is_gone(self, conn):
217
+ conn.Insert().Into("users").Values(id=4, name="Temp", age=1).execute()
218
+ conn.Delete().From("users").Where("id = ?", 4).execute()
219
+ row = conn.Select("*").From("users").Where("id = ?", 4).fetchone()
220
+ assert row is None
221
+
222
+ def test_delete_no_where_clears_table(self, conn):
223
+ count = conn.Delete().From("users").execute()
224
+ assert count == 3
225
+ rows = conn.Select("*").From("users").fetchall()
226
+ assert rows == []
@@ -1,11 +1,11 @@
1
1
  import pytest
2
2
 
3
- from sqlqb import sqlite
3
+ from sqlqb import sqlite3
4
4
 
5
5
 
6
6
  @pytest.fixture
7
7
  def conn():
8
- c = sqlite.connect(":memory:")
8
+ c = sqlite3.connect(":memory:")
9
9
  c.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
10
10
  c.executemany("INSERT INTO users VALUES (?, ?, ?)", [(1, "Alice", 30), (2, "Bob", 17), (3, "Carol", 25)])
11
11
  c.commit()
@@ -30,13 +30,7 @@ class TestSelect:
30
30
  assert cursor.fetchone() == {"id": 1, "name": "Alice", "age": 30}
31
31
 
32
32
  def test_chaining(self, conn):
33
- rows = (
34
- conn.Select("name")
35
- .From("users")
36
- .Where("age >= ?", 18)
37
- .OrderBy("name")
38
- .fetchall()
39
- )
33
+ rows = conn.Select("name").From("users").Where("age >= ?", 18).OrderBy("name").fetchall()
40
34
  assert rows == [{"name": "Alice"}, {"name": "Carol"}]
41
35
 
42
36
  def test_limit(self, conn):
@@ -107,6 +101,7 @@ class TestDelete:
107
101
 
108
102
  def test_delete_with_limit_not_supported(self, conn):
109
103
  # NOTE: sqlite library of python does not support LIMIT in delete queries
110
- import sqlite3
111
- with pytest.raises(sqlite3.OperationalError):
104
+ import sqlite3 as stdlib_sqlite3
105
+
106
+ with pytest.raises(stdlib_sqlite3.OperationalError):
112
107
  conn.Delete().From("users").Limit(2).execute()
@@ -95,6 +95,61 @@ wheels = [
95
95
  { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
96
96
  ]
97
97
 
98
+ [[package]]
99
+ name = "greenlet"
100
+ version = "3.5.2"
101
+ source = { registry = "https://pypi.org/simple" }
102
+ sdist = { url = "https://files.pythonhosted.org/packages/dd/8b/befc3cb36965f397d87e86fb3b00e3ec0dc67c1ecb0986d7f54ee528f018/greenlet-3.5.2.tar.gz", hash = "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", size = 199243, upload-time = "2026-06-17T20:19:01.317Z" }
103
+ wheels = [
104
+ { url = "https://files.pythonhosted.org/packages/3f/7a/6bc2a7835731387ed303b9390ce68a116ab053df05450a59181239200454/greenlet-3.5.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f", size = 288351, upload-time = "2026-06-17T17:36:17.019Z" },
105
+ { url = "https://files.pythonhosted.org/packages/57/1b/bd98062fcef6d0e9d0873ab6f2d029772e6ea342972ae43275bd6177900f/greenlet-3.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742", size = 604273, upload-time = "2026-06-17T18:07:20.296Z" },
106
+ { url = "https://files.pythonhosted.org/packages/25/e6/fe392c522bf45d976abe7db2793f6ef4e87b053ebb869deeaae46aeb54da/greenlet-3.5.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea", size = 616536, upload-time = "2026-06-17T18:29:43.205Z" },
107
+ { url = "https://files.pythonhosted.org/packages/68/4a/399ff81fa93a19d6a9df394cef0355f082dbc19ad41aba9593cd0ad444e2/greenlet-3.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3", size = 613749, upload-time = "2026-06-17T17:39:28.148Z" },
108
+ { url = "https://files.pythonhosted.org/packages/a5/75/f519593f12ad43d08e28c03a95cfe2eeae011707dbc9dab0c4a263ce90f9/greenlet-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094", size = 1573725, upload-time = "2026-06-17T18:22:12.023Z" },
109
+ { url = "https://files.pythonhosted.org/packages/f1/bc/bc1ea4b0754c6c51bbf9d94677b0b1f7fbda8cbb404e44a896854fc0a940/greenlet-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad", size = 1638132, upload-time = "2026-06-17T17:40:06.971Z" },
110
+ { url = "https://files.pythonhosted.org/packages/36/c0/f0f5a34247df60de285f75f22e57f14027f4b3c43820981854b5b643ca6d/greenlet-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146", size = 239393, upload-time = "2026-06-17T17:33:47.09Z" },
111
+ { url = "https://files.pythonhosted.org/packages/09/17/a8544e165445f30aea67a8d9cf2786d2bb0eb1b0e0d224b4d9bd80e2d587/greenlet-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9", size = 237723, upload-time = "2026-06-17T17:36:47.776Z" },
112
+ { url = "https://files.pythonhosted.org/packages/d0/3c/bb37b9d40d65b0741a8b040ca5c307034d0a9822994dff5f825c88dd7a6b/greenlet-3.5.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", size = 287178, upload-time = "2026-06-17T17:35:25.132Z" },
113
+ { url = "https://files.pythonhosted.org/packages/f0/a6/0c5902393f492f8ceb19d0b5cf139284e3a11b333a049739643b1036b6f8/greenlet-3.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", size = 606900, upload-time = "2026-06-17T18:07:21.692Z" },
114
+ { url = "https://files.pythonhosted.org/packages/d8/7c/42899c31d4b87148ae4e3f87f63e13398824be6241f4dde42ded95768a34/greenlet-3.5.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", size = 619265, upload-time = "2026-06-17T18:29:44.837Z" },
115
+ { url = "https://files.pythonhosted.org/packages/d3/52/4ff8c98d3cfe62b4515f8584ae14510a58f35c549cc5292b78d9b7a40b70/greenlet-3.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", size = 616187, upload-time = "2026-06-17T17:39:29.473Z" },
116
+ { url = "https://files.pythonhosted.org/packages/c9/a6/269c8bf9aefc13361ce1088f0e392b154cb21005de7862e42b5d782b81fd/greenlet-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", size = 1573778, upload-time = "2026-06-17T18:22:13.497Z" },
117
+ { url = "https://files.pythonhosted.org/packages/1f/9b/391d015cbc6323e81b14c02cf825fdca7e0049c9bb489bf4ac72883118ba/greenlet-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", size = 1638092, upload-time = "2026-06-17T17:40:08.163Z" },
118
+ { url = "https://files.pythonhosted.org/packages/49/53/5b4df711f4356c62e85d9f819d87966d526d1cfb32bae49a8f7d6fc36ea4/greenlet-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", size = 239352, upload-time = "2026-06-17T17:38:51.593Z" },
119
+ { url = "https://files.pythonhosted.org/packages/bb/b6/18efc3a329ec035c3f344b8f2b60356451950ddf9b7b64ff00023778a1dd/greenlet-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", size = 237635, upload-time = "2026-06-17T17:35:36.632Z" },
120
+ { url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" },
121
+ { url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" },
122
+ { url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" },
123
+ { url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" },
124
+ { url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" },
125
+ { url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" },
126
+ { url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" },
127
+ { url = "https://files.pythonhosted.org/packages/de/e3/3b62145fe917311732041a258adb218248add00542e3131c48bd047fbed5/greenlet-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", size = 239038, upload-time = "2026-06-17T17:37:56.792Z" },
128
+ { url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" },
129
+ { url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" },
130
+ { url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" },
131
+ { url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" },
132
+ { url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" },
133
+ { url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" },
134
+ { url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" },
135
+ { url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" },
136
+ { url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" },
137
+ { url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" },
138
+ { url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" },
139
+ { url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" },
140
+ { url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" },
141
+ { url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" },
142
+ { url = "https://files.pythonhosted.org/packages/7f/4d/86d7768bd53e9907de0333df215c2018cd01a593b3715cbd79aa82dd94b7/greenlet-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", size = 239579, upload-time = "2026-06-17T17:39:39.954Z" },
143
+ { url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" },
144
+ { url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" },
145
+ { url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" },
146
+ { url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" },
147
+ { url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" },
148
+ { url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" },
149
+ { url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" },
150
+ { url = "https://files.pythonhosted.org/packages/37/45/f794a81c91e9942c61f9110bd1f9a38a0ea565eab57f8b08cd53d3131e48/greenlet-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", size = 242062, upload-time = "2026-06-17T17:35:39.814Z" },
151
+ ]
152
+
98
153
  [[package]]
99
154
  name = "iniconfig"
100
155
  version = "2.3.0"
@@ -131,6 +186,15 @@ wheels = [
131
186
  { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
132
187
  ]
133
188
 
189
+ [[package]]
190
+ name = "pymysql"
191
+ version = "1.2.0"
192
+ source = { registry = "https://pypi.org/simple" }
193
+ sdist = { url = "https://files.pythonhosted.org/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" }
194
+ wheels = [
195
+ { url = "https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" },
196
+ ]
197
+
134
198
  [[package]]
135
199
  name = "pytest"
136
200
  version = "9.0.3"
@@ -186,9 +250,50 @@ wheels = [
186
250
  { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
187
251
  ]
188
252
 
253
+ [[package]]
254
+ name = "sqlalchemy"
255
+ version = "2.0.51"
256
+ source = { registry = "https://pypi.org/simple" }
257
+ dependencies = [
258
+ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
259
+ { name = "typing-extensions" },
260
+ ]
261
+ sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" }
262
+ wheels = [
263
+ { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" },
264
+ { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" },
265
+ { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" },
266
+ { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" },
267
+ { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" },
268
+ { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" },
269
+ { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" },
270
+ { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" },
271
+ { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" },
272
+ { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" },
273
+ { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" },
274
+ { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" },
275
+ { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" },
276
+ { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" },
277
+ { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" },
278
+ { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" },
279
+ { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" },
280
+ { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" },
281
+ { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" },
282
+ { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" },
283
+ { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" },
284
+ { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" },
285
+ { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" },
286
+ { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" },
287
+ { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" },
288
+ { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" },
289
+ { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" },
290
+ { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" },
291
+ { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
292
+ ]
293
+
189
294
  [[package]]
190
295
  name = "sqlqb"
191
- version = "0.1.2"
296
+ version = "0.1.4"
192
297
  source = { editable = "." }
193
298
 
194
299
  [package.dev-dependencies]
@@ -197,6 +302,12 @@ dev = [
197
302
  { name = "pytest-cov" },
198
303
  { name = "ruff" },
199
304
  ]
305
+ pymysql = [
306
+ { name = "pymysql" },
307
+ ]
308
+ sqlalchemy = [
309
+ { name = "sqlalchemy" },
310
+ ]
200
311
 
201
312
  [package.metadata]
202
313
 
@@ -206,3 +317,14 @@ dev = [
206
317
  { name = "pytest-cov", specifier = ">=7.1.0" },
207
318
  { name = "ruff", specifier = ">=0.15.17" },
208
319
  ]
320
+ pymysql = [{ name = "pymysql", specifier = ">=1.2.0" }]
321
+ sqlalchemy = [{ name = "sqlalchemy", specifier = ">=2.0.51" }]
322
+
323
+ [[package]]
324
+ name = "typing-extensions"
325
+ version = "4.15.0"
326
+ source = { registry = "https://pypi.org/simple" }
327
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
328
+ wheels = [
329
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
330
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes