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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlengine-lite
3
- Version: 2.1.0
3
+ Version: 2.1.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
@@ -1,17 +1,20 @@
1
1
  [project]
2
2
  name = "sqlengine-lite"
3
- version = "2.1.0"
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
- packages = ["sqlengine"]
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,5 @@
1
+ from .connection import shared_connection
2
+ from .statements import Select, Delete, Update
3
+ from .repr import to_csv
4
+
5
+ __all__ = ["shared_connection", "Select", "Delete", "Update", "to_csv"]
@@ -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_]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlengine-lite
3
- Version: 2.1.0
3
+ Version: 2.1.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
@@ -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