sqliter-py 0.9.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 (44) hide show
  1. sqliter/constants.py +4 -3
  2. sqliter/exceptions.py +43 -0
  3. sqliter/model/__init__.py +38 -3
  4. sqliter/model/foreign_key.py +153 -0
  5. sqliter/model/model.py +42 -3
  6. sqliter/model/unique.py +20 -11
  7. sqliter/orm/__init__.py +16 -0
  8. sqliter/orm/fields.py +412 -0
  9. sqliter/orm/foreign_key.py +8 -0
  10. sqliter/orm/model.py +243 -0
  11. sqliter/orm/query.py +221 -0
  12. sqliter/orm/registry.py +169 -0
  13. sqliter/query/query.py +720 -69
  14. sqliter/sqliter.py +533 -76
  15. sqliter/tui/__init__.py +62 -0
  16. sqliter/tui/__main__.py +6 -0
  17. sqliter/tui/app.py +179 -0
  18. sqliter/tui/demos/__init__.py +96 -0
  19. sqliter/tui/demos/base.py +114 -0
  20. sqliter/tui/demos/caching.py +283 -0
  21. sqliter/tui/demos/connection.py +150 -0
  22. sqliter/tui/demos/constraints.py +211 -0
  23. sqliter/tui/demos/crud.py +154 -0
  24. sqliter/tui/demos/errors.py +231 -0
  25. sqliter/tui/demos/field_selection.py +150 -0
  26. sqliter/tui/demos/filters.py +389 -0
  27. sqliter/tui/demos/models.py +248 -0
  28. sqliter/tui/demos/ordering.py +156 -0
  29. sqliter/tui/demos/orm.py +460 -0
  30. sqliter/tui/demos/results.py +241 -0
  31. sqliter/tui/demos/string_filters.py +210 -0
  32. sqliter/tui/demos/timestamps.py +126 -0
  33. sqliter/tui/demos/transactions.py +177 -0
  34. sqliter/tui/runner.py +116 -0
  35. sqliter/tui/styles/app.tcss +130 -0
  36. sqliter/tui/widgets/__init__.py +7 -0
  37. sqliter/tui/widgets/code_display.py +81 -0
  38. sqliter/tui/widgets/demo_list.py +65 -0
  39. sqliter/tui/widgets/output_display.py +92 -0
  40. {sqliter_py-0.9.0.dist-info → sqliter_py-0.16.0.dist-info}/METADATA +27 -11
  41. sqliter_py-0.16.0.dist-info/RECORD +47 -0
  42. {sqliter_py-0.9.0.dist-info → sqliter_py-0.16.0.dist-info}/WHEEL +2 -2
  43. sqliter_py-0.16.0.dist-info/entry_points.txt +3 -0
  44. sqliter_py-0.9.0.dist-info/RECORD +0 -14
