sqlsaber 0.26.0__py3-none-any.whl → 0.27.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.

@@ -0,0 +1,325 @@
1
+ """Interactive onboarding flow for first-time SQLSaber users."""
2
+
3
+ import sys
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+
8
+ from sqlsaber.cli.models import ModelManager
9
+ from sqlsaber.config.api_keys import APIKeyManager
10
+ from sqlsaber.config.auth import AuthConfigManager
11
+ from sqlsaber.config.database import DatabaseConfigManager
12
+
13
+ console = Console()
14
+
15
+
16
+ def needs_onboarding(database_arg: str | None = None) -> bool:
17
+ """Check if user needs onboarding.
18
+
19
+ Onboarding is needed if:
20
+ - No database is configured AND no database connection string provided via CLI
21
+ """
22
+ # If user provided a database argument, skip onboarding
23
+ if database_arg:
24
+ return False
25
+
26
+ # Check if databases are configured
27
+ db_manager = DatabaseConfigManager()
28
+ has_db = db_manager.has_databases()
29
+
30
+ return not has_db
31
+
32
+
33
+ def welcome_screen() -> None:
34
+ """Display welcome screen to new users."""
35
+ banner = """
36
+ ███████ ██████ ██ ███████ █████ ██████ ███████ ██████
37
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
38
+ ███████ ██ ██ ██ ███████ ███████ ██████ █████ ██████
39
+ ██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
40
+ ███████ ██████ ███████ ███████ ██ ██ ██████ ███████ ██ ██
41
+ ▀▀
42
+ """
43
+
44
+ console.print(Panel.fit(banner, style="bold blue"))
45
+ console.print()
46
+
47
+ welcome_message = """
48
+ [bold]Welcome to SQLsaber! 🎉[/bold]
49
+
50
+ SQLsaber is an agentic SQL assistant that lets you query your database using natural language.
51
+
52
+ Let's get you set up in just a few steps.
53
+ """
54
+
55
+ console.print(Panel(welcome_message.strip(), border_style="blue", padding=(1, 2)))
56
+ console.print()
57
+
58
+
59
+ async def setup_database_guided() -> str | None:
60
+ """Guide user through database setup.
61
+
62
+ Returns the name of the configured database or None if cancelled.
63
+ """
64
+ from sqlsaber.application.db_setup import (
65
+ build_config,
66
+ collect_db_input,
67
+ save_database,
68
+ test_connection,
69
+ )
70
+ from sqlsaber.application.prompts import AsyncPrompter
71
+
72
+ console.print("━" * 80, style="dim")
73
+ console.print("[bold cyan]Step 1 of 2: Database Connection[/bold cyan]")
74
+ console.print("━" * 80, style="dim")
75
+ console.print()
76
+
77
+ try:
78
+ # Ask for connection name
79
+ prompter = AsyncPrompter()
80
+ name = await prompter.text(
81
+ "What would you like to name this connection?",
82
+ default="mydb",
83
+ validate=lambda x: bool(x.strip()) or "Name cannot be empty",
84
+ )
85
+
86
+ if name is None:
87
+ return None
88
+
89
+ name = name.strip()
90
+
91
+ # Check if name already exists
92
+ db_manager = DatabaseConfigManager()
93
+ if db_manager.get_database(name):
94
+ console.print(
95
+ f"[yellow]Database connection '{name}' already exists.[/yellow]"
96
+ )
97
+ return name
98
+
99
+ # Collect database input (simplified - no SSL in onboarding)
100
+ db_input = await collect_db_input(
101
+ prompter=prompter, name=name, db_type="postgresql", include_ssl=False
102
+ )
103
+
104
+ if db_input is None:
105
+ return None
106
+
107
+ # Build config
108
+ db_config = build_config(db_input)
109
+
110
+ # Test the connection
111
+ console.print(f"[dim]Testing connection to '{name}'...[/dim]")
112
+ connection_success = await test_connection(db_config, db_input.password)
113
+
114
+ if not connection_success:
115
+ retry = await prompter.confirm(
116
+ "Would you like to try again with different settings?", default=True
117
+ )
118
+ if retry:
119
+ return await setup_database_guided()
120
+ else:
121
+ console.print(
122
+ "[yellow]You can add a database later using 'saber db add'[/yellow]"
123
+ )
124
+ return None
125
+
126
+ # Save the configuration
127
+ try:
128
+ save_database(db_manager, db_config, db_input.password)
129
+ console.print(f"[green]✓ Connection to '{name}' successful![/green]")
130
+ console.print()
131
+ return name
132
+ except Exception as e:
133
+ console.print(f"[bold red]Error saving database:[/bold red] {e}")
134
+ return None
135
+
136
+ except KeyboardInterrupt:
137
+ console.print("\n[yellow]Setup cancelled.[/yellow]")
138
+ return None
139
+ except Exception as e:
140
+ console.print(f"[bold red]Unexpected error:[/bold red] {e}")
141
+ return None
142
+
143
+
144
+ async def select_model_for_provider(provider: str) -> str | None:
145
+ """Fetch and let user select a model for the given provider.
146
+
147
+ Returns the selected model ID or None if cancelled/failed.
148
+ """
149
+ from sqlsaber.application.model_selection import choose_model, fetch_models
150
+ from sqlsaber.application.prompts import AsyncPrompter
151
+
152
+ try:
153
+ console.print()
154
+ console.print(f"[dim]Fetching available {provider.title()} models...[/dim]")
155
+
156
+ model_manager = ModelManager()
157
+ models = await fetch_models(model_manager, providers=[provider])
158
+
159
+ if not models:
160
+ console.print(
161
+ f"[yellow]Could not fetch models for {provider}. Using default.[/yellow]"
162
+ )
163
+ # Use provider-specific default or fallback to Anthropic
164
+ default_model_id = ModelManager.RECOMMENDED_MODELS.get(
165
+ provider, ModelManager.DEFAULT_MODEL
166
+ )
167
+ # Format it properly if we have a recommended model for this provider
168
+ if provider in ModelManager.RECOMMENDED_MODELS:
169
+ return f"{provider}:{ModelManager.RECOMMENDED_MODELS[provider]}"
170
+ return default_model_id
171
+
172
+ prompter = AsyncPrompter()
173
+ console.print()
174
+ selected_model = await choose_model(
175
+ prompter, models, restrict_provider=provider, use_search_filter=True
176
+ )
177
+
178
+ return selected_model
179
+
180
+ except KeyboardInterrupt:
181
+ console.print("\n[yellow]Model selection cancelled.[/yellow]")
182
+ return None
183
+ except Exception as e:
184
+ console.print(f"[yellow]Error selecting model: {e}. Using default.[/yellow]")
185
+ # Fallback to provider default
186
+ if provider in ModelManager.RECOMMENDED_MODELS:
187
+ return f"{provider}:{ModelManager.RECOMMENDED_MODELS[provider]}"
188
+ return ModelManager.DEFAULT_MODEL
189
+
190
+
191
+ async def setup_auth_guided() -> tuple[bool, str | None]:
192
+ """Guide user through auth setup.
193
+
194
+ Returns tuple of (success: bool, selected_model: str | None).
195
+ """
196
+ from sqlsaber.application.auth_setup import setup_auth
197
+ from sqlsaber.application.prompts import AsyncPrompter
198
+
199
+ console.print("━" * 80, style="dim")
200
+ console.print("[bold cyan]Step 2 of 2: Authentication[/bold cyan]")
201
+ console.print("━" * 80, style="dim")
202
+ console.print()
203
+
204
+ try:
205
+ # Run auth setup
206
+ prompter = AsyncPrompter()
207
+ auth_manager = AuthConfigManager()
208
+ api_key_manager = APIKeyManager()
209
+
210
+ success, provider = await setup_auth(
211
+ prompter=prompter,
212
+ auth_manager=auth_manager,
213
+ api_key_manager=api_key_manager,
214
+ allow_oauth=True,
215
+ default_provider="anthropic",
216
+ run_oauth_in_thread=True,
217
+ )
218
+
219
+ if not success:
220
+ console.print(
221
+ "[yellow]You can set it up later using 'saber auth setup'[/yellow]"
222
+ )
223
+ console.print()
224
+ return False, None
225
+
226
+ # If auth configured but we don't know the provider (already configured case)
227
+ if provider is None:
228
+ console.print()
229
+ return True, None
230
+
231
+ # Select model for this provider
232
+ selected_model = await select_model_for_provider(provider)
233
+ if selected_model:
234
+ model_manager = ModelManager()
235
+ model_manager.set_model(selected_model)
236
+ console.print(f"[green]✓ Model set to: {selected_model}[/green]")
237
+ console.print()
238
+ return True, selected_model
239
+
240
+ except KeyboardInterrupt:
241
+ console.print("\n[yellow]Setup cancelled.[/yellow]")
242
+ console.print()
243
+ return False, None
244
+ except Exception as e:
245
+ console.print(f"[bold red]Unexpected error:[/bold red] {e}")
246
+ console.print()
247
+ return False, None
248
+
249
+
250
+ def success_screen(
251
+ database_name: str | None, auth_configured: bool, model_name: str | None = None
252
+ ) -> None:
253
+ """Display success screen after onboarding."""
254
+ console.print("━" * 80, style="dim")
255
+ console.print("[bold green]You're all set! 🚀[/bold green]")
256
+ console.print("━" * 80, style="dim")
257
+ console.print()
258
+
259
+ if database_name and auth_configured:
260
+ console.print(
261
+ f"[green]✓ Database '{database_name}' connected and ready to use[/green]"
262
+ )
263
+ console.print("[green]✓ Authentication configured[/green]")
264
+ if model_name:
265
+ console.print(f"[green]✓ Model: {model_name}[/green]")
266
+ elif database_name:
267
+ console.print(
268
+ f"[green]✓ Database '{database_name}' connected and ready to use[/green]"
269
+ )
270
+ console.print(
271
+ "[yellow]⚠ AI authentication not configured - you'll be prompted when needed[/yellow]"
272
+ )
273
+ elif auth_configured:
274
+ console.print("[green]✓ AI authentication configured[/green]")
275
+ if model_name:
276
+ console.print(f"[green]✓ Model: {model_name}[/green]")
277
+ console.print(
278
+ "[yellow]⚠ No database configured - you'll need to provide one via -d flag[/yellow]"
279
+ )
280
+
281
+ console.print()
282
+ console.print("[dim]Starting interactive session...[/dim]")
283
+ console.print()
284
+
285
+
286
+ async def run_onboarding() -> bool:
287
+ """Run the complete onboarding flow.
288
+
289
+ Returns True if onboarding completed successfully (at least database configured),
290
+ False if user cancelled or onboarding failed.
291
+ """
292
+ try:
293
+ # Welcome screen
294
+ welcome_screen()
295
+
296
+ # Database setup
297
+ database_name = await setup_database_guided()
298
+
299
+ # If user cancelled database setup, exit
300
+ if database_name is None:
301
+ console.print("[yellow]Database setup is required to continue.[/yellow]")
302
+ console.print(
303
+ "[dim]You can also provide a connection string using: saber -d <connection-string>[/dim]"
304
+ )
305
+ return False
306
+
307
+ # Auth setup
308
+ auth_configured, model_name = await setup_auth_guided()
309
+
310
+ # Show success screen
311
+ success_screen(database_name, auth_configured, model_name)
312
+
313
+ return True
314
+
315
+ except KeyboardInterrupt:
316
+ console.print("\n[yellow]Onboarding cancelled.[/yellow]")
317
+ console.print(
318
+ "[dim]You can run setup commands manually:[/dim]\n"
319
+ "[dim] - saber db add <name> # Add database connection[/dim]\n"
320
+ "[dim] - saber auth setup # Configure authentication[/dim]"
321
+ )
322
+ sys.exit(0)
323
+ except Exception as e:
324
+ console.print(f"[bold red]Onboarding failed:[/bold red] {e}")
325
+ return False
@@ -19,7 +19,7 @@ class APIKeyManager:
19
19
 
