sqlengine-lite 2.2.0__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 .core import sqlgen, types
2
- from .core.types import Schema, Primary
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", "Primary"]
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"]
@@ -6,6 +6,7 @@ from typing import overload, Literal, Sequence
6
6
  from contextlib import contextmanager
7
7
 
8
8
  from .types import SqlValue, SqlRow
9
+ from ..exceptions import TransactionError, NestedTransactionError, OutsideTransactionError
9
10
 
10
11
 
11
12
  logger = logging.getLogger("sqlengine")
@@ -14,6 +15,16 @@ logger.setLevel(os.getenv("SQL_ENGINE_LOG_LEVEL", "WARNING").upper())
14
15
 
15
16
  class ConnectionManager:
16
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
+
17
28
  _trans : sqlite3.Connection
18
29
  _trans_cursor : sqlite3.Cursor
19
30
 
@@ -172,7 +183,7 @@ class ConnectionManager:
172
183
  def open(self) -> None:
173
184
  """ Opens unmanaged transaction """
174
185
  if self.in_transaction():
175
- raise RuntimeError("Can't re-open existing connection")
186
+ raise NestedTransactionError("Can't re-open existing connection")
176
187
 
177
188
  self._trans = self.connect()
178
189
  self._trans_cursor = self._trans.cursor()
@@ -184,7 +195,7 @@ class ConnectionManager:
184
195
  return
185
196
 
186
197
  if self._is_managed_transaction:
187
- raise RuntimeError("Can't manually close managed transaction")
198
+ raise TransactionError("Can't manually close managed transaction")
188
199
 
189
200
  self._trans_cursor.close()
190
201
  self._trans.close()
@@ -194,14 +205,14 @@ class ConnectionManager:
194
205
 
195
206
  def commit(self) -> None:
196
207
  if not self.in_transaction():
197
- raise RuntimeError("Can't commit outside transaction mode")
208
+ raise OutsideTransactionError("Can't commit outside transaction mode")
198
209
 
199
210
  self._trans.commit()
200
211
 
201
212
 
202
213
  def rollback(self) -> None:
203
214
  if not self.in_transaction():
204
- raise RuntimeError("Can't rollback outside transaction mode")
215
+ raise OutsideTransactionError("Can't rollback outside transaction mode")
205
216
 
206
217
  self._trans.rollback()
207
218
 
@@ -242,7 +253,7 @@ class ConnectionManager:
242
253
  def tx_conn(self) -> sqlite3.Connection:
243
254
  """ Gives access to connection while in transaction """
244
255
  if not self.in_transaction():
245
- raise RuntimeError("`tx_conn` is not available outside the transaction mode")
256
+ raise OutsideTransactionError("`tx_conn` is not available outside the transaction mode")
246
257
  return self._trans
247
258
 
248
259
 
@@ -250,6 +261,6 @@ class ConnectionManager:
250
261
  def tx_cursor(self) -> sqlite3.Cursor:
251
262
  """ Gives access to connection cursor while in transaction """
252
263
  if not self.in_transaction():
253
- raise RuntimeError("`tx_cursor` is not available outside the transaction mode")
264
+ raise OutsideTransactionError("`tx_cursor` is not available outside the transaction mode")
254
265
  return self._trans_cursor
255
266
 
@@ -2,10 +2,11 @@ from typing import Sequence, Literal, Generator, Self
2
2
  from abc import ABC, abstractmethod
3
3
 
4
4
  from . import sqlgen as sql
5
- from .connection import ConnectionManager
5
+ from .connection_manager import ConnectionManager
6
6
 
7
7
  from .types import SqlValue, SqlRow, Schema
8
8
  from .repr import to_html
9
+ from ..exceptions import SqlEngineError, OutsideTransactionError
9
10
 
10
11
 
11
12
  class Where[T : "Statement"]:
@@ -146,7 +147,7 @@ class Statement(ABC):
146
147
 
147
148
  self._tableschema = tableschema
148
149
  self._connection = connection
149
- self._where: Where[Self] = Where(self)
150
+ self._where = Where(self)
150
151
 
151
152
  self._custom_query : str | None = None
152
153
  self._custom_args : tuple[SqlValue, ...] = ()
@@ -206,6 +207,7 @@ class Statement(ABC):
206
207
  class MutationalStatement(Statement, ABC):
207
208
 
208
209
  def execute(self) -> None:
210
+ """ Execute built statement """
209
211
  query, args = self.build()
210
212
  self._connection.execute(query, *args)
211
213
 
@@ -222,46 +224,52 @@ class Select(Statement):
222
224
 
223
225
 
224
226
  def __call__(self, *columns : str) -> Self:
227
+ """ Shortcut to columns selector """
225
228
  return self.columns(*columns)
226
229
 
227
230
 
228
231
  def columns(self, *columns : str) -> Self:
229
- """ Column selector """
232
+ """ Columns selector """
230
233
  self._columns.extend(columns)
231
234
  return self
232
235
 
233
236
 
234
237
  def aggregate(self, by : Literal['COUNT', 'SUM', 'AVG', 'MIN', 'MAX']) -> Self:
235
-
238
+ """ Aggregate by provided method """
236
239
  if self._aggregate:
