sqlsaber 0.1.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,77 @@
1
+ """Memory manager for handling database-specific context and memories."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from sqlsaber.memory.storage import Memory, MemoryStorage
6
+
7
+
8
+ class MemoryManager:
9
+ """Manages database-specific memories and context."""
10
+
11
+ def __init__(self):
12
+ self.storage = MemoryStorage()
13
+
14
+ def add_memory(self, database_name: str, content: str) -> Memory:
15
+ """Add a new memory for the specified database."""
16
+ return self.storage.add_memory(database_name, content)
17
+
18
+ def get_memories(self, database_name: str) -> List[Memory]:
19
+ """Get all memories for the specified database."""
20
+ return self.storage.get_memories(database_name)
21
+
22
+ def remove_memory(self, database_name: str, memory_id: str) -> bool:
23
+ """Remove a specific memory by ID."""
24
+ return self.storage.remove_memory(database_name, memory_id)
25
+
26
+ def clear_memories(self, database_name: str) -> int:
27
+ """Clear all memories for the specified database."""
28
+ return self.storage.clear_memories(database_name)
29
+
30
+ def get_memory_by_id(self, database_name: str, memory_id: str) -> Optional[Memory]:
31
+ """Get a specific memory by ID."""
32
+ return self.storage.get_memory_by_id(database_name, memory_id)
33
+
34
+ def has_memories(self, database_name: str) -> bool:
35
+ """Check if database has any memories."""
36
+ return self.storage.has_memories(database_name)
37
+
38
+ def format_memories_for_prompt(self, database_name: str) -> str:
39
+ """Format memories for inclusion in system prompt."""
40
+ memories = self.get_memories(database_name)
41
+
42
+ if not memories:
43
+ return ""
44
+
45
+ formatted_memories = []
46
+ for memory in memories:
47
+ formatted_memories.append(f"- {memory.content}")
48
+
49
+ return f"""
50
+ Previous context from user:
51
+ {chr(10).join(formatted_memories)}
52
+
53
+ Use this context to better understand the user's needs and provide more relevant responses.
54
+ """
55
+
56
+ def get_memories_summary(self, database_name: str) -> dict:
57
+ """Get a summary of memories for a database."""
58
+ memories = self.get_memories(database_name)
59
+
60
+ return {
61
+ "database": database_name,
62
+ "total_memories": len(memories),
63
+ "memories": [
64
+ {
65
+ "id": memory.id,
66
+ "content": memory.content[:100] + "..."
67
+ if len(memory.content) > 100
68
+ else memory.content,
69
+ "timestamp": memory.formatted_timestamp(),
70
+ }
71
+ for memory in memories
72
+ ],
73
+ }
74
+
75
+ def list_databases_with_memories(self) -> List[str]:
76
+ """List all databases that have memories."""
77
+ return self.storage.list_databases_with_memories()
@@ -0,0 +1,176 @@
1
+ """Memory storage implementation for database-specific memories."""
2
+
3
+ import json
4
+ import os
5
+ import platform
6
+ import stat
7
+ import time
8
+ import uuid
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional
12
+
13
+ import platformdirs
14
+
15
+
16
+ @dataclass
17
+ class Memory:
18
+ """Represents a single memory entry."""
19
+
20
+ id: str
21
+ content: str
22
+ timestamp: float
23
+
24
+ def to_dict(self) -> Dict:
25
+ """Convert memory to dictionary for JSON serialization."""
26
+ return {
27
+ "id": self.id,
28
+ "content": self.content,
29
+ "timestamp": self.timestamp,
30
+ }
31
+
32
+ @classmethod
33
+ def from_dict(cls, data: Dict) -> "Memory":
34
+ """Create Memory from dictionary."""
35
+ return cls(
36
+ id=data["id"],
37
+ content=data["content"],
38
+ timestamp=data["timestamp"],
39
+ )
40
+
41
+ def formatted_timestamp(self) -> str:
42
+ """Get human-readable timestamp."""
43
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.timestamp))
44
+
45
+
46
+ class MemoryStorage:
47
+ """Handles storage and retrieval of database-specific memories."""
48
+
49
+ def __init__(self):
50
+ self.memory_dir = Path(platformdirs.user_config_dir("sqlsaber")) / "memories"
51
+ self._ensure_memory_dir()
52
+
53
+ def _ensure_memory_dir(self) -> None:
54
+ """Ensure memory directory exists with proper permissions."""
55
+ self.memory_dir.mkdir(parents=True, exist_ok=True)
56
+ self._set_secure_permissions(self.memory_dir, is_directory=True)
57
+
58
+ def _set_secure_permissions(self, path: Path, is_directory: bool = False) -> None:
59
+ """Set secure permissions cross-platform."""
60
+ try:
61
+ if platform.system() == "Windows":
62
+ # On Windows, rely on NTFS permissions and avoid chmod
63
+ return
64
+ else:
65
+ # Unix-like systems (Linux, macOS)
66
+ if is_directory:
67
+ os.chmod(
68
+ path, stat.S_IRWXU
69
+ ) # 0o700 - owner read/write/execute only
70
+ else:
71
+ os.chmod(
72
+ path, stat.S_IRUSR | stat.S_IWUSR
73
+ ) # 0o600 - owner read/write only
74
+ except (OSError, PermissionError):
75
+ # If we can't set permissions, continue anyway
76
+ pass
77
+
78
+ def _get_memory_file(self, database_name: str) -> Path:
79
+ """Get the memory file path for a specific database."""
80
+ return self.memory_dir / f"{database_name}.json"
81
+
82
+ def _load_memories(self, database_name: str) -> List[Memory]:
83
+ """Load memories for a specific database."""
84
+ memory_file = self._get_memory_file(database_name)
85
+
86
+ if not memory_file.exists():
87
+ return []
88
+
89
+ try:
90
+ with open(memory_file, "r") as f:
91
+ data = json.load(f)
92
+ return [
93
+ Memory.from_dict(memory_data)
94
+ for memory_data in data.get("memories", [])
95
+ ]
96
+ except (json.JSONDecodeError, IOError, KeyError):
97
+ return []
98
+
99
+ def _save_memories(self, database_name: str, memories: List[Memory]) -> None:
100
+ """Save memories for a specific database."""
101
+ memory_file = self._get_memory_file(database_name)
102
+
103
+ data = {
104
+ "database": database_name,
105
+ "memories": [memory.to_dict() for memory in memories],
106
+ }
107
+
108
+ with open(memory_file, "w") as f:
109
+ json.dump(data, f, indent=2)
110
+
111
+ # Set secure permissions
112
+ self._set_secure_permissions(memory_file, is_directory=False)
113
+
114
+ def add_memory(self, database_name: str, content: str) -> Memory:
115
+ """Add a new memory for the specified database."""
116
+ memory = Memory(
117
+ id=str(uuid.uuid4()),
118
+ content=content.strip(),
119
+ timestamp=time.time(),
120
+ )
121
+
122
+ memories = self._load_memories(database_name)
123
+ memories.append(memory)
124
+ self._save_memories(database_name, memories)
125
+
126
+ return memory
127
+
128
+ def get_memories(self, database_name: str) -> List[Memory]:
129
+ """Get all memories for the specified database."""
130
+ return self._load_memories(database_name)
131
+
132
+ def remove_memory(self, database_name: str, memory_id: str) -> bool:
133
+ """Remove a specific memory by ID."""
134
+ memories = self._load_memories(database_name)
135
+ original_count = len(memories)
136
+
137
+ memories = [m for m in memories if m.id != memory_id]
138
+
139
+ if len(memories) < original_count:
140
+ self._save_memories(database_name, memories)
141
+ return True
142
+
143
+ return False
144
+
145
+ def clear_memories(self, database_name: str) -> int:
146
+ """Clear all memories for the specified database."""
147
+ memories = self._load_memories(database_name)
148
+ count = len(memories)
149
+
150
+ if count > 0:
151
+ self._save_memories(database_name, [])
152
+
153
+ return count
154
+
155
+ def get_memory_by_id(self, database_name: str, memory_id: str) -> Optional[Memory]:
156
+ """Get a specific memory by ID."""
157
+ memories = self._load_memories(database_name)
158
+ return next((m for m in memories if m.id == memory_id), None)
159
+
160
+ def has_memories(self, database_name: str) -> bool:
161
+ """Check if database has any memories."""
162
+ return len(self._load_memories(database_name)) > 0
163
+
164
+ def list_databases_with_memories(self) -> List[str]:
165
+ """List all databases that have memories."""
166
+ databases = []
167
+
168
+ if not self.memory_dir.exists():
169
+ return databases
170
+
171
+ for memory_file in self.memory_dir.glob("*.json"):
172
+ database_name = memory_file.stem
173
+ if self.has_memories(database_name):
174
+ databases.append(database_name)
175
+
176
+ return databases
@@ -0,0 +1,13 @@
1
+ """Models module for SQLSaber."""
2
+
3
+ from .events import StreamEvent, SQLResponse
4
+ from .types import ColumnInfo, ForeignKeyInfo, SchemaInfo, ToolDefinition
5
+
6
+ __all__ = [
7
+ "StreamEvent",
8
+ "SQLResponse",
9
+ "ColumnInfo",
10
+ "ForeignKeyInfo",
11
+ "SchemaInfo",
12
+ "ToolDefinition",
13
+ ]
@@ -0,0 +1,28 @@
1
+ """Event models for streaming and responses."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+
6
+ class StreamEvent:
7
+ """Event emitted during streaming processing."""
8
+
9
+ def __init__(self, event_type: str, data: Any = None):
10
+ # 'tool_use', 'text', 'query_result', 'error', 'processing'
11
+ self.type = event_type
12
+ self.data = data
13
+
14
+
15
+ class SQLResponse:
16
+ """Response from the SQL agent."""
17
+
18
+ def __init__(
19
+ self,
20
+ query: Optional[str] = None,
21
+ explanation: str = "",
22
+ results: Optional[List[Dict[str, Any]]] = None,
23
+ error: Optional[str] = None,
24
+ ):
25
+ self.query = query
26
+ self.explanation = explanation
27
+ self.results = results
28
+ self.error = error
@@ -0,0 +1,40 @@
1
+ """Type definitions for SQLSaber."""
2
+
3
+ from typing import Any, Dict, List, Optional, TypedDict
4
+
5
+
6
+ class ColumnInfo(TypedDict):
7
+ """Type definition for column information."""
8
+
9
+ data_type: str
10
+ nullable: bool
11
+ default: Optional[str]
12
+ max_length: Optional[int]
13
+ precision: Optional[int]
14
+ scale: Optional[int]
15
+
16
+
17
+ class ForeignKeyInfo(TypedDict):
18
+ """Type definition for foreign key information."""
19
+
20
+ column: str
21
+ references: Dict[str, str] # {"table": "schema.table", "column": "column_name"}
22
+
23
+
24
+ class SchemaInfo(TypedDict):
25
+ """Type definition for schema information."""
26
+
27
+ schema: str
28
+ name: str
29
+ type: str
30
+ columns: Dict[str, ColumnInfo]
31
+ primary_keys: List[str]
32
+ foreign_keys: List[ForeignKeyInfo]
33
+
34
+
35
+ class ToolDefinition(TypedDict):
36
+ """Type definition for tool definition."""
37
+
38
+ name: str
39
+ description: str
40
+ input_schema: Dict[str, Any]
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlsaber
3
+ Version: 0.1.0
4
+ Summary: SQLSaber - Agentic SQL assistant like Claude Code
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: aiomysql>=0.2.0
8
+ Requires-Dist: aiosqlite>=0.21.0
9
+ Requires-Dist: anthropic>=0.54.0
10
+ Requires-Dist: asyncpg>=0.30.0
11
+ Requires-Dist: httpx>=0.28.1
12
+ Requires-Dist: keyring>=25.6.0
13
+ Requires-Dist: platformdirs>=4.0.0
14
+ Requires-Dist: questionary>=2.1.0
15
+ Requires-Dist: rich>=13.7.0
16
+ Requires-Dist: typer>=0.16.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # SQLSaber
20
+
21
+ > Use the agent Luke!
22
+
23
+ SQLSaber is an agentic SQL assistant. Think Claude Code but for SQL.
24
+
25
+ Ask your questions in natural language and it will gather the right context and answer your query by writing SQL and analyzing the results.
26
+
27
+ ## Features
28
+
29
+ - Natural language to SQL conversion
30
+ - 🔍 Automatic database schema introspection
31
+ - 🛡️ Safe query execution (read-only by default)
32
+ - 🧠 Memory management
33
+ - 💬 Interactive REPL mode
34
+ - 🎨 Beautiful formatted output with syntax highlighting
35
+ - 🗄️ Support for PostgreSQL, SQLite, and MySQL
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ uv tool install sqlsaber
41
+ ```
42
+
43
+ or
44
+
45
+ ```bash
46
+ pipx install sqlsaber
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ ### Database Connection
52
+
53
+ Set your database connection URL:
54
+
55
+ ```bash
56
+ saber db add DB_NAME
57
+ ```
58
+
59
+ This will ask you some questions about your database connection
60
+
61
+ ### AI Model Configuration
62
+
63
+ SQLSaber uses Sonnet-4 by default. You can change it using:
64
+
65
+ ```bash
66
+ saber models set
67
+
68
+ # for more model settings run:
69
+ saber models --help
70
+ ```
71
+
72
+ ### Memory Management
73
+
74
+ You can add specific context about your database to the model using the memory feature. This is similar to how you add memory/context in Claude Code.
75
+
76
+ ```bash
77
+ saber memory add 'always convert dates to string for easier formating'
78
+ ```
79
+
80
+ View all memories
81
+
82
+ ```bash
83
+ saber memory list
84
+ ```
85
+
86
+ > You can also add memories in an interactive query session by starting with the `#` sign
87
+
88
+ ## Usage
89
+
90
+ ### Interactive Mode
91
+
92
+ Start an interactive session:
93
+
94
+ ```bash
95
+ saber query
96
+ ```
97
+
98
+ > You can also add memories in an interactive session by starting your message with the `#` sign
99
+
100
+ ### Single Query
101
+
102
+ Execute a single natural language query:
103
+
104
+ ```bash
105
+ saber query "show me all users created this month"
106
+ ```
107
+
108
+ ### Database Selection
109
+
110
+ Use a specific database connection:
111
+
112
+ ```bash
113
+ # Use named database from config
114
+ saber query -d mydb "count all orders"
115
+ ```
116
+
117
+ ## Examples
118
+
119
+ ```bash
120
+ # Show database schema
121
+ saber query "what tables are in my database?"
122
+
123
+ # Count records
124
+ saber query "how many active users do we have?"
125
+
126
+ # Complex queries with joins
127
+ saber query "show me orders with customer details for this week"
128
+
129
+ # Aggregations
130
+ saber query "what's the total revenue by product category?"
131
+
132
+ # Date filtering
133
+ saber query "list users who haven't logged in for 30 days"
134
+
135
+ # Data exploration
136
+ saber query "show me the distribution of customer ages"
137
+
138
+ # Business analytics
139
+ saber query "which products had the highest sales growth last quarter?"
140
+ ```
141
+
142
+ ## How It Works
143
+
144
+ SQLSaber uses an intelligent three-step process optimized for minimal token usage:
145
+
146
+ ### 🔍 Discovery Phase
147
+
148
+ 1. **List Tables Tool**: Quickly discovers available tables with row counts
149
+ 2. **Pattern Matching**: Identifies relevant tables based on your query using SQL LIKE patterns
150
+
151
+ ### 📋 Schema Analysis
152
+
153
+ 3. **Smart Introspection**: Analyzes only the specific table structures needed for your query
154
+ 4. **Selective Loading**: Fetches schema information only for relevant tables
155
+
156
+ ### ⚡ Execution Phase
157
+
158
+ 5. **SQL Generation**: Creates optimized SQL queries based on natural language input
159
+ 6. **Safe Execution**: Runs queries with built-in protections against destructive operations
160
+ 7. **Result Formatting**: Presents results with syntax highlighting and explanations
161
+
162
+ ## Contributing
163
+
164
+ Contributions are welcome! Please feel free to open an issue to discuss your ideas or report bugs.
165
+
166
+ ## License
167
+
168
+ This project is licensed under Apache-2.0 License - see the LICENSE file for details.
@@ -0,0 +1,32 @@
1
+ sqlsaber/__init__.py,sha256=QCFi8xTVMohelfi7zOV1-6oLCcGoiXoOcKQY-HNBCk8,66
2
+ sqlsaber/__main__.py,sha256=RIHxWeWh2QvLfah-2OkhI5IJxojWfy4fXpMnVEJYvxw,78
3
+ sqlsaber/agents/__init__.py,sha256=LWeSeEUE4BhkyAYFF3TE-fx8TtLud3oyEtyB8ojFJgo,167
4
+ sqlsaber/agents/anthropic.py,sha256=CPNshN68QxxdIvWS5YjEuiXd_8V8mmMTL9aC2Mqkvts,17863
5
+ sqlsaber/agents/base.py,sha256=UUSGhoJImATXrYS7yrLR2qjg1iFW4udOUdRaV3Ryk5s,2086
6
+ sqlsaber/agents/streaming.py,sha256=0bNzd_JhLlgQB40pf9FZFMvmU9Q7W6D9BmglA1rIGqw,850
7
+ sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
8
+ sqlsaber/cli/commands.py,sha256=Adrt_0LRgykb2FZ4F0TQpuBM8Z0qgfbggn0FexcVALI,4094
9
+ sqlsaber/cli/database.py,sha256=W-tJqmihKjZhoe5AGpQKe0txzLIgRVGikZHM_ELAbnQ,9138
10
+ sqlsaber/cli/display.py,sha256=5J4AgJADmMwKi9Aq5u6_MKRO1TA6unS4F4RUfml_sfU,7651
11
+ sqlsaber/cli/interactive.py,sha256=y92rdoM49SOSwEctm9ZcrEN220fhJ_DMHPSd_7KsORg,3701
12
+ sqlsaber/cli/memory.py,sha256=LW4ZF2V6Gw6hviUFGZ4ym9ostFCwucgBTIMZ3EANO-I,7671
13
+ sqlsaber/cli/models.py,sha256=3IcXeeU15IQvemSv-V-RQzVytJ3wuQ4YmWk89nTDcSE,7813
14
+ sqlsaber/cli/streaming.py,sha256=5QGAYTAvg9mzQLxDEVtdDH-TIbGfYYzMOLoOYPrHPu0,3788
15
+ sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
16
+ sqlsaber/config/api_keys.py,sha256=kLdoExF_My9ojmdhO5Ca7-ZeowsO0v1GVa_QT5jjUPo,3658
17
+ sqlsaber/config/database.py,sha256=FX4zwmOkW-lvIH--c8xRyoyyjYLjn3OQTkSruEw-aQY,8790
18
+ sqlsaber/config/settings.py,sha256=zjQ7nS3ybcCb88Ea0tmwJox5-q0ettChZw89ZqRVpX8,3975
19
+ sqlsaber/database/__init__.py,sha256=a_gtKRJnZVO8-fEZI7g3Z8YnGa6Nio-5Y50PgVp07ss,176
20
+ sqlsaber/database/connection.py,sha256=Z1iIRBIoPQcCfBliROLeebEQeI7ggu-hh_G1l-tzhIM,6672
21
+ sqlsaber/database/schema.py,sha256=gURfCFVE--UWIqD_0StqS2NMB9VIPpqczBEoS2GnKR4,27025
22
+ sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
23
+ sqlsaber/memory/manager.py,sha256=ML2NEO5Z4Aw36sEI9eOvWVnjl-qT2VOTojViJAj7Seo,2777
24
+ sqlsaber/memory/storage.py,sha256=DvZBsSPaAfk_DqrNEn86uMD-TQsWUI6rQLfNw6PSCB8,5788
25
+ sqlsaber/models/__init__.py,sha256=RJ7p3WtuSwwpFQ1Iw4_DHV2zzCtHqIzsjJzxv8kUjUE,287
26
+ sqlsaber/models/events.py,sha256=55m41tDwMsFxnKKA5_VLJz8iV-V4Sq3LDfta4VoutJI,737
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ saber = sqlsaber.cli.commands:main
3
+ sql = sqlsaber.cli.commands:main
4
+ sqlsaber = sqlsaber.cli.commands:main