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 ADDED
@@ -0,0 +1,7 @@
1
+ from .utils import sqlgen, types
2
+ from .utils.types import Schema
3
+ from .sqltable import SqlTableMixin
4
+
5
+ __author__ = "suffermuffin"
6
+
7
+ __all__ = ["types", "sqlgen", "Schema", "SqlTableMixin"]
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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