sqliter-py 0.12.0__py3-none-any.whl → 0.17.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.
Files changed (43) hide show
  1. sqliter/constants.py +4 -3
  2. sqliter/exceptions.py +29 -0
  3. sqliter/helpers.py +27 -0
  4. sqliter/model/model.py +21 -4
  5. sqliter/orm/__init__.py +17 -0
  6. sqliter/orm/fields.py +412 -0
  7. sqliter/orm/foreign_key.py +8 -0
  8. sqliter/orm/m2m.py +784 -0
  9. sqliter/orm/model.py +308 -0
  10. sqliter/orm/query.py +221 -0
  11. sqliter/orm/registry.py +440 -0
  12. sqliter/query/query.py +573 -51
  13. sqliter/sqliter.py +182 -47
  14. sqliter/tui/__init__.py +62 -0
  15. sqliter/tui/__main__.py +6 -0
  16. sqliter/tui/app.py +179 -0
  17. sqliter/tui/demos/__init__.py +96 -0
  18. sqliter/tui/demos/base.py +114 -0
  19. sqliter/tui/demos/caching.py +283 -0
  20. sqliter/tui/demos/connection.py +150 -0
  21. sqliter/tui/demos/constraints.py +211 -0
  22. sqliter/tui/demos/crud.py +154 -0
  23. sqliter/tui/demos/errors.py +231 -0
  24. sqliter/tui/demos/field_selection.py +150 -0
  25. sqliter/tui/demos/filters.py +389 -0
  26. sqliter/tui/demos/models.py +248 -0
  27. sqliter/tui/demos/ordering.py +156 -0
  28. sqliter/tui/demos/orm.py +537 -0
  29. sqliter/tui/demos/results.py +241 -0
  30. sqliter/tui/demos/string_filters.py +210 -0
  31. sqliter/tui/demos/timestamps.py +126 -0
  32. sqliter/tui/demos/transactions.py +177 -0
  33. sqliter/tui/runner.py +116 -0
  34. sqliter/tui/styles/app.tcss +130 -0
  35. sqliter/tui/widgets/__init__.py +7 -0
  36. sqliter/tui/widgets/code_display.py +81 -0
  37. sqliter/tui/widgets/demo_list.py +65 -0
  38. sqliter/tui/widgets/output_display.py +92 -0
  39. {sqliter_py-0.12.0.dist-info → sqliter_py-0.17.0.dist-info}/METADATA +28 -14
  40. sqliter_py-0.17.0.dist-info/RECORD +48 -0
  41. {sqliter_py-0.12.0.dist-info → sqliter_py-0.17.0.dist-info}/WHEEL +2 -2
  42. sqliter_py-0.17.0.dist-info/entry_points.txt +3 -0
  43. sqliter_py-0.12.0.dist-info/RECORD +0 -15
