sqlsaber 0.28.0__py3-none-any.whl → 0.29.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.

Potentially problematic release.


This version of sqlsaber might be problematic. Click here for more details.

sqlsaber/cli/commands.py CHANGED
@@ -11,6 +11,7 @@ from sqlsaber.cli.database import create_db_app
11
11
  from sqlsaber.cli.memory import create_memory_app
12
12
  from sqlsaber.cli.models import create_models_app
13
13
  from sqlsaber.cli.onboarding import needs_onboarding, run_onboarding
14
+ from sqlsaber.cli.theme import create_theme_app
14
15
  from sqlsaber.cli.threads import create_threads_app
15
16
 
16
17
  # Lazy imports - only import what's needed for CLI parsing
@@ -35,6 +36,7 @@ app.command(create_auth_app(), name="auth")
35
36
  app.command(create_db_app(), name="db")
36
37
  app.command(create_memory_app(), name="memory")
37
38
  app.command(create_models_app(), name="models")
39
+ app.command(create_theme_app(), name="theme")
38
40
  app.command(create_threads_app(), name="threads")
39
41
 
40
42
  console = create_console()
sqlsaber/cli/theme.py ADDED
@@ -0,0 +1,146 @@
1
+ """Theme management CLI commands."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import cyclopts
10
+ import questionary
11
+ from platformdirs import user_config_dir
12
+ from pygments.styles import get_all_styles
13
+
14
+ from sqlsaber.theme.manager import DEFAULT_THEME_NAME, create_console
15
+
16
+ console = create_console()
17
+
18
+ # Create the theme management CLI app
19
+ theme_app = cyclopts.App(
20
+ name="theme",
21
+ help="Manage theme settings",
22
+ )
23
+
24
+
25
+ class ThemeManager:
26
+ """Manages theme configuration persistence."""
27
+
28
+ def __init__(self):
29
+ self.config_dir = Path(user_config_dir("sqlsaber"))
30
+ self.config_file = self.config_dir / "theme.json"
31
+
32
+ def _ensure_config_dir(self) -> None:
33
+ """Ensure config directory exists."""
34
+ self.config_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ def _load_config(self) -> dict:
37
+ """Load theme configuration from file."""
38
+ if not self.config_file.exists():
39
+ return {}
40
+
41
+ try:
42
+ with open(self.config_file, "r") as f:
43
+ return json.load(f)
44
+ except Exception:
45
+ return {}
46
+
47
+ def _save_config(self, config: dict) -> None:
48
+ """Save theme configuration to file."""
49
+ self._ensure_config_dir()
50
+
51
+ with open(self.config_file, "w") as f:
52
+ json.dump(config, f, indent=2)
53
+
54
+ def get_current_theme(self) -> str:
55
+ """Get the currently configured theme."""
56
+ config = self._load_config()
57
+ env_theme = os.getenv("SQLSABER_THEME")
58
+ if env_theme:
59
+ return env_theme
60
+ return config.get("theme", {}).get("pygments_style") or DEFAULT_THEME_NAME
61
+
62
+ def set_theme(self, theme_name: str) -> bool:
63
+ """Set the current theme."""
64
+ try:
65
+ config = self._load_config()
66
+ if "theme" not in config:
67
+ config["theme"] = {}
68
+ config["theme"]["name"] = theme_name
69
+ config["theme"]["pygments_style"] = theme_name
70
+ self._save_config(config)
71
+ return True
72
+ except Exception as e:
73
+ console.print(f"[error]Error setting theme: {e}[/error]")
74
+ return False
75
+
76
+ def reset_theme(self) -> bool:
77
+ """Reset to default theme."""
78
+ try:
79
+ if self.config_file.exists():
80
+ self.config_file.unlink()
81
+ return True
82
+ except Exception as e:
83
+ console.print(f"[error]Error resetting theme: {e}[/error]")
84
+ return False
85
+
86
+ def get_available_themes(self) -> list[str]:
87
+ """Get list of available Pygments themes."""
88
+ return sorted(get_all_styles())
89
+
90
+
91
+ theme_manager = ThemeManager()
92
+
93
+
94
+ @theme_app.command
95
+ def set():
96
+ """Set the theme to use for syntax highlighting."""
97
+
98
+ async def interactive_set():
99
+ themes = theme_manager.get_available_themes()
100
+ current_theme = theme_manager.get_current_theme()
101
+
102
+ # Create choices with current theme highlighted
103
+ choices = [
104
+ questionary.Choice(
105
+ title=f"{theme} (current)" if theme == current_theme else theme,
106
+ value=theme,
107
+ )
108
+ for theme in themes
109
+ ]
110
+
111
+ selected_theme = await questionary.select(
112
+ "Select a theme:",
113
+ choices=choices,
114
+ default=current_theme,
115
+ use_search_filter=True,
116
+ use_jk_keys=False,
117
+ ).ask_async()
118
+
119
+ if selected_theme:
120
+ if theme_manager.set_theme(selected_theme):
121
+ console.print(f"[success]✓ Theme set to: {selected_theme}[/success]")
122
+ else:
123
+ console.print("[error]✗ Failed to set theme[/error]")
124
+ sys.exit(1)
125
+ else:
126
+ console.print("[warning]Operation cancelled[/warning]")
127
+
128
+ asyncio.run(interactive_set())
129
+
130
+
131
+ @theme_app.command
132
+ def reset():
133
+ """Reset to the default theme."""
134
+
135
+ if theme_manager.reset_theme():
136
+ console.print(
137
+ f"[success]✓ Theme reset to default: {DEFAULT_THEME_NAME}[/success]"
138
+ )
139
+ else:
140
+ console.print("[error]✗ Failed to reset theme[/error]")
141
+ sys.exit(1)
142
+
143
+
144
+ def create_theme_app() -> cyclopts.App:
145
+ """Return the theme management CLI app."""
146
+ return theme_app
sqlsaber/theme/manager.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Theme management for unified theming across Rich and prompt_toolkit."""
2
2
 