20
20
  def get_api_key(self, provider: str) -> str | None:
21
21
  """Get API key for the specified provider using cascading logic."""
22
- env_var_name = self._get_env_var_name(provider)
22
+ env_var_name = self.get_env_var_name(provider)
23
23
  service_name = self._get_service_name(provider)
24
24
 
25
25
  # 1. Check environment variable first
@@ -41,7 +41,7 @@ class APIKeyManager:
41
41
  # 3. Prompt user for API key
42
42
  return self._prompt_and_store_key(provider, env_var_name, service_name)
43
43
 
44
- def _get_env_var_name(self, provider: str) -> str:
44
+ def get_env_var_name(self, provider: str) -> str:
45
45
  """Get the expected environment variable name for a provider."""
46
46
  # Normalize aliases to canonical provider keys
47
47
  key = providers.canonical(provider) or provider
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.26.0
3
+ Version: 0.27.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -4,19 +4,25 @@ sqlsaber/agents/__init__.py,sha256=qYI6rLY4q5AbF47vXH5RVoM08-yQjymBSaePh4lFIW4,1
4
4
  sqlsaber/agents/base.py,sha256=40-MKEoz5rGrqVIylV1U2DaAUSPFcC75ohRin4E3-kk,2668
5
5
  sqlsaber/agents/mcp.py,sha256=Pn8tdDRUEVLYQyEi5nHRp9MKNePwHVVoeNI-uqWcr0Y,757
