sqlengine-lite 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/PKG-INFO +53 -9
  2. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/README.md +52 -8
  3. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/pyproject.toml +5 -2
  4. sqlengine_lite-2.2.0/src/sqlengine/__init__.py +7 -0
  5. sqlengine_lite-2.2.0/src/sqlengine/core/__init__.py +4 -0
  6. sqlengine_lite-2.2.0/src/sqlengine/core/connection.py +255 -0
  7. sqlengine_lite-2.2.0/src/sqlengine/core/repr.py +52 -0
  8. sqlengine_lite-2.2.0/src/sqlengine/core/sqlgen.py +109 -0
  9. sqlengine_lite-2.2.0/src/sqlengine/core/statements.py +400 -0
  10. sqlengine_lite-2.2.0/src/sqlengine/core/types.py +104 -0
  11. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/src/sqlengine/schema.py +1 -1
  12. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/src/sqlengine/sqltable.py +162 -276
  13. sqlengine_lite-2.2.0/src/sqlengine/utils/__init__.py +4 -0
  14. sqlengine_lite-2.2.0/src/sqlengine/utils/connection.py +93 -0
  15. sqlengine_lite-2.2.0/src/sqlengine/utils/convert.py +24 -0
  16. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/src/sqlengine_lite.egg-info/PKG-INFO +53 -9
  17. sqlengine_lite-2.2.0/src/sqlengine_lite.egg-info/SOURCES.txt +20 -0
  18. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/tests/test.py +133 -32
  19. sqlengine_lite-2.1.0/src/sqlengine/__init__.py +0 -7
  20. sqlengine_lite-2.1.0/src/sqlengine_lite.egg-info/SOURCES.txt +0 -11
  21. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/LICENSE +0 -0
  22. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/setup.cfg +0 -0
  23. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/src/sqlengine_lite.egg-info/dependency_links.txt +0 -0
  24. {sqlengine_lite-2.1.0 → sqlengine_lite-2.2.0}/src/sqlengine_lite.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlengine-lite
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Cute sqlite3 wrapper for sql tables
5
5
  Project-URL: Homepage, https://github.com/suffermuffin/SQL-Engine
6
6
  Project-URL: Repository, https://github.com/suffermuffin/SQL-Engine.git
