sqlsaber 0.1.0__py3-none-any.whl → 0.2.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/database.py CHANGED
@@ -40,6 +40,20 @@ def add_database(
40
40
  None, "--database", "--db", help="Database name"
41
41
  ),
42
42
  username: Optional[str] = typer.Option(None, "--username", "-u", help="Username"),
43
+ ssl_mode: Optional[str] = typer.Option(
44
+ None,
45
+ "--ssl-mode",
46
+ help="SSL mode (disable, allow, prefer, require, verify-ca, verify-full for PostgreSQL; DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY for MySQL)",
47
+ ),
48
+ ssl_ca: Optional[str] = typer.Option(
49
+ None, "--ssl-ca", help="SSL CA certificate file path"
50
+ ),
51
+ ssl_cert: Optional[str] = typer.Option(
52
+ None, "--ssl-cert", help="SSL client certificate file path"
53
+ ),
54
+ ssl_key: Optional[str] = typer.Option(
55
+ None, "--ssl-key", help="SSL client private key file path"
56
+ ),
43
57
  interactive: bool = typer.Option(
44
58
  True, "--interactive/--no-interactive", help="Use interactive mode"
45
59
  ),
@@ -80,6 +94,63 @@ def add_database(
80
94
 
81
95
  # Ask for password
82
96
  password = getpass.getpass("Password (stored in your OS keychain): ")
97
+
98
+ # Ask for SSL configuration
99
+ if questionary.confirm("Configure SSL/TLS settings?", default=False).ask():
100
+ if type == "postgresql":
101
+ ssl_mode = (
102
+ ssl_mode
103
+ or questionary.select(
104
+ "SSL mode for PostgreSQL:",
105
+ choices=[
106
+ "disable",
107
+ "allow",
108
+ "prefer",
109
+ "require",
110
+ "verify-ca",
111
+ "verify-full",
112
+ ],
113
+ default="prefer",
114
+ ).ask()
115
+ )
116
+ elif type == "mysql":
117
+ ssl_mode = (
118
+ ssl_mode
119
+ or questionary.select(
120
+ "SSL mode for MySQL:",
121
+ choices=[
122
+ "DISABLED",
123
+ "PREFERRED",
124
+ "REQUIRED",
125
+ "VERIFY_CA",
126
+ "VERIFY_IDENTITY",
127
+ ],
128
+ default="PREFERRED",
129
+ ).ask()
130
+ )
131
+
132
+ if ssl_mode and ssl_mode not in ["disable", "DISABLED"]:
133
+ if questionary.confirm(
134
+ "Specify SSL certificate files?", default=False
135
+ ).ask():
136
+ ssl_ca = (
137
+ ssl_ca or questionary.path("SSL CA certificate file:").ask()
138
+ )
139
+ if questionary.confirm(
140
+ "Specify client certificate?", default=False
141
+ ).ask():
142
+ ssl_cert = (
143
+ ssl_cert
144
+ or questionary.path(
145
+ "SSL client certificate file:"
146
+ ).ask()
147
+ )
148
+ ssl_key = (
149
+ ssl_key
150
+ or questionary.path(
151
+ "SSL client private key file:"
152
+ ).ask()
153
+ )
83
154
  else:
84
155
  # Non-interactive mode - use provided values or defaults
85
156
  if type == "sqlite":
@@ -123,6 +194,10 @@ def add_database(
123
194
  port=port,
124
195
  database=database,
125
196
  username=username,
197
+ ssl_mode=ssl_mode,
198
+ ssl_ca=ssl_ca,
199
+ ssl_cert=ssl_cert,
200
+ ssl_key=ssl_key,
126
201
  )
127
202
 
128
203
  try:
@@ -157,10 +232,21 @@ def list_databases():
157
232
  table.add_column("Port", style="yellow")
158
233
  table.add_column("Database", style="blue")
159
234
  table.add_column("Username", style="white")
235
+ table.add_column("SSL", style="bright_green")
160
236
  table.add_column("Default", style="bold red")
161
237
 
162
238
  for db in databases:
163
239
  is_default = "✓" if db.name == default_name else ""
240
+
241
+ # Format SSL status
242
+ ssl_status = ""
243
+ if db.ssl_mode:
244
+ ssl_status = db.ssl_mode
245
+ if db.ssl_ca or db.ssl_cert:
246
+ ssl_status += " (certs)"
247
+ else:
248
+ ssl_status = "disabled" if db.type != "sqlite" else "N/A"
249
+
164
250
  table.add_row(
165
251
  db.name,
166
252
  db.type,
@@ -168,6 +254,7 @@ def list_databases():
168
254
  str(db.port) if db.port else "",
169
255
  db.database,
170
256
  db.username,
257
+ ssl_status,
171
258
  is_default,
172
259
  )
173
260
 
@@ -25,6 +25,9 @@ class DatabaseConfig:
25
25
  username: Optional[str]
26
26
  password: Optional[str] = None
27
27
  ssl_mode: Optional[str] = None
28
+ ssl_ca: Optional[str] = None
29
+ ssl_cert: Optional[str] = None
30
+ ssl_key: Optional[str] = None
28
31
  schema: Optional[str] = None
29
32
 
30
33
  def to_connection_string(self) -> str:
@@ -34,21 +37,57 @@ class DatabaseConfig:
34
37
  if self.type == "postgresql":
35
38
  if not all([self.host, self.port, self.username]):
36
39
  raise ValueError("Host, port, and username are required for PostgreSQL")
40
+
41
+ # Build base connection string
37
42
  if password:
38
43
  encoded_password = quote_plus(password)
39
- return f"postgresql://{self.username}:{encoded_password}@{self.host}:{self.port}/{self.database}"
44
+ base_url = f"postgresql://{self.username}:{encoded_password}@{self.host}:{self.port}/{self.database}"
40
45
  else:
41
- return f"postgresql://{self.username}@{self.host}:{self.port}/{self.database}"
46
+ base_url = f"postgresql://{self.username}@{self.host}:{self.port}/{self.database}"
47
+
48
+ # Add SSL parameters
49
+ ssl_params = []
50
+ if self.ssl_mode:
51
+ ssl_params.append(f"sslmode={self.ssl_mode}")
52
+ if self.ssl_ca:
53
+ ssl_params.append(f"sslrootcert={quote_plus(self.ssl_ca)}")
54
+ if self.ssl_cert:
55
+ ssl_params.append(f"sslcert={quote_plus(self.ssl_cert)}")
56
+ if self.ssl_key:
57
+ ssl_params.append(f"sslkey={quote_plus(self.ssl_key)}")
58
+
59
+ if ssl_params:
60
+ return f"{base_url}?{'&'.join(ssl_params)}"
61
+ return base_url
62
+
42
63
  elif self.type == "mysql":
43
64
  if not all([self.host, self.port, self.username]):
44
65
  raise ValueError("Host, port, and username are required for MySQL")
66
+
67
+ # Build base connection string
45
68
  if password:
46
69
  encoded_password = quote_plus(password)
47
- return f"mysql://{self.username}:{encoded_password}@{self.host}:{self.port}/{self.database}"
70
+ base_url = f"mysql://{self.username}:{encoded_password}@{self.host}:{self.port}/{self.database}"
48
71
  else:
49
- return (
72
+ base_url = (
50
73
  f"mysql://{self.username}@{self.host}:{self.port}/{self.database}"
51
74
  )
75
+
76
+ # Add SSL parameters
77
+ ssl_params = []
78
+ if self.ssl_mode:
79
+ ssl_params.append(f"ssl_mode={self.ssl_mode}")
80
+ if self.ssl_ca:
81
+ ssl_params.append(f"ssl_ca={quote_plus(self.ssl_ca)}")
82
+ if self.ssl_cert:
83
+ ssl_params.append(f"ssl_cert={quote_plus(self.ssl_cert)}")
84
+ if self.ssl_key:
85
+ ssl_params.append(f"ssl_key={quote_plus(self.ssl_key)}")
86
+
87
+ if ssl_params:
88
+ return f"{base_url}?{'&'.join(ssl_params)}"
89
+ return base_url
90
+
52
91
  elif self.type == "sqlite":
53
92
  return f"sqlite:///{self.database}"
54
93
  else:
@@ -82,6 +121,9 @@ class DatabaseConfig:
82
121
  "database": self.database,
83
122
  "username": self.username,
84
123
  "ssl_mode": self.ssl_mode,
124
+ "ssl_ca": self.ssl_ca,
125
+ "ssl_cert": self.ssl_cert,
126
+ "ssl_key": self.ssl_key,
85
127
  "schema": self.schema,
86
128
  }
87
129
 
@@ -96,6 +138,9 @@ class DatabaseConfig:
96
138
  database=data["database"],
97
139
  username=data["username"],
98
140
  ssl_mode=data.get("ssl_mode"),
141
+ ssl_ca=data.get("ssl_ca"),
142
+ ssl_cert=data.get("ssl_cert"),
143
+ ssl_key=data.get("ssl_key"),
99
144
  schema=data.get("schema"),
100
145
  )