@@ -0,0 +1,241 @@
1
+ """Query Results & Aggregation demos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+
7
+ from sqliter import SqliterDB
8
+ from sqliter.model import BaseDBModel
9
+ from sqliter.tui.demos.base import Demo, DemoCategory, extract_demo_code
10
+
11
+
12
+ def _run_fetch_all() -> str:
13
+ """Fetch all records matching a query.
14
+
15
+ Use fetch_all() to get a list of all matching records.
16
+ """
17
+ output = io.StringIO()
18
+
19
+ class User(BaseDBModel):
20
+ name: str
21
+ age: int
22
+
23
+ db = SqliterDB(memory=True)
24
+ db.create_table(User)
25
+
26
+ for i in range(5):
27
+ db.insert(User(name=f"User {i}", age=20 + i))
28
+
29
+ results = db.select(User).fetch_all()
30
+ output.write(f"Total users: {len(results)}\n")
31
+ for user in results:
32
+ output.write(f" - {user.name}, age {user.age}\n")
33
+
34
+ db.close()
35
+ return output.getvalue()
36
+
37
+
38
+ def _run_fetch_one() -> str:
39
+ """Fetch a single record or return None if not found.
40
+
41
+ Use fetch_one() to get one matching record, returning None if
42
+ no records match the query.
43
+ """
44
+ output = io.StringIO()
45
+
46
+ class Task(BaseDBModel):
47
+ title: str
48
+ priority: int
49
+
50
+ db = SqliterDB(memory=True)
51
+ db.create_table(Task)
52
+
53
+ db.insert(Task(title="High priority", priority=1))
54
+ db.insert(Task(title="Medium priority", priority=2))
55
+ db.insert(Task(title="Low priority", priority=3))
56
+
57
+ task = db.select(Task).filter(priority__eq=1).fetch_one()
58
+ if task is not None:
59
+ output.write(f"Single result: {task.title}\n")
60
+
61
+ # Also test no results case
62
+ no_task = db.select(Task).filter(priority__eq=999).fetch_one()
63
+ if no_task is None:
64
+ output.write("No task found with priority 999\n")
65
+
66
+ db.close()
67
+ return output.getvalue()
68
+
69
+
70
+ def _run_fetch_first_last() -> str:
71
+ """Fetch the first or last record from results.
72
+
73
+ Use fetch_first() or fetch_last() to get a single record
74
+ from the beginning or end of the result set.
75
+ """
76
+ output = io.StringIO()
77
+
78
+ class Item(BaseDBModel):
79
+ name: str
80
+
81
+ db = SqliterDB(memory=True)
82
+ db.create_table(Item)
83
+
84
+ for name in ["Alpha", "Beta", "Gamma", "Delta"]:
85
+ db.insert(Item(name=name))
86
+
87
+ first = db.select(Item).fetch_first()
88
+ if first is not None:
89
+ output.write(f"First: {first.name}\n")
90
+
91
+ last = db.select(Item).fetch_last()
92
+ if last is not None:
93
+ output.write(f"Last: {last.name}\n")
94
+
95
+ db.close()
96
+ return output.getvalue()
97
+
98
+
99
+ def _run_count() -> str:
100
+ """Count the number of matching records.
101
+
102
+ Use count() to efficiently count records without fetching them.
103
+ """
104
+ output = io.StringIO()
105
+
106
+ class Product(BaseDBModel):
107
+ name: str
108
+ category: str
109
+
110
+ db = SqliterDB(memory=True)
111
+ db.create_table(Product)
112
+
113
+ db.insert(Product(name="Laptop", category="electronics"))
114
+ db.insert(Product(name="Phone", category="electronics"))
115
+ db.insert(Product(name="Desk", category="furniture"))
116
+
117
+ total = db.select(Product).count()
118
+ output.write(f"Total products: {total}\n")
119
+
120
+ electronics = db.select(Product).filter(category__eq="electronics").count()
121
+ output.write(f"Electronics: {electronics}\n")
122
+
123
+ db.close()
124
+ return output.getvalue()
125
+
126
+
127
+ def _run_exists() -> str:
128
+ """Check if any records match the query.
129
+
130
+ Use exists() to efficiently check for matching records without
131
+ fetching them - returns True/False.
132
+ """
133
+ output = io.StringIO()
134
+
135
+ class User(BaseDBModel):
136
+ username: str
137
+
138
+ db = SqliterDB(memory=True)
139
+ db.create_table(User)
140
+
141
+ db.insert(User(username="alice"))
142
+ db.insert(User(username="bob"))
143
+
144
+ exists = db.select(User).filter(username__eq="alice").exists()
145
+ output.write(f"User 'alice' exists: {exists}\n")
146
+
147
+ not_exists = db.select(User).filter(username__eq="charlie").exists()
148
+ output.write(f"User 'charlie' exists: {not_exists}\n")
149
+
150
+ db.close()
151
+ return output.getvalue()
152
+
153
+
154
+ def _run_aggregates() -> str:
155
+ """Calculate aggregates using Python after fetching data.
156
+
157
+ SQLiter doesn't support SQL-level aggregates (GROUP BY, HAVING).
158
+ Use Python's sum(), len(), etc. after fetching results.
159
+ """
160
+ output = io.StringIO()
161
+
162
+ class Sale(BaseDBModel):
163
+ amount: float
164
+
165
+ db = SqliterDB(memory=True)
166
+ db.create_table(Sale)
167
+
168
+ for amount in [10.0, 20.0, 30.0, 40.0, 50.0]:
169
+ db.insert(Sale(amount=amount))
170
+
171
+ # Note: SQLiter doesn't support SQL-level aggregates (GROUP BY, HAVING)
172
+ # Use Python for calculations after fetching data
173
+ results = db.select(Sale).fetch_all()
174
+ total = sum(s.amount for s in results)
175
+ average = total / len(results)
176
+ output.write(f"Total sales: ${total:.2f}\n")
177
+ output.write(f"Average sale: ${average:.2f}\n")
178
+ output.write(f"Count: {len(results)}\n")
179
+ output.write("\n(Aggregates calculated in Python, not SQL)\n")
180
+
181
+ db.close()
182
+ return output.getvalue()
183
+
184
+
185
+ def get_category() -> DemoCategory:
186
+ """Get the Query Results demo category."""
187
+ return DemoCategory(
188
+ id="results",
189
+ title="Query Results",
190
+ icon="",
191
+ demos=[
192
+ Demo(
193
+ id="result_fetch_all",
194
+ title="Fetch All",
195
+ description="Get all matching records",
196
+ category="results",
197
+ code=extract_demo_code(_run_fetch_all),
198
+ execute=_run_fetch_all,
199
+ ),
200
+ Demo(
201
+ id="result_fetch_one",
202
+ title="Fetch One",
203
+ description="Get single record or None",
204
+ category="results",
205
+ code=extract_demo_code(_run_fetch_one),
206
+ execute=_run_fetch_one,
207
+ ),
208
+ Demo(
209
+ id="result_first_last",
210
+ title="Fetch First/Last",
211
+ description="Get first or last record",
212
+ category="results",
213
+ code=extract_demo_code(_run_fetch_first_last),
214
+ execute=_run_fetch_first_last,
215
+ ),
216
+ Demo(
217
+ id="result_count",
218
+ title="Count",
219
+ description="Count matching records",
220
+ category="results",
221
+ code=extract_demo_code(_run_count),
222
+ execute=_run_count,
223
+ ),
224
+ Demo(
225
+ id="result_exists",
226
+ title="Exists",
227
+ description="Check if any records match",
228
+ category="results",
229
+ code=extract_demo_code(_run_exists),
230
+ execute=_run_exists,
231
+ ),
232
+ Demo(
233
+ id="result_aggregates",
234
+ title="Aggregates",
235
+ description="Calculate sum, average, etc.",
236
+ category="results",
237
+ code=extract_demo_code(_run_aggregates),
238
+ execute=_run_aggregates,
239
+ ),
240
+ ],
241
+ )
@@ -0,0 +1,210 @@
1
+ """String Filter demos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+
7
+ from sqliter import SqliterDB
8
+ from sqliter.model import BaseDBModel
9
+ from sqliter.tui.demos.base import Demo, DemoCategory, extract_demo_code
10
+
11
+
12
+ def _run_startswith() -> str:
13
+ """Filter strings that start with a specific prefix.
14
+
15
+ Use __startswith to match records where a field begins with
16
+ the specified value.
17
+ """
18
+ output = io.StringIO()
19
+
20
+ class User(BaseDBModel):
21
+ username: str
22
+
23
+ db = SqliterDB(memory=True)
24
+ db.create_table(User)
25
+
26
+ db.insert(User(username="alice_wonder"))
27
+ db.insert(User(username="alice_smith"))
28
+ db.insert(User(username="bob_builder"))
29
+
30
+ # Find usernames starting with "alice"
31
+ results = db.select(User).filter(username__startswith="alice").fetch_all()
32
+ output.write(f"Users starting with 'alice': {len(results)}\n")
33
+ for user in results:
34
+ output.write(f" - {user.username}\n")
35
+
36
+ db.close()
37
+ return output.getvalue()
38
+
39
+
40
+ def _run_endswith() -> str:
41
+ """Filter strings that end with a specific suffix.
42
+
43
+ Use __endswith to match records where a field ends with
44
+ the specified value.
45
+ """
46
+ output = io.StringIO()
47
+
48
+ class File(BaseDBModel):
49
+ filename: str
50
+
51
+ db = SqliterDB(memory=True)
52
+ db.create_table(File)
53
+
54
+ db.insert(File(filename="document.txt"))
55
+ db.insert(File(filename="image.png"))
56
+ db.insert(File(filename="notes.txt"))
57
+ db.insert(File(filename="data.csv"))
58
+
59
+ # Find files ending with ".txt"
60
+ results = db.select(File).filter(filename__endswith=".txt").fetch_all()
61
+ output.write(f"Text files: {len(results)}\n")
62
+ for file in results:
63
+ output.write(f" - {file.filename}\n")
64
+
65
+ db.close()
66
+ return output.getvalue()
67
+
68
+
69
+ def _run_contains() -> str:
70
+ """Filter strings that contain a specific substring.
71
+
72
+ Use __contains to match records where a field contains
73
+ the specified value anywhere within it.
74
+ """
75
+ output = io.StringIO()
76
+
77
+ class Product(BaseDBModel):
78
+ name: str
79
+
80
+ db = SqliterDB(memory=True)
81
+ db.create_table(Product)
82
+
83
+ db.insert(Product(name="Apple iPhone"))
84
+ db.insert(Product(name="Samsung Galaxy"))
85
+ db.insert(Product(name="Apple iPad"))
86
+ db.insert(Product(name="Google Pixel"))
87
+
88
+ # Find products containing "Apple"
89
+ results = db.select(Product).filter(name__contains="Apple").fetch_all()
90
+ output.write(f"Products containing 'Apple': {len(results)}\n")
91
+ for product in results:
92
+ output.write(f" - {product.name}\n")
93
+
94
+ db.close()
95
+ return output.getvalue()
96
+
97
+
98
+ def _run_case_insensitive() -> str:
99
+ """Filter strings ignoring case with __istartswith and __iendswith.
100
+
101
+ Use case-insensitive operators to match strings regardless of
102
+ capitalization.
103
+ """
104
+ output = io.StringIO()
105
+
106
+ class User(BaseDBModel):
107
+ email: str
108
+
109
+ db = SqliterDB(memory=True)
110
+ db.create_table(User)
111
+
112
+ db.insert(User(email="ALICE@example.com"))
113
+ db.insert(User(email="bob@EXAMPLE.com"))
114
+ db.insert(User(email="charlie@test.com"))
115
+
116
+ # Find emails ending with "@example.com" (case-insensitive)
117
+ results = (
118
+ db.select(User).filter(email__iendswith="@example.com").fetch_all()
119
+ )
120
+ output.write(f"Emails ending with '@example.com': {len(results)}\n")
121
+ for user in results:
122
+ output.write(f" - {user.email}\n")
123
+
124
+ # Find emails starting with "BOB" (case-insensitive)
125
+ bob_results = db.select(User).filter(email__istartswith="BOB").fetch_all()
126
+ output.write(f"\nEmails starting with 'BOB': {len(bob_results)}\n")
127
+ for user in bob_results:
128
+ output.write(f" - {user.email}\n")
129
+
130
+ db.close()
131
+ return output.getvalue()
132
+
133
+
134
+ def _run_icontains() -> str:
135
+ """Filter strings containing a substring, ignoring case.
136
+
137
+ Use __icontains for case-insensitive substring matching.
138
+ """
139
+ output = io.StringIO()
140
+
141
+ class Article(BaseDBModel):
142
+ title: str
143
+
144
+ db = SqliterDB(memory=True)
145
+ db.create_table(Article)
146
+
147
+ db.insert(Article(title="Python Programming Guide"))
148
+ db.insert(Article(title="Advanced PYTHON Techniques"))
149
+ db.insert(Article(title="Web Development"))
150
+ db.insert(Article(title="python for Beginners"))
151
+
152
+ # Find articles containing "python" (case-insensitive)
153
+ results = db.select(Article).filter(title__icontains="python").fetch_all()
154
+ output.write(f"Articles containing 'python': {len(results)}\n")
155
+ for article in results:
156
+ output.write(f" - {article.title}\n")
157
+
158
+ db.close()
159
+ return output.getvalue()
160
+
161
+
162
+ def get_category() -> DemoCategory:
163
+ """Get the String Filters demo category."""
164
+ return DemoCategory(
165
+ id="string_filters",
166
+ title="String Filters",
167
+ icon="",
168
+ demos=[
169
+ Demo(
170
+ id="string_startswith",
171
+ title="Starts With (__startswith)",
172
+ description="Match strings starting with prefix",
173
+ category="string_filters",
174
+ code=extract_demo_code(_run_startswith),
175
+ execute=_run_startswith,
176
+ ),
177
+ Demo(
178
+ id="string_endswith",
179
+ title="Ends With (__endswith)",
180
+ description="Match strings ending with suffix",
181
+ category="string_filters",
182
+ code=extract_demo_code(_run_endswith),
183
+ execute=_run_endswith,
184
+ ),
185
+ Demo(
186
+ id="string_contains",
187
+ title="Contains (__contains)",
188
+ description="Match strings containing substring",
189
+ category="string_filters",
190
+ code=extract_demo_code(_run_contains),
191
+ execute=_run_contains,
192
+ ),
193
+ Demo(
194
+ id="string_case_insensitive",
195
+ title="Case-Insensitive (__i*)",
196
+ description="Match strings ignoring case",
197
+ category="string_filters",
198
+ code=extract_demo_code(_run_case_insensitive),
199
+ execute=_run_case_insensitive,
200
+ ),
201
+ Demo(
202
+ id="string_icontains",
203
+ title="Contains Case-Insensitive",
204
+ description="Match substring ignoring case",
205
+ category="string_filters",
206
+ code=extract_demo_code(_run_icontains),
207
+ execute=_run_icontains,
208
+ ),
209
+ ],
210
+ )
@@ -0,0 +1,126 @@
1
+ """Auto Timestamp demos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import time
7
+ from datetime import datetime, timezone
8
+
9
+ from sqliter import SqliterDB
10
+ from sqliter.model import BaseDBModel
11
+ from sqliter.tui.demos.base import Demo, DemoCategory, extract_demo_code
12
+
13
+
14
+ def _run_created_at() -> str:
15
+ """Automatically track when records are created.
16
+
17
+ The created_at field is set automatically when you insert a record.
18
+ """
19
+ output = io.StringIO()
20
+
21
+ class Article(BaseDBModel):
22
+ title: str
23
+
24
+ db = SqliterDB(memory=True)
25
+ db.create_table(Article)
26
+
27
+ article1 = db.insert(Article(title="First Post"))
28
+ dt1 = datetime.fromtimestamp(article1.created_at, tz=timezone.utc)
29
+ formatted_dt1 = dt1.strftime("%Y-%m-%d %H:%M:%S")
30
+ output.write(f"Article: {article1.title}\n")
31
+ output.write(f"Created: {article1.created_at} ({formatted_dt1} UTC)\n")
32
+
33
+ time.sleep(0.1)
34
+
35
+ article2 = db.insert(Article(title="Second Post"))
36
+ dt2 = datetime.fromtimestamp(article2.created_at, tz=timezone.utc)
37
+ formatted_dt2 = dt2.strftime("%Y-%m-%d %H:%M:%S")
38
+ output.write(f"\nArticle: {article2.title}\n")
39
+ output.write(f"Created: {article2.created_at} ({formatted_dt2} UTC)\n")
40
+
41
+ db.close()
42
+ return output.getvalue()
43
+
44
+
45
+ def _run_updated_at() -> str:
46
+ """Automatically track when records are last modified.
47
+
48
+ The updated_at field changes automatically when you update a record.
49
+ """
50
+ output = io.StringIO()
51
+
52
+ class Task(BaseDBModel):
53
+ title: str
54
+ done: bool = False
55
+
56
+ db = SqliterDB(memory=True)
57
+ db.create_table(Task)
58
+
59
+ task = db.insert(Task(title="Original Task"))
60
+ created_dt = datetime.fromtimestamp(task.created_at, tz=timezone.utc)
61
+ updated_dt = datetime.fromtimestamp(task.updated_at, tz=timezone.utc)
62
+ formatted_created_dt = created_dt.strftime("%Y-%m-%d %H:%M:%S")
63
+ formatted_updated_dt = updated_dt.strftime("%Y-%m-%d %H:%M:%S")
64
+ output.write(f"Task: {task.title}\n")
65
+ output.write(f"Created: {task.created_at} ({formatted_created_dt} UTC)\n")
66
+ output.write(f"Updated: {task.updated_at} ({formatted_updated_dt} UTC)\n")
67
+
68
+ # Sleep for 1 second to ensure different timestamps on fast machines
69
+ time.sleep(1)
70
+
71
+ task.title = "Updated Task"
72
+ task.done = True
73
+ db.update(task)
74
+ updated_task = task
75
+ updated_created_dt = datetime.fromtimestamp(
76
+ updated_task.created_at, tz=timezone.utc
77
+ )
78
+ updated_updated_dt = datetime.fromtimestamp(
79
+ updated_task.updated_at, tz=timezone.utc
80
+ )
81
+ formatted_updated_created_dt = updated_created_dt.strftime(
82
+ "%Y-%m-%d %H:%M:%S"
83
+ )
84
+ formatted_updated_updated_dt = updated_updated_dt.strftime(
85
+ "%Y-%m-%d %H:%M:%S"
86
+ )
87
+ output.write("\nAfter update:\n")
88
+ output.write(f"Title: {updated_task.title}\n")
89
+ output.write(
90
+ f"Created: {updated_task.created_at} "
91
+ f"({formatted_updated_created_dt} UTC)\n"
92
+ )
93
+ output.write(
94
+ f"Updated: {updated_task.updated_at} "
95
+ f"({formatted_updated_updated_dt} UTC)\n"
96
+ )
97
+
98
+ db.close()
99
+ return output.getvalue()
100
+
101
+
102
+ def get_category() -> DemoCategory:
103
+ """Get the Auto Timestamp demo category."""
104
+ return DemoCategory(
105
+ id="timestamps",
106
+ title="Auto Timestamps",
107
+ icon="",
108
+ demos=[
109
+ Demo(
110
+ id="timestamp_created",
111
+ title="Auto created_at",
112
+ description="Track when records are created",
113
+ category="timestamps",
114
+ code=extract_demo_code(_run_created_at),
115
+ execute=_run_created_at,
116
+ ),
117
+ Demo(
118
+ id="timestamp_updated",
119
+ title="Auto updated_at",
120
+ description="Track when records are modified",
121
+ category="timestamps",
122
+ code=extract_demo_code(_run_updated_at),
123
+ execute=_run_updated_at,
124
+ ),
125
+ ],
126
+ )