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.
- sqliter/constants.py +4 -3
- sqliter/exceptions.py +29 -0
- sqliter/helpers.py +27 -0
- sqliter/model/model.py +21 -4
- sqliter/orm/__init__.py +17 -0
- sqliter/orm/fields.py +412 -0
- sqliter/orm/foreign_key.py +8 -0
- sqliter/orm/m2m.py +784 -0
- sqliter/orm/model.py +308 -0
- sqliter/orm/query.py +221 -0
- sqliter/orm/registry.py +440 -0
- sqliter/query/query.py +573 -51
- sqliter/sqliter.py +182 -47
- sqliter/tui/__init__.py +62 -0
- sqliter/tui/__main__.py +6 -0
- sqliter/tui/app.py +179 -0
- sqliter/tui/demos/__init__.py +96 -0
- sqliter/tui/demos/base.py +114 -0
- sqliter/tui/demos/caching.py +283 -0
- sqliter/tui/demos/connection.py +150 -0
- sqliter/tui/demos/constraints.py +211 -0
- sqliter/tui/demos/crud.py +154 -0
- sqliter/tui/demos/errors.py +231 -0
- sqliter/tui/demos/field_selection.py +150 -0
- sqliter/tui/demos/filters.py +389 -0
- sqliter/tui/demos/models.py +248 -0
- sqliter/tui/demos/ordering.py +156 -0
- sqliter/tui/demos/orm.py +537 -0
- sqliter/tui/demos/results.py +241 -0
- sqliter/tui/demos/string_filters.py +210 -0
- sqliter/tui/demos/timestamps.py +126 -0
- sqliter/tui/demos/transactions.py +177 -0
- sqliter/tui/runner.py +116 -0
- sqliter/tui/styles/app.tcss +130 -0
- sqliter/tui/widgets/__init__.py +7 -0
- sqliter/tui/widgets/code_display.py +81 -0
- sqliter/tui/widgets/demo_list.py +65 -0
- sqliter/tui/widgets/output_display.py +92 -0
- {sqliter_py-0.12.0.dist-info → sqliter_py-0.17.0.dist-info}/METADATA +28 -14
- sqliter_py-0.17.0.dist-info/RECORD +48 -0
- {sqliter_py-0.12.0.dist-info → sqliter_py-0.17.0.dist-info}/WHEEL +2 -2
- sqliter_py-0.17.0.dist-info/entry_points.txt +3 -0
- sqliter_py-0.12.0.dist-info/RECORD +0 -15
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Transaction 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_context_manager_transaction() -> str:
|
|
15
|
+
"""Use context manager for automatic transaction management.
|
|
16
|
+
|
|
17
|
+
The `with db:` block auto-commits on success and rolls back on error.
|
|
18
|
+
"""
|
|
19
|
+
output = io.StringIO()
|
|
20
|
+
|
|
21
|
+
class Account(BaseDBModel):
|
|
22
|
+
name: str
|
|
23
|
+
balance: float
|
|
24
|
+
|
|
25
|
+
db = SqliterDB(memory=True)
|
|
26
|
+
db.create_table(Account)
|
|
27
|
+
|
|
28
|
+
alice: Account = db.insert(Account(name="Alice", balance=100.0))
|
|
29
|
+
bob: Account = db.insert(Account(name="Bob", balance=50.0))
|
|
30
|
+
|
|
31
|
+
output.write(f"Before: Alice=${alice.balance}, Bob=${bob.balance}\n")
|
|
32
|
+
|
|
33
|
+
# Transfer money using context manager
|
|
34
|
+
with db:
|
|
35
|
+
alice.balance = alice.balance - 20.0
|
|
36
|
+
bob.balance = bob.balance + 20.0
|
|
37
|
+
db.update(alice)
|
|
38
|
+
db.update(bob)
|
|
39
|
+
alice_updated = alice
|
|
40
|
+
bob_updated = bob
|
|
41
|
+
|
|
42
|
+
output.write(
|
|
43
|
+
f"After: Alice=${alice_updated.balance}, Bob=${bob_updated.balance}\n"
|
|
44
|
+
)
|
|
45
|
+
output.write("Transaction auto-committed on success\n")
|
|
46
|
+
|
|
47
|
+
db.close()
|
|
48
|
+
return output.getvalue()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run_rollback() -> str:
|
|
52
|
+
"""Demonstrate transaction rollback behavior.
|
|
53
|
+
|
|
54
|
+
When an exception occurs inside a `with db:` block, all changes made
|
|
55
|
+
within that transaction are automatically rolled back.
|
|
56
|
+
"""
|
|
57
|
+
output = io.StringIO()
|
|
58
|
+
|
|
59
|
+
class Item(BaseDBModel):
|
|
60
|
+
name: str
|
|
61
|
+
quantity: int
|
|
62
|
+
|
|
63
|
+
# Use file database so we can reconnect after connection closes
|
|
64
|
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
65
|
+
db_path = f.name
|
|
66
|
+
|
|
67
|
+
db = None
|
|
68
|
+
try:
|
|
69
|
+
db = SqliterDB(db_filename=db_path)
|
|
70
|
+
db.create_table(Item)
|
|
71
|
+
|
|
72
|
+
item: Item = db.insert(Item(name="Widget", quantity=10))
|
|
73
|
+
output.write(f"Initial quantity: {item.quantity}\n")
|
|
74
|
+
|
|
75
|
+
# Use context manager for automatic rollback on error
|
|
76
|
+
try:
|
|
77
|
+
with db:
|
|
78
|
+
item.quantity = 5
|
|
79
|
+
db.update(item)
|
|
80
|
+
output.write("Inside transaction: updated to 5\n")
|
|
81
|
+
# If error occurs, changes are rolled back
|
|
82
|
+
error_msg = "Intentional error for rollback"
|
|
83
|
+
raise RuntimeError(error_msg) # noqa: TRY301
|
|
84
|
+
except RuntimeError:
|
|
85
|
+
output.write("Error occurred - transaction rolled back\n")
|
|
86
|
+
# Verify rollback with NEW connection
|
|
87
|
+
db2 = SqliterDB(db_filename=db_path)
|
|
88
|
+
try:
|
|
89
|
+
restored = db2.get(Item, item.pk)
|
|
90
|
+
if restored is not None:
|
|
91
|
+
qty_attr = "quantity" # db.get returns BaseDBModel
|
|
92
|
+
restored_quantity = getattr(restored, qty_attr)
|
|
93
|
+
msg = f"Database value: {restored_quantity}\n"
|
|
94
|
+
output.write(msg)
|
|
95
|
+
expected_quantity = 10
|
|
96
|
+
if restored_quantity == expected_quantity:
|
|
97
|
+
output.write("✓ Rollback worked correctly\n")
|
|
98
|
+
else:
|
|
99
|
+
msg = (
|
|
100
|
+
f"✗ Rollback failed: expected {expected_quantity}, "
|
|
101
|
+
f"got {restored_quantity}\n"
|
|
102
|
+
)
|
|
103
|
+
output.write(msg)
|
|
104
|
+
finally:
|
|
105
|
+
db2.close()
|
|
106
|
+
finally:
|
|
107
|
+
if db is not None:
|
|
108
|
+
db.close()
|
|
109
|
+
Path(db_path).unlink(missing_ok=True)
|
|
110
|
+
|
|
111
|
+
return output.getvalue()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _run_manual_commit() -> str:
|
|
115
|
+
"""Manually control transactions with explicit commit.
|
|
116
|
+
|
|
117
|
+
Call db.commit() to persist changes when not using context manager.
|
|
118
|
+
"""
|
|
119
|
+
output = io.StringIO()
|
|
120
|
+
|
|
121
|
+
class Log(BaseDBModel):
|
|
122
|
+
message: str
|
|
123
|
+
|
|
124
|
+
db = SqliterDB(memory=True)
|
|
125
|
+
db.create_table(Log)
|
|
126
|
+
|
|
127
|
+
# Manual transaction control
|
|
128
|
+
db.connect()
|
|
129
|
+
log1 = db.insert(Log(message="First entry"))
|
|
130
|
+
output.write(f"Inserted: {log1.message}\n")
|
|
131
|
+
output.write("Not committed yet\n")
|
|
132
|
+
db.commit()
|
|
133
|
+
output.write("Committed\n")
|
|
134
|
+
|
|
135
|
+
db.insert(Log(message="Second entry"))
|
|
136
|
+
db.commit()
|
|
137
|
+
|
|
138
|
+
all_logs = db.select(Log).fetch_all()
|
|
139
|
+
output.write(f"Total logs: {len(all_logs)}\n")
|
|
140
|
+
|
|
141
|
+
db.close()
|
|
142
|
+
return output.getvalue()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_category() -> DemoCategory:
|
|
146
|
+
"""Get the Transactions demo category."""
|
|
147
|
+
return DemoCategory(
|
|
148
|
+
id="transactions",
|
|
149
|
+
title="Transactions",
|
|
150
|
+
icon="",
|
|
151
|
+
demos=[
|
|
152
|
+
Demo(
|
|
153
|
+
id="txn_context",
|
|
154
|
+
title="Context Manager",
|
|
155
|
+
description="Auto commit/rollback with 'with' statement",
|
|
156
|
+
category="transactions",
|
|
157
|
+
code=extract_demo_code(_run_context_manager_transaction),
|
|
158
|
+
execute=_run_context_manager_transaction,
|
|
159
|
+
),
|
|
160
|
+
Demo(
|
|
161
|
+
id="txn_rollback",
|
|
162
|
+
title="Rollback",
|
|
163
|
+
description="Automatic rollback on errors",
|
|
164
|
+
category="transactions",
|
|
165
|
+
code=extract_demo_code(_run_rollback),
|
|
166
|
+
execute=_run_rollback,
|
|
167
|
+
),
|
|
168
|
+
Demo(
|
|
169
|
+
id="txn_manual",
|
|
170
|
+
title="Manual Commit",
|
|
171
|
+
description="Manually control transactions",
|
|
172
|
+
category="transactions",
|
|
173
|
+
code=extract_demo_code(_run_manual_commit),
|
|
174
|
+
execute=_run_manual_commit,
|
|
175
|
+
),
|
|
176
|
+
],
|
|
177
|
+
)
|
sqliter/tui/runner.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Demo execution engine with output capture."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import io
|
|
7
|
+
import traceback
|
|
8
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
13
|
+
from sqliter.tui.demos.base import Demo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ExecutionResult:
|
|
18
|
+
"""Result of executing a demo.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
success: Whether the demo ran without exceptions
|
|
22
|
+
output: Combined stdout/stderr and returned output
|
|
23
|
+
error: Exception message if failed
|
|
24
|
+
traceback: Full traceback string if failed
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
success: bool
|
|
28
|
+
output: str
|
|
29
|
+
error: str | None = None
|
|
30
|
+
traceback: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DemoRunner:
|
|
34
|
+
"""Executes demos and captures all output."""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
"""Initialize the demo runner."""
|
|
38
|
+
self._last_result: ExecutionResult | None = None
|
|
39
|
+
|
|
40
|
+
def run(self, demo: Demo) -> ExecutionResult:
|
|
41
|
+
"""Execute a demo and capture all output.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
demo: The demo to execute.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ExecutionResult with output and status.
|
|
48
|
+
"""
|
|
49
|
+
stdout_capture = io.StringIO()
|
|
50
|
+
stderr_capture = io.StringIO()
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
with (
|
|
54
|
+
redirect_stdout(stdout_capture),
|
|
55
|
+
redirect_stderr(stderr_capture),
|
|
56
|
+
):
|
|
57
|
+
output = demo.execute()
|
|
58
|
+
|
|
59
|
+
# Combine all output sources
|
|
60
|
+
combined = ""
|
|
61
|
+
stdout_val = stdout_capture.getvalue()
|
|
62
|
+
stderr_val = stderr_capture.getvalue()
|
|
63
|
+
|
|
64
|
+
if stdout_val:
|
|
65
|
+
combined += stdout_val
|
|
66
|
+
if output:
|
|
67
|
+
if combined and not combined.endswith("\n"):
|
|
68
|
+
combined += "\n"
|
|
69
|
+
combined += output
|
|
70
|
+
if stderr_val:
|
|
71
|
+
combined += f"\n[stderr]\n{stderr_val}"
|
|
72
|
+
|
|
73
|
+
self._last_result = ExecutionResult(
|
|
74
|
+
success=True,
|
|
75
|
+
output=combined or "(No output)",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Catching Exception is necessary here since demo code may raise any
|
|
79
|
+
# type
|
|
80
|
+
except Exception: # noqa: BLE001
|
|
81
|
+
tb = traceback.format_exc()
|
|
82
|
+
|
|
83
|
+
# Combine stdout and stderr for error output
|
|
84
|
+
combined_output = stdout_capture.getvalue()
|
|
85
|
+
stderr_val = stderr_capture.getvalue()
|
|
86
|
+
if stderr_val:
|
|
87
|
+
combined_output += f"\n[stderr]\n{stderr_val}"
|
|
88
|
+
|
|
89
|
+
self._last_result = ExecutionResult(
|
|
90
|
+
success=False,
|
|
91
|
+
output=combined_output,
|
|
92
|
+
error="Exception in demo code",
|
|
93
|
+
traceback=tb,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
finally:
|
|
97
|
+
# Run teardown if defined, ignoring any errors
|
|
98
|
+
if demo.teardown:
|
|
99
|
+
with contextlib.suppress(Exception):
|
|
100
|
+
demo.teardown()
|
|
101
|
+
|
|
102
|
+
return self._last_result
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def last_result(self) -> ExecutionResult | None:
|
|
106
|
+
"""Get the last execution result."""
|
|
107
|
+
return self._last_result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Global runner instance
|
|
111
|
+
_runner = DemoRunner()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_demo(demo: Demo) -> ExecutionResult:
|
|
115
|
+
"""Run a demo using the global runner."""
|
|
116
|
+
return _runner.run(demo)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* Main application styles for SQLiter TUI Demo */
|
|
2
|
+
|
|
3
|
+
Screen {
|
|
4
|
+
background: $surface;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
#main-container {
|
|
8
|
+
layout: grid;
|
|
9
|
+
grid-size: 2;
|
|
10
|
+
grid-columns: auto 1fr;
|
|
11
|
+
height: 100%;
|
|
12
|
+
padding: 0 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Left panel - Demo list */
|
|
16
|
+
#demo-list {
|
|
17
|
+
border: solid $primary;
|
|
18
|
+
border-title-color: $text;
|
|
19
|
+
padding: 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#demo-list Tree {
|
|
23
|
+
padding: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#demo-list Tree > .tree--cursor {
|
|
27
|
+
background: $primary;
|
|
28
|
+
color: $text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#demo-list Tree > .tree--highlight {
|
|
32
|
+
background: $primary 30%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Right panel container */
|
|
36
|
+
#right-panel {
|
|
37
|
+
padding-left: 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Code display */
|
|
41
|
+
#code-display {
|
|
42
|
+
height: 55%;
|
|
43
|
+
border: solid $primary;
|
|
44
|
+
border-title-color: $text;
|
|
45
|
+
padding: 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#code-display Static {
|
|
49
|
+
padding: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Output display */
|
|
53
|
+
#output-display {
|
|
54
|
+
height: 35%;
|
|
55
|
+
border: solid $secondary;
|
|
56
|
+
border-title-color: $text;
|
|
57
|
+
padding: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#output-display.success {
|
|
61
|
+
border: solid $success;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#output-display.error {
|
|
65
|
+
border: solid $error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#output-display Static {
|
|
69
|
+
padding: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Button bar */
|
|
73
|
+
#button-bar {
|
|
74
|
+
height: auto;
|
|
75
|
+
align: center middle;
|
|
76
|
+
padding: 1 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#button-bar Button {
|
|
80
|
+
margin: 0 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#run-btn {
|
|
84
|
+
background: $success;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#clear-btn {
|
|
88
|
+
background: $primary-darken-2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Help screen */
|
|
92
|
+
HelpScreen {
|
|
93
|
+
align: center middle;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#help-scroll {
|
|
97
|
+
width: 70;
|
|
98
|
+
height: auto;
|
|
99
|
+
max-height: 80%;
|
|
100
|
+
padding: 2;
|
|
101
|
+
border: solid $primary;
|
|
102
|
+
background: $surface;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#help-content {
|
|
106
|
+
padding: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Footer styling */
|
|
110
|
+
Footer {
|
|
111
|
+
background: $primary-darken-3;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Header styling */
|
|
115
|
+
Header {
|
|
116
|
+
background: $primary;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Scrollbars */
|
|
120
|
+
ScrollableContainer > .scrollbar {
|
|
121
|
+
background: $surface;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ScrollableContainer > .scrollbar--bar {
|
|
125
|
+
background: $primary 50%;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ScrollableContainer > .scrollbar--bar:hover {
|
|
129
|
+
background: $primary;
|
|
130
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""TUI widgets for SQLiter demo app."""
|
|
2
|
+
|
|
3
|
+
from sqliter.tui.widgets.code_display import CodeDisplay
|
|
4
|
+
from sqliter.tui.widgets.demo_list import DemoList, DemoSelected
|
|
5
|
+
from sqliter.tui.widgets.output_display import OutputDisplay
|
|
6
|
+
|
|
7
|
+
__all__ = ["CodeDisplay", "DemoList", "DemoSelected", "OutputDisplay"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Syntax-highlighted code display widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
from textual.containers import ScrollableContainer
|
|
9
|
+
from textual.css.query import NoMatches
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
13
|
+
from textual.app import ComposeResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CodeDisplay(ScrollableContainer):
|
|
17
|
+
"""Display syntax-highlighted Python code."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
code: str = "",
|
|
22
|
+
*,
|
|
23
|
+
widget_id: str | None = None,
|
|
24
|
+
classes: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Initialize the code display.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
code: Initial code to display.
|
|
30
|
+
widget_id: Widget ID.
|
|
31
|
+
classes: CSS classes for the widget.
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(id=widget_id, classes=classes)
|
|
34
|
+
self._code = code
|
|
35
|
+
|
|
36
|
+
def compose(self) -> ComposeResult:
|
|
37
|
+
"""Compose the code display."""
|
|
38
|
+
yield Static(id="code-content")
|
|
39
|
+
|
|
40
|
+
def on_mount(self) -> None:
|
|
41
|
+
"""Initialize the display on mount."""
|
|
42
|
+
self._update_display()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def code(self) -> str:
|
|
46
|
+
"""Get the current code."""
|
|
47
|
+
return self._code
|
|
48
|
+
|
|
49
|
+
@code.setter
|
|
50
|
+
def code(self, value: str) -> None:
|
|
51
|
+
"""Set new code and update display."""
|
|
52
|
+
self._code = value
|
|
53
|
+
self._update_display()
|
|
54
|
+
|
|
55
|
+
def _update_display(self) -> None:
|
|
56
|
+
"""Update the code display with syntax highlighting."""
|
|
57
|
+
try:
|
|
58
|
+
content = self.query_one("#code-content", Static)
|
|
59
|
+
except NoMatches:
|
|
60
|
+
return # Not mounted yet
|
|
61
|
+
|
|
62
|
+
if not self._code:
|
|
63
|
+
content.update("Select a demo to view code")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Use Rich's Syntax for highlighting
|
|
67
|
+
syntax = Syntax(
|
|
68
|
+
self._code.strip(),
|
|
69
|
+
"python",
|
|
70
|
+
theme="monokai",
|
|
71
|
+
line_numbers=True,
|
|
72
|
+
word_wrap=True,
|
|
73
|
+
)
|
|
74
|
+
content.update(syntax)
|
|
75
|
+
|
|
76
|
+
# Scroll to top when content changes
|
|
77
|
+
self.scroll_home()
|
|
78
|
+
|
|
79
|
+
def set_code(self, code: str) -> None:
|
|
80
|
+
"""Set the code to display (public API)."""
|
|
81
|
+
self.code = code
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Demo list widget with expandable categories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from textual.containers import ScrollableContainer
|
|
8
|
+
from textual.message import Message
|
|
9
|
+
from textual.widgets import Tree
|
|
10
|
+
|
|
11
|
+
from sqliter.tui.demos import DemoRegistry
|
|
12
|
+
from sqliter.tui.demos.base import Demo, DemoCategory
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
15
|
+
from textual.app import ComposeResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DemoSelected(Message):
|
|
19
|
+
"""Message sent when a demo is selected."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, demo: Demo) -> None:
|
|
22
|
+
"""Initialize the message.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
demo: The demo that was selected.
|
|
26
|
+
"""
|
|
27
|
+
self.demo = demo
|
|
28
|
+
super().__init__()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DemoList(ScrollableContainer):
|
|
32
|
+
"""Scrollable tree of demo categories and items."""
|
|
33
|
+
|
|
34
|
+
def compose(self) -> ComposeResult:
|
|
35
|
+
"""Compose the demo tree."""
|
|
36
|
+
tree: Tree[Demo | DemoCategory] = Tree("SQLiter Demos", id="demo-tree")
|
|
37
|
+
tree.show_root = False
|
|
38
|
+
|
|
39
|
+
# Populate tree with categories and demos
|
|
40
|
+
for category in DemoRegistry.get_categories():
|
|
41
|
+
label = (
|
|
42
|
+
f"{category.icon} {category.title}"
|
|
43
|
+
if category.icon
|
|
44
|
+
else category.title
|
|
45
|
+
)
|
|
46
|
+
cat_node = tree.root.add(
|
|
47
|
+
label,
|
|
48
|
+
data=category,
|
|
49
|
+
expand=category.expanded,
|
|
50
|
+
)
|
|
51
|
+
for demo in category.demos:
|
|
52
|
+
cat_node.add_leaf(
|
|
53
|
+
demo.title,
|
|
54
|
+
data=demo,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
yield tree
|
|
58
|
+
|
|
59
|
+
def on_tree_node_selected(
|
|
60
|
+
self,
|
|
61
|
+
event: Tree.NodeSelected[Demo | DemoCategory],
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Handle tree node selection."""
|
|
64
|
+
if isinstance(event.node.data, Demo):
|
|
65
|
+
self.post_message(DemoSelected(event.node.data))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Output display widget with status indication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from textual.containers import ScrollableContainer
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
12
|
+
from textual.app import ComposeResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OutputDisplay(ScrollableContainer):
|
|
16
|
+
"""Display demo execution output with status styling."""
|
|
17
|
+
|
|
18
|
+
_PLACEHOLDER = "Run a demo to see output here"
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
widget_id: str | None = None,
|
|
24
|
+
classes: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Initialize the output display.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
widget_id: Widget ID.
|
|
30
|
+
classes: CSS classes for the widget.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(id=widget_id, classes=classes)
|
|
33
|
+
|
|
34
|
+
def compose(self) -> ComposeResult:
|
|
35
|
+
"""Compose the output display."""
|
|
36
|
+
yield Static(self._PLACEHOLDER, id="output-content")
|
|
37
|
+
|
|
38
|
+
def show_output(self, output: str, *, success: bool = True) -> None:
|
|
39
|
+
"""Display output from demo execution.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
output: The output text to display
|
|
43
|
+
success: Whether the demo ran successfully
|
|
44
|
+
"""
|
|
45
|
+
content = self.query_one("#output-content", Static)
|
|
46
|
+
|
|
47
|
+
self.remove_class("error", "success")
|
|
48
|
+
self.add_class("success" if success else "error")
|
|
49
|
+
|
|
50
|
+
if success:
|
|
51
|
+
text = Text()
|
|
52
|
+
text.append("Success\n\n", style="bold green")
|
|
53
|
+
text.append(output)
|
|
54
|
+
else:
|
|
55
|
+
text = Text()
|
|
56
|
+
text.append("Error\n\n", style="bold red")
|
|
57
|
+
text.append(output, style="red")
|
|
58
|
+
|
|
59
|
+
content.update(text)
|
|
60
|
+
self.scroll_home()
|
|
61
|
+
|
|
62
|
+
def show_error(
|
|
63
|
+
self,
|
|
64
|
+
error: str,
|
|
65
|
+
traceback_str: str | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Display an error message.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
error: The error message
|
|
71
|
+
traceback_str: Optional full traceback
|
|
72
|
+
"""
|
|
73
|
+
content = self.query_one("#output-content", Static)
|
|
74
|
+
|
|
75
|
+
self.remove_class("error", "success")
|
|
76
|
+
self.add_class("error")
|
|
77
|
+
|
|
78
|
+
text = Text()
|
|
79
|
+
text.append("Error\n\n", style="bold red")
|
|
80
|
+
text.append(error, style="red")
|
|
81
|
+
if traceback_str:
|
|
82
|
+
text.append("\n\nTraceback:\n", style="bold")
|
|
83
|
+
text.append(traceback_str, style="dim red")
|
|
84
|
+
|
|
85
|
+
content.update(text)
|
|
86
|
+
self.scroll_home()
|
|
87
|
+
|
|
88
|
+
def clear(self) -> None:
|
|
89
|
+
"""Clear the output display."""
|
|
90
|
+
content = self.query_one("#output-content", Static)
|
|
91
|
+
content.update(self._PLACEHOLDER)
|
|
92
|
+
self.remove_class("error", "success")
|