3
+ import json
3
4
  import os
4
- import tomllib
5
5
  from dataclasses import dataclass
6
6
  from functools import lru_cache
7
7
  from typing import Dict
@@ -10,6 +10,8 @@ from platformdirs import user_config_dir
10
10
  from prompt_toolkit.styles import Style as PTStyle
11
11
  from prompt_toolkit.styles.pygments import style_from_pygments_cls
12
12
  from pygments.styles import get_style_by_name
13
+ from pygments.token import Token
14
+ from pygments.util import ClassNotFound
13
15
  from rich.console import Console
14
16
  from rich.theme import Theme
15
17
 
@@ -43,89 +45,97 @@ DEFAULT_ROLE_PALETTE = {
43
45
  "title": "bold $success",
44
46
  }
45
47
 
46
- # Theme presets using exact Pygments colors
47
- THEME_PRESETS = {
48
- # Nord - exact colors from pygments nord theme
49
- "nord": {
50
- "primary": "#81a1c1", # Keyword (frost)
51
- "accent": "#b48ead", # Number (aurora purple)
52
- "success": "#a3be8c", # String (aurora green)
53
- "warning": "#ebcb8b", # String.Escape (aurora yellow)
54
- "error": "#bf616a", # Error/Generic.Error (aurora red)
55
- "info": "#88c0d0", # Name.Function (frost cyan)
56
- "muted": "dim",
57
- },
58
- # Dracula - exact colors from pygments dracula theme
59
- "dracula": {
60
- "primary": "#bd93f9", # purple
61
- "accent": "#ff79c6", # pink
62
- "success": "#50fa7b", # green
63
- "warning": "#f1fa8c", # yellow
64
- "error": "#ff5555", # red
65
- "info": "#8be9fd", # cyan
66
- "muted": "dim",
67
- },
68
- # Solarized Light - exact colors from pygments solarized-light theme
69
- "solarized-light": {
70
- "primary": "#268bd2", # blue
71
- "accent": "#d33682", # magenta
72
- "success": "#859900", # green
73
- "warning": "#b58900", # yellow
74
- "error": "#dc322f", # red
75
- "info": "#2aa198", # cyan
76
- "muted": "dim",
77
- },
78
- # VS (Visual Studio Light) - exact colors from pygments vs theme
79
- "vs": {
80
- "primary": "#0000ff", # Keyword (blue)
81
- "accent": "#2b91af", # Keyword.Type/Name.Class
82
- "success": "#008000", # Comment (green)
83
- "warning": "#b58900", # (using solarized yellow as fallback)
84
- "error": "#dc322f", # (using solarized red as fallback)
85
- "info": "#2aa198", # (using solarized cyan as fallback)
86
- "muted": "dim",
87
- },
88
- # Material (approximation based on material design colors)
89
- "material": {
90
- "primary": "#89ddff", # cyan
91
- "accent": "#f07178", # pink/red
92
- "success": "#c3e88d", # green
93
- "warning": "#ffcb6b", # yellow
94
- "error": "#ff5370", # red
95
- "info": "#82aaff", # blue
96
- "muted": "dim",
97
- },
98
- # One Dark - exact colors from pygments one-dark theme
99
- "one-dark": {
100
- "primary": "#c678dd", # Keyword (purple)
101
- "accent": "#e06c75", # Name (red)
102
- "success": "#98c379", # String (green)
103
- "warning": "#e5c07b", # Keyword.Type (yellow)
104
- "error": "#e06c75", # Name (red, used for errors)
105
- "info": "#61afef", # Name.Function (blue)
106
- "muted": "dim",
107
- },
108
- # Lightbulb - exact colors from pygments lightbulb theme (minimal dark)
109
- "lightbulb": {
110
- "primary": "#73d0ff", # Keyword.Type/Name.Class (blue_1)
111
- "accent": "#dfbfff", # Number (magenta_1)
112
- "success": "#d5ff80", # String (green_1)
113
- "warning": "#ffd173", # Name.Function (yellow_1)
114
- "error": "#f88f7f", # Error (red_1)
115
- "info": "#95e6cb", # Name.Entity (cyan_1)
116
- "muted": "dim",
117
- },
48
+ ROLE_TOKEN_PREFERENCES: dict[str, tuple] = {
49
+ "primary": (
50
+ Token.Keyword,
51
+ Token.Keyword.Namespace,
52
+ Token.Name.Tag,
53
+ ),
54
+ "accent": (
55
+ Token.Name.Tag,
56
+ Token.Keyword.Type,
57
+ Token.Literal.Number,
58
+ Token.Operator.Word,
59
+ ),
60
+ "success": (
61
+ Token.Literal.String,
62
+ Token.Generic.Inserted,
63
+ Token.Name.Attribute,
64
+ ),
65
+ "warning": (
66
+ Token.Literal.String.Escape,
67
+ Token.Name.Constant,
68
+ Token.Generic.Emph,
69
+ ),
70
+ "error": (
71
+ Token.Error,
72
+ Token.Generic.Error,
73
+ Token.Generic.Deleted,
74
+ Token.Name.Exception,
75
+ ),
76
+ "info": (
77
+ Token.Name.Function,
78
+ Token.Name.Builtin,
79
+ Token.Keyword.Type,
80
+ ),
81
+ "muted": (
82
+ Token.Comment,
83
+ Token.Generic.Subheading,
84
+ Token.Text,
85
+ ),
118
86
  }
119
87
 
120
88
 
89
+ def _normalize_hex(color: str | None) -> str | None:
90
+ if not color:
91
+ return None
92
+ color = color.strip()
93
+ if not color:
94
+ return None
95
+ if color.startswith("#"):
96
+ color = color[1:]
97
+ if len(color) == 3:
98
+ color = "".join(ch * 2 for ch in color)
99
+ if len(color) != 6:
100
+ return None
101
+ return f"#{color.lower()}"
102
+
103
+
104
+ def _build_role_palette_from_style(style_name: str) -> dict[str, str]:
105
+ try:
106
+ style_cls = get_style_by_name(style_name)
107
+ except ClassNotFound:
108
+ return {}
109
+
110
+ palette: dict[str, str] = {}
111
+ try:
112
+ base_color = _normalize_hex(style_cls.style_for_token(Token.Text).get("color"))
113
+ except KeyError:
114
+ base_color = None
115
+ for role, tokens in ROLE_TOKEN_PREFERENCES.items():
116
+ for token in tokens:
117
+ try:
118
+ style_def = style_cls.style_for_token(token)
119
+ except KeyError:
120
+ continue
121
+ color = _normalize_hex(style_def.get("color"))
122
+ if not color or color == base_color:
123
+ continue
124
+ if role == "accent" and color == palette.get("primary"):
125
+ continue
126
+ palette[role] = color
127
+ break
128
+ return palette
129
+
130
+
121
131
  def _load_user_theme_config() -> dict:
122
132
  """Load theme configuration from user config directory."""
123
133
  cfg_dir = user_config_dir("sqlsaber")
124
- path = os.path.join(cfg_dir, "theme.toml")
134
+ path = os.path.join(cfg_dir, "theme.json")
125
135
  if not os.path.exists(path):
126
136
  return {}
127
- with open(path, "rb") as f:
128
- return tomllib.load(f)
137
+ with open(path, "r") as f:
138
+ return json.load(f)
129
139
 
130
140
 
131
141
  def _resolve_refs(palette: dict[str, str]) -> dict[str, str]:
@@ -204,7 +214,7 @@ def get_theme_manager() -> ThemeManager:
204
214
  pygments_style = user_cfg.get("theme", {}).get("pygments_style") or name
205
215
 
206
216
  roles = dict(DEFAULT_ROLE_PALETTE)
207
- roles.update(THEME_PRESETS.get(name, {}))
217
+ roles.update(_build_role_palette_from_style(pygments_style))
208
218
  roles.update(user_cfg.get("roles", {}))
209
219
 
210
220
  cfg = ThemeConfig(name=name, pygments_style=pygments_style, roles=roles)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.28.0
3
+ Version: 0.29.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -11,7 +11,7 @@ sqlsaber/application/model_selection.py,sha256=xZI-nvUgYZikaTK38SCmEWvWSfRsDpFu2
11
11
  sqlsaber/application/prompts.py,sha256=4rMGcWpYJbNWPMzqVWseUMx0nwvXOkWS6GaTAJ5mhfc,3473
12
12
  sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
13
13
  sqlsaber/cli/auth.py,sha256=ysDBXEFR8Jz7wYbIP6X7yWA2ivd8SDnUp_jUg_qYNWk,6088
14
- sqlsaber/cli/commands.py,sha256=-rTxr-kW7j2rR8wAg0tATKoh284pMDPKVMpQKaJwtqk,8540
14
+ sqlsaber/cli/commands.py,sha256=LiHLMAZF9fNgQXShtF67dzDvp-w4LlSbmeZIa47ntC0,8634
15
15
  sqlsaber/cli/completers.py,sha256=g-hLDq5fiBx7gg8Bte1Lq8GU-ZxCYVs4dcPsmHPIcK4,6574
16
16
  sqlsaber/cli/database.py,sha256=hh8PdWnhaD0fO2jwvSSQyxsjwk-JyvmcY7f5tuHfnAQ,10663
17
17
  sqlsaber/cli/display.py,sha256=WB5JCumhXadziDEX1EZHG3vN1Chol5FNAaTXHieqFK0,17892
@@ -20,6 +20,7 @@ sqlsaber/cli/memory.py,sha256=gKP-JJ0w1ya1YTM_Lk7Gw-7wL9ptyj6cZtg-uoW8K7A,7818
20
20
  sqlsaber/cli/models.py,sha256=NozZbnisSjbPKo7PW7CltJMIkGcPqTDpDQEY-C_eLhk,8504
21
21
  sqlsaber/cli/onboarding.py,sha256=l6FFWn8J1OVQUxr-xIAzKaFhAz8rFh6IEWwIyPWqR6U,11438
22
22
  sqlsaber/cli/streaming.py,sha256=1XoZGPPMoTmBQVgp_Bqk483MR93j9oXxSV6Tx_-TpOg,6923
23
+ sqlsaber/cli/theme.py,sha256=hP0kmsMLCtqaT7b5wB1dk1hW1hV94oP4BHdz8S6887A,4243
23
24
  sqlsaber/cli/threads.py,sha256=5EV4ckRzKqhWeTKpTfQSNCBuqs3onsJURKT09g4E4XM,13634
24
25
  sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
25
26
  sqlsaber/config/api_keys.py,sha256=bjogRmIuxNNGusyKXKi0ZpJWeS5Fyn53zrAD8hsoYx4,3671
@@ -44,7 +45,7 @@ sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,6
44
45
  sqlsaber/memory/manager.py,sha256=p3fybMVfH-E4ApT1ZRZUnQIWSk9dkfUPCyfkmA0HALs,2739
45
46
  sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,5745
46
47
  sqlsaber/theme/__init__.py,sha256=qCICX1Cg4B6yCbZ1UrerxglWxcqldRFVSRrSs73na_8,188
47
- sqlsaber/theme/manager.py,sha256=0DWuVXn7JoC8NvAl5FSqc61eagKFTx5YnoY8SoCTxGM,7236
48
+ sqlsaber/theme/manager.py,sha256=7QXoqPvl-aKfvqTRduWYwnHsySu66Gg8a3-QQkVM5Ss,6551
48
49
  sqlsaber/threads/__init__.py,sha256=Hh3dIG1tuC8fXprREUpslCIgPYz8_6o7aRLx4yNeO48,139
49
50
  sqlsaber/threads/storage.py,sha256=rsUdxT4CR52D7xtGir9UlsFnBMk11jZeflzDrk2q4ME,11183
50
51
  sqlsaber/tools/__init__.py,sha256=x3YdmX_7P0Qq_HtZHAgfIVKTLxYqKk6oc4tGsujQWsc,586
@@ -54,8 +55,8 @@ sqlsaber/tools/instructions.py,sha256=X-x8maVkkyi16b6Tl0hcAFgjiYceZaSwyWTfmrvx8U
54
55
  sqlsaber/tools/registry.py,sha256=HWOQMsNIdL4XZS6TeNUyrL-5KoSDH6PHsWd3X66o-18,3211
55
56
  sqlsaber/tools/sql_guard.py,sha256=dTDwcZP-N4xPGzcr7MQtKUxKrlDzlc1irr9aH5a4wvk,6182
56
57
  sqlsaber/tools/sql_tools.py,sha256=ujmAcfLkNaBrb5LWEgWcINQEQSX0LRPX3VK5Dag1Sj4,9178
57
- sqlsaber-0.28.0.dist-info/METADATA,sha256=oY9Awl9jLkdeJLa1oeTPHPGH94KktJ_HmBVUVLQs-do,7174
58
- sqlsaber-0.28.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
59
- sqlsaber-0.28.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
60
- sqlsaber-0.28.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
- sqlsaber-0.28.0.dist-info/RECORD,,
58
+ sqlsaber-0.29.0.dist-info/METADATA,sha256=5Ocm_GD3MIdPLji_VpknTjiQ0ndDd2waD14dr6_IOjg,7174
59
+ sqlsaber-0.29.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
60
+ sqlsaber-0.29.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
61
+ sqlsaber-0.29.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
62
+ sqlsaber-0.29.0.dist-info/RECORD,,