101
146
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from typing import Any, Dict, List, Optional
5
- from urllib.parse import urlparse
5
+ from urllib.parse import urlparse, parse_qs
6
+ import ssl
6
7
 
7
8
  import aiomysql
8
9
  import aiosqlite
@@ -42,13 +43,65 @@ class PostgreSQLConnection(BaseDatabaseConnection):
42
43
  def __init__(self, connection_string: str):
43
44
  super().__init__(connection_string)
44
45
  self._pool: Optional[asyncpg.Pool] = None
46
+ self._ssl_context = self._create_ssl_context()
47
+
48
+ def _create_ssl_context(self) -> Optional[ssl.SSLContext]:
49
+ """Create SSL context from connection string parameters."""
50
+ parsed = urlparse(self.connection_string)
51
+ if not parsed.query:
52
+ return None
53
+
54
+ params = parse_qs(parsed.query)
55
+ ssl_mode = params.get("sslmode", [None])[0]
56
+
57
+ if not ssl_mode or ssl_mode == "disable":
58
+ return None
59
+
60
+ # Create SSL context based on mode
61
+ if ssl_mode in ["require", "verify-ca", "verify-full"]:
62
+ ssl_context = ssl.create_default_context()
63
+
64
+ # Configure certificate verification
65
+ if ssl_mode == "require":
66
+ ssl_context.check_hostname = False
67
+ ssl_context.verify_mode = ssl.CERT_NONE
68
+ elif ssl_mode == "verify-ca":
69
+ ssl_context.check_hostname = False
70
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
71
+ elif ssl_mode == "verify-full":
72
+ ssl_context.check_hostname = True
73
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
74
+
75
+ # Load certificates if provided
76
+ ssl_ca = params.get("sslrootcert", [None])[0]
77
+ ssl_cert = params.get("sslcert", [None])[0]
78
+ ssl_key = params.get("sslkey", [None])[0]
79
+
80
+ if ssl_ca:
81
+ ssl_context.load_verify_locations(ssl_ca)
82
+
83
+ if ssl_cert and ssl_key:
84
+ ssl_context.load_cert_chain(ssl_cert, ssl_key)
85
+
86
+ return ssl_context
87
+
88
+ return None
45
89
 
