sqlengine-lite 2.1.1__py3-none-any.whl → 2.2.1__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,11 @@
1
- from .utils import sqlgen, types
2
- from .utils.types import Schema
1
+ from ._internal import sqlgen
2
+ from ._internal import ConnectionManager
3
+ from ._internal.types import Schema, Primary, register_type
3
4
  from .sqltable import SqlTableMixin
4
5
 
5
6
  __author__ = "suffermuffin"
6
7
 
7
- __all__ = ["types", "sqlgen", "Schema", "SqlTableMixin"]
8
+ __all__ = [
9
+ "sqlgen", "Schema", "SqlTableMixin",
10
+ "Primary", "ConnectionManager", "register_type"
11
+ ]
@@ -0,0 +1,4 @@
1
+ from .statements import Select, Delete, Update
2
+ from .connection_manager import ConnectionManager
3
+
4
+ __all__ = ["Select", "Delete", "Update", "ConnectionManager"]
@@ -0,0 +1,266 @@
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
+ from ..exceptions import TransactionError, NestedTransactionError, OutsideTransactionError
10
+
11
+
12
+ logger = logging.getLogger("sqlengine")
13
+ logger.setLevel(os.getenv("SQL_ENGINE_LOG_LEVEL", "WARNING").upper())
14
+
15
+
16
+ class ConnectionManager:
17
+
18
+ """
19
+ Connection manager for sqlite3
20
+
21
+ Args:
22
+
23
+ database (str): database filename to connect to. If `":memory:"` is passed, then database will be set in memory and you will have to
24
+ create table manually with `create_table()` method inside `transaction()` block.
25
+ **connection_params: Params to create connection with. Reference: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
26
+ """
27
+
28
+ _trans : sqlite3.Connection
29
+ _trans_cursor : sqlite3.Cursor
30
+
31
+ def __init__(self, database : str, **connection_params) -> None:
32
+
33
+ self.database = database
34
+ self.connection_params = connection_params
35
+
36
+ self._is_managed_transaction = False
37
+
38
+
39
+ @overload
40
+ def _fetch(self, query : str, args : SqlRow,
41
+ method : Literal["fetchone"]) -> SqlRow: ...
42
+ @overload
43
+ def _fetch(self, query : str, args : SqlRow,
44
+ method : Literal["fetchall"]) -> list[SqlRow]: ...
45
+
46
+ def _fetch(
47
+ self,
48
+ query : str,
49
+ args : SqlRow = (),
50
+ method : Literal["fetchone", "fetchall"] = "fetchall"
51
+ ) -> SqlRow | list[SqlRow]:
52
+
53
+ logger.debug(f"{self.database}: {query} {args}")
54
+
55
+ if self.in_transaction():
56
+ self._trans_cursor.execute(query, args)
57
+ return getattr(self._trans_cursor, method)()
58
+
59
+ with self.connect() as conn:
60
+ cursor = conn.cursor()
61
+ cursor.execute(query, args)
62
+ return getattr(cursor, method)()
63
+
64
+
65
+ @overload
66
+ def _execute(self, query : str, args : tuple[SqlValue, ...], method : Literal["execute"]) -> None: ...
67
+ @overload
68
+ def _execute(self, query : str, args : Sequence[SqlRow], method : Literal["executemany"]) -> None: ...
69
+
70
+ def _execute(
71
+ self,
72
+ query : str,
73
+ args : tuple[SqlValue, ...] | Sequence[SqlRow] = (),
74
+ method : Literal["execute", "executemany"] = "execute"
75
+ ) -> None:
76
+ """
77
+ Shortcut to connect() -> execute[<many>]() -> commit() for single operations.
78
+ Can be used in transaction using `transaction()` manager.
79
+
80
+ Args:
81
+ query (str): SQL query to execute on SQLite3 DB
82
+ *args (Any): Arguments to the execution
83
+ method (str): "execute" or "executemany"
84
+ """
85
+ logger.debug(f"{self.database}: {query} {args}")
86
+
87
+ if self.in_transaction():
88
+ getattr(self._trans_cursor, method)(query, args)
89
+ return
90
+
91
+ with self.connect() as conn:
92
+ cursor = conn.cursor()
93
+ getattr(cursor, method)(query, args)
94
+ conn.commit()
95
+
96
+
97
+ def execute(self, query : str, *args : SqlValue) -> None:
98
+ """
99
+ Shortcut to connect() -> execute() -> commit() for single operations.
100
+ Can be used in transaction using `transaction()` manager.
101
+
102
+ Args:
103
+ query (str): SQL query to execute on SQLite3 DB
104
+ *args (tuple[SqlValue, ...]): Arguments to the execution
105
+ """
106
+ return self._execute(query, args, method="execute")
107
+
108
+
109
+ def executemany(self, query : str, args : Sequence[SqlRow]) -> None:
110
+ """
111
+ Shortcut to connect() -> executemany() -> commit() for single operations.
112
+ Can be used in transaction using `transaction()` manager.
113
+
114
+ Args:
115
+ query (str): SQL query to execute on SQLite3 DB
116
+ args (list[tuple[SqlValue, ...]]): Arguments to the execution
117
+ """
118
+ return self._execute(query, args, method="executemany")
119
+
120
+
121
+ def fetchone(self, query : str, *args : SqlValue) -> SqlRow:
122
+ """
123
+ Fetch first row based on `query`
124
+
125
+ Args:
126
+ query (str): SQL query
127
+ *args (tuple[SqlValue, ...]): Arguments to the execution
128
+
129
+ Returns:
130
+ row (SqlRow): Single row
131
+ """
132
+ return self._fetch(query, args, method="fetchone")
133
+
134
+
135
+ def fetchmany(self, query : str, *args : SqlValue, size : int = 1) -> list[SqlRow]:
136
+ """
137
+ Fetch first `size` rows based on `query`
138
+
139
+ Args:
140
+ query (str): SQL query
141
+ *args (tuple[SqlValue, ...]): Arguments to the execution
142
+ size (str): Number of rows to return
143
+
144
+ Returns:
145
+ rows (list[SqlRow]): list of `size` rows
146
+ """
147
+ logger.debug(f"{self.database}: {query} {args}")
148
+
149
+ if self.in_transaction():
150
+ self._trans_cursor.execute(query, args)
151
+ return self._trans_cursor.fetchmany(size)
152
+
153
+ with self.connect() as conn:
154
+ cursor = conn.cursor()
155
+ cursor.execute(query, args)
156
+ return cursor.fetchmany(size)
157
+
158
+
159
+ def fetchall(self, query : str, *args : SqlValue) -> list[SqlRow]:
160
+ """
161
+ Fetch all rows based on `query`
162
+
163
+ Args:
164
+ query (str): SQL query
165
+ *args (tuple[SqlValue, ...]): Arguments to the execution
166
+
167
+ Returns:
168
+ rows (list[SqlRow]): list of rows
169
+ """
170
+ return self._fetch(query, args, method="fetchall")
171
+
172
+
173
+ def in_transaction(self) -> bool:
174
+ """ Returns True if instance is in transaction """
175
+ return hasattr(self, "_trans") and hasattr(self, "_trans_cursor")
176
+
177
+
178
+ def connect(self) -> sqlite3.Connection:
179
+ """ Shortcut to sqlite3 connection context manager """
180
+ return sqlite3.connect(self.database, **self.connection_params)
181
+
182
+
183
+ def open(self) -> None:
184
+ """ Opens unmanaged transaction """
185
+ if self.in_transaction():
186
+ raise NestedTransactionError("Can't re-open existing connection")
187
+
188
+ self._trans = self.connect()
189
+ self._trans_cursor = self._trans.cursor()
190
+
191
+
192
+ def close(self) -> None:
193
+ """ Closes unmanaged transaction """
194
+ if not self.in_transaction():
195
+ return
196
+
197
+ if self._is_managed_transaction:
198
+ raise TransactionError("Can't manually close managed transaction")
199
+
200
+ self._trans_cursor.close()
201
+ self._trans.close()
202
+ del(self._trans_cursor)
203
+ del(self._trans)
204
+
205
+
206
+ def commit(self) -> None:
207
+ if not self.in_transaction():
208
+ raise OutsideTransactionError("Can't commit outside transaction mode")
209
+
210
+ self._trans.commit()
211
+
212
+
213
+ def rollback(self) -> None:
214
+ if not self.in_transaction():
215
+ raise OutsideTransactionError("Can't rollback outside transaction mode")
216
+
217
+ self._trans.rollback()
218
+
219
+
220
+ @contextmanager
221
+ def transaction(self, autocommit : bool = True):
222
+ """
223
+ Creates context manager to use class methods in transaction
224
+
225
+ Args:
226
+ autocommit (bool): If `True`, will commit changes at the end of transaction
227
+ """
228
+
229
+ self.open()
230
+ self._is_managed_transaction = True
231
+ logger.debug(f"{self.database}: Transaction started")
232
+
233
+ try:
234
+ yield
235
+
236
+ except Exception as e:
237
+ logger.error(f"{self.database}: Error while in transaction: {e}")
238
+ logger.debug(e, exc_info=True)
239
+ self._trans.rollback()
240
+ raise e
241
+
242
+ else:
243
+ if autocommit:
244
+ self._trans.commit()
245
+
246
+ finally:
247
+ self._is_managed_transaction = False
248
+ self.close()
249
+ logger.debug(f"{self.database}: Transaction finished")
250
+
251
+
252
+ @property
253
+ def tx_conn(self) -> sqlite3.Connection:
254
+ """ Gives access to connection while in transaction """
255
+ if not self.in_transaction():
256
+ raise OutsideTransactionError("`tx_conn` is not available outside the transaction mode")
257
+ return self._trans
258
+
259
+
260
+ @property
261
+ def tx_cursor(self) -> sqlite3.Cursor:
262
+ """ Gives access to connection cursor while in transaction """
263
+ if not self.in_transaction():
264
+ raise OutsideTransactionError("`tx_cursor` is not available outside the transaction mode")
265
+ return self._trans_cursor
266
+
@@ -1,12 +1,5 @@
1
- from __future__ import annotations
2
1
  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
