thoth-dbmanager 0.4.0__py3-none-any.whl → 0.4.2__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 (43) hide show
  1. thoth_dbmanager/ThothDbManager.py +459 -0
  2. thoth_dbmanager/__init__.py +136 -0
  3. thoth_dbmanager/adapters/__init__.py +21 -0
  4. thoth_dbmanager/adapters/mariadb.py +165 -0
  5. thoth_dbmanager/adapters/mysql.py +165 -0
  6. thoth_dbmanager/adapters/oracle.py +554 -0
  7. thoth_dbmanager/adapters/postgresql.py +444 -0
  8. thoth_dbmanager/adapters/qdrant.py +189 -0
  9. thoth_dbmanager/adapters/sqlite.py +385 -0
  10. thoth_dbmanager/adapters/sqlserver.py +583 -0
  11. thoth_dbmanager/adapters/supabase.py +249 -0
  12. thoth_dbmanager/core/__init__.py +13 -0
  13. thoth_dbmanager/core/factory.py +272 -0
  14. thoth_dbmanager/core/interfaces.py +271 -0
  15. thoth_dbmanager/core/registry.py +220 -0
  16. thoth_dbmanager/documents.py +155 -0
  17. thoth_dbmanager/dynamic_imports.py +250 -0
  18. thoth_dbmanager/helpers/__init__.py +0 -0
  19. thoth_dbmanager/helpers/multi_db_generator.py +508 -0
  20. thoth_dbmanager/helpers/preprocess_values.py +159 -0
  21. thoth_dbmanager/helpers/schema.py +376 -0
  22. thoth_dbmanager/helpers/search.py +117 -0
  23. thoth_dbmanager/lsh/__init__.py +21 -0
  24. thoth_dbmanager/lsh/core.py +182 -0
  25. thoth_dbmanager/lsh/factory.py +76 -0
  26. thoth_dbmanager/lsh/manager.py +170 -0
  27. thoth_dbmanager/lsh/storage.py +96 -0
  28. thoth_dbmanager/plugins/__init__.py +23 -0
  29. thoth_dbmanager/plugins/mariadb.py +436 -0
  30. thoth_dbmanager/plugins/mysql.py +408 -0
  31. thoth_dbmanager/plugins/oracle.py +150 -0
  32. thoth_dbmanager/plugins/postgresql.py +145 -0
  33. thoth_dbmanager/plugins/qdrant.py +41 -0
  34. thoth_dbmanager/plugins/sqlite.py +170 -0
  35. thoth_dbmanager/plugins/sqlserver.py +149 -0
  36. thoth_dbmanager/plugins/supabase.py +224 -0
  37. {thoth_dbmanager-0.4.0.dist-info → thoth_dbmanager-0.4.2.dist-info}/METADATA +9 -6
  38. thoth_dbmanager-0.4.2.dist-info/RECORD +41 -0
  39. thoth_dbmanager-0.4.2.dist-info/top_level.txt +1 -0
  40. thoth_dbmanager-0.4.0.dist-info/RECORD +0 -5
  41. thoth_dbmanager-0.4.0.dist-info/top_level.txt +0 -1
  42. {thoth_dbmanager-0.4.0.dist-info → thoth_dbmanager-0.4.2.dist-info}/WHEEL +0 -0
  43. {thoth_dbmanager-0.4.0.dist-info → thoth_dbmanager-0.4.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,249 @@
1
+ """
2
+ Supabase adapter implementation.
3
+ """
4
+ import logging
5
+ from typing import Any, Dict, List, Optional, Union
6
+ import psycopg2
7
+ from psycopg2.extras import RealDictCursor
8
+ from sqlalchemy import create_engine, text
9
+ from sqlalchemy.exc import SQLAlchemyError
10
+ from urllib.parse import urlparse, parse_qs
11
+
12
+ from .postgresql import PostgreSQLAdapter
13
+ from ..core.interfaces import DbAdapter
14
+ from ..documents import (
15
+ TableDocument,
16
+ ColumnDocument,
17
+ SchemaDocument,
18
+ ForeignKeyDocument,
19
+ IndexDocument
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class SupabaseAdapter(PostgreSQLAdapter):
26
+ """
27
+ Supabase database adapter implementation.
28
+ Extends PostgreSQL adapter with Supabase-specific features.
29
+ """
30
+
31
+ def __init__(self, connection_params: Dict[str, Any]):
32
+ super().__init__(connection_params)
33
+ self.supabase_url = None
34
+ self.api_key = None
35
+ self.use_rest_api = False
36
+
37
+ def connect(self) -> None:
38
+ """Establish Supabase connection with SSL enforcement"""
39
+ try:
40
+ # Check if we should use REST API or direct database connection
41
+ self.use_rest_api = self.connection_params.get('use_rest_api', False)
42
+
43
+ if self.use_rest_api:
44
+ # REST API connection setup
45
+ self.supabase_url = self.connection_params.get('project_url')
46
+ self.api_key = self.connection_params.get('api_key')
47
+
48
+ if not self.supabase_url or not self.api_key:
49
+ raise ValueError("project_url and api_key are required for REST API mode")
50
+
51
+ logger.info("Supabase REST API connection established")
52
+ else:
53
+ # Direct database connection (PostgreSQL with SSL)
54
+ super().connect()
55
+
56
+ # Ensure SSL is enabled for Supabase
57
+ if hasattr(self, 'engine') and self.engine:
58
+ # Update connection string to enforce SSL
59
+ connection_string = self._build_connection_string()
60
+ if 'sslmode=' not in connection_string:
61
+ connection_string += '?sslmode=require'
62
+
63
+ self.engine = create_engine(connection_string, echo=False)
64
+
65
+ # Test connection
66
+ with self.engine.connect() as conn:
67
+ conn.execute(text("SELECT 1"))
68
+
69
+ logger.info("Supabase database connection established with SSL")
70
+
71
+ except Exception as e:
72
+ logger.error(f"Failed to connect to Supabase: {e}")
73
+ raise
74
+
75
+ def _build_connection_string(self) -> str:
76
+ """Build SQLAlchemy connection string with Supabase-specific parameters"""
77
+ params = self.connection_params
78
+
79
+ if self.use_rest_api:
80
+ return params.get('project_url')
81
+
82
+ # Direct database connection
83
+ host = params.get('host')
84
+ port = params.get('port', 5432)
85
+ database = params.get('database')
86
+ user = params.get('user')
87
+ password = params.get('password')
88
+
89
+ if not all([host, database, user, password]):
90
+ raise ValueError("Missing required connection parameters: host, database, user, password")
91
+
92
+ # Ensure SSL mode for Supabase
93
+ ssl_mode = params.get('sslmode', 'require')
94
+
95
+ connection_string = f"postgresql://{user}:{password}@{host}:{port}/{database}?sslmode={ssl_mode}"
96
+
97
+ # Add additional SSL parameters if provided
98
+ if params.get('sslcert'):
99
+ connection_string += f"&sslcert={params['sslcert']}"
100
+ if params.get('sslkey'):
101
+ connection_string += f"&sslkey={params['sslkey']}"
102
+ if params.get('sslrootcert'):
103
+ connection_string += f"&sslrootcert={params['sslrootcert']}"
104
+
105
+ return connection_string
106
+
107
+ def _get_psycopg2_params(self) -> Dict[str, Any]:
108
+ """Get parameters for psycopg2 connection with SSL"""
109
+ params = super()._get_psycopg2_params()
110
+
111
+ # Ensure SSL is enabled for Supabase
112
+ params['sslmode'] = self.connection_params.get('sslmode', 'require')
113
+
114
+ # Add SSL certificates if provided
115
+ if self.connection_params.get('sslcert'):
116
+ params['sslcert'] = self.connection_params['sslcert']
117
+ if self.connection_params.get('sslkey'):
118
+ params['sslkey'] = self.connection_params['sslkey']
119
+ if self.connection_params.get('sslrootcert'):
120
+ params['sslrootcert'] = self.connection_params['sslrootcert']
121
+
122
+ return params
123
+
124
+ def execute_query(self, query: str, params: Optional[Dict] = None, fetch: Union[str, int] = "all", timeout: int = 60) -> Any:
125
+ """Execute SQL queries with Supabase-specific optimizations"""
126
+ if self.use_rest_api:
127
+ return self._execute_rest_query(query, params, fetch, timeout)
128
+ else:
129
+ return super().execute_query(query, params, fetch, timeout)
130
+
131
+ def _execute_rest_query(self, query: str, params: Optional[Dict] = None, fetch: Union[str, int] = "all", timeout: int = 60) -> Any:
132
+ """Execute query using Supabase REST API"""
133
+ try:
134
+ from supabase import create_client
135
+ from postgrest.exceptions import APIError
136
+
137
+ # Create Supabase client
138
+ supabase = create_client(self.supabase_url, self.api_key)
139
+
140
+ # For REST API, we need to convert SQL to Postgrest queries
141
+ # This is a simplified implementation - in practice, you'd need a SQL parser
142
+ if query.strip().upper().startswith('SELECT'):
143
+ # Extract table name and conditions from query
144
+ table_name = self._extract_table_name(query)
145
+
146
+ # Build Postgrest query
147
+ result = supabase.table(table_name).select('*').execute()
148
+
149
+ if fetch == "all":
150
+ return result.data
151
+ elif fetch == "one":
152
+ return result.data[0] if result.data else None
153
+ elif isinstance(fetch, int):
154
+ return result.data[:fetch]
155
+ else:
156
+ return result.data
157
+ else:
158
+ # For non-SELECT queries, use RPC
159
+ result = supabase.rpc('execute_sql', {'sql': query}).execute()
160
+ return result.data
161
+
162
+ except ImportError:
163
+ raise RuntimeError("supabase-py package is required for REST API mode")
164
+ except APIError as e:
165
+ logger.error(f"Supabase REST API error: {e}")
166
+ raise
167
+
168
+ def _extract_table_name(self, query: str) -> str:
169
+ """Extract table name from SQL query (simplified)"""
170
+ # This is a basic implementation - in practice, you'd use a proper SQL parser
171
+ query = query.upper()
172
+ from_index = query.find('FROM')
173
+ if from_index != -1:
174
+ after_from = query[from_index + 4:].strip()
175
+ # Find first space or end of string
176
+ space_index = after_from.find(' ')
177
+ if space_index != -1:
178
+ return after_from[:space_index].lower()
179
+ else:
180
+ return after_from.lower()
181
+ return "unknown"
182
+
183
+ def get_tables_as_documents(self) -> List[TableDocument]:
184
+ """Get tables with Supabase schema considerations"""
185
+ tables = super().get_tables_as_documents()
186
+
187
+ # Filter out Supabase system schemas
188
+ filtered_tables = []
189
+ for table in tables:
190
+ if table.schema_name not in ['auth', 'storage', 'realtime', 'supabase_functions']:
191
+ filtered_tables.append(table)
192
+
193
+ return filtered_tables
194
+
195
+ def get_columns_as_documents(self, table_name: str) -> List[ColumnDocument]:
196
+ """Get columns with Supabase-specific handling"""
197
+ columns = super().get_columns_as_documents(table_name)
198
+
199
+ # Add Supabase-specific metadata
200
+ for column in columns:
201
+ if column.column_name in ['created_at', 'updated_at']:
202
+ column.comment = f"{column.comment} (Supabase auto-timestamp)"
203
+ elif column.column_name == 'id':
204
+ column.comment = f"{column.comment} (Supabase auto-increment)"
205
+
206
+ return columns
207
+
208
+ def get_unique_values(self) -> Dict[str, Dict[str, List[str]]]:
209
+ """Get unique values with Supabase schema filtering"""
210
+ result = super().get_unique_values()
211
+
212
+ # Filter out Supabase system tables
213
+ filtered_result = {}
214
+ for table_name, columns in result.items():
215
+ if not table_name.startswith('auth_') and not table_name.startswith('storage_'):
216
+ filtered_result[table_name] = columns
217
+
218
+ return filtered_result
219
+
220
+ def get_example_data(self, table_name: str, number_of_rows: int = 30) -> Dict[str, List[Any]]:
221
+ """Get example data with Supabase-specific handling"""
222
+ if self.use_rest_api:
223
+ return self._get_example_data_rest(table_name, number_of_rows)
224
+ else:
225
+ return super().get_example_data(table_name, number_of_rows)
226
+
227
+ def _get_example_data_rest(self, table_name: str, number_of_rows: int = 30) -> Dict[str, List[Any]]:
228
+ """Get example data using REST API"""
229
+ try:
230
+ from supabase import create_client
231
+
232
+ supabase = create_client(self.supabase_url, self.api_key)
233
+
234
+ # Get data from REST API
235
+ result = supabase.table(table_name).select('*').limit(number_of_rows).execute()
236
+
237
+ # Convert to the expected format
238
+ example_data = {}
239
+ if result.data:
240
+ for key in result.data[0].keys():
241
+ example_data[key] = [row.get(key) for row in result.data]
242
+
243
+ return example_data
244
+
245
+ except ImportError:
246
+ raise RuntimeError("supabase-py package is required for REST API mode")
247
+ except Exception as e:
248
+ logger.error(f"Error getting example data via REST API: {e}")
249
+ return {}
@@ -0,0 +1,13 @@
1
+ """
2
+ Core components for Thoth SQL Database Manager.
3
+ """
4
+ from .interfaces import DbPlugin, DbAdapter
5
+ from .registry import DbPluginRegistry
6
+ from .factory import ThothDbFactory
7
+
8
+ __all__ = [
9
+ "DbPlugin",
10
+ "DbAdapter",
11
+ "DbPluginRegistry",
12
+ "ThothDbFactory",
13
+ ]
@@ -0,0 +1,272 @@
1
+ """
2
+ Factory for creating database manager instances with plugin support.
3
+ """
4
+ import logging
5
+ from typing import Any, Dict, List, Optional
6
+ from .registry import DbPluginRegistry
7
+ from .interfaces import DbPlugin
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class ThothDbFactory:
13
+ """
14
+ Factory class for creating database manager instances.
15
+ Provides plugin-based instantiation with backward compatibility.
16
+ """
17
+
18
+ @staticmethod
19
+ def create_manager(db_type: str, db_root_path: Optional[str] = None, db_mode: str = "dev", **kwargs) -> DbPlugin:
20
+ """
21
+ Create a database manager instance using the plugin system.
22
+
23
+ Args:
24
+ db_type: Database type identifier (e.g., 'postgresql', 'sqlite')
25
+ db_root_path: Path to database root directory (can be in kwargs)
26
+ db_mode: Database mode (dev, prod, etc.)
27
+ **kwargs: Database-specific connection parameters
28
+
29
+ Returns:
30
+ Database plugin instance
31
+
32
+ Raises:
33
+ ValueError: If database type is not supported
34
+ RuntimeError: If plugin initialization fails
35
+ """
36
+ try:
37
+ # Handle db_root_path from kwargs for backward compatibility
38
+ if db_root_path is None:
39
+ db_root_path = kwargs.get('db_root_path')
40
+ if db_root_path is None:
41
+ raise ValueError("db_root_path is required")
42
+
43
+ # Handle db_mode from kwargs for backward compatibility
44
+ if 'db_mode' in kwargs:
45
+ db_mode = kwargs.get('db_mode', db_mode)
46
+
47
+ # Remove extracted parameters from kwargs to avoid duplicate parameter errors
48
+ kwargs_clean = kwargs.copy()
49
+ kwargs_clean.pop('db_root_path', None)
50
+ kwargs_clean.pop('db_mode', None)
51
+
52
+ # Create plugin instance
53
+ plugin = DbPluginRegistry.create_plugin(
54
+ db_type=db_type,
55
+ db_root_path=db_root_path,
56
+ db_mode=db_mode,
57
+ **kwargs_clean
58
+ )
59
+
60
+ # Initialize the plugin
61
+ plugin.initialize(**kwargs_clean)
62
+
63
+ logger.info(f"Successfully created {db_type} manager for {db_root_path}")
64
+ return plugin
65
+
66
+ except Exception as e:
67
+ logger.error(f"Failed to create {db_type} manager: {e}")
68
+ raise RuntimeError(f"Failed to create {db_type} manager: {e}") from e
69
+
70
+ @staticmethod
71
+ def list_available_databases() -> List[str]:
72
+ """
73
+ List all available database types.
74
+
75
+ Returns:
76
+ List of supported database type identifiers
77
+ """
78
+ return DbPluginRegistry.list_plugins()
79
+
80
+ @staticmethod
81
+ def get_database_info(db_type: Optional[str] = None) -> Dict[str, Any]:
82
+ """
83
+ Get information about available database plugins.
84
+
85
+ Args:
86
+ db_type: Specific database type, or None for all
87
+
88
+ Returns:
89
+ Database plugin information
90
+ """
91
+ return DbPluginRegistry.get_plugin_info(db_type)
92
+
93
+ @staticmethod
94
+ def validate_database_type(db_type: str) -> bool:
95
+ """
96
+ Check if a database type is supported.
97
+
98
+ Args:
99
+ db_type: Database type identifier
100
+
101
+ Returns:
102
+ True if supported, False otherwise
103
+ """
104
+ return db_type in DbPluginRegistry.list_plugins()
105
+
106
+ @staticmethod
107
+ def get_required_parameters(db_type: str) -> Dict[str, Any]:
108
+ """
109
+ Get required connection parameters for a database type.
110
+
111
+ Args:
112
+ db_type: Database type identifier
113
+
114
+ Returns:
115
+ Dictionary describing required parameters
116
+ """
117
+ try:
118
+ plugin_class = DbPluginRegistry.get_plugin_class(db_type)
119
+
120
+ # This would ideally be defined in the plugin class
121
+ # For now, return common parameters based on database type
122
+ common_params = {
123
+ "postgresql": {
124
+ "required": ["host", "port", "database", "user", "password"],
125
+ "optional": ["schema", "sslmode", "connect_timeout"]
126
+ },
127
+ "supabase": {
128
+ "required": ["host", "port", "database", "user", "password"],
129
+ "optional": ["schema", "sslmode", "connect_timeout", "project_url", "api_key", "use_rest_api"]
130
+ },
131
+ "sqlite": {
132
+ "required": ["database_path"],
133
+ "optional": ["timeout", "check_same_thread"]
134
+ },
135
+ "mysql": {
136
+ "required": ["host", "port", "database", "user", "password"],
137
+ "optional": ["charset", "autocommit", "connect_timeout"]
138
+ },
139
+ "mariadb": {
140
+ "required": ["host", "port", "database", "user", "password"],
141
+ "optional": ["charset", "autocommit", "connect_timeout"]
142
+ },
143
+ "sqlserver": {
144
+ "required": ["server", "database", "user", "password"],
145
+ "optional": ["driver", "trusted_connection", "timeout"]
146
+ },
147
+ "oracle": {
148
+ "required": ["host", "port", "service_name", "user", "password"],
149
+ "optional": ["encoding", "nencoding", "threaded"]
150
+ },
151
+ "informix": {
152
+ "required": ["server", "database", "host", "user", "password"],
153
+ "optional": ["protocol", "service", "timeout"]
154
+ }
155
+ }
156
+
157
+ return common_params.get(db_type, {
158
+ "required": [],
159
+ "optional": [],
160
+ "note": f"Parameters for {db_type} not defined. Check plugin documentation."
161
+ })
162
+
163
+ except ValueError:
164
+ return {
165
+ "error": f"Database type '{db_type}' not supported",
166
+ "available_types": DbPluginRegistry.list_plugins()
167
+ }
168
+
169
+ @staticmethod
170
+ def create_with_validation(db_type: str, db_root_path: str, db_mode: str = "dev", **kwargs) -> DbPlugin:
171
+ """
172
+ Create a database manager with parameter validation.
173
+
174
+ Args:
175
+ db_type: Database type identifier
176
+ db_root_path: Path to database root directory
177
+ db_mode: Database mode
178
+ **kwargs: Connection parameters
179
+
180
+ Returns:
181
+ Database plugin instance
182
+
183
+ Raises:
184
+ ValueError: If parameters are invalid
185
+ RuntimeError: If creation fails
186
+ """
187
+ # Validate database type
188
+ if not ThothDbFactory.validate_database_type(db_type):
189
+ available = ThothDbFactory.list_available_databases()
190
+ raise ValueError(f"Unsupported database type '{db_type}'. Available: {available}")
191
+
192
+ # Get required parameters
193
+ param_info = ThothDbFactory.get_required_parameters(db_type)
194
+
195
+ if "required" in param_info:
196
+ # Check required parameters
197
+ missing_params = []
198
+ for param in param_info["required"]:
199
+ if param not in kwargs:
200
+ missing_params.append(param)
201
+
202
+ if missing_params:
203
+ raise ValueError(f"Missing required parameters for {db_type}: {missing_params}")
204
+
205
+ # Create the manager
206
+ return ThothDbFactory.create_manager(db_type, db_root_path, db_mode, **kwargs)
207
+
208
+ @staticmethod
209
+ def create_from_config(config: Dict[str, Any]) -> DbPlugin:
210
+ """
211
+ Create a database manager from a configuration dictionary.
212
+
213
+ Args:
214
+ config: Configuration dictionary containing all parameters
215
+
216
+ Returns:
217
+ Database plugin instance
218
+
219
+ Example config:
220
+ {
221
+ "db_type": "postgresql",
222
+ "db_root_path": "/path/to/db",
223
+ "db_mode": "dev",
224
+ "host": "localhost",
225
+ "port": 5432,
226
+ "database": "mydb",
227
+ "user": "user",
228
+ "password": "pass"
229
+ }
230
+ """
231
+ # Extract factory parameters
232
+ db_type = config.pop("db_type")
233
+ db_root_path = config.pop("db_root_path")
234
+ db_mode = config.pop("db_mode", "dev")
235
+
236
+ # Remaining parameters are connection parameters
237
+ return ThothDbFactory.create_with_validation(
238
+ db_type=db_type,
239
+ db_root_path=db_root_path,
240
+ db_mode=db_mode,
241
+ **config
242
+ )
243
+
244
+ @staticmethod
245
+ def get_plugin_status() -> Dict[str, Any]:
246
+ """
247
+ Get status information about all registered plugins.
248
+
249
+ Returns:
250
+ Status information for all plugins
251
+ """
252
+ plugins = DbPluginRegistry.list_plugins()
253
+ status = {
254
+ "total_plugins": len(plugins),
255
+ "available_types": plugins,
256
+ "plugins": {}
257
+ }
258
+
259
+ for db_type in plugins:
260
+ try:
261
+ plugin_info = DbPluginRegistry.get_plugin_info(db_type)
262
+ status["plugins"][db_type] = {
263
+ "status": "available",
264
+ "info": plugin_info
265
+ }
266
+ except Exception as e:
267
+ status["plugins"][db_type] = {
268
+ "status": "error",
269
+ "error": str(e)
270
+ }
271
+
272
+ return status