sqlengine-lite 2.1.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 +7 -0
- sqlengine/schema.py +114 -0
- sqlengine/sqltable.py +593 -0
- sqlengine_lite-2.1.0.dist-info/METADATA +287 -0
- sqlengine_lite-2.1.0.dist-info/RECORD +8 -0
- sqlengine_lite-2.1.0.dist-info/WHEEL +5 -0
- sqlengine_lite-2.1.0.dist-info/licenses/LICENSE +21 -0
- sqlengine_lite-2.1.0.dist-info/top_level.txt +1 -0
sqlengine/__init__.py
ADDED
sqlengine/schema.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
from typing import overload
|
|
4
|
+
|
|
5
|
+
from .sqltable import SqlTableMixin
|
|
6
|
+
from .utils.types import Schema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_database_tablenames(database : str, cursor : sqlite3.Cursor | None = None) -> list[str]:
|
|
10
|
+
""" Returns all table names of provided `database` path """
|
|
11
|
+
|
|
12
|
+
query = (
|
|
13
|
+
"SELECT name FROM sqlite_schema WHERE "
|
|
14
|
+
"type = 'table' AND name NOT LIKE 'sqlite_%';"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if cursor:
|
|
18
|
+
cursor.execute(query)
|
|
19
|
+
names_raw = cursor.fetchall()
|
|
20
|
+
|
|
21
|
+
else:
|
|
22
|
+
with sqlite3.connect(database) as conn:
|
|
23
|
+
cursor = conn.cursor()
|
|
24
|
+
cursor.execute(query)
|
|
25
|
+
names_raw = cursor.fetchall()
|
|
26
|
+
|
|
27
|
+
return [name[0] for name in names_raw if name[0] is not None]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_table_schema(database : str, tablename : str, cursor : sqlite3.Cursor | None = None) -> Schema | None:
|
|
31
|
+
""" Constructs `Schema` from provided `database` and `tablename` if this table exists """
|
|
32
|
+
|
|
33
|
+
query = f"PRAGMA table_info({tablename});"
|
|
34
|
+
|
|
35
|
+
if cursor:
|
|
36
|
+
cursor.execute(query)
|
|
37
|
+
column_types = cursor.fetchall()
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
with sqlite3.connect(database) as conn:
|
|
41
|
+
cur = conn.cursor()
|
|
42
|
+
cur.execute(query)
|
|
43
|
+
column_types = cur.fetchall()
|
|
44
|
+
|
|
45
|
+
if not column_types:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
columns = [it[1] for it in column_types]
|
|
49
|
+
types = [it[2] for it in column_types]
|
|
50
|
+
primary = [it[1] for it in column_types if it[-1] > 0]
|
|
51
|
+
|
|
52
|
+
return Schema(tablename=tablename, columns=columns, types=types, primary=primary)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_database_schemas(database : str) -> list[Schema]:
|
|
56
|
+
""" Reads provided `database` path and outputs gathered schemas of tables inside it """
|
|
57
|
+
|
|
58
|
+
if not os.path.exists(database):
|
|
59
|
+
raise FileNotFoundError(f"File not exists: {database}")
|
|
60
|
+
|
|
61
|
+
with sqlite3.connect(database) as conn:
|
|
62
|
+
cursor = conn.cursor()
|
|
63
|
+
names = get_database_tablenames(database, cursor)
|
|
64
|
+
|
|
65
|
+
schemas : list[Schema] = []
|
|
66
|
+
|
|
67
|
+
for tablename in names:
|
|
68
|
+
schema = get_table_schema(database, tablename, cursor)
|
|
69
|
+
|
|
70
|
+
if schema:
|
|
71
|
+
schemas.append(schema)
|
|
72
|
+
|
|
73
|
+
return schemas
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def table_from_schema(database : str, schema : Schema, **kwargs) -> SqlTableMixin:
|
|
77
|
+
""" Dynamically builds class from provided schema """
|
|
78
|
+
|
|
79
|
+
new_class = type(
|
|
80
|
+
schema["tablename"],
|
|
81
|
+
(SqlTableMixin,),
|
|
82
|
+
{
|
|
83
|
+
"__tablename__" : schema["tablename"],
|
|
84
|
+
"__columns__" : schema["columns"],
|
|
85
|
+
"__types__" : schema["types"],
|
|
86
|
+
"__primary__" : schema["primary"],
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
return new_class(database, **kwargs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@overload
|
|
93
|
+
def table_from_database(database : str, tablename : str, **kwargs) -> SqlTableMixin: ...
|
|
94
|
+
@overload
|
|
95
|
+
def table_from_database(database : str, tablename : None = None, **kwargs) -> list[SqlTableMixin]: ...
|
|
96
|
+
|
|
97
|
+
def table_from_database(database : str, tablename : str | None = None, **kwargs) -> list[SqlTableMixin] | SqlTableMixin:
|
|
98
|
+
""" Dynamically builds table class(es) from provided database """
|
|
99
|
+
|
|
100
|
+
schemas = get_database_schemas(database)
|
|
101
|
+
|
|
102
|
+
if len(schemas) < 1:
|
|
103
|
+
raise sqlite3.DatabaseError(f"No schemas in {database}")
|
|
104
|
+
|
|
105
|
+
if not tablename:
|
|
106
|
+
return [table_from_schema(database, schema, **kwargs) for schema in schemas]
|
|
107
|
+
|
|
108
|
+
schema_map = {schema["tablename"] : schema for schema in schemas}
|
|
109
|
+
target_schema = schema_map.get(tablename, None)
|
|
110
|
+
|
|
111
|
+
if not target_schema:
|
|
112
|
+
raise sqlite3.DatabaseError(f"No tablename `{tablename}` in {database}. Available tables: {list(schema_map.keys())}")
|
|
113
|
+
|
|
114
|
+
return table_from_schema(database, target_schema, **kwargs)
|
sqlengine/sqltable.py
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sqlite3
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Sequence, Literal, overload
|
|
7
|
+
|
|
8
|
+
from .utils import sqlgen as sql
|
|
9
|
+
from .utils.statements import Select, Update, Delete
|
|
10
|
+
from .utils.repr import to_html
|
|
11
|
+
|
|
12
|
+
from .utils.types import SqlRow, SqlValue, SqlType, Schema
|
|
13
|
+
from .utils.types import register_type, is_custom_type, pytype_to_sqltype
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("sqlengine")
|
|
16
|
+
logger.setLevel(os.getenv("SQL_ENGINE_LOG_LEVEL", "WARNING").upper())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SqlTableMixin:
|
|
20
|
+
"""
|
|
21
|
+
Lightweight wrapper for SQLite3 tables
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
database (str): database filename to connect to. If it not exists - will create new one first.
|
|
25
|
+
If `":memory:"` is passed, then database will be created in memory and you will have to
|
|
26
|
+
create table manually with `create_table()` method inside `transaction()` block.
|
|
27
|
+
force_drop (bool): If `True` - will drop existing table.
|
|
28
|
+
**connection_params (dict): Params to create connection with.
|
|
29
|
+
Reference: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
__tablename__ (Optional[str]): Name of the table that will be used in queries.
|
|
33
|
+
If omitted in inherited class declaration, then it will take the class name.
|
|
34
|
+
__columns__ (list[str]): Colum names of the table
|
|
35
|
+
__types__ (list[SqlType | str]): Colum types of the table
|
|
36
|
+
__primary__ (list[str]): List of primary keys
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
>>> class Employees(SqlTableMixin):
|
|
40
|
+
>>> __columns__ = ["ID", "name", "surname", "salary", "position"]
|
|
41
|
+
>>> __types__ = [int, str, str, float, "TEXT NOT NULL"]
|
|
42
|
+
>>> __primary__ = ["ID", "name"]
|
|
43
|
+
>>>
|
|
44
|
+
>>> table = Employees(":memory:")
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
__tablename__ : str
|
|
48
|
+
__columns__ : list[str]
|
|
49
|
+
__types__ : list[SqlType | str]
|
|
50
|
+
__primary__ : list[str]
|
|
51
|
+
__types_sql__ : list[str]
|
|
52
|
+
|
|
53
|
+
def __init__(self, database: str | Literal[":memory:"], force_drop : bool = False, **connection_params) -> None:
|
|
54
|
+
|
|
55
|
+
self.database = database
|
|
56
|
+
self.connection_params = connection_params
|
|
57
|
+
self._is_managed_transaction = False
|
|
58
|
+
|
|
59
|
+
self._validate_attributes()
|
|
60
|
+
self._register_types()
|
|
61
|
+
self._write_db(force_drop)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _validate_attributes(self) -> None:
|
|
65
|
+
|
|
66
|
+
if not hasattr(self, "__tablename__") or self.__tablename__ is None:
|
|
67
|
+
self.__tablename__ = self.__class__.__name__
|
|
68
|
+
|
|
69
|
+
missing_attrs = [
|
|
70
|
+
attr for attr in
|
|
71
|
+
[ "__columns__", "__types__", "__primary__"]
|
|
72
|
+
if not hasattr(self, attr)
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
if missing_attrs:
|
|
76
|
+
raise AttributeError(f'{self.tablename} is missing attributes: {missing_attrs}')
|
|
77
|
+
|
|
78
|
+
n_types, n_cols = len(self.__types__), len(self.__columns__)
|
|
79
|
+
|
|
80
|
+
if not n_types == n_cols:
|
|
81
|
+
raise AttributeError(f'`__types__` and `__columns__`: length mismatch: types = {n_types}, columns = {n_cols}')
|
|
82
|
+
|
|
83
|
+
wrong_primaries = [
|
|
84
|
+
prim for prim in self.__primary__ if
|
|
85
|
+
prim not in self.__columns__
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
if wrong_primaries:
|
|
89
|
+
raise AttributeError(f'`__primary__`: Keys {wrong_primaries} can\'t be primaries as they are not declared in __columns__')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _register_types(self) -> None:
|
|
93
|
+
|
|
94
|
+
resolved : list[str] = []
|
|
95
|
+
assert_register_types = False
|
|
96
|
+
|
|
97
|
+
for type_ in self.__types__:
|
|
98
|
+
|
|
99
|
+
if isinstance(type_, str):
|
|
100
|
+
resolved.append(type_)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if is_custom_type(type_):
|
|
104
|
+
ctname = type_.__name__.upper()
|
|
105
|
+
|
|
106
|
+
register_type(type_, ctname)
|
|
107
|
+
resolved.append(ctname)
|
|
108
|
+
|
|
109
|
+
if not assert_register_types:
|
|
110
|
+
assert_register_types = True
|
|
111
|
+
|
|
112
|
+
logger.debug(f'Registered type `{ctname}` in sqlite3')
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
sql_type = pytype_to_sqltype(type_)
|
|
116
|
+
resolved.append(sql_type)
|
|
117
|
+
|
|
118
|
+
if assert_register_types and ("detect_types" not in self.connection_params):
|
|
119
|
+
self.connection_params.update(dict(detect_types=sqlite3.PARSE_DECLTYPES))
|
|
120
|
+
|
|
121
|
+
self.__types_sql__ = resolved
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _write_db(self, force_drop : bool) -> None:
|
|
125
|
+
|
|
126
|
+
if self.database == ":memory:":
|
|
127
|
+
logger.debug(f"{self.tablename}: Using in-memory database")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
if not os.path.exists(self.database):
|
|
131
|
+
logger.debug(f'{self.tablename}: {self.database} does not exist. Creating...')
|
|
132
|
+
parent_dir = self.database.removesuffix(os.path.basename(self.database))
|
|
133
|
+
if parent_dir:
|
|
134
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
elif force_drop is True:
|
|
137
|
+
self.drop_table(confirm=True)
|
|
138
|
+
|
|
139
|
+
self.create_table()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def create_table(self) -> None:
|
|
143
|
+
""" Create table if not exists """
|
|
144
|
+
|
|
145
|
+
query = sql.create_table(
|
|
146
|
+
self.tablename, self.columns,
|
|
147
|
+
self.types_sql, self.primary
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
self.execute(query)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def drop_table(self, confirm : bool = False) -> None:
|
|
154
|
+
""" Drops table if it exists. """
|
|
155
|
+
|
|
156
|
+
if not confirm:
|
|
157
|
+
raise ValueError("To drop table you have to pass `confirm=True`")
|
|
158
|
+
|
|
159
|
+
self.execute(sql.drop_table(self.tablename))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def connect(self) -> sqlite3.Connection:
|
|
163
|
+
""" Shortcut to sqlite3 connection context manager """
|
|
164
|
+
return sqlite3.connect(self.database, **self.connection_params)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def open_connection(self) -> None:
|
|
168
|
+
""" Opens unmanaged transaction """
|
|
169
|
+
if self.in_transaction():
|
|
170
|
+
raise RuntimeError("Can't re-open existing connection")
|
|
171
|
+
|
|
172
|
+
self._trans = self.connect()
|
|
173
|
+
self._trans_cursor = self._trans.cursor()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def close_connection(self) -> None:
|
|
177
|
+
""" Closes unmanaged transaction """
|
|
178
|
+
if not self.in_transaction():
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
if self._is_managed_transaction:
|
|
182
|
+
raise RuntimeError("Can't manually close managed transaction")
|
|
183
|
+
|
|
184
|
+
self._trans_cursor.close()
|
|
185
|
+
self._trans.close()
|
|
186
|
+
del(self._trans_cursor)
|
|
187
|
+
del(self._trans)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def commit(self) -> None:
|
|
191
|
+
if not self.in_transaction():
|
|
192
|
+
raise RuntimeError("Can't commit outside transaction mode")
|
|
193
|
+
|
|
194
|
+
self._trans.commit()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def rollback(self) -> None:
|
|
198
|
+
if not self.in_transaction():
|
|
199
|
+
raise RuntimeError("Can't rollback outside transaction mode")
|
|
200
|
+
|
|
201
|
+
self._trans.rollback()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@contextmanager
|
|
205
|
+
def transaction(self, autocommit : bool = True):
|
|
206
|
+
"""
|
|
207
|
+
Creates context manager to use class methods in transaction
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
autocommit (bool): If `True`, will commit changes at the end of transaction
|
|
211
|
+
|
|
212
|
+
Examples:
|
|
213
|
+
|
|
214
|
+
>>> with table.transaction():
|
|
215
|
+
>>> for idx, age in table.select("ID", "Age"):
|
|
216
|
+
>>> table.update("Age", age + 1).where.eq("ID", idx).then.execute()
|
|
217
|
+
>>> print(table.select)
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
self.open_connection()
|
|
221
|
+
self._is_managed_transaction = True
|
|
222
|
+
logger.debug(f"{self.tablename}: Transaction started")
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
yield
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.error(f"{self.tablename}: Error while in transaction: {e}")
|
|
229
|
+
logger.debug(e, exc_info=True)
|
|
230
|
+
self._trans.rollback()
|
|
231
|
+
raise e
|
|
232
|
+
|
|
233
|
+
else:
|
|
234
|
+
if autocommit:
|
|
235
|
+
self._trans.commit()
|
|
236
|
+
|
|
237
|
+
finally:
|
|
238
|
+
self._is_managed_transaction = False
|
|
239
|
+
self.close_connection()
|
|
240
|
+
logger.debug(f"{self.tablename}: Transaction finished")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def in_transaction(self) -> bool:
|
|
244
|
+
""" Returns True if instance is in transaction """
|
|
245
|
+
return hasattr(self, "_trans") and hasattr(self, "_trans_cursor")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@overload
|
|
249
|
+
def _fetch(self, query : str, args : tuple[SqlValue, ...], method : Literal["fetchone"]) -> SqlRow: ...
|
|
250
|
+
@overload
|
|
251
|
+
def _fetch(self, query : str, args : tuple[SqlValue, ...], method : Literal["fetchall"]) -> list[SqlRow]: ...
|
|
252
|
+
|
|
253
|
+
def _fetch(self, query : str, args : tuple[SqlValue, ...] = (), method : Literal["fetchone", "fetchall"] = "fetchall") -> SqlRow | list[SqlRow]:
|
|
254
|
+
|
|
255
|
+
logger.debug(f"{self.tablename}: {query} {args}")
|
|
256
|
+
|
|
257
|
+
if self.in_transaction():
|
|
258
|
+
self._trans_cursor.execute(query, args)
|
|
259
|
+
return getattr(self._trans_cursor, method)()
|
|
260
|
+
|
|
261
|
+
with self.connect() as conn:
|
|
262
|
+
cursor = conn.cursor()
|
|
263
|
+
cursor.execute(query, args)
|
|
264
|
+
return getattr(cursor, method)()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@overload
|
|
268
|
+
def _execute(self, query : str, args : tuple[SqlValue, ...], method : Literal["execute"]) -> None: ...
|
|
269
|
+
@overload
|
|
270
|
+
def _execute(self, query : str, args : Sequence[SqlRow], method : Literal["executemany"]) -> None: ...
|
|
271
|
+
|
|
272
|
+
def _execute(self, query : str, args : tuple[SqlValue, ...] | Sequence[SqlRow] = (), method : Literal["execute", "executemany"] = "execute") -> None:
|
|
273
|
+
"""
|
|
274
|
+
Shortcut to connect() -> execute[<many>]() -> commit() for single operations.
|
|
275
|
+
Can be used in transaction using `transaction()` manager.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
query (str): SQL query to execute on SQLite3 DB
|
|
279
|
+
*args (Any): Arguments to the execution
|
|
280
|
+
method (str): "execute" or "executemany"
|
|
281
|
+
"""
|
|
282
|
+
logger.debug(f"{self.tablename}: {query} {args}")
|
|
283
|
+
|
|
284
|
+
if self.in_transaction():
|
|
285
|
+
getattr(self._trans_cursor, method)(query, args)
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
with self.connect() as conn:
|
|
289
|
+
cursor = conn.cursor()
|
|
290
|
+
getattr(cursor, method)(query, args)
|
|
291
|
+
conn.commit()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def execute(self, query : str, *args : SqlValue) -> None:
|
|
295
|
+
"""
|
|
296
|
+
Shortcut to connect() -> execute() -> commit() for single operations.
|
|
297
|
+
Can be used in transaction using `transaction()` manager.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
query (str): SQL query to execute on SQLite3 DB
|
|
301
|
+
*args (tuple[SqlValue, ...]): Arguments to the execution
|
|
302
|
+
"""
|
|
303
|
+
return self._execute(query, args, method="execute")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def executemany(self, query : str, args : Sequence[SqlRow]) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Shortcut to connect() -> executemany() -> commit() for single operations.
|
|
309
|
+
Can be used in transaction using `transaction()` manager.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
query (str): SQL query to execute on SQLite3 DB
|
|
313
|
+
*args (list[tuple[SqlValue, ...]]): Arguments to the execution
|
|
314
|
+
"""
|
|
315
|
+
return self._execute(query, args, method="executemany")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def fetchone(self, query : str, *args : SqlValue) -> SqlRow:
|
|
319
|
+
"""
|
|
320
|
+
Fetch first row based on `query`
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
query (str): SQL query
|
|
324
|
+
*args (tuple[SqlValue, ...]): Arguments to the execution
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
row (SqlRow): Single row
|
|
328
|
+
"""
|
|
329
|
+
return self._fetch(query, args, method="fetchone")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def fetchmany(self, query : str, *args : SqlValue, size : int = 1) -> list[SqlRow]:
|
|
333
|
+
"""
|
|
334
|
+
Fetch first `size` rows based on `query`
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
query (str): SQL query
|
|
338
|
+
*args (tuple[SqlValue, ...]): Arguments to the execution
|
|
339
|
+
size (str): Number of rows to return
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
rows (list[SqlRow]): list of `size` rows
|
|
343
|
+
"""
|
|
344
|
+
logger.debug(f"{self.tablename}: {query} {args}")
|
|
345
|
+
|
|
346
|
+
if self.in_transaction():
|
|
347
|
+
self._trans_cursor.execute(query, args)
|
|
348
|
+
return self._trans_cursor.fetchmany(size)
|
|
349
|
+
|
|
350
|
+
with self.connect() as conn:
|
|
351
|
+
cursor = conn.cursor()
|
|
352
|
+
cursor.execute(query, args)
|
|
353
|
+
return cursor.fetchmany(size)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def fetchall(self, query : str, *args : SqlValue) -> list[SqlRow]:
|
|
357
|
+
"""
|
|
358
|
+
Fetch all rows based on `query`
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
query (str): SQL query
|
|
362
|
+
*args (tuple[SqlValue, ...]): Arguments to the execution
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
rows (list[SqlRow]): list of rows
|
|
366
|
+
"""
|
|
367
|
+
return self._fetch(query, args, method="fetchall")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def insert(self, *args, **kwargs) -> None:
|
|
371
|
+
"""
|
|
372
|
+
Insert single row
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
*args (Any): Arguments in order of declared __columns__
|
|
376
|
+
**kwargs (Any): Unused
|
|
377
|
+
|
|
378
|
+
Example:
|
|
379
|
+
>>> table = MyTable("mydb.db")
|
|
380
|
+
>>> table.columns
|
|
381
|
+
>>> # ["ID", "Name", "Age"]
|
|
382
|
+
>>> table.insert(0, "Daniel", 27)
|
|
383
|
+
"""
|
|
384
|
+
query = sql.insert_row(self.tablename, self.columns)
|
|
385
|
+
self.execute(query, *args)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def upsert(self, *args, **kwargs) -> None:
|
|
389
|
+
"""
|
|
390
|
+
Upsert (update or insert) single row, resolving conflicts
|
|
391
|
+
via the declared `primary` key
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
*args (Any): Arguments in order of declared __columns__
|
|
395
|
+
**kwargs (Any): Unused
|
|
396
|
+
|
|
397
|
+
Example:
|
|
398
|
+
>>> table = MyTable("mydb.db")
|
|
399
|
+
>>> table.columns
|
|
400
|
+
>>> # ["ID", "Name", "Age"]
|
|
401
|
+
>>> table.upsert(0, "Daniel", 27)
|
|
402
|
+
>>> table.upsert(0, "Daniel", 21)
|
|
403
|
+
"""
|
|
404
|
+
query = sql.upsert(self.tablename, self.columns, self.primary)
|
|
405
|
+
self.execute(query, *args)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def insert_many(self, rows: Sequence[SqlRow]) -> None:
|
|
409
|
+
"""
|
|
410
|
+
Bulk insert multiple rows
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
rows (list[SqlRow]): List of tuples, each tuple contains
|
|
414
|
+
values for one row in the order of __columns__
|
|
415
|
+
"""
|
|
416
|
+
query = sql.insert_row(self.tablename, self.columns)
|
|
417
|
+
return self.executemany(query, rows)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def head(self, n : int = 5) -> list[SqlRow]:
|
|
421
|
+
""" Returns first `n` rows unordered """
|
|
422
|
+
return self.select.limit(n).fetchall()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def __repr__(self) -> str:
|
|
426
|
+
return (
|
|
427
|
+
f"{self.__class__.__name__}("
|
|
428
|
+
f"database={self.database}, "
|
|
429
|
+
f"tablename={self.tablename}, "
|
|
430
|
+
f"columns={sql.format_list(self.columns)}, "
|
|
431
|
+
f"types={sql.format_list(self.types)}, "
|
|
432
|
+
f"primary={sql.format_list(self.primary)})"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _repr_html_(self) -> str | None:
|
|
437
|
+
|
|
438
|
+
if self.database == ":memory:":
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
return to_html(self.tablename, self.columns, self.head(11), 10)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def __len__(self) -> int:
|
|
445
|
+
|
|
446
|
+
length = self.select.aggregate("COUNT").fetchone()[0]
|
|
447
|
+
|
|
448
|
+
if not isinstance(length, int):
|
|
449
|
+
raise ValueError("Unreachable")
|
|
450
|
+
return length
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@overload
|
|
454
|
+
def __getitem__(self, key : tuple[SqlValue, ...] | SqlValue) -> SqlRow: ...
|
|
455
|
+
@overload
|
|
456
|
+
def __getitem__(self, key : slice) -> list[SqlRow]: ...
|
|
457
|
+
|
|
458
|
+
def __getitem__(self, key : tuple[SqlValue, ...] | SqlValue | slice ) -> SqlRow | list[SqlRow]:
|
|
459
|
+
""" Get row by primary key """
|
|
460
|
+
|
|
461
|
+
if len(self.primary) > 1:
|
|
462
|
+
if (not isinstance(key, tuple)) or (not len(key) == len(self.primary)):
|
|
463
|
+
raise IndexError("`key` expected to be a tuple of equal leght to `primary` for multi index tables")
|
|
464
|
+
|
|
465
|
+
select = self.select
|
|
466
|
+
|
|
467
|
+
if isinstance(key, slice):
|
|
468
|
+
|
|
469
|
+
primary = self.primary[0]
|
|
470
|
+
|
|
471
|
+
if key.start is None:
|
|
472
|
+
start = select(primary).aggregate('MIN').fetchone()[0] or 0
|
|
473
|
+
select.reset()
|
|
474
|
+
else:
|
|
475
|
+
start = key.start
|
|
476
|
+
|
|
477
|
+
if key.stop is None:
|
|
478
|
+
stop = select(primary).aggregate('MAX').fetchone()[0] or 0
|
|
479
|
+
select.reset()
|
|
480
|
+
else:
|
|
481
|
+
stop = key.stop
|
|
482
|
+
|
|
483
|
+
if key.step is None:
|
|
484
|
+
step = 1
|
|
485
|
+
else:
|
|
486
|
+
step = key.step
|
|
487
|
+
|
|
488
|
+
if not (isinstance(start, int) and isinstance(stop, int)):
|
|
489
|
+
raise ValueError("Looks like like `primary` key is not integer type, or you passed non-integer slice")
|
|
490
|
+
|
|
491
|
+
if abs(step) == 1:
|
|
492
|
+
_start = min(start, stop)
|
|
493
|
+
_stop = max(start, stop)
|
|
494
|
+
|
|
495
|
+
select.order_by(primary, step > 0).where.between(primary, _start, _stop)
|
|
496
|
+
return select.fetchall()
|
|
497
|
+
|
|
498
|
+
ids = [i for i in range(start, stop, step)]
|
|
499
|
+
|
|
500
|
+
select.order_by(primary, step > 0).where.in_(primary, ids)
|
|
501
|
+
return select.fetchall()
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
if isinstance(key, tuple):
|
|
505
|
+
|
|
506
|
+
for col, val in zip(self.primary, key):
|
|
507
|
+
select.where.eq(col, val)
|
|
508
|
+
return select.fetchone()
|
|
509
|
+
|
|
510
|
+
select.where.eq(self.primary[0], key)
|
|
511
|
+
return select.fetchone()
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def update(self) -> Update:
|
|
516
|
+
""" UPDATE statement builder and executor """
|
|
517
|
+
return Update(self)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def delete(self) -> Delete:
|
|
522
|
+
""" DELETE statement builder and executor """
|
|
523
|
+
return Delete(self)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def select(self) -> Select:
|
|
528
|
+
""" SELECT statement builder and fetcher """
|
|
529
|
+
return Select(self)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def columns(self) -> list[str]:
|
|
534
|
+
""" List of table column names """
|
|
535
|
+
return self.__columns__
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def types(self) -> list[SqlType | str]:
|
|
540
|
+
""" List of table column dtypes as declared"""
|
|
541
|
+
return self.__types__
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@property
|
|
545
|
+
def types_sql(self) -> list[str]:
|
|
546
|
+
""" List of table column dtypes converted to SQL native and registered types """
|
|
547
|
+
return self.__types_sql__
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def primary(self) -> list[str]:
|
|
552
|
+
""" List of table column primary keys """
|
|
553
|
+
return self.__primary__
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@property
|
|
557
|
+
def tablename(self) -> str:
|
|
558
|
+
""" Name of the table """
|
|
559
|
+
return self.__tablename__
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def shape(self) -> tuple[int, int]:
|
|
564
|
+
""" Table shape (n_cols, n_rows) """
|
|
565
|
+
return (len(self.columns), len(self))
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@property
|
|
569
|
+
def tx_conn(self) -> sqlite3.Connection:
|
|
570
|
+
""" Gives access to connection while in transaction """
|
|
571
|
+
if not self.in_transaction():
|
|
572
|
+
raise RuntimeError("`tx_conn` is not available outside the transaction mode")
|
|
573
|
+
return self._trans
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@property
|
|
577
|
+
def tx_cursor(self) -> sqlite3.Cursor:
|
|
578
|
+
""" Gives access to connection cursor while in transaction """
|
|
579
|
+
if not self.in_transaction():
|
|
580
|
+
raise RuntimeError("`tx_cursor` is not available outside the transaction mode")
|
|
581
|
+
return self._trans_cursor
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@property
|
|
585
|
+
def schema(self) -> Schema:
|
|
586
|
+
""" Table schema """
|
|
587
|
+
|
|
588
|
+
return Schema({
|
|
589
|
+
"tablename": self.__tablename__,
|
|
590
|
+
"columns" : self.__columns__,
|
|
591
|
+
"types" : self.__types__,
|
|
592
|
+
"primary" : self.__primary__
|
|
593
|
+
})
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlengine-lite
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Cute sqlite3 wrapper for sql tables
|
|
5
|
+
Project-URL: Homepage, https://github.com/suffermuffin/SQL-Engine
|
|
6
|
+
Project-URL: Repository, https://github.com/suffermuffin/SQL-Engine.git
|
|
7
|
+
Project-URL: Documentation, https://github.com/suffermuffin/SQL-Engine/blob/main/docs/index.md
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
- [Sql-Engine](#sql-engine)
|
|
14
|
+
- [Features](#features)
|
|
15
|
+
- [Purpose](#purpose)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Env](#env)
|
|
18
|
+
- [Quick Start](#quick-start)
|
|
19
|
+
- [Table Declaration](#table-declaration)
|
|
20
|
+
- [Instantiation](#instantiation)
|
|
21
|
+
- [Row insertion](#row-insertion)
|
|
22
|
+
- [Jupyter view](#jupyter-view)
|
|
23
|
+
- [Select Query](#select-query)
|
|
24
|
+
- [Update Query](#update-query)
|
|
25
|
+
- [Delete Query](#delete-query)
|
|
26
|
+
- [Transaction](#transaction)
|
|
27
|
+
- [Get Item](#get-item)
|
|
28
|
+
- [Csv Converter](#csv-converter)
|
|
29
|
+
- [Full Documentation](#full-documentation)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Sql-Engine
|
|
33
|
+
|
|
34
|
+
My Sql-Engine is a cute little wrapper for `sqlite3` table manipulations without any third party dependencies.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
Abstracts SQL queries into tiny little methods like, `insert`, `insert_many`, `upsert`, and not so little and tiny query builders a-la `select`, `delete`, `update`, etc. Sql-Engine also provides bulk insertion and transaction methods, like `insert_many` and `select.fetchmany_iterator`. Methods can be executed in transaction mode thanks to `transaction` context manager.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Sql-Engine implements Jupyter integration and dynamic schema building. You can easily instantiate existing database table and view it in cute little html representation.
|
|
43
|
+
|
|
44
|
+
```py
|
|
45
|
+
from sqlengine import schema
|
|
46
|
+
|
|
47
|
+
table = schema.table_from_database("temp/chinook.db", "Invoice")
|
|
48
|
+
table
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
<table style="border-collapse: collapse; font-size: 14px;"><caption style="font-size: 18px; font-weight: bold;">Invoice</caption><thead><tr><td style="border: 1px solid #555; text-align: center;">InvoiceId</td><td style="border: 1px solid #555; text-align: center;">CustomerId</td><td style="border: 1px solid #555; text-align: center;">InvoiceDate</td><td style="border: 1px solid #555; text-align: center;">BillingAddress</td><td style="border: 1px solid #555; text-align: center;">BillingCity</td><td style="border: 1px solid #555; text-align: center;">BillingState</td><td style="border: 1px solid #555; text-align: center;">BillingCountry</td><td style="border: 1px solid #555; text-align: center;">BillingPostalCode</td><td style="border: 1px solid #555; text-align: center;">Total</td></tr></thead><tbody><tr><td style="border: 1px solid #000; text-align: center;">1</td><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">2021-01-01 00:00:00</td><td style="border: 1px solid #000; text-align: center;">Theodor-Heuss-Straße 34</td><td style="border: 1px solid #000; text-align: center;">Stuttgart</td><td style="border: 1px solid #000; text-align: center;">None</td><td style="border: 1px solid #000; text-align: center;">Germany</td><td style="border: 1px solid #000; text-align: center;">70174</td><td style="border: 1px solid #000; text-align: center;">1.98</td></tr><tr><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">4</td><td style="border: 1px solid #000; text-align: center;">2021-01-02 00:00:00</td><td style="border: 1px solid #000; text-align: center;">Ullevålsveien 14</td><td style="border: 1px solid #000; text-align: center;">Oslo</td><td style="border: 1px solid #000; text-align: center;">None</td><td style="border: 1px solid #000; text-align: center;">Norway</td><td style="border: 1px solid #000; text-align: center;">0171</td><td style="border: 1px solid #000; text-align: center;">3.96</td></tr><tr><td style="border: 1px solid #000; text-align: center;">3</td><td style="border: 1px solid #000; text-align: center;">8</td><td style="border: 1px solid #000; text-align: center;">2021-01-03 00:00:00</td><td style="border: 1px solid #000; text-align: center;">Grétrystraat 63</td><td style="border: 1px solid #000; text-align: center;">Brussels</td><td style="border: 1px solid #000; text-align: center;">None</td><td style="border: 1px solid #000; text-align: center;">Belgium</td><td style="border: 1px solid #000; text-align: center;">1000</td><td style="border: 1px solid #000; text-align: center;">5.94</td></tr><tr><td style="border: 1px solid #000; text-align: center;">4</td><td style="border: 1px solid #000; text-align: center;">14</td><td style="border: 1px solid #000; text-align: center;">2021-01-06 00:00:00</td><td style="border: 1px solid #000; text-align: center;">8210 111 ST NW</td><td style="border: 1px solid #000; text-align: center;">Edmonton</td><td style="border: 1px solid #000; text-align: center;">AB</td><td style="border: 1px solid #000; text-align: center;">Canada</td><td style="border: 1px solid #000; text-align: center;">T6G 2C7</td><td style="border: 1px solid #000; text-align: center;">8.91</td></tr><tr><td style="border: 1px solid #000; text-align: center;">5</td><td style="border: 1px solid #000; text-align: center;">23</td><td style="border: 1px solid #000; text-align: center;">2021-01-11 00:00:00</td><td style="border: 1px solid #000; text-align: center;">69 Salem Street</td><td style="border: 1px solid #000; text-align: center;">Boston</td><td style="border: 1px solid #000; text-align: center;">MA</td><td style="border: 1px solid #000; text-align: center;">USA</td><td style="border: 1px solid #000; text-align: center;">2113</td><td style="border: 1px solid #000; text-align: center;">13.86</td></tr><tr><td style="border: 1px solid #000; text-align: center;">6</td><td style="border: 1px solid #000; text-align: center;">37</td><td style="border: 1px solid #000; text-align: center;">2021-01-19 00:00:00</td><td style="border: 1px solid #000; text-align: center;">Berger Straße 10</td><td style="border: 1px solid #000; text-align: center;">Frankfurt</td><td style="border: 1px solid #000; text-align: center;">None</td><td style="border: 1px solid #000; text-align: center;">Germany</td><td style="border: 1px solid #000; text-align: center;">60316</td><td style="border: 1px solid #000; text-align: center;">0.99</td></tr><tr><td style="border: 1px solid #000; text-align: center;">7</td><td style="border: 1px solid #000; text-align: center;">38</td><td style="border: 1px solid #000; text-align: center;">2021-02-01 00:00:00</td><td style="border: 1px solid #000; text-align: center;">Barbarossastraße 19</td><td style="border: 1px solid #000; text-align: center;">Berlin</td><td style="border: 1px solid #000; text-align: center;">None</td><td style="border: 1px solid #000; text-align: center;">Germany</td><td style="border: 1px solid #000; text-align: center;">10779</td><td style="border: 1px solid #000; text-align: center;">1.98</td></tr><tr><td style="border: 1px solid #000; text-align: center;">8</td><td style="border: 1px solid #000; text-align: center;">40</td><td style="border: 1px solid #000; text-align: center;">2021-02-01 00:00:00</td><td style="border: 1px solid #000; text-align: center;">8, Rue Hanovre</td><td style="border: 1px solid #000; text-align: center;">Paris</td><td style="border: 1px solid #000; text-align: center;">None</td><td style="border: 1px solid #000; text-align: center;">France</td><td style="border: 1px solid #000; text-align: center;">75002</td><td style="border: 1px solid #000; text-align: center;">1.98</td></tr><tr><td style="border: 1px solid #000; text-align: center;">9</td><td style="border: 1px solid #000; text-align: center;">42</td><td style="border: 1px solid #000; text-align: center;">2021-02-02 00:00:00</td><td style="border: 1px solid #000; text-align: center;">9, Place Louis Barthou</td><td style="border: 1px solid #000; text-align: center;">Bordeaux</td><td style="border: 1px solid #000; text-align: center;">None</td><td style="border: 1px solid #000; text-align: center;">France</td><td style="border: 1px solid #000; text-align: center;">33000</td><td style="border: 1px solid #000; text-align: center;">3.96</td></tr><tr><td style="border: 1px solid #000; text-align: center;">10</td><td style="border: 1px solid #000; text-align: center;">46</td><td style="border: 1px solid #000; text-align: center;">2021-02-03 00:00:00</td><td style="border: 1px solid #000; text-align: center;">3 Chatham Street</td><td style="border: 1px solid #000; text-align: center;">Dublin</td><td style="border: 1px solid #000; text-align: center;">Dublin</td><td style="border: 1px solid #000; text-align: center;">Ireland</td><td style="border: 1px solid #000; text-align: center;">None</td><td style="border: 1px solid #000; text-align: center;">5.94</td></tr><tr><td colspan="9" style="text-align:center;color:#888;font-style:italic;padding:8px;">... more rows ...</td></tr></tbody></table>
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
You can preview select statements before fetching data to your variables.
|
|
56
|
+
|
|
57
|
+
```py
|
|
58
|
+
table.select("InvoiceId", "CustomerId", "BillingAddress", "BillingCountry", "Total")\
|
|
59
|
+
.where\
|
|
60
|
+
.gte("Total", 2.0)\
|
|
61
|
+
.then\
|
|
62
|
+
.order_by("CustomerId")\
|
|
63
|
+
.limit(10)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
<table style="border-collapse: collapse; font-size: 14px;"><caption style="font-size: 18px; font-weight: bold;">Invoice</caption><thead><tr><td style="border: 1px solid #555; text-align: center;">InvoiceId</td><td style="border: 1px solid #555; text-align: center;">CustomerId</td><td style="border: 1px solid #555; text-align: center;">BillingAddress</td><td style="border: 1px solid #555; text-align: center;">BillingCountry</td><td style="border: 1px solid #555; text-align: center;">Total</td></tr></thead><tbody><tr><td style="border: 1px solid #000; text-align: center;">98</td><td style="border: 1px solid #000; text-align: center;">1</td><td style="border: 1px solid #000; text-align: center;">Av. Brigadeiro Faria Lima, 2170</td><td style="border: 1px solid #000; text-align: center;">Brazil</td><td style="border: 1px solid #000; text-align: center;">3.98</td></tr><tr><td style="border: 1px solid #000; text-align: center;">121</td><td style="border: 1px solid #000; text-align: center;">1</td><td style="border: 1px solid #000; text-align: center;">Av. Brigadeiro Faria Lima, 2170</td><td style="border: 1px solid #000; text-align: center;">Brazil</td><td style="border: 1px solid #000; text-align: center;">3.96</td></tr><tr><td style="border: 1px solid #000; text-align: center;">143</td><td style="border: 1px solid #000; text-align: center;">1</td><td style="border: 1px solid #000; text-align: center;">Av. Brigadeiro Faria Lima, 2170</td><td style="border: 1px solid #000; text-align: center;">Brazil</td><td style="border: 1px solid #000; text-align: center;">5.94</td></tr><tr><td style="border: 1px solid #000; text-align: center;">327</td><td style="border: 1px solid #000; text-align: center;">1</td><td style="border: 1px solid #000; text-align: center;">Av. Brigadeiro Faria Lima, 2170</td><td style="border: 1px solid #000; text-align: center;">Brazil</td><td style="border: 1px solid #000; text-align: center;">13.86</td></tr><tr><td style="border: 1px solid #000; text-align: center;">382</td><td style="border: 1px solid #000; text-align: center;">1</td><td style="border: 1px solid #000; text-align: center;">Av. Brigadeiro Faria Lima, 2170</td><td style="border: 1px solid #000; text-align: center;">Brazil</td><td style="border: 1px solid #000; text-align: center;">8.91</td></tr><tr><td style="border: 1px solid #000; text-align: center;">12</td><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">Theodor-Heuss-Straße 34</td><td style="border: 1px solid #000; text-align: center;">Germany</td><td style="border: 1px solid #000; text-align: center;">13.86</td></tr><tr><td style="border: 1px solid #000; text-align: center;">67</td><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">Theodor-Heuss-Straße 34</td><td style="border: 1px solid #000; text-align: center;">Germany</td><td style="border: 1px solid #000; text-align: center;">8.91</td></tr><tr><td style="border: 1px solid #000; text-align: center;">219</td><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">Theodor-Heuss-Straße 34</td><td style="border: 1px solid #000; text-align: center;">Germany</td><td style="border: 1px solid #000; text-align: center;">3.96</td></tr><tr><td style="border: 1px solid #000; text-align: center;">241</td><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">Theodor-Heuss-Straße 34</td><td style="border: 1px solid #000; text-align: center;">Germany</td><td style="border: 1px solid #000; text-align: center;">5.94</td></tr><tr><td style="border: 1px solid #000; text-align: center;">99</td><td style="border: 1px solid #000; text-align: center;">3</td><td style="border: 1px solid #000; text-align: center;">1498 rue Bélanger</td><td style="border: 1px solid #000; text-align: center;">Canada</td><td style="border: 1px solid #000; text-align: center;">3.98</td></tr></tbody></table>
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## Purpose
|
|
70
|
+
|
|
71
|
+
It's a tiny little modern ORM that lets you prototype your databases locally with great flexibility. Also, it can be used in production apps to store and retrieve data, because all select, update, delete queries are parametrized. But it does not restrict you from using your own queries which might not be paramerized with methods like `select.custom()` and `where.custom()`.
|
|
72
|
+
|
|
73
|
+
And last (but not least) is data inspection. If you need to quickly inspect existing .db file but don't want to install yet another heavy ORM with a lot of unused dependencies, you might look into Sql-Engine, as it uses only native python modules.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
To install `sqlengine`, you can use `pip`:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
pip install sqlengine-lite
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Env
|
|
85
|
+
|
|
86
|
+
You may set environment variable for logging. By default it's `WARNING`.
|
|
87
|
+
|
|
88
|
+
```console
|
|
89
|
+
SQL_ENGINE_LOG_LEVEL=INFO
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
# Quick Start
|
|
93
|
+
|
|
94
|
+
All you have to do to create your own cute little table is to [inherit](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/table_declaration.md#class-declaration) `SqlTableMixin` class or to create your own [schema](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/table_declaration.md#schema-declaration) and declare desired properties of your table's columns. They are:
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
_Name of the table that will be used in queries. If omitted in inherited class declaration, then it will take the class name._
|
|
98
|
+
```py
|
|
99
|
+
__tablename__ : Optional[str]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
_Column names of the table_
|
|
103
|
+
```py
|
|
104
|
+
__columns__ : list[str]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
_Column types of the table_
|
|
108
|
+
```py
|
|
109
|
+
__types__ : list[SqlType | str]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
_List of primary keys_
|
|
113
|
+
```py
|
|
114
|
+
__primary__ : list[str]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Table Declaration
|
|
118
|
+
|
|
119
|
+
More details at [Declaration](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/table_declaration.md#table-declaration).
|
|
120
|
+
|
|
121
|
+
```py
|
|
122
|
+
from sqlengine import SqlTableMixin
|
|
123
|
+
|
|
124
|
+
# Helper constants for column names
|
|
125
|
+
ID = "ID"
|
|
126
|
+
Name = "Name"
|
|
127
|
+
Occupation = "Occupation"
|
|
128
|
+
Salary = "Salary"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Employees(SqlTableMixin):
|
|
132
|
+
|
|
133
|
+
__columns__ = [ID, Name, Occupation, Salary]
|
|
134
|
+
__types__ = [int, str, str, float]
|
|
135
|
+
__primary__ = [ID]
|
|
136
|
+
|
|
137
|
+
# You may overwrite your insert methods for type consistency
|
|
138
|
+
def insert(self, id : int, name : str, occupation : str, salary : float) -> None:
|
|
139
|
+
return super().insert(id, name, occupation, salary)
|
|
140
|
+
|
|
141
|
+
def upsert(self, id : int, name : str, occupation : str, salary : float) -> None:
|
|
142
|
+
return super().upsert(id, name, occupation, salary)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Instantiation
|
|
146
|
+
|
|
147
|
+
More details at [Instantiation](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/table_declaration.md#table-instantiation).
|
|
148
|
+
|
|
149
|
+
```py
|
|
150
|
+
# Create an instance of the table class
|
|
151
|
+
# with provided path to create or connect to
|
|
152
|
+
|
|
153
|
+
table = Employees("temp/data.db")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Row insertion
|
|
157
|
+
|
|
158
|
+
```py
|
|
159
|
+
# Insert one row
|
|
160
|
+
|
|
161
|
+
table.insert(1, "John Doe", "Software Engineer", 75000.0)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```py
|
|
165
|
+
# Bulk insert multiple rows
|
|
166
|
+
|
|
167
|
+
employees_data = [
|
|
168
|
+
(2, "Jane Smith", "Data Scientist", 80000.0),
|
|
169
|
+
(3, "Alice Johnson", "Product Manager", 90000.0),
|
|
170
|
+
(4, "Bob Brown", "Project Manager", 78000.0),
|
|
171
|
+
(5, "Charlie Davis", "UI/UX Designer", 65000.0),
|
|
172
|
+
(6, "David Wilson", "DevOps Engineer", 82000.0),
|
|
173
|
+
(7, "Eve Taylor", "Customer Support", 45000.0),
|
|
174
|
+
(8, "Frank White", "Quality Assurance", 53000.0),
|
|
175
|
+
(9, "Grace Hall", "Marketing Manager", 68000.0),
|
|
176
|
+
(10, "Henry Lee", "Technical Writer", 52000.0)
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
table.insert_many(employees_data)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
```py
|
|
184
|
+
# Upsert one row
|
|
185
|
+
|
|
186
|
+
table.upsert(1, "Jane Doe", "Data Scientist", 80000.0)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Jupyter view
|
|
190
|
+
|
|
191
|
+
```py
|
|
192
|
+
# Inspect tables in Jupyter Notebook
|
|
193
|
+
|
|
194
|
+
table
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
<table style="border-collapse: collapse; font-size: 14px;"><caption style="font-size: 18px; font-weight: bold;">Employees</caption><thead><tr><td style="border: 1px solid #555; text-align: center;">ID</td><td style="border: 1px solid #555; text-align: center;">Name</td><td style="border: 1px solid #555; text-align: center;">Occupation</td><td style="border: 1px solid #555; text-align: center;">Salary</td></tr></thead><tbody><tr><td style="border: 1px solid #000; text-align: center;">1</td><td style="border: 1px solid #000; text-align: center;">Jane Doe</td><td style="border: 1px solid #000; text-align: center;">Data Scientist</td><td style="border: 1px solid #000; text-align: center;">80000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">Jane Smith</td><td style="border: 1px solid #000; text-align: center;">Data Scientist</td><td style="border: 1px solid #000; text-align: center;">80000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">3</td><td style="border: 1px solid #000; text-align: center;">Alice Johnson</td><td style="border: 1px solid #000; text-align: center;">Product Manager</td><td style="border: 1px solid #000; text-align: center;">90000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">4</td><td style="border: 1px solid #000; text-align: center;">Bob Brown</td><td style="border: 1px solid #000; text-align: center;">Project Manager</td><td style="border: 1px solid #000; text-align: center;">78000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">5</td><td style="border: 1px solid #000; text-align: center;">Charlie Davis</td><td style="border: 1px solid #000; text-align: center;">UI/UX Designer</td><td style="border: 1px solid #000; text-align: center;">65000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">6</td><td style="border: 1px solid #000; text-align: center;">David Wilson</td><td style="border: 1px solid #000; text-align: center;">DevOps Engineer</td><td style="border: 1px solid #000; text-align: center;">82000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">7</td><td style="border: 1px solid #000; text-align: center;">Eve Taylor</td><td style="border: 1px solid #000; text-align: center;">Customer Support</td><td style="border: 1px solid #000; text-align: center;">45000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">8</td><td style="border: 1px solid #000; text-align: center;">Frank White</td><td style="border: 1px solid #000; text-align: center;">Quality Assurance</td><td style="border: 1px solid #000; text-align: center;">53000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">9</td><td style="border: 1px solid #000; text-align: center;">Grace Hall</td><td style="border: 1px solid #000; text-align: center;">Marketing Manager</td><td style="border: 1px solid #000; text-align: center;">68000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">10</td><td style="border: 1px solid #000; text-align: center;">Henry Lee</td><td style="border: 1px solid #000; text-align: center;">Technical Writer</td><td style="border: 1px solid #000; text-align: center;">52000.0</td></tr></tbody></table>
|
|
198
|
+
|
|
199
|
+
## Select Query
|
|
200
|
+
|
|
201
|
+
More details at [Statements](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/statements.md).
|
|
202
|
+
|
|
203
|
+
```py
|
|
204
|
+
# Query select and fetch
|
|
205
|
+
|
|
206
|
+
table.select.where.between(ID, 3, 5).then.fetchall()
|
|
207
|
+
|
|
208
|
+
# ->
|
|
209
|
+
# [(3, 'Alice Johnson', 'Product Manager', 90000.0),
|
|
210
|
+
# (4, 'Bob Brown', 'Project Manager', 78000.0),
|
|
211
|
+
# (5, 'Charlie Davis', 'UI/UX Designer', 65000.0)]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```py
|
|
215
|
+
# Inspect query select in Jupyter
|
|
216
|
+
|
|
217
|
+
table.select(Name, Salary).where.lt(Salary, 70_000)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
<table style="border-collapse: collapse; font-size: 14px;"><caption style="font-size: 18px; font-weight: bold;">Employees</caption><thead><tr><td style="border: 1px solid #555; text-align: center;">Name</td><td style="border: 1px solid #555; text-align: center;">Salary</td></tr></thead><tbody><tr><td style="border: 1px solid #000; text-align: center;">Charlie Davis</td><td style="border: 1px solid #000; text-align: center;">65000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">Eve Taylor</td><td style="border: 1px solid #000; text-align: center;">45000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">Frank White</td><td style="border: 1px solid #000; text-align: center;">53000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">Grace Hall</td><td style="border: 1px solid #000; text-align: center;">68000.0</td></tr><tr><td style="border: 1px solid #000; text-align: center;">Henry Lee</td><td style="border: 1px solid #000; text-align: center;">52000.0</td></tr></tbody></table>
|
|
221
|
+
|
|
222
|
+
## Update Query
|
|
223
|
+
|
|
224
|
+
```py
|
|
225
|
+
# equal to ...update.set(Salary, 50_000)...
|
|
226
|
+
table.update(Salary, 50_000).where.eq(Name, "Eve Taylor").then.execute()
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Delete Query
|
|
230
|
+
|
|
231
|
+
```py
|
|
232
|
+
table.delete.where.eq(ID, 5).then.execute()
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Transaction
|
|
236
|
+
|
|
237
|
+
More details at [Transaction](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/transactions.md).
|
|
238
|
+
|
|
239
|
+
```py
|
|
240
|
+
# Operate within a transaction
|
|
241
|
+
|
|
242
|
+
with table.transaction():
|
|
243
|
+
for row in employees_data:
|
|
244
|
+
table.upsert(*row)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Get Item
|
|
248
|
+
|
|
249
|
+
More details at [Syntax Sugar](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/syntax_sugar.md).
|
|
250
|
+
|
|
251
|
+
```py
|
|
252
|
+
# Fetch row by primary key
|
|
253
|
+
|
|
254
|
+
table[9]
|
|
255
|
+
|
|
256
|
+
# -> (9, 'Grace Hall', 'Marketing Manager', 68000.0)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```py
|
|
260
|
+
# Fetch slice by integer primary key
|
|
261
|
+
|
|
262
|
+
table[4:10:2]
|
|
263
|
+
|
|
264
|
+
# ->
|
|
265
|
+
# [(4, 'Bob Brown', 'Project Manager', 78000.0),
|
|
266
|
+
# (6, 'David Wilson', 'DevOps Engineer', 82000.0),
|
|
267
|
+
# (8, 'Frank White', 'Quality Assurance', 53000.0)]
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Csv Converter
|
|
271
|
+
|
|
272
|
+
```py
|
|
273
|
+
# Save table to csv
|
|
274
|
+
from sqlengine.utils import to_csv
|
|
275
|
+
|
|
276
|
+
to_csv(table, "temp/table.csv")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```py
|
|
280
|
+
# Save query result to csv
|
|
281
|
+
|
|
282
|
+
to_csv(table.select.where.gt(Salary, 70_000), "temp/query.csv")
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
# Full Documentation
|
|
286
|
+
|
|
287
|
+
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,8 @@
|
|
|
1
|
+
sqlengine/__init__.py,sha256=s6KuRJuFt3Gxft4tUgW4yPH4_Gs_gTU5C_iAHtET44c,189
|
|
2
|
+
sqlengine/schema.py,sha256=Lpo3w-Hw4sPU2xlQJLFjfHIjRxybNEOjS3SkdInPIJs,3688
|
|
3
|
+
sqlengine/sqltable.py,sha256=Yrn3ULa6iBpwHe67eAlu4Nw9lczVmWb3253FERN173Y,19653
|
|
4
|
+
sqlengine_lite-2.1.0.dist-info/licenses/LICENSE,sha256=aepvve4t1ho5uHMQG88dErp5L5CgoNAXctIjMdoceqY,1069
|
|
5
|
+
sqlengine_lite-2.1.0.dist-info/METADATA,sha256=CMM0K5CTWPxxML39veR87bPgTaDyi_oSQHMl7x4e6z4,23049
|
|
6
|
+
sqlengine_lite-2.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
sqlengine_lite-2.1.0.dist-info/top_level.txt,sha256=KG_FG0LCB_mIEu2qJ5LGLQMpan4QmF5d8xLEcqTghCE,10
|
|
8
|
+
sqlengine_lite-2.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 suffermuffin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sqlengine
|