6
6
  sqlsaber/agents/pydantic_ai_agent.py,sha256=wBxKz0pjOkL-HI-TXV6B67bczZNgu7k26Rr3w5usR3o,10064
7
+ sqlsaber/application/__init__.py,sha256=KY_-d5nEdQyAwNOsK5r-f7Tb69c63XbuEkHPeLpJal8,84
8
+ sqlsaber/application/auth_setup.py,sha256=aIZ4vyfdjO5zCcMy4k4FmqjeGgruypPIRacaOJDuVRY,5080
9
+ sqlsaber/application/db_setup.py,sha256=VipoXACDVs8auYs7XeuNYrUWjVHpxqxWphx9yUl1_2I,6826
10
+ sqlsaber/application/model_selection.py,sha256=eRWga3vRpoyyTmw0BsVDRvUtyUnxvP8plAQ2Nv7tN3o,3163
11
+ sqlsaber/application/prompts.py,sha256=4rMGcWpYJbNWPMzqVWseUMx0nwvXOkWS6GaTAJ5mhfc,3473
7
12
  sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
8
- sqlsaber/cli/auth.py,sha256=jTsRgbmlGPlASSuIKmdjjwfqtKvjfKd_cTYxX0-QqaQ,7400
9
- sqlsaber/cli/commands.py,sha256=n25CErTLgLeRSkoJI0Ickwtns5EH6O7RLVPgPs6UBxA,7986
13
+ sqlsaber/cli/auth.py,sha256=Hz7nuHdX1iMr2UR1VVtu4EAvBYYIhsgthluqpiOXH2A,6064
14
+ sqlsaber/cli/commands.py,sha256=R63HF4P0JZwbGlI3TimkEUg3OUaFXYkVvLw1VIy0UFI,8516
10
15
  sqlsaber/cli/completers.py,sha256=g-hLDq5fiBx7gg8Bte1Lq8GU-ZxCYVs4dcPsmHPIcK4,6574