237
- raise ValueError("Can't aggregate columns multiple times")
240
+ raise SqlEngineError("Can't aggregate columns multiple times")
238
241
 
239
242
  self._aggregate = by
240
243
  return self
241
244
 
242
245
 
243
246
  def order_by(self, column : str, ascending : bool = True) -> Self:
247
+ """ Orders returned rows by provided column """
244
248
  order = "ASC" if ascending else "DESC"
245
249
  self._order_by.append(f"{column} {order}")
246
250
  return self
247
251
 
248
252
 
249
253
  def limit(self, n : int) -> Self:
254
+ """ Limit number of returned rows """
250
255
  self._limit = n
251
256
  return self
252
257
 
253
258
 
254
259
  def fetchone(self) -> SqlRow:
260
+ """ Fetch first row """
255
261
  query, args = self.build()
256
262
  return self._connection.fetchone(query, *args)
257
263
 
258
264
 
259
265
  def fetchmany(self, size : int = 1) -> list[SqlRow]:
266
+ """ Fetch first `size` rows """
260
267
  query, args = self.build()
261
268
  return self._connection.fetchmany(query, *args, size=size)
262
269
 
263
270
 
264
271
  def fetchall(self) -> list[SqlRow]:
272
+ """ Fetch all rows """
265
273
  query, args = self.build()
266
274
  return self._connection.fetchall(query, *args)
267
275
 
@@ -275,13 +283,15 @@ class Select(Statement):
275
283
 
276
284
  Examples:
277
285
 
278
- >>> with table.transaction():
279
- >>> for batch in table.select.where.gt("Age", 30).then.fetchmany_iterator(1000):
280
- >>> 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
+ ```
281
291
  """
282
292
  if not self._connection.in_transaction():
283
- raise RuntimeError("To use the `fetchall_iterator()` method you have \
284
- to keep open the transaction of the table with `transaction()` manager")
293
+ raise OutsideTransactionError("To use the `fetchall_iterator()` method you have \
294
+ to keep open the transaction of the table")
285
295
 
286
296
  query, exec_args = self.build()
287
297
 
@@ -293,11 +303,22 @@ class Select(Statement):
293
303
 
294
304
 
295
305
  def __iter__(self) -> Generator[SqlRow, None, None]:
296
- """ 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
+ """
297
318
 
298
319
  if not self._connection.in_transaction():
299
- raise RuntimeError("To use the __iter__ method you have \
300
- to keep open the transaction of the table with `transaction()` manager")
320
+ raise OutsideTransactionError("To use the __iter__ method you have \
321
+ to keep open the transaction of the table")
301
322
 
302
323
  query, exec_args = self.build()
303
324
 
@@ -360,7 +381,7 @@ class Delete(MutationalStatement):
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
386
  query = sql.delete_rows(self._tableschema["tablename"], where_clause)
366
387
  return query, args
@@ -379,6 +400,7 @@ class Update(MutationalStatement):
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
 
@@ -12,15 +12,16 @@ class CustomType(Protocol):
12
12
  ...
13
13
 
14
14
 
15
- type SqlValue = str | int | float | bytes | None | CustomType
16
- type SqlRow = tuple[SqlValue, ...]
17
- type SqlType = type[str | int | float | bytes | CustomType]
15
+ type SqlValue = str | int | float | bytes | None | CustomType
16
+ type SqlRow = tuple[SqlValue, ...]
17
+ type SqlType = type[str | int | float | bytes | CustomType]
18
+ type ColumnType = SqlType | str | UnionType
18
19
 
19
20
 
20
21
  class Schema(TypedDict):
21
22
  tablename : str
22
23
  columns : list[str]
23
- types : list[SqlType | str]
24
+ types : list[ColumnType]
24
25
  primary : list[str]
25
26
 
26
27
 
@@ -41,7 +42,7 @@ _TYPES_MAP : dict[type | UnionType, str] = {
41
42
  }
42
43
 
43
44
 
44
- def is_custom_type(type_: SqlType) -> TypeGuard[type[CustomType]]:
45
+ def is_custom_type(type_: SqlType | UnionType) -> TypeGuard[type[CustomType]]:
45
46
  return (
46
47
  type_ not in (str, int, float, bytes)
47
48
  and isinstance(type_, type)
@@ -52,7 +53,7 @@ def is_custom_type(type_: SqlType) -> TypeGuard[type[CustomType]]:
52
53
 
53
54
  def register_type(cls : type[CustomType], type_name : str | None = None) -> None:
54
55
  """
55
- Register custom type to be able to store it in tables
56
+ Register custom type into sqlite3 to be able to store it in tables
56
57
 
57
58
  Args:
58
59
  cls (CustomType): Class that implements `from_sql(cls, sql : bytes) -> Self` and `
@@ -65,7 +66,7 @@ def register_type(cls : type[CustomType], type_name : str | None = None) -> None
65
66
  sqlite3.register_converter(type_name, cls.from_sql)
66
67
 
67
68
 
68
- def pytype_to_sqltype(type_ : type) -> str:
69
+ def pytype_to_sqltype(type_ : type | UnionType) -> str:
69
70
  """ Converts python type to sql type """
70
71
  if type_ not in _TYPES_MAP:
71
72
  raise TypeError(f"{type_} is not natively supported by sqlite3")