@@ -0,0 +1,62 @@
1
+ """TUI demo application for SQLiter.
2
+
3
+ This module provides an interactive terminal-based demonstration of SQLiter
4
+ features using the Textual library.
5
+
6
+ Usage:
7
+ python -m sqliter.tui
8
+ # or
9
+ sqliter-demo
10
+
11
+ Requires:
12
+ uv add sqliter-py[demo]
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from importlib.util import find_spec
18
+ from typing import TYPE_CHECKING
19
+
20
+ _TEXTUAL_AVAILABLE = find_spec("textual") is not None
21
+
22
+ if TYPE_CHECKING: # pragma: no cover
23
+ from sqliter.tui.app import SQLiterDemoApp
24
+
25
+
26
+ def _missing_dependency_error() -> None:
27
+ """Raise informative error when textual is not installed."""
28
+ msg = (
29
+ "The SQLiter TUI demo requires the 'textual' library.\n"
30
+ "Install it with: uv add sqliter-py[demo]\n"
31
+ )
32
+ raise ImportError(msg)
33
+
34
+
35
+ def get_app() -> SQLiterDemoApp:
36
+ """Get the TUI application instance.
37
+
38
+ Returns:
39
+ The SQLiterDemoApp instance.
40
+
41
+ Raises:
42
+ ImportError: If textual is not installed.
43
+ """
44
+ if not _TEXTUAL_AVAILABLE:
45
+ _missing_dependency_error()
46
+
47
+ from sqliter.tui.app import SQLiterDemoApp # noqa: PLC0415
48
+
49
+ return SQLiterDemoApp()
50
+
51
+
52
+ def run() -> None:
53
+ """Run the TUI demo application.
54
+
55
+ Raises:
56
+ ImportError: If textual is not installed.
57
+ """
58
+ app = get_app()
59
+ app.run()
60
+
61
+
62
+ __all__ = ["get_app", "run"]
@@ -0,0 +1,6 @@
1
+ """Entry point for running SQLiter TUI as a module."""
2
+
3
+ from sqliter.tui import run
4
+
5
+ if __name__ == "__main__": # pragma: no cover
6
+ run()
sqliter/tui/app.py ADDED
@@ -0,0 +1,179 @@
1
+ """Main SQLiter TUI demo application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import suppress
6
+ from typing import TYPE_CHECKING, ClassVar, cast
7
+
8
+ from textual.app import App, ComposeResult
9
+ from textual.binding import Binding, BindingType
10
+ from textual.containers import (
11
+ Container,
12
+ Horizontal,
13
+ Vertical,
14
+ VerticalScroll,
15
+ )
16
+ from textual.css.query import NoMatches
17
+ from textual.screen import ModalScreen
18
+ from textual.widgets import Button, Footer, Header, Markdown, Tree
19
+
20
+ if TYPE_CHECKING: # pragma: no cover
21
+ from sqliter.tui.demos.base import Demo
22
+
23
+ from sqliter.tui.demos import DemoRegistry
24
+ from sqliter.tui.runner import run_demo
25
+ from sqliter.tui.widgets import (
26
+ CodeDisplay,
27
+ DemoList,
28
+ DemoSelected,
29
+ OutputDisplay,
30
+ )
31
+
32
+
33
+ class HelpScreen(ModalScreen[None]):
34
+ """Modal help screen."""
35
+
36
+ BINDINGS: ClassVar[list[BindingType]] = [
37
+ Binding("escape", "dismiss", "Close"),
38
+ Binding("q", "dismiss", "Close"),
39
+ ]
40
+
41
+ def compose(self) -> ComposeResult:
42
+ """Compose the help screen."""
43
+ with VerticalScroll(id="help-scroll"):
44
+ yield Markdown(
45
+ """
46
+ # SQLiter Demo - Help
47
+
48
+ ## Navigation
49
+
50
+ | Key | Action |
51
+ |-----|--------|
52
+ | Up/Down or j/k | Navigate demo list |
53
+ | Left/Right or h/l | Collapse/expand category |
54
+ | Enter | Select demo / Run demo |
55
+ | Tab | Move focus between panels |
56
+
57
+ ## Actions
58
+
59
+ | Key | Action |
60
+ |-----|--------|
61
+ | F5 | Run selected demo |
62
+ | F8 | Clear output |
63
+ | ? or F1 | Show this help |
64
+ | q | Quit application |
65
+
66
+ ## Mouse
67
+
68
+ - Click categories to expand/collapse
69
+ - Click demos to select and view code
70
+ - Click buttons to run/clear
71
+
72
+ Press Escape or q to close this help.
73
+ """,
74
+ id="help-content",
75
+ )
76
+
77
+
78
+ class SQLiterDemoApp(App[None]):
79
+ """Main SQLiter TUI demo application."""
80
+
81
+ CSS_PATH = "styles/app.tcss"
82
+ TITLE = "SQLiter Interactive Demo"
83
+
84
+ BINDINGS: ClassVar[list[BindingType]] = [
85
+ Binding("q", "quit", "Quit", show=True, priority=True),
86
+ Binding("f5", "run_demo", "Run", show=True),
87
+ Binding("f8", "clear_output", "Clear", show=True),
88
+ Binding("question_mark", "show_help", "Help", show=True),
89
+ Binding("f1", "show_help", show=False),
90
+ Binding("j", "tree_cursor_down", show=False),
91
+ Binding("k", "tree_cursor_up", show=False),
92
+ ]
93
+
94
+ def __init__(self) -> None:
95
+ """Initialize the application."""
96
+ super().__init__()
97
+ self._current_demo: Demo | None = None
98
+
99
+ def compose(self) -> ComposeResult:
100
+ """Compose the application layout."""
101
+ yield Header()
102
+ with Container(id="main-container"):
103
+ yield DemoList(id="demo-list")
104
+ with Vertical(id="right-panel"):
105
+ yield CodeDisplay(widget_id="code-display")
106
+ yield OutputDisplay(widget_id="output-display")
107
+ with Horizontal(id="button-bar"):
108
+ yield Button(
109
+ "Run Demo (F5)", id="run-btn", variant="primary"
110
+ )
111
+ yield Button("Clear Output (F8)", id="clear-btn")
112
+ yield Footer()
113
+
114
+ def on_mount(self) -> None:
115
+ """Set initial focus on the demo list."""
116
+ with suppress(NoMatches):
117
+ demo_list = self.query_one("#demo-list", DemoList)
118
+ tree = demo_list.query_one("#demo-tree", Tree)
119
+ tree.focus()
120
+
121
+ def on_demo_selected(self, event: DemoSelected) -> None:
122
+ """Handle demo selection from the list."""
123
+ self._current_demo = event.demo
124
+ code_display = self.query_one("#code-display", CodeDisplay)
125
+ code_display.set_code(DemoRegistry.get_demo_code(event.demo.id))
126
+
127
+ def on_button_pressed(self, event: Button.Pressed) -> None:
128
+ """Handle button presses."""
129
+ if event.button.id == "run-btn":
130
+ self.action_run_demo()
131
+ elif event.button.id == "clear-btn":
132
+ self.action_clear_output()
133
+
134
+ def action_run_demo(self) -> None:
135
+ """Run the currently selected demo."""
136
+ if self._current_demo is None:
137
+ output_display = self.query_one("#output-display", OutputDisplay)
138
+ output_display.show_output(
139
+ "Please select a demo first.", success=False
140
+ )
141
+ return
142
+
143
+ result = run_demo(self._current_demo)
144
+ output_display = self.query_one("#output-display", OutputDisplay)
145
+
146
+ if result.success:
147
+ output_display.show_output(result.output, success=True)
148
+ else:
149
+ output_display.show_error(
150
+ result.error or "Unknown error",
151
+ result.traceback,
152
+ )
153
+
154
+ def action_clear_output(self) -> None:
155
+ """Clear the output display."""
156
+ output_display = self.query_one("#output-display", OutputDisplay)
157
+ output_display.clear()
158
+
159
+ def action_show_help(self) -> None:
160
+ """Show the help screen."""
161
+ self.push_screen(HelpScreen())
162
+
163
+ def action_tree_cursor_down(self) -> None:
164
+ """Move cursor down in the tree (vim-style j key)."""
165
+ try:
166
+ demo_list = self.query_one(DemoList)
167
+ tree = cast("Tree[object]", demo_list.query_one("#demo-tree"))
168
+ tree.action_cursor_down()
169
+ except NoMatches:
170
+ pass # Tree might not be focused
171
+
172
+ def action_tree_cursor_up(self) -> None:
173
+ """Move cursor up in the tree (vim-style k key)."""
174
+ try:
175
+ demo_list = self.query_one(DemoList)
176
+ tree = cast("Tree[object]", demo_list.query_one("#demo-tree"))
177
+ tree.action_cursor_up()
178
+ except NoMatches:
179
+ pass # Tree might not be focused
@@ -0,0 +1,96 @@
1
+ """Demo registry for managing all available demos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, ClassVar
6
+
7
+ from sqliter.tui.demos import (
8
+ caching,
9
+ connection,
10
+ constraints,
11
+ crud,
12
+ errors,
13
+ field_selection,
14
+ filters,
15
+ models,
16
+ ordering,
17
+ orm,
18
+ results,
19
+ string_filters,
20
+ timestamps,
21
+ transactions,
22
+ )
23
+
24
+ if TYPE_CHECKING: # pragma: no cover
25
+ from collections.abc import Sequence
26
+
27
+ from sqliter.tui.demos.base import Demo, DemoCategory
28
+
29
+
30
+ class DemoRegistry:
31
+ """Registry for all available demos."""
32
+
33
+ _categories: ClassVar[list[DemoCategory]] = []
34
+ _demos_by_id: ClassVar[dict[str, Demo]] = {}
35
+
36
+ @classmethod
37
+ def register_category(cls, category: DemoCategory) -> None:
38
+ """Register a demo category with all its demos."""
39
+ cls._categories.append(category)
40
+ for demo in category.demos:
41
+ if demo.id in cls._demos_by_id:
42
+ msg = f"Duplicate demo id: {demo.id}"
43
+ raise ValueError(msg)
44
+ cls._demos_by_id[demo.id] = demo
45
+
46
+ @classmethod
47
+ def get_categories(cls) -> Sequence[DemoCategory]:
48
+ """Get all registered categories in order."""
49
+ return tuple(cls._categories)
50
+
51
+ @classmethod
52
+ def get_demo(cls, demo_id: str) -> Demo | None:
53
+ """Get a demo by its unique ID."""
54
+ return cls._demos_by_id.get(demo_id)
55
+
56
+ @classmethod
57
+ def get_demo_code(cls, demo_id: str) -> str:
58
+ """Get the display code for a demo (including setup if any)."""
59
+ demo = cls.get_demo(demo_id)
60
+ if demo is None:
61
+ return ""
62
+ code_parts: list[str] = []
63
+ if demo.setup_code:
64
+ code_parts.append(f"# Setup\n{demo.setup_code}\n")
65
+ code_parts.append(demo.code)
66
+ return "\n".join(code_parts)
67
+
68
+ @classmethod
69
+ def reset(cls) -> None:
70
+ """Reset the registry (for testing).
71
+
72
+ After calling reset(), call _init_registry() to repopulate.
73
+ """
74
+ cls._categories = []
75
+ cls._demos_by_id = {}
76
+
77
+
78
+ def _init_registry() -> None:
79
+ """Initialize the demo registry with all categories."""
80
+ DemoRegistry.register_category(connection.get_category())
81
+ DemoRegistry.register_category(models.get_category())
82
+ DemoRegistry.register_category(crud.get_category())
83
+ DemoRegistry.register_category(filters.get_category())
84
+ DemoRegistry.register_category(results.get_category())
85
+ DemoRegistry.register_category(ordering.get_category())
86
+ DemoRegistry.register_category(field_selection.get_category())
87
+ DemoRegistry.register_category(string_filters.get_category())
88
+ DemoRegistry.register_category(constraints.get_category())
89
+ DemoRegistry.register_category(orm.get_category())
90
+ DemoRegistry.register_category(caching.get_category())
91
+ DemoRegistry.register_category(timestamps.get_category())
92
+ DemoRegistry.register_category(transactions.get_category())
93
+ DemoRegistry.register_category(errors.get_category())
94
+
95
+
96
+ _init_registry()
@@ -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