@@ -25,6 +25,7 @@ Dynamic: license-file
25
25
  - [Delete Query](#delete-query)
26
26
  - [Transaction](#transaction)
27
27
  - [Get Item](#get-item)
28
+ - [Custom Types](#custom-types)
28
29
  - [Csv Converter](#csv-converter)
29
30
  - [Full Documentation](#full-documentation)
30
31
 
@@ -44,17 +45,21 @@ Sql-Engine implements Jupyter integration and dynamic schema building. You can e
44
45
  ```py
45
46
  from sqlengine import schema
46
47
 
47
- table = schema.table_from_database("temp/chinook.db", "Invoice")
48
+ table = schema.table_from_database("temp/chinook.db", "Album")
48
49
  table
49
50
  ```
50
51
 
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
+ <table style="border-collapse: collapse; font-size: 14px;"><caption style="font-size: 18px; font-weight: bold;">Album</caption><thead><tr><td style="border: 1px solid #555; text-align: center;">AlbumId</td><td style="border: 1px solid #555; text-align: center;">Title</td><td style="border: 1px solid #555; text-align: center;">ArtistId</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;">For Those About To Rock We Salute You</td><td style="border: 1px solid #000; text-align: center;">1</td></tr><tr><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">Balls to the Wall</td><td style="border: 1px solid #000; text-align: center;">2</td></tr><tr><td style="border: 1px solid #000; text-align: center;">3</td><td style="border: 1px solid #000; text-align: center;">Restless and Wild</td><td style="border: 1px solid #000; text-align: center;">2</td></tr><tr><td style="border: 1px solid #000; text-align: center;">4</td><td style="border: 1px solid #000; text-align: center;">Let There Be Rock</td><td style="border: 1px solid #000; text-align: center;">1</td></tr><tr><td style="border: 1px solid #000; text-align: center;">5</td><td style="border: 1px solid #000; text-align: center;">Big Ones</td><td style="border: 1px solid #000; text-align: center;">3</td></tr><tr><td style="border: 1px solid #000; text-align: center;">6</td><td style="border: 1px solid #000; text-align: center;">Jagged Little Pill</td><td style="border: 1px solid #000; text-align: center;">4</td></tr><tr><td style="border: 1px solid #000; text-align: center;">7</td><td style="border: 1px solid #000; text-align: center;">Facelift</td><td style="border: 1px solid #000; text-align: center;">5</td></tr><tr><td style="border: 1px solid #000; text-align: center;">8</td><td style="border: 1px solid #000; text-align: center;">Warner 25 Anos</td><td style="border: 1px solid #000; text-align: center;">6</td></tr><tr><td style="border: 1px solid #000; text-align: center;">9</td><td style="border: 1px solid #000; text-align: center;">Plays Metallica By Four Cellos</td><td style="border: 1px solid #000; text-align: center;">7</td></tr><tr><td style="border: 1px solid #000; text-align: center;">10</td><td style="border: 1px solid #000; text-align: center;">Audioslave</td><td style="border: 1px solid #000; text-align: center;">8</td></tr><tr><td colspan="3" style="text-align:center;color:#888;font-style:italic;padding:8px;">... more rows ...</td></tr></tbody></table>
53
+
54
+ Note that GitHub's html rendering is simplified.
52
55
 
53
56
  ---
54
57
 
55
58
  You can preview select statements before fetching data to your variables.
56
59
 
57
60
  ```py
61
+ table = schema.table_from_database("temp/chinook.db", "Invoice")
62
+
58
63
  table.select("InvoiceId", "CustomerId", "BillingAddress", "BillingCountry", "Total")\
59
64
  .where\
60
65
  .gte("Total", 2.0)\
@@ -68,7 +73,7 @@ table.select("InvoiceId", "CustomerId", "BillingAddress", "BillingCountry", "Tot
68
73
 
69
74
  ## Purpose
70
75
 
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()`.
76
+ It's a tiny little modern ORM-like 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
77
 
73
78
  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
79
 
@@ -119,7 +124,7 @@ __primary__ : list[str]
119
124
  More details at [Declaration](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/table_declaration.md#table-declaration).
120
125
 
121
126
  ```py
122
- from sqlengine import SqlTableMixin
127
+ from sqlengine import SqlTableMixin, Primary
123
128
 
124
129
  # Helper constants for column names
125
130
  ID = "ID"
@@ -130,9 +135,10 @@ Salary = "Salary"
130
135
 
131
136
  class Employees(SqlTableMixin):
132
137
 
133
- __columns__ = [ID, Name, Occupation, Salary]
134
- __types__ = [int, str, str, float]
135
- __primary__ = [ID]
138
+ ID : Primary[int]
139
+ Name : str
140
+ Occupation : str
141
+ Salary : float
136
142
 
137
143
  # You may overwrite your insert methods for type consistency
138
144
  def insert(self, id : int, name : str, occupation : str, salary : float) -> None:
@@ -149,8 +155,9 @@ More details at [Instantiation](https://github.com/suffermuffin/SQL-Engine/blob/
149
155
  ```py
150
156
  # Create an instance of the table class
151
157
  # with provided path to create or connect to
158
+ # `force_drop=True` to overwrite existing table if exists
152
159
 
153
- table = Employees("temp/data.db")
160
+ table = Employees("temp/data.db", force_drop=True)
154
161
  ```
155
162
 
156
163
  ## Row insertion
@@ -267,6 +274,43 @@ table[4:10:2]
267
274
  # (8, 'Frank White', 'Quality Assurance', 53000.0)]
268
275
  ```
269
276
 
277
+ ## Custom Types
278
+
279
+ More details at [Custom Types](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/custom_types.md).
280
+
281
+ ```py
282
+ from datetime import datetime
283
+ from sqlengine import SqlTableMixin, Primary
284
+
285
+ class DateTime(datetime):
286
+
287
+ def to_sql(self) -> str:
288
+ return self.strftime("%Y-%m-%d %H:%M")
289
+
290
+ @classmethod
291
+ def from_sql(cls, sql : bytes):
292
+ return cls.fromisoformat(sql.decode('utf-8'))
293
+
294
+ def __repr__(self):
295
+ return f"DateTime({self.time})"
296
+
297
+
298
+ class ReservationIndex(SqlTableMixin):
299
+
300
+ user_id : Primary[int]
301
+ room_id : Primary[str]
302
+ time_at : Primary[DateTime]
303
+ user_name : str | None
304
+
305
+
306
+ table = ReservationIndex("temp/data.db")
307
+
308
+ table.insert(1, "loft_1", DateTime.now(), None)
309
+ table
310
+ ```
311
+
312
+ <table style="border-collapse: collapse; font-size: 14px;"><caption style="font-size: 18px; font-weight: bold;">ReservationIndex</caption><thead><tr><td style="border: 1px solid #555; text-align: center;">user_id</td><td style="border: 1px solid #555; text-align: center;">room_id</td><td style="border: 1px solid #555; text-align: center;">time_at</td><td style="border: 1px solid #555; text-align: center;">user_name</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;">loft_1</td><td style="border: 1px solid #000; text-align: center;">2026-05-29 21:29:00</td><td style="border: 1px solid #000; text-align: center;">None</td></tr></tbody></table>
313
+
270
314
  ## Csv Converter
271
315
 
272
316
  ```py
@@ -13,6 +13,7 @@
13
13
  - [Delete Query](#delete-query)
14
14
  - [Transaction](#transaction)
15
15
  - [Get Item](#get-item)
16
+ - [Custom Types](#custom-types)
16
17
  - [Csv Converter](#csv-converter)
17
18
  - [Full Documentation](#full-documentation)
18
19
 
@@ -32,17 +33,21 @@ Sql-Engine implements Jupyter integration and dynamic schema building. You can e
32
33
  ```py
33
34
  from sqlengine import schema
34
35
 
35
- table = schema.table_from_database("temp/chinook.db", "Invoice")
36
+ table = schema.table_from_database("temp/chinook.db", "Album")
36
37
  table
37
38
  ```
38
39
 
39
- <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>
40
+ <table style="border-collapse: collapse; font-size: 14px;"><caption style="font-size: 18px; font-weight: bold;">Album</caption><thead><tr><td style="border: 1px solid #555; text-align: center;">AlbumId</td><td style="border: 1px solid #555; text-align: center;">Title</td><td style="border: 1px solid #555; text-align: center;">ArtistId</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;">For Those About To Rock We Salute You</td><td style="border: 1px solid #000; text-align: center;">1</td></tr><tr><td style="border: 1px solid #000; text-align: center;">2</td><td style="border: 1px solid #000; text-align: center;">Balls to the Wall</td><td style="border: 1px solid #000; text-align: center;">2</td></tr><tr><td style="border: 1px solid #000; text-align: center;">3</td><td style="border: 1px solid #000; text-align: center;">Restless and Wild</td><td style="border: 1px solid #000; text-align: center;">2</td></tr><tr><td style="border: 1px solid #000; text-align: center;">4</td><td style="border: 1px solid #000; text-align: center;">Let There Be Rock</td><td style="border: 1px solid #000; text-align: center;">1</td></tr><tr><td style="border: 1px solid #000; text-align: center;">5</td><td style="border: 1px solid #000; text-align: center;">Big Ones</td><td style="border: 1px solid #000; text-align: center;">3</td></tr><tr><td style="border: 1px solid #000; text-align: center;">6</td><td style="border: 1px solid #000; text-align: center;">Jagged Little Pill</td><td style="border: 1px solid #000; text-align: center;">4</td></tr><tr><td style="border: 1px solid #000; text-align: center;">7</td><td style="border: 1px solid #000; text-align: center;">Facelift</td><td style="border: 1px solid #000; text-align: center;">5</td></tr><tr><td style="border: 1px solid #000; text-align: center;">8</td><td style="border: 1px solid #000; text-align: center;">Warner 25 Anos</td><td style="border: 1px solid #000; text-align: center;">6</td></tr><tr><td style="border: 1px solid #000; text-align: center;">9</td><td style="border: 1px solid #000; text-align: center;">Plays Metallica By Four Cellos</td><td style="border: 1px solid #000; text-align: center;">7</td></tr><tr><td style="border: 1px solid #000; text-align: center;">10</td><td style="border: 1px solid #000; text-align: center;">Audioslave</td><td style="border: 1px solid #000; text-align: center;">8</td></tr><tr><td colspan="3" style="text-align:center;color:#888;font-style:italic;padding:8px;">... more rows ...</td></tr></tbody></table>
41
+
42
+ Note that GitHub's html rendering is simplified.
40
43
 
41
44
  ---
42
45
 
43
46
  You can preview select statements before fetching data to your variables.
44
47
 
45
48
  ```py
49
+ table = schema.table_from_database("temp/chinook.db", "Invoice")
50
+
46
51
  table.select("InvoiceId", "CustomerId", "BillingAddress", "BillingCountry", "Total")\
47
52
  .where\
48
53
  .gte("Total", 2.0)\
@@ -56,7 +61,7 @@ table.select("InvoiceId", "CustomerId", "BillingAddress", "BillingCountry", "Tot
56
61
 
57
62
  ## Purpose
58
63
 
59
- 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()`.
64
+ It's a tiny little modern ORM-like 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()`.
60
65
 
61
66
  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.
62
67
 
@@ -107,7 +112,7 @@ __primary__ : list[str]
107
112
  More details at [Declaration](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/table_declaration.md#table-declaration).
108
113
 
109
114
  ```py
110
- from sqlengine import SqlTableMixin
115
+ from sqlengine import SqlTableMixin, Primary
111
116
 
112
117
  # Helper constants for column names
113
118
  ID = "ID"
@@ -118,9 +123,10 @@ Salary = "Salary"
118
123
 
119
124
  class Employees(SqlTableMixin):
120
125
 
121
- __columns__ = [ID, Name, Occupation, Salary]
122
- __types__ = [int, str, str, float]
123
- __primary__ = [ID]
126
+ ID : Primary[int]
127
+ Name : str
128
+ Occupation : str
129
+ Salary : float
124
130
 
125
131
  # You may overwrite your insert methods for type consistency
126
132
  def insert(self, id : int, name : str, occupation : str, salary : float) -> None:
@@ -137,8 +143,9 @@ More details at [Instantiation](https://github.com/suffermuffin/SQL-Engine/blob/
137
143
  ```py
138
144
  # Create an instance of the table class
139
145
  # with provided path to create or connect to
146
+ # `force_drop=True` to overwrite existing table if exists
140
147
 
141
- table = Employees("temp/data.db")
148
+ table = Employees("temp/data.db", force_drop=True)
142
149
  ```
143
150
 
144
151
  ## Row insertion
@@ -255,6 +262,43 @@ table[4:10:2]
255
262
  # (8, 'Frank White', 'Quality Assurance', 53000.0)]
256
263
  ```
257
264
 
265
+ ## Custom Types
266
+
267
+ More details at [Custom Types](https://github.com/suffermuffin/SQL-Engine/blob/main/docs/custom_types.md).
268
+
269
+ ```py
270
+ from datetime import datetime
271
+ from sqlengine import SqlTableMixin, Primary
272
+
273
+ class DateTime(datetime):
274
+
275
+ def to_sql(self) -> str:
276
+ return self.strftime("%Y-%m-%d %H:%M")
277
+
278
+ @classmethod
279
+ def from_sql(cls, sql : bytes):
280
+ return cls.fromisoformat(sql.decode('utf-8'))
281
+
282
+ def __repr__(self):
283
+ return f"DateTime({self.time})"
284
+
285
+
286
+ class ReservationIndex(SqlTableMixin):
287
+
288
+ user_id : Primary[int]
289
+ room_id : Primary[str]
290
+ time_at : Primary[DateTime]
291
+ user_name : str | None
292
+
293
+
294
+ table = ReservationIndex("temp/data.db")
295
+
296
+ table.insert(1, "loft_1", DateTime.now(), None)
297
+ table
298
+ ```
299
+
300
+ <table style="border-collapse: collapse; font-size: 14px;"><caption style="font-size: 18px; font-weight: bold;">ReservationIndex</caption><thead><tr><td style="border: 1px solid #555; text-align: center;">user_id</td><td style="border: 1px solid #555; text-align: center;">room_id</td><td style="border: 1px solid #555; text-align: center;">time_at</td><td style="border: 1px solid #555; text-align: center;">user_name</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;">loft_1</td><td style="border: 1px solid #000; text-align: center;">2026-05-29 21:29:00</td><td style="border: 1px solid #000; text-align: center;">None</td></tr></tbody></table>
301
+
258
302
  ## Csv Converter
259
303
 
260
304
  ```py
@@ -1,17 +1,20 @@
1
1
  [project]
2
2
  name = "sqlengine-lite"
3
- version = "2.1.0"
3
+ version = "2.2.0"
4
4
  description = "Cute sqlite3 wrapper for sql tables"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  dependencies = []
8
8
 
9
9
  [tool.setuptools]
10
- packages = ["sqlengine"]
10
+
11
11
 
12
12
  [tool.setuptools.package-dir]
13
13
  "" = "src"
14
14
 
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+
15
18
  [build-system]
16
19
  requires = ["setuptools>=61.0"]
17
20
  build-backend = "setuptools.build_meta"
@@ -0,0 +1,7 @@
1
+ from .core import sqlgen, types
2
+ from .core.types import Schema, Primary
3
+ from .sqltable import SqlTableMixin
4
+
5
+ __author__ = "suffermuffin"
6
+
7
+ __all__ = ["types", "sqlgen", "Schema", "SqlTableMixin", "Primary"]
@@ -0,0 +1,4 @@
1
+ from .statements import Select, Delete, Update
2
+ from .connection import ConnectionManager
3
+
4
+ __all__ = ["Select", "Delete", "Update", "ConnectionManager"]
@@ -0,0 +1,255 @@
1
+ import sqlite3
2
+ import logging
3
+ import os
4
+
5
+ from typing import overload, Literal, Sequence
6
+ from contextlib import contextmanager
7
+
8
+ from .types import SqlValue, SqlRow
9
+
10
+
11
+ logger = logging.getLogger("sqlengine")
12
+ logger.setLevel(os.getenv("SQL_ENGINE_LOG_LEVEL", "WARNING").upper())
13
+
14
+
15
+ class ConnectionManager:
16
+
17
+ _trans : sqlite3.Connection
18
+ _trans_cursor : sqlite3.Cursor
19
+
20
+ def __init__(self, database : str, **connection_params) -> None:
21
+
22
+ self.database = database
23
+ self.connection_params = connection_params
24
+
25
+ self._is_managed_transaction = False
26
+
27
+
28
+ @overload
29
+ def _fetch(self, query : str, args : SqlRow,
30
+ method : Literal["fetchone"]) -> SqlRow: ...
31
+ @overload
32
+ def _fetch(self, query : str, args : SqlRow,
33
+ method : Literal["fetchall"]) -> list[SqlRow]: ...
34
+
35
+ def _fetch(
36
+ self,
37
+ query : str,
38
+ args : SqlRow = (),
39
+ method : Literal["fetchone", "fetchall"] = "fetchall"
40
+ ) -> SqlRow | list[SqlRow]:
41
+
42
+ logger.debug(f"{self.database}: {query} {args}")
43
+
44
+ if self.in_transaction():
45
+ self._trans_cursor.execute(query, args)
46
+ return getattr(self._trans_cursor, method)()
47
+
48
+ with self.connect() as conn:
49
+ cursor = conn.cursor()
50
+ cursor.execute(query, args)
51
+ return getattr(cursor, method)()
52
+
53
+
54
+ @overload
55
+ def _execute(self, query : str, args : tuple[SqlValue, ...], method : Literal["execute"]) -> None: ...
56
+ @overload
57
+ def _execute(self, query : str, args : Sequence[SqlRow], method : Literal["executemany"]) -> None: ...
58
+
59
+ def _execute(
60
+ self,
61
+ query : str,
62
+ args : tuple[SqlValue, ...] | Sequence[SqlRow] = (),
63
+ method : Literal["execute", "executemany"] = "execute"
64
+ ) -> None:
65
+ """
66
+ Shortcut to connect() -> execute[<many>]() -> commit() for single operations.
67
+ Can be used in transaction using `transaction()` manager.
68
+
69
+ Args:
70
+ query (str): SQL query to execute on SQLite3 DB
71
+ *args (Any): Arguments to the execution
72
+ method (str): "execute" or "executemany"
73
+ """
74
+ logger.debug(f"{self.database}: {query} {args}")
75
+
76
+ if self.in_transaction():
77
+ getattr(self._trans_cursor, method)(query, args)
78
+ return
79
+
80
+ with self.connect() as conn:
81
+ cursor = conn.cursor()
82
+ getattr(cursor, method)(query, args)
83
+ conn.commit()
84
+
85
+
86
+ def execute(self, query : str, *args : SqlValue) -> None:
87
+ """
88
+ Shortcut to connect() -> execute() -> commit() for single operations.
89
+ Can be used in transaction using `transaction()` manager.
90
+
91
+ Args:
92
+ query (str): SQL query to execute on SQLite3 DB
93
+ *args (tuple[SqlValue, ...]): Arguments to the execution
94
+ """
95
+ return self._execute(query, args, method="execute")
96
+
97
+
98
+ def executemany(self, query : str, args : Sequence[SqlRow]) -> None:
99
+ """
100
+ Shortcut to connect() -> executemany() -> commit() for single operations.
101
+ Can be used in transaction using `transaction()` manager.
102
+
103
+ Args:
104
+ query (str): SQL query to execute on SQLite3 DB
105
+ args (list[tuple[SqlValue, ...]]): Arguments to the execution
106
+ """
107
+ return self._execute(query, args, method="executemany")
108
+
109
+
110
+ def fetchone(self, query : str, *args : SqlValue) -> SqlRow:
111
+ """
112
+ Fetch first row based on `query`
113
+
114
+ Args:
115
+ query (str): SQL query
116
+ *args (tuple[SqlValue, ...]): Arguments to the execution
117
+
118
+ Returns:
119
+ row (SqlRow): Single row
120
+ """
121
+ return self._fetch(query, args, method="fetchone")
122
+
123
+
124
+ def fetchmany(self, query : str, *args : SqlValue, size : int = 1) -> list[SqlRow]:
125
+ """
126
+ Fetch first `size` rows based on `query`
127
+
128
+ Args:
129
+ query (str): SQL query
130
+ *args (tuple[SqlValue, ...]): Arguments to the execution
131
+ size (str): Number of rows to return
132
+
133
+ Returns:
134
+ rows (list[SqlRow]): list of `size` rows
135
+ """
136
+ logger.debug(f"{self.database}: {query} {args}")
137
+
138
+ if self.in_transaction():
139
+ self._trans_cursor.execute(query, args)
140
+ return self._trans_cursor.fetchmany(size)
141
+
142
+ with self.connect() as conn:
143
+ cursor = conn.cursor()
144
+ cursor.execute(query, args)
145
+ return cursor.fetchmany(size)
146
+
147
+
148
+ def fetchall(self, query : str, *args : SqlValue) -> list[SqlRow]:
149
+ """
150
+ Fetch all rows based on `query`
151
+
152
+ Args:
153
+ query (str): SQL query
154
+ *args (tuple[SqlValue, ...]): Arguments to the execution
155
+
156
+ Returns:
157
+ rows (list[SqlRow]): list of rows
158
+ """
159
+ return self._fetch(query, args, method="fetchall")
160
+
161
+
162
+ def in_transaction(self) -> bool:
163
+ """ Returns True if instance is in transaction """
164
+ return hasattr(self, "_trans") and hasattr(self, "_trans_cursor")
165
+
166
+
167
+ def connect(self) -> sqlite3.Connection:
168
+ """ Shortcut to sqlite3 connection context manager """
169
+ return sqlite3.connect(self.database, **self.connection_params)
170
+
171
+
172
+ def open(self) -> None:
173
+ """ Opens unmanaged transaction """
174
+ if self.in_transaction():
175
+ raise RuntimeError("Can't re-open existing connection")
176
+
177
+ self._trans = self.connect()
178
+ self._trans_cursor = self._trans.cursor()
179
+
180
+
181
+ def close(self) -> None:
182
+ """ Closes unmanaged transaction """
183
+ if not self.in_transaction():
184
+ return
185
+
186
+ if self._is_managed_transaction:
187
+ raise RuntimeError("Can't manually close managed transaction")
188
+
189
+ self._trans_cursor.close()
190
+ self._trans.close()
191
+ del(self._trans_cursor)
192
+ del(self._trans)
193
+
194
+
195
+ def commit(self) -> None:
196
+ if not self.in_transaction():
197
+ raise RuntimeError("Can't commit outside transaction mode")
198
+
199
+ self._trans.commit()
200
+
201
+
202
+ def rollback(self) -> None:
203
+ if not self.in_transaction():
204
+ raise RuntimeError("Can't rollback outside transaction mode")
205
+
206
+ self._trans.rollback()
207
+
208
+
209
+ @contextmanager
210
+ def transaction(self, autocommit : bool = True):
211
+ """
212
+ Creates context manager to use class methods in transaction
213
+
214
+ Args:
215
+ autocommit (bool): If `True`, will commit changes at the end of transaction
216
+ """
217
+
218
+ self.open()
219
+ self._is_managed_transaction = True
220
+ logger.debug(f"{self.database}: Transaction started")
221
+
222
+ try:
223
+ yield
224
+
225
+ except Exception as e:
226
+ logger.error(f"{self.database}: Error while in transaction: {e}")
227
+ logger.debug(e, exc_info=True)
228
+ self._trans.rollback()
229
+ raise e
230
+
231
+ else:
232
+ if autocommit:
233
+ self._trans.commit()
234
+
235
+ finally:
236
+ self._is_managed_transaction = False
237
+ self.close()
238
+ logger.debug(f"{self.database}: Transaction finished")
239
+
240
+
241
+ @property
242
+ def tx_conn(self) -> sqlite3.Connection:
243
+ """ Gives access to connection while in transaction """
244
+ if not self.in_transaction():
245
+ raise RuntimeError("`tx_conn` is not available outside the transaction mode")
246
+ return self._trans
247
+
248
+
249
+ @property
250
+ def tx_cursor(self) -> sqlite3.Cursor:
251
+ """ Gives access to connection cursor while in transaction """
252
+ if not self.in_transaction():
253
+ raise RuntimeError("`tx_cursor` is not available outside the transaction mode")
254
+ return self._trans_cursor
255
+
@@ -0,0 +1,52 @@
1
+ from html import escape
2
+ from typing import Sequence
3
+
4
+ from .types import SqlRow
5
+
6
+
7
+ def to_html(tablename : str, columns : list[str], repr_rows : Sequence[SqlRow], limit : int = 25) -> str:
8
+
9
+ tablestyle = "<table style=\"border-collapse: collapse; font-size: 14px;\">"
10
+ tablenamestyle = "<caption style=\"font-size: 18px; font-weight: bold;\">{}</caption>"
11
+ colstyle = "<td style=\"border: 1px solid #555; text-align: center;\">{}</td>"
12
+ cellstyle = "<td style=\"border: 1px solid #000; text-align: center;\">{}</td>"
13
+ morestyle = "<td colspan=\"{}\" style=\"text-align:center;color:#888;font-style:italic;padding:8px;\">... more rows ...</td>"
14
+
15
+ cols = [colstyle.format(escape(col)) for col in columns]
16
+ tablename = tablenamestyle.format(escape(tablename))
17
+ more = morestyle.format(len(columns))
18
+
19
+ html = [
20
+ tablestyle,
21
+ tablename,
22
+ "<thead>",
23
+ "<tr>",
24
+ *cols,
25
+ "</tr>",
26
+ "</thead>",
27
+ "<tbody>",
28
+ ]
29
+
30
+ for row in repr_rows[:limit]:
31
+ values = [cellstyle.format(escape(str(val))) for val in row]
32
+
33
+ html.extend([
34
+ "<tr>",
35
+ *values,
36
+ "</tr>"
37
+ ])
38
+
39
+ if len(repr_rows) > limit:
40
+ html.extend([
41
+ "<tr>",
42
+ more,
43
+ "</tr>"
44
+ ])
45
+
46
+ html.extend([
47
+ "</tbody>",
48
+ "</table>",
49
+ ])
50
+
51
+ return "".join(html)
52
+