@@ -73,7 +74,7 @@ def pytype_to_sqltype(type_ : type) -> str:
73
74
  return _TYPES_MAP[type_]
74
75
 
75
76
 
76
- def register_resolve_types(types : list[SqlType | str], **connection_params) -> tuple[list[str], dict[str, Any]]:
77
+ def register_resolve_types(types : list[ColumnType], **connection_params) -> tuple[list[str], dict[str, Any]]:
77
78
  """ Converts py types to sql types, registers custom types, resolves type names, updates connection params """
78
79
 
79
80
  resolved : list[str] = []
@@ -0,0 +1,20 @@
1
+
2
+ class SqlEngineError(Exception):
3
+ """ Errors linked to sqlengine module """
4
+ pass
5
+
6
+ class TransactionError(SqlEngineError):
7
+ """ Errors within transactions """
8
+ pass
9
+
10
+ class OutsideTransactionError(TransactionError):
11
+ """ Errors of prohibited outside tranasctions operations """
12
+ pass
13
+
14
+ class NestedTransactionError(TransactionError):
15
+ """ Errors of nested transaction operations """
16
+ pass
17
+
18
+ class TableDeclarationError(SqlEngineError):
19
+ """ Errors of table declaration """
20
+ pass
sqlengine/schema.py CHANGED
@@ -3,7 +3,7 @@ import sqlite3
3
3
  from typing import overload
4
4
 
5
5
  from .sqltable import SqlTableMixin
6
- from .core.types import Schema
6
+ from ._internal.types import Schema
7
7
 
8
8
 
9
9
  def get_database_tablenames(database : str, cursor : sqlite3.Cursor | None = None) -> list[str]:
sqlengine/sqltable.py CHANGED
@@ -5,13 +5,15 @@ import sqlite3
5
5
  from typing import Sequence, Literal, Any
6
6
  from typing import get_origin, get_args, overload, get_type_hints
7
7
 
8
- from .core import sqlgen as sql
9
- from .core.repr import to_html
8
+ from .exceptions import TableDeclarationError
10
9
 
11
- from .core import ConnectionManager, Select, Update, Delete
12
- from .core.types import SqlRow, SqlValue, SqlType
13
- from .core.types import Schema, Primary
14
- from .core.types import register_resolve_types
10
+ from ._internal import sqlgen as sql
11
+ from ._internal.repr import to_html
12
+
13
+ from ._internal import ConnectionManager, Select, Update, Delete
14
+ from ._internal.types import SqlRow, SqlValue, ColumnType
15
+ from ._internal.types import Schema, Primary
16
+ from ._internal.types import register_resolve_types
15
17
 
16
18
  logger = logging.getLogger("sqlengine")
17
19
  logger.setLevel(os.getenv("SQL_ENGINE_LOG_LEVEL", "WARNING").upper())
@@ -23,50 +25,44 @@ class SqlTableMixin:
23
25
 
24
26
  Args:
25
27
  database (str): database filename to connect to. If it not exists - will create new one first.
26
- If `":memory:"` is passed, then database will be created in memory and you will have to
28
+ If `":memory:"` is passed, then database will be set in memory and you will have to
27
29
  create table manually with `create_table()` method inside `transaction()` block.
28
30
  force_drop (bool): If `True` - will drop existing table.
29
- **connection_params (dict): Params to create connection with.
30
- Reference: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
31
+ **connection_params: Params to create connection with. Reference: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
31
32
 
32
33
  Attributes:
33
34
  __tablename__ (Optional[str]): Name of the table that will be used in queries.
34
35
  If omitted in inherited class declaration, then it will take the class name.
35
- __columns__ (list[str]): Colum names of the table
36
- __types__ (list[SqlType | str]): Colum types of the table
36
+ __columns__ (list[str]): Column names of the table
37
+ __types__ (list[ColumnType]): Column types of the table
37
38
  __primary__ (list[str]): List of primary keys
38
39
 
39
40
  Examples:
40
- >>> from sqlengine import SqlTableMixin, Primary
41
- >>>
42
- >>> class Employees(SqlTableMixin):
43
- >>> __columns__ = ["ID", "name", "surname", "salary", "position"]
44
- >>> __types__ = [int, str, str, float, "TEXT NOT NULL"]
45
- >>> __primary__ = ["ID", "name"]
46
- >>>
47
- >>> table = Employees(":memory:")
48
- >>>
49
- >>>
50
- >>> class Employees(SqlTableMixin):
51
- >>> ID : Primary[int]
52
- >>> name : Primary[str]
53
- >>> surname : str | None
54
- >>> salary : float | None
55
- >>> position : str
56
- >>>
57
- >>> table = Employees("mydb.sqlite3")
41
+ ```python
42
+ from sqlengine import SqlTableMixin, Primary
43
+
44
+ class Employees(SqlTableMixin):
45
+ ID : Primary[int]
46
+ name : Primary[str]
47
+ surname : str | None
48
+ salary : float | None
49
+ position : str
50
+
51
+ table = Employees("mydb.sqlite3")
52
+ ```
58
53
  """
59
54
 
60
55
  __tablename__ : str
61
56
  __columns__ : list[str]
62
- __types__ : list[SqlType | str]
57
+ __types__ : list[ColumnType]
63
58
  __primary__ : list[str]
64
59
 
65
60
  def __init__(self, database: str | Literal[":memory:"], force_drop : bool = False, **connection_params) -> None:
66
61
 
62
+ self.database = database
63
+
67
64
  resolved_types, connection_params = register_resolve_types(self.__types__, **connection_params)
68
65
 
69
- self.database = database
70
66
  self._connection_manager = ConnectionManager(database, **connection_params)
71
67
  self.__types_sql__ = resolved_types
72
68
 
@@ -90,7 +86,7 @@ class SqlTableMixin:
90
86
  continue
91
87
 
92
88
  if name in columns:
93
- raise AttributeError(f"Annotated column `{name}` is already in __columns__")
89
+ raise TableDeclarationError(f"Annotated column `{name}` is already in __columns__")
94
90
 
95
91
  columns.append(name)
96
92
 
@@ -99,12 +95,13 @@ class SqlTableMixin:
99
95
  continue
100
96
 
101
97
  if name in primary:
102
- raise AttributeError(f"Annotated primary column `{name}` is already in __primary__")
98
+ raise TableDeclarationError(f"Annotated primary column `{name}` is already in __primary__")
103
99
 
104
100
  primary_type = get_args(type_)[0]
105
101
 
106
102
  if not primary_type:
107
- raise AttributeError("Primary type was declared without the type. Usage: `my_column : Primary[T]`, where T is desired type")
103
+ raise TableDeclarationError(("Primary type was declared without the type. "
104
+ "Usage: `my_column : Primary[T]`, where T is desired type"))
108
105
 
109
106
  types.append(primary_type)
110
107
  primary.append(name)
@@ -128,15 +125,15 @@ class SqlTableMixin:
128
125
  ]
129
126
 
130
127
  if missing_attrs:
131
- raise AttributeError(f'{cls.__name__} is missing attributes: {missing_attrs}')
128
+ raise TableDeclarationError(f'{cls.__name__} is missing attributes: {missing_attrs}')
132
129
 
133
130
  n_types, n_cols = len(cls.__types__), len(cls.__columns__)
134
131
 
135
132
  if not n_types == n_cols:
136
- raise AttributeError(f'`__types__` and `__columns__`: length mismatch: types = {n_types}, columns = {n_cols}')
133
+ raise TableDeclarationError((f'`__types__` and `__columns__`: length mismatch: types = {n_types}, columns = {n_cols}'))
137
134
 
138
135
  if len(cls.__primary__) < 1:
139
- raise AttributeError(f'`__primary__`: Number of primary keys must be at least 1')
136
+ raise TableDeclarationError(f'`__primary__`: Number of primary keys must be at least 1')
140
137
 
141
138
  wrong_primaries = [
142
139
  prim for prim in cls.__primary__ if
@@ -144,7 +141,7 @@ class SqlTableMixin:
144
141
  ]
145
142
 
146
143
  if wrong_primaries:
147
- raise AttributeError(f'`__primary__`: Keys {wrong_primaries} can\'t be primaries as they are not declared in __columns__')
144
+ raise TableDeclarationError(f'`__primary__`: Keys {wrong_primaries} can\'t be primaries as they are not declared in __columns__')
148
145
 
149
146
 
150
147
  def _write_db(self, force_drop : bool) -> None:
@@ -193,10 +190,12 @@ class SqlTableMixin:
193
190
  autocommit (bool): If `True`, will commit changes at the end of transaction
194
191
 
195
192
  Examples:
196
-
197
- >>> with table.transaction():
198
- >>> for idx, age in table.select("ID", "Age"):
199
- >>> table.update("Age", age + 1).where.eq("ID", idx).then.execute()
193
+
194
+ ```python
195
+ with table.transaction():
196
+ for idx, age in table.select("ID", "Age"):
197
+ table.update("Age", age + 1).where.eq("ID", idx).then.execute()
198
+ ```
200
199
  """
201
200
 
202
201
  return self._connection_manager.transaction(autocommit)
@@ -207,17 +206,22 @@ class SqlTableMixin:
207
206
  Insert single row
208
207
 
209
208
  Args:
210
- *args (Any): Arguments in order of declared __columns__
211
- **kwargs (Any): Unused
209
+ *args (SqlValue): Arguments in order of declared __columns__
210
+ **kwargs (SqlValue): Column to value mapping
212
211
 