46
90
  async def get_pool(self) -> asyncpg.Pool:
47
91
  """Get or create connection pool."""
48
92
  if self._pool is None:
49
- self._pool = await asyncpg.create_pool(
50
- self.connection_string, min_size=1, max_size=10
51
- )
93
+ # Create pool with SSL context if configured
94
+ if self._ssl_context:
95
+ self._pool = await asyncpg.create_pool(
96
+ self.connection_string,
97
+ min_size=1,
98
+ max_size=10,
99
+ ssl=self._ssl_context,
100
+ )
101
+ else:
102
+ self._pool = await asyncpg.create_pool(
103
+ self.connection_string, min_size=1, max_size=10
104
+ )
52
105
  return self._pool
53
106
 
54
107
  async def close(self):
@@ -94,19 +147,65 @@ class MySQLConnection(BaseDatabaseConnection):
94
147
  self.user = parsed.username or ""
95
148
  self.password = parsed.password or ""
96
149
 
150
+ # Parse SSL parameters
151
+ self.ssl_params = {}
152
+ if parsed.query:
153
+ params = parse_qs(parsed.query)
154
+
155
+ ssl_mode = params.get("ssl_mode", [None])[0]
156
+ if ssl_mode:
157
+ # Map SSL modes to aiomysql SSL parameters
158
+ if ssl_mode.upper() == "DISABLED":
159
+ self.ssl_params["ssl"] = None
160
+ elif ssl_mode.upper() in [
161
+ "PREFERRED",
162
+ "REQUIRED",
163
+ "VERIFY_CA",
164
+ "VERIFY_IDENTITY",
165
+ ]:
166
+ ssl_context = ssl.create_default_context()
167
+
168
+ if ssl_mode.upper() == "REQUIRED":
169
+ ssl_context.check_hostname = False
170
+ ssl_context.verify_mode = ssl.CERT_NONE
171
+ elif ssl_mode.upper() == "VERIFY_CA":
172
+ ssl_context.check_hostname = False
173
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
174
+ elif ssl_mode.upper() == "VERIFY_IDENTITY":
175
+ ssl_context.check_hostname = True
176
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
177
+
178
+ # Load certificates if provided
179
+ ssl_ca = params.get("ssl_ca", [None])[0]
180
+ ssl_cert = params.get("ssl_cert", [None])[0]
181
+ ssl_key = params.get("ssl_key", [None])[0]
182
+
183
+ if ssl_ca:
184
+ ssl_context.load_verify_locations(ssl_ca)
185
+
186
+ if ssl_cert and ssl_key:
187
+ ssl_context.load_cert_chain(ssl_cert, ssl_key)
188
+
189
+ self.ssl_params["ssl"] = ssl_context
190
+
97
191
  async def get_pool(self) -> aiomysql.Pool:
98
192
  """Get or create connection pool."""
99
193
  if self._pool is None:
100
- self._pool = await aiomysql.create_pool(
101
- host=self.host,
102
- port=self.port,
103
- user=self.user,
104
- password=self.password,
105
- db=self.database,
106
- minsize=1,
107
- maxsize=10,
108
- autocommit=False,
109
- )
194
+ pool_kwargs = {
195
+ "host": self.host,
196
+ "port": self.port,
197
+ "user": self.user,
198
+ "password": self.password,
199
+ "db": self.database,
200
+ "minsize": 1,
201
+ "maxsize": 10,
202
+ "autocommit": False,
203
+ }
204
+
205
+ # Add SSL parameters if configured
206
+ pool_kwargs.update(self.ssl_params)
207
+
208
+ self._pool = await aiomysql.create_pool(**pool_kwargs)
110
209
  return self._pool
111
210
 
112
211
  async def close(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: SQLSaber - Agentic SQL assistant like Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -6,7 +6,7 @@ sqlsaber/agents/base.py,sha256=UUSGhoJImATXrYS7yrLR2qjg1iFW4udOUdRaV3Ryk5s,2086
6
6
  sqlsaber/agents/streaming.py,sha256=0bNzd_JhLlgQB40pf9FZFMvmU9Q7W6D9BmglA1rIGqw,850
7
7
  sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
8
8
  sqlsaber/cli/commands.py,sha256=Adrt_0LRgykb2FZ4F0TQpuBM8Z0qgfbggn0FexcVALI,4094
9
- sqlsaber/cli/database.py,sha256=W-tJqmihKjZhoe5AGpQKe0txzLIgRVGikZHM_ELAbnQ,9138
9
+ sqlsaber/cli/database.py,sha256=ZOaW94dCM8_RUT5OZzI6-lbVmSYxSjdmAdYa3Nf_d9g,12633
10
10
  sqlsaber/cli/display.py,sha256=5J4AgJADmMwKi9Aq5u6_MKRO1TA6unS4F4RUfml_sfU,7651
11
11
  sqlsaber/cli/interactive.py,sha256=y92rdoM49SOSwEctm9ZcrEN220fhJ_DMHPSd_7KsORg,3701
12
12
  sqlsaber/cli/memory.py,sha256=LW4ZF2V6Gw6hviUFGZ4ym9ostFCwucgBTIMZ3EANO-I,7671
@@ -14,10 +14,10 @@ sqlsaber/cli/models.py,sha256=3IcXeeU15IQvemSv-V-RQzVytJ3wuQ4YmWk89nTDcSE,7813
14
14
  sqlsaber/cli/streaming.py,sha256=5QGAYTAvg9mzQLxDEVtdDH-TIbGfYYzMOLoOYPrHPu0,3788
15
15
  sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
16
16
  sqlsaber/config/api_keys.py,sha256=kLdoExF_My9ojmdhO5Ca7-ZeowsO0v1GVa_QT5jjUPo,3658
17
- sqlsaber/config/database.py,sha256=FX4zwmOkW-lvIH--c8xRyoyyjYLjn3OQTkSruEw-aQY,8790
17
+ sqlsaber/config/database.py,sha256=hrIr5IIqhkOpjJ2A4oaeMvTqWqNDdlJiVWQTaBudVQM,10388
18
18
  sqlsaber/config/settings.py,sha256=zjQ7nS3ybcCb88Ea0tmwJox5-q0ettChZw89ZqRVpX8,3975
19
19
  sqlsaber/database/__init__.py,sha256=a_gtKRJnZVO8-fEZI7g3Z8YnGa6Nio-5Y50PgVp07ss,176
20
- sqlsaber/database/connection.py,sha256=Z1iIRBIoPQcCfBliROLeebEQeI7ggu-hh_G1l-tzhIM,6672
20
+ sqlsaber/database/connection.py,sha256=1lMQ2LhxlUIkl_msGzuNXczI_tNp0WBgfsdqDC2nxsw,10479
21
21
  sqlsaber/database/schema.py,sha256=gURfCFVE--UWIqD_0StqS2NMB9VIPpqczBEoS2GnKR4,27025
22
22
  sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
23
23
  sqlsaber/memory/manager.py,sha256=ML2NEO5Z4Aw36sEI9eOvWVnjl-qT2VOTojViJAj7Seo,2777
@@ -25,8 +25,8 @@ sqlsaber/memory/storage.py,sha256=DvZBsSPaAfk_DqrNEn86uMD-TQsWUI6rQLfNw6PSCB8,57
25
25
  sqlsaber/models/__init__.py,sha256=RJ7p3WtuSwwpFQ1Iw4_DHV2zzCtHqIzsjJzxv8kUjUE,287
26
26
  sqlsaber/models/events.py,sha256=55m41tDwMsFxnKKA5_VLJz8iV-V4Sq3LDfta4VoutJI,737
27
27
  sqlsaber/models/types.py,sha256=3U_30n91EB3IglBTHipwiW4MqmmaA2qfshfraMZyPps,896
28
- sqlsaber-0.1.0.dist-info/METADATA,sha256=37Xik234wv415wWsvIWr4XFjzHiQV39EnDAYyZsMgXA,3953
29
- sqlsaber-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
- sqlsaber-0.1.0.dist-info/entry_points.txt,sha256=POwcsEskUp7xQQWabrAi6Eawz4qc5eBlB3KzAiBq-Y0,124
31
- sqlsaber-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
32
- sqlsaber-0.1.0.dist-info/RECORD,,
28
+ sqlsaber-0.2.0.dist-info/METADATA,sha256=EUgK7o9feWxmsx_pr9DwER6_t6db7ZaYSghwUKKYPao,3953
29
+ sqlsaber-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ sqlsaber-0.2.0.dist-info/entry_points.txt,sha256=POwcsEskUp7xQQWabrAi6Eawz4qc5eBlB3KzAiBq-Y0,124
31
+ sqlsaber-0.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
32
+ sqlsaber-0.2.0.dist-info/RECORD,,