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 +3 -0
- sqlqb-0.1.4/.pythonrc.py +1 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/PKG-INFO +1 -1
- {sqlqb-0.1.2 → sqlqb-0.1.4}/pyproject.toml +7 -1
- {sqlqb-0.1.2 → sqlqb-0.1.4}/src/sqlqb/delete.py +15 -3
- {sqlqb-0.1.2/src/sqlqb/mysql → sqlqb-0.1.4/src/sqlqb/pymysql}/__init__.py +30 -12
- {sqlqb-0.1.2 → sqlqb-0.1.4}/src/sqlqb/select.py +29 -7
- sqlqb-0.1.4/src/sqlqb/sqlalchemy/__init__.py +97 -0
- {sqlqb-0.1.2/src/sqlqb/sqlite → sqlqb-0.1.4/src/sqlqb/sqlite3}/__init__.py +13 -11
- {sqlqb-0.1.2 → sqlqb-0.1.4}/src/sqlqb/update.py +21 -4
- sqlqb-0.1.4/tests/test_pymysql.py +223 -0
- sqlqb-0.1.4/tests/test_sqlalchemy.py +226 -0
- sqlqb-0.1.2/tests/test_sqlite.py → sqlqb-0.1.4/tests/test_sqlite3.py +6 -11
- {sqlqb-0.1.2 → sqlqb-0.1.4}/uv.lock +123 -1
- {sqlqb-0.1.2 → sqlqb-0.1.4}/.gitignore +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/Makefile +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/README.md +0 -0
- /sqlqb-0.1.2/test.db → /sqlqb-0.1.4/data.db +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/src/sqlqb/__init__.py +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/src/sqlqb/insert.py +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/tests/__init__.py +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/tests/test_delete.py +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/tests/test_insert.py +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/tests/test_select.py +0 -0
- {sqlqb-0.1.2 → sqlqb-0.1.4}/tests/test_update.py +0 -0
sqlqb-0.1.4/.envrc
ADDED
sqlqb-0.1.4/.pythonrc.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from sqlqb import * # noqa: F403
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlqb"
|
|
3
|
-
version = "0.1.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
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) ->
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
+
from sqlqb import sqlite3
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
@pytest.fixture
|
|
7
7
|
def conn():
|
|
8
|
-
c =
|
|
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
|
-
|
|
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.
|
|
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
|