213
- Example:
214
- >>> table = MyTable("mydb.db")
215
- >>> table.columns
216
- >>> # ["ID", "Name", "Age"]
217
- >>> table.insert(0, "Daniel", 27)
212
+ Examples:
213
+
214
+ ```python
215
+ table = MyTable("mydb.db")
216
+ table.columns # -> ["ID", "Name", "Age"]
217
+ table.insert(0, "Daniel", 27)
218
+ table.insert(ID=1, name="Boris", age=26)
219
+ ```
218
220
  """
219
- query = sql.insert_row(self.tablename, self.columns)
220
- self._connection_manager.execute(query, *args)
221
+ columns = self.columns[:len(args)]
222
+ columns.extend(kwargs.keys())
223
+ query = sql.insert_row(self.tablename, columns)
224
+ self._connection_manager.execute(query, *args, *kwargs.values())
221
225
 
222
226
 
223
227
  def upsert(self, *args, **kwargs) -> None:
@@ -226,18 +230,22 @@ class SqlTableMixin:
226
230
  via the declared `primary` key
227
231
 
228
232
  Args:
229
- *args (Any): Arguments in order of declared __columns__
230
- **kwargs (Any): Unused
233
+ **args (SqlValue): Arguments in order of declared __columns__
234
+ **kwargs (SqlValue): Column to value mapping
231
235
 
232
236
  Example:
233
- >>> table = MyTable("mydb.db")
234
- >>> table.columns
235
- >>> # ["ID", "Name", "Age"]
236
- >>> table.upsert(0, "Daniel", 27)
237
- >>> table.upsert(0, "Daniel", 21)
237
+
238
+ ```python
239
+ table = MyTable("mydb.db")
240
+ table.columns # -> ["ID", "Name", "Age"]
241
+ table.upsert(0, "Daniel", 27)
242
+ table.upsert(ID=0, age=21)
243
+ ```
238
244
  """
239
- query = sql.upsert(self.tablename, self.columns, self.primary)
240
- self._connection_manager.execute(query, *args)
245
+ columns = self.columns[:len(args)]
246
+ columns.extend(kwargs.keys())
247
+ query = sql.upsert(self.tablename, columns, self.primary)
248
+ self._connection_manager.execute(query, *args, *kwargs.values())
241
249
 
242
250
 
243
251
  def insert_many(self, rows: Sequence[SqlRow]) -> None:
@@ -386,19 +394,43 @@ class SqlTableMixin:
386
394
 
387
395
  @property
388
396
  def update(self) -> Update:
389
- """ UPDATE statement builder and executor """
397
+ """
398
+ UPDATE statement builder and executor
399
+
400
+ Examples:
401
+
402
+ ```python
403
+ table.update.set("City", "Karaganda").where.eq("Country", "Czech Republic").then.execute()
404
+ ```
405
+ """
390
406
  return Update(self.conn, self.schema)
391
407
 
392
408
 
393
409
  @property
394
410
  def delete(self) -> Delete:
395
- """ DELETE statement builder and executor """
411
+ """
412
+ DELETE statement builder and executor
413
+
414
+ Examples:
415
+
416
+ ```python
417
+ table.delete.where.eq("ID", 0).then.execute()
418
+ ```
419
+ """
396
420
  return Delete(self.conn, self.schema)
397
421
 
398
422
 
399
423
  @property
400
424
  def select(self) -> Select:
401
- """ SELECT statement builder and fetcher """
425
+ """
426
+ SELECT statement builder and fetcher
427
+
428
+ Examples:
429
+
430
+ ```python
431
+ table.select("Email").where.eq("SupportRepId", 3).then.aggregate("COUNT").fetchone()
432
+ ```
433
+ """
402
434
  return Select(self.conn, self.schema)
403
435
 
404
436
 
@@ -409,7 +441,7 @@ class SqlTableMixin:
409
441
 
410
442
 
411
443
  @property
412
- def types(self) -> list[SqlType | str]:
444
+ def types(self) -> list[ColumnType]:
413
445
  """ List of table column dtypes as declared"""
414
446
  return self.__types__
415
447
 
@@ -452,7 +484,7 @@ class SqlTableMixin:
452
484
 
453
485
  @property
454
486
  def conn(self) -> ConnectionManager:
455
- """ Access connection manager instance """
487
+ """ Access connection manager instance """
456
488
  return self._connection_manager
457
489
 
458
490
 
@@ -476,4 +508,4 @@ class SqlTableMixin:
476
508
 
477
509
  @connection_params.setter
478
510
  def connection_params(self, value : dict[str, Any]) -> None:
479
- self._connection_manager.connection_params = value
511
+ self._connection_manager.connection_params = value
@@ -1,4 +1,4 @@
1
1
  from .connection import shared_connection
2
- from .convert import to_csv
2
+ from .convert import to_csv, to_dicts, to_dicts_stream
3
3
 
4
- __all__ = ["shared_connection", "to_csv"]
4
+ __all__ = ["shared_connection", "to_csv", "to_dicts", "to_dicts_stream"]
@@ -4,8 +4,9 @@ import sqlite3
4
4
 
5
5
  from contextlib import contextmanager
6
6
 
7
- from ..sqltable import SqlTableMixin
8
- from ..core.connection import ConnectionManager
7
+ from ..sqltable import SqlTableMixin
8
+ from .._internal import ConnectionManager
9
+ from ..exceptions import NestedTransactionError
9
10
 
10
11
 
11
12
  logger = logging.getLogger("sqlengine")
