sqliter-py 0.12.0__py3-none-any.whl → 0.16.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 (41) hide show
  1. sqliter/constants.py +4 -3
  2. sqliter/exceptions.py +13 -0
  3. sqliter/model/model.py +42 -3
  4. sqliter/orm/__init__.py +16 -0
  5. sqliter/orm/fields.py +412 -0
  6. sqliter/orm/foreign_key.py +8 -0
  7. sqliter/orm/model.py +243 -0
  8. sqliter/orm/query.py +221 -0
  9. sqliter/orm/registry.py +169 -0
  10. sqliter/query/query.py +573 -51
  11. sqliter/sqliter.py +141 -47
  12. sqliter/tui/__init__.py +62 -0
  13. sqliter/tui/__main__.py +6 -0
  14. sqliter/tui/app.py +179 -0
  15. sqliter/tui/demos/__init__.py +96 -0
  16. sqliter/tui/demos/base.py +114 -0
  17. sqliter/tui/demos/caching.py +283 -0
  18. sqliter/tui/demos/connection.py +150 -0
  19. sqliter/tui/demos/constraints.py +211 -0
  20. sqliter/tui/demos/crud.py +154 -0
  21. sqliter/tui/demos/errors.py +231 -0
  22. sqliter/tui/demos/field_selection.py +150 -0
  23. sqliter/tui/demos/filters.py +389 -0
  24. sqliter/tui/demos/models.py +248 -0
  25. sqliter/tui/demos/ordering.py +156 -0
  26. sqliter/tui/demos/orm.py +460 -0
  27. sqliter/tui/demos/results.py +241 -0
  28. sqliter/tui/demos/string_filters.py +210 -0
  29. sqliter/tui/demos/timestamps.py +126 -0
  30. sqliter/tui/demos/transactions.py +177 -0
  31. sqliter/tui/runner.py +116 -0
  32. sqliter/tui/styles/app.tcss +130 -0
  33. sqliter/tui/widgets/__init__.py +7 -0
  34. sqliter/tui/widgets/code_display.py +81 -0
  35. sqliter/tui/widgets/demo_list.py +65 -0
  36. sqliter/tui/widgets/output_display.py +92 -0
  37. {sqliter_py-0.12.0.dist-info → sqliter_py-0.16.0.dist-info}/METADATA +23 -7
  38. sqliter_py-0.16.0.dist-info/RECORD +47 -0
  39. {sqliter_py-0.12.0.dist-info → sqliter_py-0.16.0.dist-info}/WHEEL +2 -2
  40. sqliter_py-0.16.0.dist-info/entry_points.txt +3 -0
  41. sqliter_py-0.12.0.dist-info/RECORD +0 -15
