sqlengine-lite 2.1.0__tar.gz → 2.1.1__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.
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/PKG-INFO +1 -1
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/pyproject.toml +5 -2
- sqlengine_lite-2.1.1/src/sqlengine/utils/__init__.py +5 -0
- sqlengine_lite-2.1.1/src/sqlengine/utils/connection.py +87 -0
- sqlengine_lite-2.1.1/src/sqlengine/utils/repr.py +81 -0
- sqlengine_lite-2.1.1/src/sqlengine/utils/sqlgen.py +111 -0
- sqlengine_lite-2.1.1/src/sqlengine/utils/statements.py +400 -0
- sqlengine_lite-2.1.1/src/sqlengine/utils/types.py +65 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/src/sqlengine_lite.egg-info/PKG-INFO +1 -1
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/src/sqlengine_lite.egg-info/SOURCES.txt +6 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/LICENSE +0 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/README.md +0 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/setup.cfg +0 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/src/sqlengine/__init__.py +0 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/src/sqlengine/schema.py +0 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/src/sqlengine/sqltable.py +0 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/src/sqlengine_lite.egg-info/dependency_links.txt +0 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/src/sqlengine_lite.egg-info/top_level.txt +0 -0
- {sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/tests/test.py +0 -0
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlengine-lite"
|
|
3
|
-
version = "2.1.
|
|
3
|
+
version = "2.1.1"
|
|
4
4
|
description = "Cute sqlite3 wrapper for sql tables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
dependencies = []
|
|
8
8
|
|
|
9
9
|
[tool.setuptools]
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
|
|
12
12
|
[tool.setuptools.package-dir]
|
|
13
13
|
"" = "src"
|
|
14
14
|
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
where = ["src"]
|
|
17
|
+
|
|
15
18
|
[build-system]
|
|
16
19
|
requires = ["setuptools>=61.0"]
|
|
17
20
|
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from ..sqltable import SqlTableMixin
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("sqlengine")
|
|
9
|
+
logger.setLevel(os.getenv("SQL_ENGINE_LOG_LEVEL", "WARNING").upper())
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@contextmanager
|
|
13
|
+
def shared_connection(*args : SqlTableMixin, autocommit : bool = True, **connection_params):
|
|
14
|
+
"""
|
|
15
|
+
Creates shared connection across one or more databases for multiple tables by
|
|
16
|
+
manipulating their transaction attributes
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
*args (SqlTableMixin): tuple of instances of table classes inherited from `SqlTableMixin`
|
|
20
|
+
autocommit (bool): If `True`, will commit changes at the end of transaction
|
|
21
|
+
**connection_params (dict): Params to create connections with. This argument will be shared
|
|
22
|
+
across different connections. Reference: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
|
|
26
|
+
>>> from sqlengine.utils import shared_connection
|
|
27
|
+
>>> with shared_connection(table1, table2, **table1.connection_params):
|
|
28
|
+
>>> for (id1,), (id2, temp) in zip(table1.select("ID").limit(20), table2.select("ID", "Temperature").limit(20)):
|
|
29
|
+
>>> if id1 == id2:
|
|
30
|
+
>>> table.update.where.eq("ID", id2).then.set("Salary", temp).execute()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
tables_in_trans = [(f"{table.__class__.__name__=}, {table.tablename=}, {table.database=}") for table in args if table.in_transaction()]
|
|
34
|
+
|
|
35
|
+
if tables_in_trans:
|
|
36
|
+
raise RuntimeError(f"Tables {tables_in_trans} are already in transaction")
|
|
37
|
+
|
|
38
|
+
unique_databases = set(table.database for table in args)
|
|
39
|
+
database_map : dict[str, list[SqlTableMixin]] = {}
|
|
40
|
+
|
|
41
|
+
for db in unique_databases:
|
|
42
|
+
database_map[db] = [table for table in args if table.database == db]
|
|
43
|
+
|
|
44
|
+
connections : list[sqlite3.Connection] = []
|
|
45
|
+
|
|
46
|
+
for database, tables in database_map.items():
|
|
47
|
+
|
|
48
|
+
con = sqlite3.connect(database, **connection_params)
|
|
49
|
+
connections.append(con)
|
|
50
|
+
|
|
51
|
+
for table in tables:
|
|
52
|
+
table_cur = con.cursor()
|
|
53
|
+
setattr(table, "_trans", con)
|
|
54
|
+
setattr(table, "_trans_cursor", table_cur)
|
|
55
|
+
table._is_managed_transaction = True
|
|
56
|
+
|
|
57
|
+
logger.debug(f"Starting shared transaction across {len(database_map)} databases")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
yield
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Error while in shared transaction: {e}")
|
|
64
|
+
logger.debug(e, exc_info=True)
|
|
65
|
+
|
|
66
|
+
for con in connections:
|
|
67
|
+
con.rollback()
|
|
68
|
+
|
|
69
|
+
raise e
|
|
70
|
+
|
|
71
|
+
else:
|
|
72
|
+
if autocommit:
|
|
73
|
+
for con in connections:
|
|
74
|
+
con.commit()
|
|
75
|
+
|
|
76
|
+
finally:
|
|
77
|
+
|
|
78
|
+
for table in args:
|
|
79
|
+
table._is_managed_transaction = False
|
|
80
|
+
table.tx_cursor.close()
|
|
81
|
+
delattr(table, "_trans_cursor")
|
|
82
|
+
delattr(table, "_trans")
|
|
83
|
+
|
|
84
|
+
for con in connections:
|
|
85
|
+
con.close()
|
|
86
|
+
|
|
87
|
+
logger.debug("Shared transaction finished")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from html import escape
|
|
3
|
+
from typing import Sequence, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .statements import Select, Where
|
|
9
|
+
from ..sqltable import SqlTableMixin
|
|
10
|
+
|
|
11
|
+
from .types import SqlRow
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def to_html(tablename : str, columns : list[str], repr_rows : Sequence[SqlRow], limit : int = 25) -> str:
|
|
15
|
+
|
|
16
|
+
tablestyle = "<table style=\"border-collapse: collapse; font-size: 14px;\">"
|
|
17
|
+
tablenamestyle = "<caption style=\"font-size: 18px; font-weight: bold;\">{}</caption>"
|
|
18
|
+
colstyle = "<td style=\"border: 1px solid #555; text-align: center;\">{}</td>"
|
|
19
|
+
cellstyle = "<td style=\"border: 1px solid #000; text-align: center;\">{}</td>"
|
|
20
|
+
morestyle = "<td colspan=\"{}\" style=\"text-align:center;color:#888;font-style:italic;padding:8px;\">... more rows ...</td>"
|
|
21
|
+
|
|
22
|
+
cols = [colstyle.format(escape(col)) for col in columns]
|
|
23
|
+
tablename = tablenamestyle.format(escape(tablename))
|
|
24
|
+
more = morestyle.format(len(columns))
|
|
25
|
+
|
|
26
|
+
html = [
|
|
27
|
+
tablestyle,
|
|
28
|
+
tablename,
|
|
29
|
+
"<thead>",
|
|
30
|
+
"<tr>",
|
|
31
|
+
*cols,
|
|
32
|
+
"</tr>",
|
|
33
|
+
"</thead>",
|
|
34
|
+
"<tbody>",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for row in repr_rows[:limit]:
|
|
38
|
+
values = [cellstyle.format(escape(str(val))) for val in row]
|
|
39
|
+
|
|
40
|
+
html.extend([
|
|
41
|
+
"<tr>",
|
|
42
|
+
*values,
|
|
43
|
+
"</tr>"
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
if len(repr_rows) > limit:
|
|
47
|
+
html.extend([
|
|
48
|
+
"<tr>",
|
|
49
|
+
more,
|
|
50
|
+
"</tr>"
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
html.extend([
|
|
54
|
+
"</tbody>",
|
|
55
|
+
"</table>",
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
return "".join(html)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def to_csv(builder : Select | Where[Select] | SqlTableMixin, path : str) -> None:
|
|
62
|
+
|
|
63
|
+
from ..sqltable import SqlTableMixin
|
|
64
|
+
from .statements import Where
|
|
65
|
+
|
|
66
|
+
match builder:
|
|
67
|
+
case Where():
|
|
68
|
+
builder = builder.then
|
|
69
|
+
case SqlTableMixin():
|
|
70
|
+
builder = builder.select
|
|
71
|
+
|
|
72
|
+
if builder._aggregate:
|
|
73
|
+
raise AssertionError("Aggregated queries are not supported")
|
|
74
|
+
|
|
75
|
+
columns = builder._table.columns if len(builder._columns) == 0 or "*" in builder._columns else builder._columns
|
|
76
|
+
repr_rows = builder.fetchall()
|
|
77
|
+
|
|
78
|
+
with open(path, 'w', newline='') as file:
|
|
79
|
+
writer = csv.writer(file)
|
|
80
|
+
writer.writerow(columns)
|
|
81
|
+
writer.writerows(repr_rows)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_list(items : Sequence | set, brackets : bool = True) -> str:
|
|
5
|
+
""" Formats list into `(item1, item2, ...)` format """
|
|
6
|
+
items_str = ', '.join([str(i) for i in items])
|
|
7
|
+
if not brackets:
|
|
8
|
+
return items_str
|
|
9
|
+
return f'({items_str})'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_table(
|
|
13
|
+
tablename: str,
|
|
14
|
+
columns : list[str],
|
|
15
|
+
types : list[str],
|
|
16
|
+
primary : list[str]
|
|
17
|
+
) -> str:
|
|
18
|
+
|
|
19
|
+
columns_types = format_list([
|
|
20
|
+
f'{col} {dtype}' for col, dtype
|
|
21
|
+
in zip(columns, types)], brackets=False)
|
|
22
|
+
|
|
23
|
+
primary_keys = format_list(primary)
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
f"CREATE TABLE IF NOT EXISTS {tablename} "
|
|
27
|
+
f"({columns_types}, PRIMARY KEY {primary_keys});"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def drop_table(tablename : str) -> str:
|
|
32
|
+
return f"DROP TABLE IF EXISTS {tablename}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def values_placeholder(n_values : int) -> str:
|
|
36
|
+
""" Creates placeholder `(?, ?, ?, ...)` with "?" `n_values` times """
|
|
37
|
+
return format_list(['?']*n_values)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def bulk_placeholder(n_values : int, n_rows : int) -> str:
|
|
41
|
+
""" Creates placeholders `(?, ?, ..), (?, ?, ..), ...` for each row """
|
|
42
|
+
place_holder = values_placeholder(n_values)
|
|
43
|
+
return f"{format_list([place_holder]*n_rows, False)}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def insert(tablename : str, columns : list[str], values : str) -> str:
|
|
47
|
+
""" Creates insert query """
|
|
48
|
+
return f"INSERT INTO {tablename} {format_list(columns)} VALUES {values};"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def insert_row(tablename : str, columns : list[str]) -> str:
|
|
52
|
+
""" Creates insert query for 1 row """
|
|
53
|
+
n_values = len(columns)
|
|
54
|
+
placeholder = values_placeholder(n_values)
|
|
55
|
+
return insert(tablename, columns, placeholder)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def insert_many(tablename : str, columns : list[str], n_rows : int) -> str:
|
|
59
|
+
""" Creates insert query for multiple rows """
|
|
60
|
+
n_values = len(columns)
|
|
61
|
+
placeholder = bulk_placeholder(n_values, n_rows)
|
|
62
|
+
return insert(tablename, columns, placeholder)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def delete_rows(tablename : str, where_clause : str) -> str:
|
|
66
|
+
return f"DELETE FROM {tablename} WHERE {where_clause};"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def select(
|
|
70
|
+
tablename : str,
|
|
71
|
+
columns : str | list[str] = "*",
|
|
72
|
+
where_clause: str | None = None,
|
|
73
|
+
order_by : str | None = None,
|
|
74
|
+
limit : int | str | None = None
|
|
75
|
+
) -> str:
|
|
76
|
+
""" Creates select query """
|
|
77
|
+
|
|
78
|
+
_columns = columns if isinstance(columns, str) else format_list(columns, False)
|
|
79
|
+
|
|
80
|
+
query = f"SELECT {_columns} FROM {tablename}"
|
|
81
|
+
query += f" WHERE {where_clause}" if where_clause else ""
|
|
82
|
+
query += f" ORDER BY {order_by}" if order_by else ""
|
|
83
|
+
query += f" LIMIT {limit}" if limit else ""
|
|
84
|
+
query += ";"
|
|
85
|
+
|
|
86
|
+
return query
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def upsert(tablename : str, columns : list[str], primary_key : list[str]) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Creates query to upsert (update or insert) row based on `primary_key`
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
tablename (str): name of the table in db
|
|
95
|
+
columns (list[str]): list of table columns
|
|
96
|
+
primary_key (list[str]): list of primary keys
|
|
97
|
+
"""
|
|
98
|
+
placeholder = values_placeholder(len(columns))
|
|
99
|
+
non_primary = set(columns) - set(primary_key)
|
|
100
|
+
updated_list = [f"{col}=excluded.{col}" for col in non_primary]
|
|
101
|
+
|
|
102
|
+
columns_str = format_list(columns)
|
|
103
|
+
updated_str = format_list(updated_list, False)
|
|
104
|
+
primary_str = format_list(primary_key)
|
|
105
|
+
|
|
106
|
+
query = (
|
|
107
|
+
f"INSERT INTO {tablename} {columns_str} VALUES {placeholder} "
|
|
108
|
+
f"ON CONFLICT {primary_str} "
|
|
109
|
+
f"DO UPDATE SET {updated_str};"
|
|
110
|
+
)
|
|
111
|
+
return query
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Sequence, Literal, Generator, Self, TYPE_CHECKING
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .statements import Statement
|
|
7
|
+
from ..sqltable import SqlTableMixin
|
|
8
|
+
|
|
9
|
+
from . import sqlgen as sql
|
|
10
|
+
from .types import SqlValue, SqlRow
|
|
11
|
+
from .repr import to_html
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Where[T : Statement]:
|
|
15
|
+
""" Where clause build helper """
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __init__(self, statement : T):
|
|
19
|
+
|
|
20
|
+
self._statement = statement
|
|
21
|
+
|
|
22
|
+
self._clause : list[str] = []
|
|
23
|
+
self._args : list[SqlValue] = []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def then(self) -> T:
|
|
28
|
+
""" Returns upper statement object """
|
|
29
|
+
return self._statement
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def __call__(self, where_clasuse : str, *args : SqlValue) -> Self:
|
|
33
|
+
""" Shortcut to custom where clause """
|
|
34
|
+
return self.custom(where_clasuse, *args)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def op(self, column : str, value : SqlValue, operator : str) -> Self:
|
|
38
|
+
self._clause.append(f"{column} {operator} ?")
|
|
39
|
+
self._args.append(value)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def join(self, lop : str = "AND") -> Self:
|
|
44
|
+
""" Joins previous expression via logical operator `lop` """
|
|
45
|
+
joined = f" {lop} ".join(self._clause)
|
|
46
|
+
self._clause = [f"({joined})"]
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def eq(self, column : str, value : SqlValue) -> Self:
|
|
51
|
+
return self.op(column, value, "=")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def neq(self, column : str, value : SqlValue) -> Self:
|
|
55
|
+
return self.op(column, value, "!=")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def gt(self, column : str, value : SqlValue) -> Self:
|
|
59
|
+
return self.op(column, value, ">")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def gte(self, column : str, value : SqlValue) -> Self:
|
|
63
|
+
return self.op(column, value, ">=")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def lt(self, column : str, value : SqlValue) -> Self:
|
|
67
|
+
return self.op(column, value, "<")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def lte(self, column : str, value : SqlValue) -> Self:
|
|
71
|
+
return self.op(column, value, "<=")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def like(self, column : str, pattern : str) -> Self:
|
|
75
|
+
"""
|
|
76
|
+
Like operator. Pattern is a SQL wildcard pattern
|
|
77
|
+
(i.e. `%` for any string, `_` for one character).
|
|
78
|
+
"""
|
|
79
|
+
return self.op(column, pattern, "LIKE")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_null(self, column : str) -> Self:
|
|
83
|
+
self._clause.append(f"{column} IS NULL")
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def inverted(self) -> Self:
|
|
88
|
+
""" Invert last where clause with NOT """
|
|
89
|
+
self._clause[-1] = f"NOT ({self._clause[-1]})"
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def in_(self, column : str, values : Sequence[SqlValue]) -> Self:
|
|
94
|
+
if isinstance(values, str):
|
|
95
|
+
raise ValueError("Got string as sequence of values in in_, expected tuple/list/etc...")
|
|
96
|
+
placeholder = sql.values_placeholder(len(values))
|
|
97
|
+
self._clause.append(f"{column} IN {placeholder}")
|
|
98
|
+
self._args.extend(values)
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def between(self, column : str, start : SqlValue, stop : SqlValue) -> Self:
|
|
103
|
+
self._clause.append(f"{column} BETWEEN ? AND ?")
|
|
104
|
+
self._args.extend((start, stop))
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def custom(self, where_clause : str, *args : SqlValue) -> Self:
|
|
109
|
+
""" Add custom where clause (e.g. `where.custom("Age > ? AND Age != ?", 10, 25)`) """
|
|
110
|
+
self._clause.append(where_clause)
|
|
111
|
+
self._args.extend(args)
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build(self, lop : str = "AND") -> tuple[str, tuple[SqlValue, ...]]:
|
|
116
|
+
where_clause = f" {lop} ".join(self._clause).strip()
|
|
117
|
+
args = tuple(self._args)
|
|
118
|
+
return where_clause, args
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def reset(self) -> None:
|
|
122
|
+
self._args = []
|
|
123
|
+
self._clause = []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def __str__(self) -> str:
|
|
127
|
+
return self._statement.__str__()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def __repr__(self) -> str:
|
|
131
|
+
return self._statement.__repr__()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _repr_html_(self) -> str | None:
|
|
135
|
+
if isinstance(self._statement, Select):
|
|
136
|
+
return self._statement._repr_html_()
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def __len__(self) -> int:
|
|
141
|
+
return len(self._args)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Statement(ABC):
|
|
145
|
+
"""
|
|
146
|
+
Statement object that helps you build queries and execute them
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
__command__ : Literal["SELECT", "INSERT", "UPDATE", "DELETE"]
|
|
150
|
+
|
|
151
|
+
def __init__(self, table : SqlTableMixin) -> None:
|
|
152
|
+
|
|
153
|
+
self._table = table
|
|
154
|
+
self._where: Where[Self] = Where(self)
|
|
155
|
+
|
|
156
|
+
self._custom_query : str | None = None
|
|
157
|
+
self._custom_args : tuple[SqlValue, ...] = ()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def custom_query(self, query : str, *args) -> Self:
|
|
161
|
+
""" Custom query that completely replaces builder's expression """
|
|
162
|
+
self._custom_query = query
|
|
163
|
+
self._custom_args = args
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build(self) -> tuple[str, tuple[SqlValue, ...]]:
|
|
168
|
+
""" Build complete expression with sorted arguments and operations """
|
|
169
|
+
if self._custom_query:
|
|
170
|
+
return self._custom_query, self._custom_args
|
|
171
|
+
|
|
172
|
+
where_clause, args = self._where.build()
|
|
173
|
+
query, args = self._build(where_clause, *args)
|
|
174
|
+
return query, args
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def reset(self) -> None:
|
|
178
|
+
""" Resets statement to reuse object """
|
|
179
|
+
self._where.reset()
|
|
180
|
+
self._custom_query = None
|
|
181
|
+
self._custom_args = ()
|
|
182
|
+
self._reset()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def where(self) -> Where[Self]:
|
|
187
|
+
""" Where clause builder """
|
|
188
|
+
return self._where
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
def _build(self, where_clause : str, *args : SqlValue) -> tuple[str, tuple[SqlValue, ...]]:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@abstractmethod
|
|
197
|
+
def _reset(self) -> None:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def __repr__(self) -> str:
|
|
202
|
+
query, args = self.build()
|
|
203
|
+
return f"{query} {args}"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def __str__(self) -> str:
|
|
207
|
+
query, _ = self.build()
|
|
208
|
+
return query
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class MutationalStatement(Statement, ABC):
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def execute(self) -> None:
|
|
215
|
+
query, args = self.build()
|
|
216
|
+
self._table.execute(query, *args)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Select(Statement):
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def __init__(self, table : SqlTableMixin) -> None:
|
|
223
|
+
super().__init__(table)
|
|
224
|
+
|
|
225
|
+
self._columns : list[str] = []
|
|
226
|
+
self._order_by : list[str] = []
|
|
227
|
+
self._aggregate : str | None = None
|
|
228
|
+
self._limit : int | None = None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def __call__(self, *columns : str) -> Self:
|
|
232
|
+
return self.columns(*columns)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def columns(self, *columns : str) -> Self:
|
|
236
|
+
""" Column selector """
|
|
237
|
+
self._columns.extend(columns)
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def aggregate(self, by : Literal['COUNT', 'SUM', 'AVG', 'MIN', 'MAX']) -> Self:
|
|
242
|
+
|
|
243
|
+
if self._aggregate:
|
|
244
|
+
raise ValueError("Can't aggregate columns multiple times")
|
|
245
|
+
|
|
246
|
+
self._aggregate = by
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def order_by(self, column : str, ascending : bool = True) -> Self:
|
|
251
|
+
order = "ASC" if ascending else "DESC"
|
|
252
|
+
self._order_by.append(f"{column} {order}")
|
|
253
|
+
return self
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def limit(self, n : int) -> Self:
|
|
257
|
+
self._limit = n
|
|
258
|
+
return self
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def fetchone(self) -> SqlRow:
|
|
262
|
+
query, args = self.build()
|
|
263
|
+
return self._table.fetchone(query, *args)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def fetchmany(self, size : int = 1) -> list[SqlRow]:
|
|
267
|
+
query, args = self.build()
|
|
268
|
+
return self._table.fetchmany(query, *args, size=size)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def fetchall(self) -> list[SqlRow]:
|
|
272
|
+
query, args = self.build()
|
|
273
|
+
return self._table.fetchall(query, *args)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def fetchmany_iterator(self, batch_size: int) -> Generator[list[SqlRow], None, None]:
|
|
277
|
+
"""
|
|
278
|
+
Yields all rows in batches, each batch in its own transaction.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
batch_size (int): Size of each batch
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
|
|
285
|
+
>>> with table.transaction():
|
|
286
|
+
>>> for batch in table.select.where.gt("Age", 30).then.fetchmany_iterator(1000):
|
|
287
|
+
>>> process_batch(batch)
|
|
288
|
+
"""
|
|
289
|
+
if not self._table.in_transaction():
|
|
290
|
+
raise RuntimeError("To use the `fetchall_iterator()` method you have \
|
|
291
|
+
to keep open the transaction of the table with `transaction()` manager")
|
|
292
|
+
|
|
293
|
+
query, exec_args = self.build()
|
|
294
|
+
|
|
295
|
+
iter_cursor = self._table.tx_conn.cursor()
|
|
296
|
+
iter_cursor.execute(query, exec_args)
|
|
297
|
+
|
|
298
|
+
while batch := iter_cursor.fetchmany(batch_size):
|
|
299
|
+
yield batch
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def __iter__(self) -> Generator[SqlRow, None, None]:
|
|
303
|
+
""" Select statement rows iterator """
|
|
304
|
+
|
|
305
|
+
if not self._table.in_transaction():
|
|
306
|
+
raise RuntimeError("To use the __iter__ method you have \
|
|
307
|
+
to keep open the transaction of the table with `transaction()` manager")
|
|
308
|
+
|
|
309
|
+
query, exec_args = self.build()
|
|
310
|
+
|
|
311
|
+
iter_cursor = self._table.tx_conn.cursor()
|
|
312
|
+
iter_cursor.execute(query, exec_args)
|
|
313
|
+
|
|
314
|
+
while row := iter_cursor.fetchone():
|
|
315
|
+
yield row
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _build(self, where_clause : str, *args : SqlValue) -> tuple[str, tuple[SqlValue, ...]]:
|
|
319
|
+
|
|
320
|
+
order = sql.format_list(self._order_by, brackets=False)
|
|
321
|
+
columns = sql.format_list(self._columns, brackets=False)
|
|
322
|
+
|
|
323
|
+
columns = "*" if not columns else columns
|
|
324
|
+
columns = columns if not self._aggregate else f"{self._aggregate}({columns})"
|
|
325
|
+
|
|
326
|
+
if self._limit is not None:
|
|
327
|
+
limit = "?"
|
|
328
|
+
args = (*args, self._limit)
|
|
329
|
+
|
|
330
|
+
else:
|
|
331
|
+
limit = None
|
|
332
|
+
|
|
333
|
+
query = sql.select(self._table.tablename, columns, where_clause, order, limit)
|
|
334
|
+
|
|
335
|
+
return query, args
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _reset(self) -> None:
|
|
339
|
+
self._columns = []
|
|
340
|
+
self._order_by = []
|
|
341
|
+
self._aggregate = None
|
|
342
|
+
self._limit = None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _repr_html_(self) -> str | None:
|
|
346
|
+
|
|
347
|
+
if self._aggregate:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
limit = 26
|
|
351
|
+
columns = self._table.columns if len(self._columns) == 0 or "*" in self._columns else self._columns
|
|
352
|
+
repr_rows = self.fetchmany(limit)
|
|
353
|
+
|
|
354
|
+
return to_html(self._table.tablename, columns, repr_rows, limit=limit-1)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class Delete(MutationalStatement):
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _build(self, where_clause : str, *args : SqlValue) -> tuple[str, tuple[SqlValue, ...]]:
|
|
361
|
+
|
|
362
|
+
if not where_clause:
|
|
363
|
+
raise ValueError("Delete statement must have a where clause")
|
|
364
|
+
|
|
365
|
+
query = sql.delete_rows(self._table.tablename, where_clause)
|
|
366
|
+
return query, args
|
|
367
|
+
|
|
368
|
+
def _reset(self) -> None:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class Update(MutationalStatement):
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def __init__(self, table : SqlTableMixin) -> None:
|
|
376
|
+
super().__init__(table)
|
|
377
|
+
self._set_clauses : list[str] = []
|
|
378
|
+
self._set_args : list[SqlValue] = []
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def __call__(self, column : str, value : SqlValue) -> Self:
|
|
382
|
+
return self.set(column, value)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def set(self, column : str, value : SqlValue) -> Self:
|
|
386
|
+
""" Set value to a column """
|
|
387
|
+
self._set_clauses.append(f"{column} = ?")
|
|
388
|
+
self._set_args.append(value)
|
|
389
|
+
return self
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _build(self, where_clause : str, *args : SqlValue) -> tuple[str, tuple[SqlValue, ...]]:
|
|
393
|
+
set_clause = sql.format_list(self._set_clauses, brackets=False)
|
|
394
|
+
query = f"UPDATE {self._table.tablename} SET {set_clause} WHERE {where_clause};"
|
|
395
|
+
return query, (*self._set_args, *args)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _reset(self) -> None:
|
|
399
|
+
self._set_clauses = []
|
|
400
|
+
self._set_args = []
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from typing import Protocol, Self, TypeGuard, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CustomType(Protocol):
|
|
6
|
+
@classmethod
|
|
7
|
+
def from_sql(cls, sql : bytes) -> Self:
|
|
8
|
+
""" Method that accepts bytes and returns object instance """
|
|
9
|
+
...
|
|
10
|
+
def to_sql(self) -> str | int | float | str | bytes | None:
|
|
11
|
+
""" Method that converts object instance to native sqlite3 value """
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
type SqlValue = str | int | float | bytes | None | CustomType
|
|
16
|
+
type SqlRow = tuple[SqlValue, ...]
|
|
17
|
+
type SqlType = type[str | int | float | bytes | CustomType]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Schema(TypedDict):
|
|
21
|
+
tablename : str
|
|
22
|
+
columns : list[str]
|
|
23
|
+
types : list[SqlType | str]
|
|
24
|
+
primary : list[str]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# https://docs.python.org/3/library/sqlite3.html#sqlite-and-python-types
|
|
28
|
+
_TYPES_MAP : dict[type, str] = {
|
|
29
|
+
int : "INTEGER",
|
|
30
|
+
float : "REAL",
|
|
31
|
+
str : "TEXT",
|
|
32
|
+
bytes : "BLOB",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_custom_type(type_: SqlType) -> TypeGuard[type[CustomType]]:
|
|
37
|
+
return (
|
|
38
|
+
type_ not in (str, int, float, bytes)
|
|
39
|
+
and isinstance(type_, type)
|
|
40
|
+
and hasattr(type_, "from_sql")
|
|
41
|
+
and hasattr(type_, "to_sql")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def register_type(cls : type[CustomType], type_name : str | None = None) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Register custom type to be able to store it in tables
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
cls (CustomType): Class that implements `from_sql(cls, sql : bytes) -> Self` and `
|
|
51
|
+
to_sql(self) -> str | int | float | str | bytes | None`
|
|
52
|
+
type_name (str | None): Colname that would be linked to this type
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
type_name = type_name if type_name else cls.__name__
|
|
56
|
+
sqlite3.register_adapter(cls, lambda x: x.to_sql())
|
|
57
|
+
sqlite3.register_converter(type_name, cls.from_sql)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def pytype_to_sqltype(type_ : type) -> str:
|
|
61
|
+
""" Converts python type to sql type """
|
|
62
|
+
if type_ not in _TYPES_MAP:
|
|
63
|
+
raise TypeError(f"{type_} is not natively supported by sqlite3")
|
|
64
|
+
|
|
65
|
+
return _TYPES_MAP[type_]
|
|
@@ -4,6 +4,12 @@ pyproject.toml
|
|
|
4
4
|
src/sqlengine/__init__.py
|
|
5
5
|
src/sqlengine/schema.py
|
|
6
6
|
src/sqlengine/sqltable.py
|
|
7
|
+
src/sqlengine/utils/__init__.py
|
|
8
|
+
src/sqlengine/utils/connection.py
|
|
9
|
+
src/sqlengine/utils/repr.py
|
|
10
|
+
src/sqlengine/utils/sqlgen.py
|
|
11
|
+
src/sqlengine/utils/statements.py
|
|
12
|
+
src/sqlengine/utils/types.py
|
|
7
13
|
src/sqlengine_lite.egg-info/PKG-INFO
|
|
8
14
|
src/sqlengine_lite.egg-info/SOURCES.txt
|
|
9
15
|
src/sqlengine_lite.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlengine_lite-2.1.0 → sqlengine_lite-2.1.1}/src/sqlengine_lite.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|