2
+ from typing import Sequence
10
3
 
11
4
  from .types import SqlRow
12
5
 
@@ -57,25 +50,3 @@ def to_html(tablename : str, columns : list[str], repr_rows : Sequence[SqlRow],
57
50
 
58
51
  return "".join(html)
59
52
 
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)
@@ -4,9 +4,7 @@ from typing import Sequence
4
4
  def format_list(items : Sequence | set, brackets : bool = True) -> str:
5
5
  """ Formats list into `(item1, item2, ...)` format """
6
6
  items_str = ', '.join([str(i) for i in items])
7
- if not brackets:
8
- return items_str
9
- return f'({items_str})'
7
+ return f'({items_str})' if brackets else items_str
10
8
 
11
9
 
12
10
  def create_table(
@@ -40,7 +38,7 @@ def values_placeholder(n_values : int) -> str:
40
38
  def bulk_placeholder(n_values : int, n_rows : int) -> str:
41
39
  """ Creates placeholders `(?, ?, ..), (?, ?, ..), ...` for each row """
42
40
  place_holder = values_placeholder(n_values)
43
- return f"{format_list([place_holder]*n_rows, False)}"
41
+ return format_list([place_holder]*n_rows, False)
44
42
 
45
43
 
46
44
  def insert(tablename : str, columns : list[str], values : str) -> str:
@@ -1,20 +1,17 @@
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
1
+ from typing import Sequence, Literal, Generator, Self
2
+ from abc import ABC, abstractmethod
8
3
 
9
4
  from . import sqlgen as sql
10
- from .types import SqlValue, SqlRow
5
+ from .connection_manager import ConnectionManager
6
+
7
+ from .types import SqlValue, SqlRow, Schema
11
8
  from .repr import to_html
9
+ from ..exceptions import SqlEngineError, OutsideTransactionError
12
10
 
13
11
 
14
- class Where[T : Statement]:
12
+ class Where[T : "Statement"]:
15
13
  """ Where clause build helper """
16
14
 
17
-
18
15
  def __init__(self, statement : T):
19
16
 
20
17
  self._statement = statement
@@ -146,12 +143,11 @@ class Statement(ABC):
146
143
  Statement object that helps you build queries and execute them
147
144
  """
148
145
 
149
- __command__ : Literal["SELECT", "INSERT", "UPDATE", "DELETE"]
146
+ def __init__(self, connection : ConnectionManager, tableschema : Schema) -> None:
150
147
 
151
- def __init__(self, table : SqlTableMixin) -> None:
152
-
153
- self._table = table
154
- self._where: Where[Self] = Where(self)
148
+ self._tableschema = tableschema
149
+ self._connection = connection
150
+ self._where = Where(self)
155
151
 
156
152
  self._custom_query : str | None = None
157
153
  self._custom_args : tuple[SqlValue, ...] = ()
@@ -175,7 +171,7 @@ class Statement(ABC):
175
171
 
176
172
 
177
173
  def reset(self) -> None:
178
- """ Resets statement to reuse object """
174
+ """ Reset statement to reuse the object """
179
175
  self._where.reset()
180
176
  self._custom_query = None
181
177
  self._custom_args = ()
@@ -209,18 +205,17 @@ class Statement(ABC):
209
205
 
210
206
 
211
207
  class MutationalStatement(Statement, ABC):
212
-
213
208
 
214
209
  def execute(self) -> None:
210
+ """ Execute built statement """
215
211
  query, args = self.build()
216
- self._table.execute(query, *args)
212
+ self._connection.execute(query, *args)
217
213
 
218
214
 
219
215
  class Select(Statement):
220
216
 
221
-
222
- def __init__(self, table : SqlTableMixin) -> None:
223
- super().__init__(table)
217
+ def __init__(self, connection : ConnectionManager, tableschema : Schema) -> None:
218
+ super().__init__(connection, tableschema)
224
219
 
225
220
  self._columns : list[str] = []
226
221
  self._order_by : list[str] = []
@@ -229,48 +224,54 @@ class Select(Statement):
229
224
 
230
225
 
231
226
  def __call__(self, *columns : str) -> Self:
227
+ """ Shortcut to columns selector """
232
228
  return self.columns(*columns)
233
229
 
234
230
 
235
231
  def columns(self, *columns : str) -> Self:
236
- """ Column selector """
232
+ """ Columns selector """
237
233
  self._columns.extend(columns)
238
234
  return self
239
235
 
240
236
 
241
237
  def aggregate(self, by : Literal['COUNT', 'SUM', 'AVG', 'MIN', 'MAX']) -> Self:
242
-
238
+ """ Aggregate by provided method """
243
239
  if self._aggregate:
244
- raise ValueError("Can't aggregate columns multiple times")
240
+ raise SqlEngineError("Can't aggregate columns multiple times")
245
241
 
246
242
  self._aggregate = by
247
243
  return self
248
244
 
249
245
 
250
246
  def order_by(self, column : str, ascending : bool = True) -> Self:
247
+ """ Orders returned rows by provided column """
251
248
  order = "ASC" if ascending else "DESC"
252
249
  self._order_by.append(f"{column} {order}")
253
250
  return self
254
251
 
255
252
 
256
253
  def limit(self, n : int) -> Self:
254
+ """ Limit number of returned rows """
257
255
  self._limit = n
258
256
  return self
259
257
 
260
258
 
261
259
  def fetchone(self) -> SqlRow:
260
+ """ Fetch first row """
262
261
  query, args = self.build()
263
- return self._table.fetchone(query, *args)
262
+ return self._connection.fetchone(query, *args)
264
263
 
265
264
 
266
265
  def fetchmany(self, size : int = 1) -> list[SqlRow]:
266
+ """ Fetch first `size` rows """
267
267
  query, args = self.build()
268
- return self._table.fetchmany(query, *args, size=size)
268
+ return self._connection.fetchmany(query, *args, size=size)
269
269
 
270
270
 
271
271
  def fetchall(self) -> list[SqlRow]:
272
+ """ Fetch all rows """
272
273
  query, args = self.build()
273
- return self._table.fetchall(query, *args)
274
+ return self._connection.fetchall(query, *args)
274
275
 
275
276
 
276
277
  def fetchmany_iterator(self, batch_size: int) -> Generator[list[SqlRow], None, None]:
@@ -282,17 +283,19 @@ class Select(Statement):
282
283
 
283
284
  Examples:
284
285
 
285
- >>> with table.transaction():
286
- >>> for batch in table.select.where.gt("Age", 30).then.fetchmany_iterator(1000):
287
- >>> process_batch(batch)
286
+ ```python
287
+ with table.transaction():
288
+ for batch in table.select.where.gt("Age", 30).then.fetchmany_iterator(1000):
289
+ process_batch(batch)
290
+ ```
288
291
  """
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
+ if not self._connection.in_transaction():
293
+ raise OutsideTransactionError("To use the `fetchall_iterator()` method you have \
294
+ to keep open the transaction of the table")
292
295
 
293
296
  query, exec_args = self.build()
294
297
 
295
- iter_cursor = self._table.tx_conn.cursor()
298
+ iter_cursor = self._connection.tx_conn.cursor()
296
299
  iter_cursor.execute(query, exec_args)
297
300
 
298
301
  while batch := iter_cursor.fetchmany(batch_size):
@@ -300,15 +303,26 @@ class Select(Statement):
300
303
 
301
304
 
302
305
  def __iter__(self) -> Generator[SqlRow, None, None]:
303
- """ Select statement rows iterator """
306
+ """
307
+ Select statement rows iterator
308
+
309
+ Examples:
310
+
311
+ ```python
312
+ with table.transaction():
313
+ # here `then` is used to link back to the `select` instance from `where` object
314
+ for row in table.select.where.gt("Age", 30).then:
315
+ process_row(row)
316
+ ```
317
+ """
304
318
 
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")
319
+ if not self._connection.in_transaction():
320
+ raise OutsideTransactionError("To use the __iter__ method you have \
321
+ to keep open the transaction of the table")
308
322
 
309
323
  query, exec_args = self.build()
310
324
 
311
- iter_cursor = self._table.tx_conn.cursor()
325
+ iter_cursor = self._connection.tx_conn.cursor()
312
326
  iter_cursor.execute(query, exec_args)
313
327
 
314
328
  while row := iter_cursor.fetchone():
@@ -330,7 +344,7 @@ class Select(Statement):
330
344
  else:
331
345
  limit = None
332
346
 
333
- query = sql.select(self._table.tablename, columns, where_clause, order, limit)
347
+ query = sql.select(self._tableschema["tablename"], columns, where_clause, order, limit)
334
348
 
335
349
  return query, args
336
350
 
@@ -342,43 +356,51 @@ class Select(Statement):
342
356
  self._limit = None
343
357
 
344
358
 
359
+ def _resolve_columns(self) -> list[str]:
360
+ return (
361
+ self._tableschema["columns"]
362
+ if len(self._columns) == 0 or "*" in self._columns
363
+ else self._columns
364
+ )
365
+
366
+
345
367
  def _repr_html_(self) -> str | None:
346
368
 
347
369
  if self._aggregate:
348
370
  return None
349
371
 
350
372
  limit = 26
351
- columns = self._table.columns if len(self._columns) == 0 or "*" in self._columns else self._columns
373
+ columns = self._resolve_columns()
352
374
  repr_rows = self.fetchmany(limit)
353
375
 
354
- return to_html(self._table.tablename, columns, repr_rows, limit=limit-1)
376
+ return to_html(self._tableschema["tablename"], columns, repr_rows, limit=limit-1)
355
377
 
356
378
 
357
379
  class Delete(MutationalStatement):
358
380
 
359
-
360
381
  def _build(self, where_clause : str, *args : SqlValue) -> tuple[str, tuple[SqlValue, ...]]:
361
382
 
362
383
  if not where_clause:
363
- raise ValueError("Delete statement must have a where clause")
384
+ raise SqlEngineError("Delete statement must have a where clause")
364
385
 
365
- query = sql.delete_rows(self._table.tablename, where_clause)
386
+ query = sql.delete_rows(self._tableschema["tablename"], where_clause)
366
387
  return query, args
367
388
 
389
+
368
390
  def _reset(self) -> None:
369
391
  pass
370
392
 
371
393
 
372
394
  class Update(MutationalStatement):
373
395
 
374
-
375
- def __init__(self, table : SqlTableMixin) -> None:
376
- super().__init__(table)
396
+ def __init__(self, connection : ConnectionManager, tableschema : Schema) -> None:
397
+ super().__init__(connection, tableschema)
377
398
  self._set_clauses : list[str] = []
378
399
  self._set_args : list[SqlValue] = []
379
400
 
380
401
 
381
402
  def __call__(self, column : str, value : SqlValue) -> Self:
403
+ """ Shortcut to set value to a column """
382
404
  return self.set(column, value)
383
405
 
384
406
 
@@ -391,7 +413,7 @@ class Update(MutationalStatement):
391
413
 
392
414
  def _build(self, where_clause : str, *args : SqlValue) -> tuple[str, tuple[SqlValue, ...]]:
393
415
  set_clause = sql.format_list(self._set_clauses, brackets=False)
394
- query = f"UPDATE {self._table.tablename} SET {set_clause} WHERE {where_clause};"
416
+ query = f"UPDATE {self._tableschema["tablename"]} SET {set_clause} WHERE {where_clause};"
395
417
  return query, (*self._set_args, *args)
396
418
 
397
419