11
- sqlsaber/cli/database.py,sha256=qil7nZGWKm3tULL0cUsAQ_KvhU1oikK0XVh9MibrvP0,13413
12
- sqlsaber/cli/display.py,sha256=32QaNS0RDgRz93AVy6nPo9blahvMPEoVMFC5spzh0-Y,17041
13
- sqlsaber/cli/interactive.py,sha256=jGbWNNcEgZuQRZamc5tX5eIf1Rv1T6Sj5NI_WvonTrA,13624
16
+ sqlsaber/cli/database.py,sha256=6SdxNuP8w-e2nbV937q4hxfAIwKu1MqoirogD7USaMU,10639
17
+ sqlsaber/cli/display.py,sha256=JQBSdLC_a3nkmJ4QfLMhhMPCYY-otS2eXF1XrfEe19E,17045
18
+ sqlsaber/cli/interactive.py,sha256=RG4DJN9lprfKi60eomxVt-w3gTsWoqbfEcfO36iibXM,13694
14
19
  sqlsaber/cli/memory.py,sha256=OufHFJFwV0_GGn7LvKRTJikkWhV1IwNIUDOxFPHXOaQ,7794
15
- sqlsaber/cli/models.py,sha256=ZewtwGQwhd9b-yxBAPKePolvI1qQG-EkmeWAGMqtWNQ,8986
20
+ sqlsaber/cli/models.py,sha256=v_wrJWV5NXiM3zGLSMyNJf4YhgozK6hnf76Ua8HvysE,8480
21
+ sqlsaber/cli/onboarding.py,sha256=5p-rHG2eGM-7t9yg_kNDrXpY13jr__dlFII3K69VtKc,11414
16
22
  sqlsaber/cli/streaming.py,sha256=YViLCxUv-7WN5TCphLYtAR02HXvuHYuPttGGDZKDUKU,6921