@@ -26,11 +27,13 @@ def shared_connection(*args : SqlTableMixin, autocommit : bool = True, **connect
26
27
 
27
28
  Examples:
28
29
 
29
- >>> from sqlengine.utils import shared_connection
30
- >>> with shared_connection(table1, table2, **table1.connection_params):
31
- >>> for (id1,), (id2, temp) in zip(table1.select("ID").limit(20), table2.select("ID", "Temperature").limit(20)):
32
- >>> if id1 == id2:
33
- >>> table.update.where.eq("ID", id2).then.set("Salary", temp).execute()
30
+ ```python
31
+ from sqlengine.utils import shared_connection
32
+ with shared_connection(table1, table2, **table1.connection_params):
33
+ for (id1,), (id2, temp) in zip(table1.select("ID").limit(20), table2.select("ID", "Temperature").limit(20)):
34
+ if id1 == id2:
35
+ table.update.where.eq("ID", id2).then.set("Salary", temp).execute()
36
+ ```
34
37
  """
35
38
 
36
39
  tables_in_trans = [
@@ -38,7 +41,7 @@ def shared_connection(*args : SqlTableMixin, autocommit : bool = True, **connect
38
41
  ]
39
42
 
40
43
  if tables_in_trans:
41
- raise RuntimeError(f"Tables {tables_in_trans} are already in transaction")
44
+ raise NestedTransactionError(f"Tables {tables_in_trans} are already in transaction")
42
45
 
43
46
  unique_databases = set(table.database for table in args)
44
47
  database_map : dict[str, list[ConnectionManager]] = {}
@@ -1,24 +1,129 @@
1
1
  import csv
2
+ from typing import Generator
2
3
 
3
4
  from ..sqltable import SqlTableMixin
4
- from ..core.statements import Where, Select
5
+ from .._internal.statements import Where, Select
6
+ from .._internal.types import SqlValue
5
7
 
6
8
 
7
- def to_csv(builder : Select | Where[Select] | SqlTableMixin, path : str) -> None:
9
+ def _get_select(builder : Select | Where[Select] | SqlTableMixin) -> Select:
8
10
 
9
11
  match builder:
10
12
  case Where():
11
- builder = builder.then
13
+ return builder.then
12
14
  case SqlTableMixin():
13
- builder = builder.select
15
+ return builder.select
16
+ case Select():
17
+ return builder
18
+ case _:
19
+ raise TypeError(f"Unexpected builder type {type(builder)}")
20
+
21
+
22
+ def to_csv(
23
+ builder : Select | Where[Select] | SqlTableMixin,
24
+ filename : str,
25
+ stream_batch_size : int | None = None
26
+ ) -> None:
27
+ """
28
+ Writes query or whole table to a csv file
29
+
30
+ Args:
31
+ builder (Select | Where[Select] | SqlTableMixin): Object to convert to csv
32
+ filename (str): Path to write to
33
+ stream_bach_size (int | None): If not None or 0, will stream all rows to csv in batches of provided size
34
+ """
35
+
36
+ builder = _get_select(builder)
14
37
 
15
38
  if builder._aggregate:
16
39
  raise AssertionError("Aggregated queries are not supported")
17
40
 
18
- columns = builder._resolve_columns()
19
- repr_rows = builder.fetchall()
20
-
21
- with open(path, 'w', newline='') as file:
41
+ if stream_batch_size and not builder._connection.in_transaction():
42
+ raise RuntimeError("To stream to csv you have to keep open the `transaction`")
43
+
44
+ columns = builder._resolve_columns()
45
+
46
+ with open(filename, 'w', newline='') as file:
22
47
  writer = csv.writer(file)
23
48
  writer.writerow(columns)
24
- writer.writerows(repr_rows)
49
+
50
+ if not stream_batch_size:
51
+ repr_rows = builder.fetchall()
52
+ writer.writerows(repr_rows)
53
+ return
54
+
55
+ for rows in builder.fetchmany_iterator(stream_batch_size):
56
+ writer.writerows(rows)
57
+
58
+
59
+ def to_dicts(builder : Select | Where[Select] | SqlTableMixin) -> list[dict[str, SqlValue]]:
60
+ """
61
+ Converts query or whole table to pandas friendly format
62
+
63
+ Args:
64
+ builder (Select | Where[Select] | SqlTableMixin): Object to convert to list of dicts
65
+
66
+ Returns:
67
+ out (list[dict[str, SqlValue]]): list of rows mappings
68
+
69
+ Examples:
70
+
71
+ ```python
72
+ import pandas as pd
73
+
74
+ df = pd.DataFrame(to_dict(table))
75
+ ```
76
+ """
77
+
78
+ builder = _get_select(builder)
79
+
80
+ if builder._aggregate:
81
+ raise AssertionError("Aggregated queries are not supported")
82
+
83
+ columns = builder._resolve_columns()
84
+ rows = builder.fetchall()
85
+
86
+ return [dict(zip(columns, row)) for row in rows]
87
+
88
+
89
+ def to_dicts_stream(
90
+ builder : Select | Where[Select] | SqlTableMixin,
91
+ batch_size : int
92
+ ) -> Generator[list[dict[str, SqlValue]], None, None]:
93
+ """
94
+ Converts query or whole table to pandas friendly format and yields it in batches
95
+
96
+ Args:
97
+ builder (Select | Where[Select] | SqlTableMixin): Object to convert to list of dicts
98
+ batch_size (int): Size of each yiedled batch
99
+
100
+ Yields:
101
+ batch (list[dict[str, SqlValue]]): list of rows mappings
102
+
103
+ Examples:
104
+
105
+ ```python
106
+ import pandas as pd
107
+
108
+ df = pd.DataFrame(columns=table.columns)
109
+
110
+ with table.transaction():
111
+ for batch in to_dict_stream(table, 100):
112
+ df = pd.concat([df, pd.DataFrame(batch)], axis=0)
113
+
114
+ df.set_index("ID", inplace=True)
115
+ ```
116
+ """
117
+
118
+ builder = _get_select(builder)
119
+
120
+ if builder._aggregate:
121
+ raise AssertionError("Aggregated queries are not supported")
122
+
123
+ if not builder._connection.in_transaction():
124
+ raise RuntimeError("To stream to csv you have to keep open the `transaction`")
125
+
126
+ columns = builder._resolve_columns()
127
+
128
+ for rows in builder.fetchmany_iterator(batch_size):
129
+ yield [dict(zip(columns, row)) for row in rows]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlengine-lite
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Cute sqlite3 wrapper for sql tables
5
5
  Project-URL: Homepage, https://github.com/suffermuffin/SQL-Engine
6
6
  Project-URL: Repository, https://github.com/suffermuffin/SQL-Engine.git
@@ -27,6 +27,7 @@ Dynamic: license-file
27
27
  - [Get Item](#get-item)
28
28
  - [Custom Types](#custom-types)
29
29
  - [Csv Converter](#csv-converter)
30
+ - [Pandas-like Converter](#pandas-like-converter)
30
31
  - [Full Documentation](#full-documentation)
31
32
 
32
33
 
@@ -127,25 +128,19 @@ More details at [Declaration](https://github.com/suffermuffin/SQL-Engine/blob/ma
127
128
  from sqlengine import SqlTableMixin, Primary
128
129
 
129
130
  # Helper constants for column names
130
- ID = "ID"
131
- Name = "Name"
131
+ ID = "ID"
132
+ Name = "Name"
132
133
  Occupation = "Occupation"
133
- Salary = "Salary"
134
+ Salary = "Salary"
134
135
 
135
136
 
136
137
  class Employees(SqlTableMixin):
137
138
 
138
139
  ID : Primary[int]
139
- Name : str
140
+ Name : str | None
140
141
  Occupation : str
141
142
  Salary : float
142
143
 
143
- # You may overwrite your insert methods for type consistency
144
- def insert(self, id : int, name : str, occupation : str, salary : float) -> None:
145
- return super().insert(id, name, occupation, salary)
146
-
147
- def upsert(self, id : int, name : str, occupation : str, salary : float) -> None:
148
- return super().upsert(id, name, occupation, salary)
149
144
  ```
150
145
 
151
146
  ## Instantiation
@@ -168,11 +163,16 @@ table = Employees("temp/data.db", force_drop=True)
168
163
  table.insert(1, "John Doe", "Software Engineer", 75000.0)
169
164
  ```
170
165
 
166
+ ```py
167
+ # Use kwargs mapping to insert/upsert one row
168
+
169
+ table.insert(2, salary=80000.0, name="Jane Smith", occupation="Data Scientist")
170
+ ```
171
+
171
172
  ```py
172
173
  # Bulk insert multiple rows
173
174
 
174
175
  employees_data = [
175
- (2, "Jane Smith", "Data Scientist", 80000.0),
176
176
  (3, "Alice Johnson", "Product Manager", 90000.0),
177
177
  (4, "Bob Brown", "Project Manager", 78000.0),
178
178
  (5, "Charlie Davis", "UI/UX Designer", 65000.0),
@@ -322,10 +322,40 @@ to_csv(table, "temp/table.csv")
322
322
 
323
323
  ```py
324
324
  # Save query result to csv
325
-
326
325
  to_csv(table.select.where.gt(Salary, 70_000), "temp/query.csv")
327
326
  ```
328
327
 
328
+ ```py
329
+ # Stream to csv
330
+ with table.transaction():
331
+ to_csv(table, "temp/query.csv", stream_batch_size=1000)
332
+ ```
333
+
334
+ ## Pandas-like Converter
335
+
336
+ ```py
337
+ # via one shot
338
+ import pandas as pd
339
+ from sqlengine.utils import to_dicts
340
+
341
+ df = pd.DataFrame(to_dicts(table))
342
+ df.set_index("ID", inplace=True)
343
+ ```
344
+
345
+ ```py
346
+ # via generator
347
+ import pandas as pd
348
+ from sqlengine.utils import to_dicts_stream
349
+
350
+ df = pd.DataFrame(columns=table.columns)
351
+
352
+ with table.transaction():
353
+ for batch in to_dicts_stream(table, batch_size=1000):
354
+ df = pd.concat([df, pd.DataFrame(batch)], axis=0)
355
+
356
+ df.set_index("ID", inplace=True)
357
+ ```
358
+
329
359
  # Full Documentation
330
360
 
331
361
  For detailed usage, API reference, and advanced examples, see the [full documentation](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/index.md).
@@ -0,0 +1,18 @@
1
+ sqlengine/__init__.py,sha256=_t3mutixVp60g1MwD6BZ6WcvXZPFz7JkV29nXXl1bOw,305
2
+ sqlengine/exceptions.py,sha256=Sd12r8dHgtKr6RKUaiHNhaEpdJr3Jr-JXePwrBQojC4,506
3
+ sqlengine/schema.py,sha256=HUk11gqOSzQTdiKBYU65EEQ52iCbUj3fxv3jigi1tG8,3692
4
+ sqlengine/sqltable.py,sha256=FOwXyTghcIxF8vhxMgzFeF6wAZNRjVt6r7Ebhqs-iJ0,16198
5
+ sqlengine/_internal/__init__.py,sha256=9cQIPDgLssRJNeROpuDYA0rJzY9B_N9VsY4rJbgzBnk,159
6
+ sqlengine/_internal/connection_manager.py,sha256=2ooDsAUvSfXTBpUJtNDS2GIO96tJo1GnE_R7dRBVflY,8466
7
+ sqlengine/_internal/repr.py,sha256=PEwC3MdOoTE8ORvcVXTCmY502boJfr5V2LU5jJ6qWG0,1478
8
+ sqlengine/_internal/sqlgen.py,sha256=b8usEqBAX5JDEdMR6Xeaona0ZuOi7vtptl858qXhD78,3473
9
+ sqlengine/_internal/statements.py,sha256=ntJrOeomPW15KowXUbnma9_ah-H5_ZmWKjemCIsf3Rw,12395
10
+ sqlengine/_internal/types.py,sha256=bA-tsS8r13pvRB3M13wFNsd3Fc9KAaqGW47aXhkKkDQ,3265
11
+ sqlengine/utils/__init__.py,sha256=EGg576qs61VsXlXd1bxF0oDytlmpcgqouVLdPxwqTrI,173
12
+ sqlengine/utils/connection.py,sha256=H86Y2PUJcTIc9iD398MJoRR-IyMYSVjOSfu_bk9UuoM,3119
13
+ sqlengine/utils/convert.py,sha256=Il5KAbc0Mbe2SIxBWyvj9rJ68XTJmpDFboJc1vifaaI,3694
14
+ sqlengine_lite-2.2.1.dist-info/licenses/LICENSE,sha256=aepvve4t1ho5uHMQG88dErp5L5CgoNAXctIjMdoceqY,1069
15
+ sqlengine_lite-2.2.1.dist-info/METADATA,sha256=XAAXT6IKmRIUkYtVkqij11U1W5UMZNwjfGg9sgbT81w,20593
16
+ sqlengine_lite-2.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ sqlengine_lite-2.2.1.dist-info/top_level.txt,sha256=KG_FG0LCB_mIEu2qJ5LGLQMpan4QmF5d8xLEcqTghCE,10
18
+ sqlengine_lite-2.2.1.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- from .statements import Select, Delete, Update
2
- from .connection import ConnectionManager
3
-
4
- __all__ = ["Select", "Delete", "Update", "ConnectionManager"]
@@ -1,17 +0,0 @@
1
- sqlengine/__init__.py,sha256=CxA4Z-rx9RfDNBntCbY7wQ7IQGxKc9bcBjHY9g7T8YQ,207
2
- sqlengine/schema.py,sha256=2PW1RsBgZejozS3xIz63Mu80tSKpJ7pcKyWt8mgWgKA,3687
3
- sqlengine/sqltable.py,sha256=Oa0CPqBbYZT5TsMK53gRrxIo4vFfhsbLnOYEDgsTUXQ,15652
4
- sqlengine/core/__init__.py,sha256=1HPCmL4ca9WLFVsgZVtEEirYUF7PwoEGm4G9yrBIiX4,151
5
- sqlengine/core/connection.py,sha256=Fz9_P8-xKggAEOSk-7Dlqm2so-qXXHyi8yIiCjGBNI8,7876
6
- sqlengine/core/repr.py,sha256=PEwC3MdOoTE8ORvcVXTCmY502boJfr5V2LU5jJ6qWG0,1478
7
- sqlengine/core/sqlgen.py,sha256=b8usEqBAX5JDEdMR6Xeaona0ZuOi7vtptl858qXhD78,3473
8
- sqlengine/core/statements.py,sha256=mAvaM3poFDEWl6cu8FemeCLpRDFGwqM-QKGQym446xM,11683
9
- sqlengine/core/types.py,sha256=o-P9o4KRjWeubrwDdkEKsSaU7KVEBxazHY6BGaNG2Xw,3185
10
- sqlengine/utils/__init__.py,sha256=2D55vx-Gzlz0Yjl09zVn3S0tPh3NjbTHbrqw3BZbGL4,115
11
- sqlengine/utils/connection.py,sha256=bNGMN360FUoxscWv_-YuJA2UEQ0ZQLwjrSwBINXhFmw,3082
12
- sqlengine/utils/convert.py,sha256=aVpGy0ZC25ZTdeu36gbHxwzdP6je-N8bPVcFrq6HZms,654
13
- sqlengine_lite-2.2.0.dist-info/licenses/LICENSE,sha256=aepvve4t1ho5uHMQG88dErp5L5CgoNAXctIjMdoceqY,1069
14
- sqlengine_lite-2.2.0.dist-info/METADATA,sha256=i77esz2My03c_7zqcNYnqvR7CEa9zhQJOrzQjCZUfcs,20190
15
- sqlengine_lite-2.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
- sqlengine_lite-2.2.0.dist-info/top_level.txt,sha256=KG_FG0LCB_mIEu2qJ5LGLQMpan4QmF5d8xLEcqTghCE,10
17
- sqlengine_lite-2.2.0.dist-info/RECORD,,
File without changes
File without changes