sqlengine-lite 2.1.0__py3-none-any.whl → 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sqlengine/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
- from .utils import sqlgen, types
2
- from .utils.types import Schema
1
+ from .core import sqlgen, types
2
+ from .core.types import Schema, Primary
3
3
  from .sqltable import SqlTableMixin
4
4
 
5
5
  __author__ = "suffermuffin"
6
6
 
7
- __all__ = ["types", "sqlgen", "Schema", "SqlTableMixin"]
7
+ __all__ = ["types", "sqlgen", "Schema", "SqlTableMixin", "Primary"]
@@ -0,0 +1,4 @@
1
+ from .statements import Select, Delete, Update
2
+ from .connection import ConnectionManager
3
+
4
+ __all__ = ["Select", "Delete", "Update", "ConnectionManager"]
@@ -0,0 +1,255 @@
1
+ import sqlite3
2
+ import logging
3
+ import os
4
+
5
+ from typing import overload, Literal, Sequence
6
+ from contextlib import contextmanager
7
+
8
+ from .types import SqlValue, SqlRow
9
+
10
+
11
+ logger = logging.getLogger("sqlengine")
12
+ logger.setLevel(os.getenv("SQL_ENGINE_LOG_LEVEL", "WARNING").upper())
13
+
14
+
15
+ class ConnectionManager:
16
+
17
+ _trans : sqlite3.Connection
18
+ _trans_cursor : sqlite3.Cursor
19
+
20
+ def __init__(self, database : str, **connection_params) -> None:
21
+
22
+ self.database = database
23
+ self.connection_params = connection_params
24
+
25
+ self._is_managed_transaction = False
26
+
27
+
28
+ @overload
29
+ def _fetch(self, query : str, args : SqlRow,
30
+ method : Literal["fetchone"]) -> SqlRow: ...
31
+ @overload
32
+ def _fetch(self, query : str, args : SqlRow,
33
+ method : Literal["fetchall"]) -> list[SqlRow]: ...
34
+
35
+ def _fetch(
36
+ self,
37
+ query : str,
38
+ args : SqlRow = (),
39
+ method : Literal["fetchone", "fetchall"] = "fetchall"
40
+ ) -> SqlRow | list[SqlRow]:
41
+
42
+ logger.debug(f"{self.database}: {query} {args}")
43
+
44
+ if self.in_transaction():
45
+ self._trans_cursor.execute(query, args)
46
+ return getattr(self._trans_cursor, method)()
47
+
48
+ with self.connect() as conn:
49
+ cursor = conn.cursor()
50
+ cursor.execute(query, args)
51
+ return getattr(cursor, method)()
52
+
53
+
54
+ @overload
55
+ def _execute(self, query : str, args : tuple[SqlValue, ...], method : Literal["execute"]) -> None: ...
56
+ @overload
57
+ def _execute(self, query : str, args : Sequence[SqlRow], method : Literal["executemany"]) -> None: ...
58
+
59
+ def _execute(
60
+ self,
61
+ query : str,
62
+ args : tuple[SqlValue, ...] | Sequence[SqlRow] = (),
63
+ method : Literal["execute", "executemany"] = "execute"
64
+ ) -> None:
65
+ """
66
+ Shortcut to connect() -> execute[<many>]() -> commit() for single operations.
67
+ Can be used in transaction using `transaction()` manager.
68
+
69
+ Args:
70
+ query (str): SQL query to execute on SQLite3 DB
71
+ *args (Any): Arguments to the execution
72
+ method (str): "execute" or "executemany"
73
+ """
74
+ logger.debug(f"{self.database}: {query} {args}")
75
+
76
+ if self.in_transaction():
77
+ getattr(self._trans_cursor, method)(query, args)
78
+ return
79
+
80
+ with self.connect() as conn:
81
+ cursor = conn.cursor()
82
+ getattr(cursor, method)(query, args)
83
+ conn.commit()
84
+
85
+
86
+ def execute(self, query : str, *args : SqlValue) -> None:
87
+ """
88
+ Shortcut to connect() -> execute() -> commit() for single operations.
89
+ Can be used in transaction using `transaction()` manager.
90
+
91
+ Args:
92
+ query (str): SQL query to execute on SQLite3 DB
93
+ *args (tuple[SqlValue, ...]): Arguments to the execution
94
+ """
95
+ return self._execute(query, args, method="execute")
96
+
97
+
98
+ def executemany(self, query : str, args : Sequence[SqlRow]) -> None:
99
+ """
100
+ Shortcut to connect() -> executemany() -> commit() for single operations.
101
+ Can be used in transaction using `transaction()` manager.
102
+
103
+ Args:
104
+ query (str): SQL query to execute on SQLite3 DB
105
+ args (list[tuple[SqlValue, ...]]): Arguments to the execution
106
+ """
107
+ return self._execute(query, args, method="executemany")
108
+
109
+
110
+ def fetchone(self, query : str, *args : SqlValue) -> SqlRow:
111
+ """
112
+ Fetch first row based on `query`
113
+
114
+ Args:
115
+ query (str): SQL query
116
+ *args (tuple[SqlValue, ...]): Arguments to the execution
117
+
118
+ Returns:
119
+ row (SqlRow): Single row
120
+ """
121
+ return self._fetch(query, args, method="fetchone")
122
+
123
+
124
+ def fetchmany(self, query : str, *args : SqlValue, size : int = 1) -> list[SqlRow]:
125
+ """
126
+ Fetch first `size` rows based on `query`
127
+
128
+ Args:
129
+ query (str): SQL query
130
+ *args (tuple[SqlValue, ...]): Arguments to the execution
131
+ size (str): Number of rows to return
132
+
133
+ Returns:
134
+ rows (list[SqlRow]): list of `size` rows
135
+ """
136
+ logger.debug(f"{self.database}: {query} {args}")
137
+
138
+ if self.in_transaction():
139
+ self._trans_cursor.execute(query, args)
140
+ return self._trans_cursor.fetchmany(size)
141
+
142
+ with self.connect() as conn:
143
+ cursor = conn.cursor()
144
+ cursor.execute(query, args)
145
+ return cursor.fetchmany(size)
146
+
147
+
148
+ def fetchall(self, query : str, *args : SqlValue) -> list[SqlRow]:
149
+ """
150
+ Fetch all rows based on `query`
151
+
152
+ Args:
153
+ query (str): SQL query
154
+ *args (tuple[SqlValue, ...]): Arguments to the execution
155
+
156
+ Returns:
157
+ rows (list[SqlRow]): list of rows
158
+ """
159
+ return self._fetch(query, args, method="fetchall")
160
+
161
+
162
+ def in_transaction(self) -> bool:
163
+ """ Returns True if instance is in transaction """
164
+ return hasattr(self, "_trans") and hasattr(self, "_trans_cursor")
165
+
166
+
167
+ def connect(self) -> sqlite3.Connection:
168
+ """ Shortcut to sqlite3 connection context manager """
169
+ return sqlite3.connect(self.database, **self.connection_params)
170
+
171
+
172
+ def open(self) -> None:
173
+ """ Opens unmanaged transaction """
174
+ if self.in_transaction():
175
+ raise RuntimeError("Can't re-open existing connection")
176
+
177
+ self._trans = self.connect()
178
+ self._trans_cursor = self._trans.cursor()
179
+
180
+
181
+ def close(self) -> None:
182
+ """ Closes unmanaged transaction """
183
+ if not self.in_transaction():
184
+ return
185
+
186
+ if self._is_managed_transaction:
187
+ raise RuntimeError("Can't manually close managed transaction")
188
+
189
+ self._trans_cursor.close()
190
+ self._trans.close()
191
+ del(self._trans_cursor)
192
+ del(self._trans)
193
+
194
+
195
+ def commit(self) -> None:
196
+ if not self.in_transaction():
197
+ raise RuntimeError("Can't commit outside transaction mode")
198
+
199
+ self._trans.commit()
200
+
201
+
202
+ def rollback(self) -> None:
203
+ if not self.in_transaction():
204
+ raise RuntimeError("Can't rollback outside transaction mode")
205
+
206
+ self._trans.rollback()
207
+
208
+
209
+ @contextmanager
210
+ def transaction(self, autocommit : bool = True):
211
+ """
212
+ Creates context manager to use class methods in transaction
213
+
214
+ Args:
215
+ autocommit (bool): If `True`, will commit changes at the end of transaction
216
+ """
217
+
218
+ self.open()
219
+ self._is_managed_transaction = True
220
+ logger.debug(f"{self.database}: Transaction started")
221
+
222
+ try:
223
+ yield
224
+
225
+ except Exception as e:
226
+ logger.error(f"{self.database}: Error while in transaction: {e}")
227
+ logger.debug(e, exc_info=True)
228
+ self._trans.rollback()
229
+ raise e
230
+
231
+ else:
232
+ if autocommit:
233
+ self._trans.commit()
234
+
235
+ finally:
236
+ self._is_managed_transaction = False
237
+ self.close()
238
+ logger.debug(f"{self.database}: Transaction finished")
239
+
240
+
241
+ @property
242
+ def tx_conn(self) -> sqlite3.Connection:
243
+ """ Gives access to connection while in transaction """
244
+ if not self.in_transaction():
245
+ raise RuntimeError("`tx_conn` is not available outside the transaction mode")
246
+ return self._trans
247
+
248
+
249
+ @property
250
+ def tx_cursor(self) -> sqlite3.Cursor:
251
+ """ Gives access to connection cursor while in transaction """
252
+ if not self.in_transaction():
253
+ raise RuntimeError("`tx_cursor` is not available outside the transaction mode")
254
+ return self._trans_cursor
255
+
sqlengine/core/repr.py ADDED
@@ -0,0 +1,52 @@
1
+ from html import escape
2
+ from typing import Sequence
3
+
4
+ from .types import SqlRow
5
+
6
+
7
+ def to_html(tablename : str, columns : list[str], repr_rows : Sequence[SqlRow], limit : int = 25) -> str:
8
+
9
+ tablestyle = "<table style=\"border-collapse: collapse; font-size: 14px;\">"
10
+ tablenamestyle = "<caption style=\"font-size: 18px; font-weight: bold;\">{}</caption>"
11
+ colstyle = "<td style=\"border: 1px solid #555; text-align: center;\">{}</td>"
12
+ cellstyle = "<td style=\"border: 1px solid #000; text-align: center;\">{}</td>"
13
+ morestyle = "<td colspan=\"{}\" style=\"text-align:center;color:#888;font-style:italic;padding:8px;\">... more rows ...</td>"
14
+
15
+ cols = [colstyle.format(escape(col)) for col in columns]
16
+ tablename = tablenamestyle.format(escape(tablename))
17
+ more = morestyle.format(len(columns))
18
+
19
+ html = [
20
+ tablestyle,
21
+ tablename,
22
+ "<thead>",
23
+ "<tr>",
24
+ *cols,
25
+ "</tr>",
26
+ "</thead>",
27
+ "<tbody>",
28
+ ]
29
+
30
+ for row in repr_rows[:limit]:
31
+ values = [cellstyle.format(escape(str(val))) for val in row]
32
+
33
+ html.extend([
34
+ "<tr>",
35
+ *values,
36
+ "</tr>"
37
+ ])
38
+
39
+ if len(repr_rows) > limit:
40
+ html.extend([
41
+ "<tr>",
42
+ more,
43
+ "</tr>"
44
+ ])
45
+
46
+ html.extend([
47
+ "</tbody>",
48
+ "</table>",
49
+ ])
50
+
51
+ return "".join(html)
52
+
@@ -0,0 +1,109 @@
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
+ return f'({items_str})' if brackets else items_str
8
+
9
+
10
+ def create_table(
11
+ tablename: str,
12
+ columns : list[str],
13
+ types : list[str],
14
+ primary : list[str]
15
+ ) -> str:
16
+
17
+ columns_types = format_list([
18
+ f'{col} {dtype}' for col, dtype
19
+ in zip(columns, types)], brackets=False)
20
+
21
+ primary_keys = format_list(primary)
22
+
23
+ return (
24
+ f"CREATE TABLE IF NOT EXISTS {tablename} "
25
+ f"({columns_types}, PRIMARY KEY {primary_keys});"
26
+ )
27
+
28
+
29
+ def drop_table(tablename : str) -> str:
30
+ return f"DROP TABLE IF EXISTS {tablename}"
31
+
32
+
33
+ def values_placeholder(n_values : int) -> str:
34
+ """ Creates placeholder `(?, ?, ?, ...)` with "?" `n_values` times """
35
+ return format_list(['?']*n_values)
36
+
37
+
38
+ def bulk_placeholder(n_values : int, n_rows : int) -> str:
39
+ """ Creates placeholders `(?, ?, ..), (?, ?, ..), ...` for each row """
40
+ place_holder = values_placeholder(n_values)
41
+ return format_list([place_holder]*n_rows, False)
42
+
43
+
44
+ def insert(tablename : str, columns : list[str], values : str) -> str:
45
+ """ Creates insert query """
46
+ return f"INSERT INTO {tablename} {format_list(columns)} VALUES {values};"
47
+
48
+
49
+ def insert_row(tablename : str, columns : list[str]) -> str:
50
+ """ Creates insert query for 1 row """
51
+ n_values = len(columns)
52
+ placeholder = values_placeholder(n_values)
53
+ return insert(tablename, columns, placeholder)
54
+
55
+
56
+ def insert_many(tablename : str, columns : list[str], n_rows : int) -> str:
57
+ """ Creates insert query for multiple rows """
58
+ n_values = len(columns)
59
+ placeholder = bulk_placeholder(n_values, n_rows)
60
+ return insert(tablename, columns, placeholder)
61
+
62
+
63
+ def delete_rows(tablename : str, where_clause : str) -> str:
64
+ return f"DELETE FROM {tablename} WHERE {where_clause};"
65
+
66
+
67
+ def select(
68
+ tablename : str,
69
+ columns : str | list[str] = "*",
70
+ where_clause: str | None = None,
71
+ order_by : str | None = None,
72
+ limit : int | str | None = None
73
+ ) -> str:
74
+ """ Creates select query """
75
+
76
+ _columns = columns if isinstance(columns, str) else format_list(columns, False)
77
+
78
+ query = f"SELECT {_columns} FROM {tablename}"
79
+ query += f" WHERE {where_clause}" if where_clause else ""
80
+ query += f" ORDER BY {order_by}" if order_by else ""
81
+ query += f" LIMIT {limit}" if limit else ""
82
+ query += ";"
83
+
84
+ return query
85
+
86
+
87
+ def upsert(tablename : str, columns : list[str], primary_key : list[str]) -> str:
88
+ """
89
+ Creates query to upsert (update or insert) row based on `primary_key`
90
+
91
+ Args:
92
+ tablename (str): name of the table in db
93
+ columns (list[str]): list of table columns
94
+ primary_key (list[str]): list of primary keys
95
+ """
96
+ placeholder = values_placeholder(len(columns))
97
+ non_primary = set(columns) - set(primary_key)
98
+ updated_list = [f"{col}=excluded.{col}" for col in non_primary]
99
+
100
+ columns_str = format_list(columns)
101
+ updated_str = format_list(updated_list, False)
102
+ primary_str = format_list(primary_key)
103
+
104
+ query = (
105
+ f"INSERT INTO {tablename} {columns_str} VALUES {placeholder} "
106
+ f"ON CONFLICT {primary_str} "
107
+ f"DO UPDATE SET {updated_str};"
108
+ )
109
+ return query