17
23
  sqlsaber/cli/threads.py,sha256=zVlbOuD3GjjEVNebXwANKeKt4I_Lunf6itiBUL0TaKA,12877
18
24
  sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
19
- sqlsaber/config/api_keys.py,sha256=RqWQCko1tY7sES7YOlexgBH5Hd5ne_kGXHdBDNqcV2U,3649
25
+ sqlsaber/config/api_keys.py,sha256=7X63B-ow66GZWzYYgc_7f1T26F7gRhfyYQM4RpdkCpI,3647
20
26
  sqlsaber/config/auth.py,sha256=b5qB2h1doXyO9Bn8z0CcL8LAR2jF431gGXBGKLgTmtQ,2756
21
27
  sqlsaber/config/database.py,sha256=Yec6_0wdzq-ADblMNnbgvouYCimYOY_DWHT9oweaISc,11449
22
28
  sqlsaber/config/oauth_flow.py,sha256=A3bSXaBLzuAfXV2ZPA94m9NV33c2MyL6M4ii9oEkswQ,10291
@@ -45,8 +51,8 @@ sqlsaber/tools/enums.py,sha256=CH32mL-0k9ZA18911xLpNtsgpV6tB85TktMj6uqGz54,411
45
51
  sqlsaber/tools/instructions.py,sha256=X-x8maVkkyi16b6Tl0hcAFgjiYceZaSwyWTfmrvx8U8,9024
46
52
  sqlsaber/tools/registry.py,sha256=HWOQMsNIdL4XZS6TeNUyrL-5KoSDH6PHsWd3X66o-18,3211
47
53
  sqlsaber/tools/sql_tools.py,sha256=2xLD_pkd0t8wKndQAKIr4c9UpWzVWeHbAFpkwo5j4kY,9954
48
- sqlsaber-0.26.0.dist-info/METADATA,sha256=o4vaJVAG_1U5Tybcx2MY3lX0FvYjBEdKxFDWhFC9xYs,7138
49
- sqlsaber-0.26.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
- sqlsaber-0.26.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
51
- sqlsaber-0.26.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
52
- sqlsaber-0.26.0.dist-info/RECORD,,
54
+ sqlsaber-0.27.0.dist-info/METADATA,sha256=-QcrEttuC3vwUmNdtjHzA_laIfTkcMGoghmSCvNn3nM,7138
55
+ sqlsaber-0.27.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
+ sqlsaber-0.27.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
57
+ sqlsaber-0.27.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
58
+ sqlsaber-0.27.0.dist-info/RECORD,,