@@ -0,0 +1,114 @@
1
+ """Base classes for demo definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import textwrap
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, Optional
9
+
10
+ if TYPE_CHECKING: # pragma: no cover
11
+ from collections.abc import Callable
12
+
13
+
14
+ def extract_demo_code(func: Callable[..., str]) -> str:
15
+ """Extract and format source code from a demo function.
16
+
17
+ This function uses Python's inspect module to dynamically extract the
18
+ source code from a demo function, ensuring the displayed code always
19
+ matches what's actually executed.
20
+
21
+ Removes demo infrastructure (function definition, output setup, return)
22
+ but keeps the docstring for context.
23
+
24
+ Args:
25
+ func: The demo function to extract code from
26
+
27
+ Returns:
28
+ Formatted source code string with proper dedentation and whitespace
29
+ """
30
+ # Get the source code and split into lines
31
+ source = inspect.getsource(func)
32
+ lines = source.splitlines()
33
+
34
+ # Skip decorator lines and function definition
35
+ start_idx = 0
36
+ for i, line in enumerate(lines):
37
+ stripped = line.strip()
38
+ if stripped.startswith(("def ", "async def ")):
39
+ start_idx = i + 1
40
+ break
41
+ lines = lines[start_idx:]
42
+
43
+ # Dedent the remaining code
44
+ code = textwrap.dedent("\n".join(lines))
45
+ lines = code.splitlines()
46
+
47
+ # Filter out unwanted lines
48
+ filtered: list[str] = []
49
+ for original_line in lines:
50
+ # Skip output setup
51
+ if "output = io.StringIO()" in original_line:
52
+ continue
53
+ # Rename output.write to print
54
+ line = original_line
55
+ if "output.write(" in line:
56
+ line = line.replace("output.write(", "print(")
57
+ # Stop at return statement
58
+ if "return output.getvalue()" in line:
59
+ break
60
+
61
+ filtered.append(line)
62
+
63
+ # Remove trailing empty lines
64
+ while filtered and not filtered[-1].strip():
65
+ filtered.pop()
66
+
67
+ return "\n".join(filtered).strip()
68
+
69
+
70
+ __all__ = ["Demo", "DemoCategory", "extract_demo_code"]
71
+
72
+
73
+ @dataclass
74
+ class Demo:
75
+ """Represents a single demo example.
76
+
77
+ Attributes:
78
+ id: Unique identifier for the demo (e.g., "conn_memory")
79
+ title: Display title in the list (e.g., "In-memory Database")
80
+ description: Brief description shown as tooltip/subtitle
81
+ category: Category ID this demo belongs to
82
+ code: The Python code to display (syntax highlighted)
83
+ execute: Callable that runs the demo and returns output string
84
+ setup_code: Optional setup code shown before main code
85
+ teardown: Optional cleanup function called after execution
86
+ """
87
+
88
+ id: str
89
+ title: str
90
+ description: str
91
+ category: str
92
+ code: str
93
+ execute: Callable[[], str]
94
+ setup_code: Optional[str] = None
95
+ teardown: Optional[Callable[[], None]] = None
96
+
97
+
98
+ @dataclass
99
+ class DemoCategory:
100
+ """A category of related demos.
101
+
102
+ Attributes:
103
+ id: Unique identifier (e.g., "connection")
104
+ title: Display title (e.g., "Connection & Setup")
105
+ icon: Optional emoji icon for the category
106
+ demos: List of demos in this category
107
+ expanded: Whether category starts expanded in the tree
108
+ """
109
+
110
+ id: str
111
+ title: str
112
+ icon: str = ""
113
+ demos: list[Demo] = field(default_factory=list)
114
+ expanded: bool = False
@@ -0,0 +1,283 @@
1
+ """Caching demos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import tempfile
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from sqliter import SqliterDB
11
+ from sqliter.model import BaseDBModel
12
+ from sqliter.tui.demos.base import Demo, DemoCategory, extract_demo_code
13
+
14
+
15
+ def _run_enable_cache() -> str:
16
+ """Demonstrate enabling query result caching for performance.
17
+
18
+ Caching stores query results in memory, speeding up repeated queries
19
+ by avoiding disk I/O. Benefits are most apparent with complex queries
20
+ and large datasets.
21
+ """
22
+ output = io.StringIO()
23
+
24
+ class User(BaseDBModel):
25
+ name: str
26
+ email: str
27
+ age: int
28
+
29
+ # Use file-based database to show real caching benefits
30
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
31
+ db_path = f.name
32
+
33
+ db = None
34
+ try:
35
+ db = SqliterDB(db_path, cache_enabled=True)
36
+ db.create_table(User)
37
+
38
+ # Insert more data for a more realistic demo
39
+ for i in range(50):
40
+ db.insert(
41
+ User(
42
+ name=f"User {i}",
43
+ email=f"user{i}@example.com",
44
+ age=20 + i,
45
+ )
46
+ )
47
+
48
+ output.write("Inserted 50 users\n")
49
+ output.write("Caching stores query results to avoid repeated I/O\n\n")
50
+
51
+ # Query with filter (more expensive than simple pk lookup)
52
+ # First query - cache miss
53
+ start = time.perf_counter()
54
+ users = db.select(User).filter(age__gte=40).fetch_all()
55
+ miss_time = (time.perf_counter() - start) * 1000
56
+ output.write(f"First query (cache miss): {miss_time:.3f}ms\n")
57
+ output.write(f"Found {len(users)} users age 40+\n")
58
+
59
+ # Second query with same filter - cache hit
60
+ start = time.perf_counter()
61
+ users = db.select(User).filter(age__gte=40).fetch_all()
62
+ hit_time = (time.perf_counter() - start) * 1000
63
+ output.write(f"Second query (cache hit): {hit_time:.3f}ms\n")
64
+ output.write(f"Found {len(users)} users age 40+\n")
65
+
66
+ # Show speedup
67
+ if hit_time > 0:
68
+ speedup = miss_time / hit_time
69
+ output.write(f"\nCache hit is {speedup:.1f}x faster!\n")
70
+ output.write("(Benefits increase with query complexity and data size)")
71
+ finally:
72
+ if db is not None:
73
+ db.close()
74
+ # Cleanup
75
+ Path(db_path).unlink(missing_ok=True)
76
+
77
+ return output.getvalue()
78
+
79
+
80
+ def _run_cache_stats() -> str:
81
+ """Show how to view cache hit/miss statistics.
82
+
83
+ Use get_cache_stats() to monitor cache performance and see how
84
+ effective your caching strategy is.
85
+ """
86
+ output = io.StringIO()
87
+
88
+ class Product(BaseDBModel):
89
+ name: str
90
+ price: float
91
+
92
+ db = SqliterDB(memory=True, cache_enabled=True)
93
+ db.create_table(Product)
94
+
95
+ product = db.insert(Product(name="Widget", price=19.99))
96
+
97
+ # Perform queries
98
+ for _ in range(5):
99
+ db.get(Product, product.pk)
100
+
101
+ stats = db.get_cache_stats()
102
+ output.write("Cache statistics:\n")
103
+ output.write(f" - Total queries: {stats['total']}\n")
104
+ output.write(f" - Cache hits: {stats['hits']}\n")
105
+ output.write(f" - Cache misses: {stats['misses']}\n")
106
+ output.write(f" - Hit rate: {stats['hit_rate']}%\n")
107
+
108
+ db.close()
109
+ return output.getvalue()
110
+
111
+
112
+ def _run_get_cache_controls() -> str:
113
+ """Show get() caching, bypass, and TTL overrides."""
114
+ output = io.StringIO()
115
+
116
+ class Product(BaseDBModel):
117
+ name: str
118
+ price: float
119
+
120
+ db = SqliterDB(memory=True, cache_enabled=True, cache_ttl=60)
121
+ db.create_table(Product)
122
+
123
+ product = db.insert(Product(name="Widget", price=19.99))
124
+
125
+ db.get(Product, product.pk)
126
+ stats = db.get_cache_stats()
127
+ output.write("After first get (miss):\n")
128
+ output.write(f" - Hits: {stats['hits']}\n")
129
+ output.write(f" - Misses: {stats['misses']}\n")
130
+
131
+ db.get(Product, product.pk)
132
+ stats = db.get_cache_stats()
133
+ output.write("After second get (hit):\n")
134
+ output.write(f" - Hits: {stats['hits']}\n")
135
+ output.write(f" - Misses: {stats['misses']}\n")
136
+
137
+ db.get(Product, product.pk, bypass_cache=True)
138
+ stats = db.get_cache_stats()
139
+ output.write("After bypass_cache=True (stats unchanged):\n")
140
+ output.write(f" - Hits: {stats['hits']}\n")
141
+ output.write(f" - Misses: {stats['misses']}\n")
142
+
143
+ db.get(Product, product.pk, cache_ttl=5)
144
+ output.write("Per-call TTL override set to 5s for this lookup\n")
145
+
146
+ db.close()
147
+ return output.getvalue()
148
+
149
+
150
+ def _run_cache_bypass() -> str:
151
+ """Bypass the cache to fetch fresh data from the database.
152
+
153
+ Use bypass_cache() when you need to ensure you're getting the most
154
+ up-to-date data, ignoring any cached results.
155
+ """
156
+ output = io.StringIO()
157
+
158
+ class Item(BaseDBModel):
159
+ name: str
160
+
161
+ db = SqliterDB(memory=True, cache_enabled=True)
162
+ db.create_table(Item)
163
+
164
+ # Insert item to query
165
+ db.insert(Item(name="Item 1"))
166
+
167
+ # First query - uses cache
168
+ db.select(Item).filter(name__eq="Item 1").fetch_one()
169
+ output.write("First query: cached\n")
170
+
171
+ # Bypass cache for fresh data - skips cache, hits DB
172
+ db.select(Item).filter(name__eq="Item 1").bypass_cache().fetch_one()
173
+ output.write("Second query: bypassed cache for fresh data\n")
174
+
175
+ db.close()
176
+ return output.getvalue()
177
+
178
+
179
+ def _run_cache_ttl() -> str:
180
+ """Set a time-to-live (TTL) for cached entries.
181
+
182
+ Cache entries automatically expire after the specified number of seconds,
183
+ ensuring stale data isn't served indefinitely.
184
+ """
185
+ output = io.StringIO()
186
+
187
+ class Article(BaseDBModel):
188
+ title: str
189
+
190
+ db = SqliterDB(memory=True, cache_enabled=True, cache_ttl=60)
191
+ db.create_table(Article)
192
+
193
+ article = db.insert(Article(title="News Article"))
194
+ output.write(f"Created: {article.title}\n")
195
+ output.write("Cache TTL set to 60 seconds\n")
196
+ output.write("Cached entries expire after TTL\n")
197
+
198
+ db.close()
199
+ return output.getvalue()
200
+
201
+
202
+ def _run_cache_clear() -> str:
203
+ """Manually clear the cache to free memory or force refresh.
204
+
205
+ Use clear_cache() when you need to invalidate all cached results
206
+ and start fresh.
207
+ """
208
+ output = io.StringIO()
209
+
210
+ class Document(BaseDBModel):
211
+ title: str
212
+
213
+ db = SqliterDB(memory=True, cache_enabled=True)
214
+ db.create_table(Document)
215
+
216
+ doc = db.insert(Document(title="Doc 1"))
217
+ db.get(Document, doc.pk)
218
+ output.write("Query executed and cached\n")
219
+
220
+ db.clear_cache()
221
+ output.write("Cache cleared\n")
222
+
223
+ db.close()
224
+ return output.getvalue()
225
+
226
+
227
+ def get_category() -> DemoCategory:
228
+ """Get the Caching demo category."""
229
+ return DemoCategory(
230
+ id="caching",
231
+ title="Caching",
232
+ icon="",
233
+ demos=[
234
+ Demo(
235
+ id="cache_enable",
236
+ title="Enable Cache",
237
+ description="Enable query result caching",
238
+ category="caching",
239
+ code=extract_demo_code(_run_enable_cache),
240
+ execute=_run_enable_cache,
241
+ ),
242
+ Demo(
243
+ id="cache_stats",
244
+ title="Cache Statistics",
245
+ description="View cache hit/miss statistics",
246
+ category="caching",
247
+ code=extract_demo_code(_run_cache_stats),
248
+ execute=_run_cache_stats,
249
+ ),
250
+ Demo(
251
+ id="cache_get_controls",
252
+ title="Get Cache Controls",
253
+ description="Cache, bypass, and TTL for get()",
254
+ category="caching",
255
+ code=extract_demo_code(_run_get_cache_controls),
256
+ execute=_run_get_cache_controls,
257
+ ),
258
+ Demo(
259
+ id="cache_bypass",
260
+ title="Cache Bypass",
261
+ description="Bypass cache for fresh data",
262
+ category="caching",
263
+ code=extract_demo_code(_run_cache_bypass),
264
+ execute=_run_cache_bypass,
265
+ ),
266
+ Demo(
267
+ id="cache_ttl",
268
+ title="Cache TTL",
269
+ description="Set cache expiration time",
270
+ category="caching",
271
+ code=extract_demo_code(_run_cache_ttl),
272
+ execute=_run_cache_ttl,
273
+ ),
274
+ Demo(
275
+ id="cache_clear",
276
+ title="Clear Cache",
277
+ description="Manually clear the cache",
278
+ category="caching",
279
+ code=extract_demo_code(_run_cache_clear),
280
+ execute=_run_cache_clear,
281
+ ),
282
+ ],
283
+ )
@@ -0,0 +1,150 @@
1
+ """Connection & Setup demos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import tempfile
7
+ from pathlib import Path
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_memory_db() -> str:
15
+ """Create an in-memory SQLite database.
16
+
17
+ Use memory=True for fast, temporary databases that don't persist.
18
+ """
19
+ output = io.StringIO()
20
+
21
+ db = SqliterDB(memory=True)
22
+ output.write(f"Created database: {db}\n")
23
+ output.write(f"Is memory: {db.is_memory}\n")
24
+ output.write(f"Filename: {db.filename}\n")
25
+
26
+ db.connect()
27
+ output.write(f"Connected: {db.is_connected}\n")
28
+
29
+ db.close()
30
+ output.write(f"After close: {db.is_connected}\n")
31
+
32
+ return output.getvalue()
33
+
34
+
35
+ def _run_file_db() -> str:
36
+ """Create a file-based SQLite database for persistent storage.
37
+
38
+ Provide a file path to store data that persists across sessions.
39
+ """
40
+ output = io.StringIO()
41
+
42
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
43
+ db_path = f.name
44
+
45
+ try:
46
+ db = SqliterDB(db_path)
47
+ output.write("Created file database\n")
48
+ output.write(f"Filename: {db.filename}\n")
49
+ output.write(f"Is memory: {db.is_memory}\n")
50
+
51
+ db.connect()
52
+ output.write(f"Connected to: {db_path}\n")
53
+ db.close()
54
+ finally:
55
+ Path(db_path).unlink(missing_ok=True)
56
+ output.write("Cleaned up database file\n")
57
+
58
+ return output.getvalue()
59
+
60
+
61
+ def _run_debug_mode() -> str:
62
+ """Enable debug mode to see SQL queries being executed.
63
+
64
+ Set debug=True to log all SQL queries to the console for debugging.
65
+ """
66
+ output = io.StringIO()
67
+
68
+ output.write("Debug mode enables SQL query logging.\n")
69
+ output.write("When debug=True, all SQL queries are logged.\n\n")
70
+
71
+ class User(BaseDBModel):
72
+ name: str
73
+
74
+ db = SqliterDB(memory=True, debug=True)
75
+ db.create_table(User)
76
+
77
+ output.write("SQL queries would be logged to console:\n")
78
+ output.write(' CREATE TABLE IF NOT EXISTS "users" (...)\n')
79
+
80
+ db.close()
81
+ return output.getvalue()
82
+
83
+
84
+ def _run_context_manager() -> str:
85
+ """Use context manager for automatic connection management.
86
+
87
+ The `with db:` block handles connection, transactions, and cleanup.
88
+ """
89
+ output = io.StringIO()
90
+
91
+ class Task(BaseDBModel):
92
+ title: str
93
+ done: bool = False
94
+
95
+ output.write("Using context manager for transactions:\n\n")
96
+
97
+ db = SqliterDB(memory=True)
98
+
99
+ with db:
100
+ db.create_table(Task)
101
+ task = db.insert(Task(title="Learn SQLiter", done=False))
102
+ output.write(f"Inserted: {task.title} (pk={task.pk})\n")
103
+ output.write("Transaction auto-commits on exit\n")
104
+
105
+ output.write(f"\nAfter context: connected={db.is_connected}\n")
106
+ return output.getvalue()
107
+
108
+
109
+ def get_category() -> DemoCategory:
110
+ """Get the Connection & Setup demo category."""
111
+ return DemoCategory(
112
+ id="connection",
113
+ title="Connection & Setup",
114
+ icon="",
115
+ demos=[
116
+ Demo(
117
+ id="conn_memory",
118
+ title="In-memory Database",
119
+ description="Create a temporary in-memory database",
120
+ category="connection",
121
+ code=extract_demo_code(_run_memory_db),
122
+ execute=_run_memory_db,
123
+ ),
124
+ Demo(
125
+ id="conn_file",
126
+ title="File-based Database",
127
+ description="Create a persistent file database",
128
+ category="connection",
129
+ code=extract_demo_code(_run_file_db),
130
+ execute=_run_file_db,
131
+ ),
132
+ Demo(
133
+ id="conn_debug",
134
+ title="Debug Mode",
135
+ description="Enable SQL query logging",
136
+ category="connection",
137
+ code=extract_demo_code(_run_debug_mode),
138
+ execute=_run_debug_mode,
139
+ ),
140
+ Demo(
141
+ id="conn_context",
142
+ title="Context Manager",
143
+ description="Auto commit/rollback with 'with' statement",
144
+ category="connection",
145
+ code=extract_demo_code(_run_context_manager),
146
+ execute=_run_context_manager,
147
+ ),
148
+ ],
149
+ expanded=True, # First category starts expanded
150
+ )