memorisdk 1.0.2__py3-none-any.whl → 2.0.1__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 memorisdk might be problematic. Click here for more details.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +491 -68
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1683 -532
- memori/core/providers.py +217 -0
- memori/database/adapters/__init__.py +10 -0
- memori/database/adapters/mysql_adapter.py +331 -0
- memori/database/adapters/postgresql_adapter.py +291 -0
- memori/database/adapters/sqlite_adapter.py +229 -0
- memori/database/auto_creator.py +320 -0
- memori/database/connection_utils.py +207 -0
- memori/database/connectors/base_connector.py +283 -0
- memori/database/connectors/mysql_connector.py +240 -18
- memori/database/connectors/postgres_connector.py +277 -4
- memori/database/connectors/sqlite_connector.py +178 -3
- memori/database/models.py +400 -0
- memori/database/queries/base_queries.py +1 -1
- memori/database/queries/memory_queries.py +91 -2
- memori/database/query_translator.py +222 -0
- memori/database/schema_generators/__init__.py +7 -0
- memori/database/schema_generators/mysql_schema_generator.py +215 -0
- memori/database/search/__init__.py +8 -0
- memori/database/search/mysql_search_adapter.py +255 -0
- memori/database/search/sqlite_search_adapter.py +180 -0
- memori/database/search_service.py +700 -0
- memori/database/sqlalchemy_manager.py +888 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- memori/tools/memory_tool.py +94 -4
- memori/utils/input_validator.py +395 -0
- memori/utils/pydantic_models.py +138 -36
- memori/utils/query_builder.py +530 -0
- memori/utils/security_audit.py +594 -0
- memori/utils/security_integration.py +339 -0
- memori/utils/transaction_manager.py +547 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/METADATA +56 -23
- memorisdk-2.0.1.dist-info/RECORD +66 -0
- memori/scripts/llm_text.py +0 -50
- memorisdk-1.0.2.dist-info/RECORD +0 -44
- memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Auto-Creation System
|
|
3
|
+
|
|
4
|
+
This module automatically creates databases if they don't exist, supporting
|
|
5
|
+
PostgreSQL and MySQL with proper error handling and security validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ssl
|
|
9
|
+
from typing import Dict
|
|
10
|
+
from urllib.parse import parse_qs, urlparse
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
from sqlalchemy import create_engine, text
|
|
14
|
+
from sqlalchemy.exc import OperationalError, ProgrammingError
|
|
15
|
+
|
|
16
|
+
from .connection_utils import DatabaseConnectionUtils
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatabaseAutoCreator:
|
|
20
|
+
"""Handles automatic database creation for PostgreSQL and MySQL"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, schema_init: bool = True):
|
|
23
|
+
"""
|
|
24
|
+
Initialize database auto-creator.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
schema_init: Whether to enable automatic database creation
|
|
28
|
+
"""
|
|
29
|
+
self.schema_init = schema_init
|
|
30
|
+
self.utils = DatabaseConnectionUtils()
|
|
31
|
+
|
|
32
|
+
def ensure_database_exists(self, connection_string: str) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Ensure target database exists, creating it if necessary.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
connection_string: Original database connection string
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Connection string to use (may be unchanged if creation not needed)
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
DatabaseCreationError: If database creation fails
|
|
44
|
+
"""
|
|
45
|
+
if not self.schema_init:
|
|
46
|
+
logger.debug("Auto-creation disabled, using original connection string")
|
|
47
|
+
return connection_string
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Parse connection string
|
|
51
|
+
components = self.utils.parse_connection_string(connection_string)
|
|
52
|
+
|
|
53
|
+
# SQLite doesn't need database creation
|
|
54
|
+
if not components["needs_creation"]:
|
|
55
|
+
logger.debug(
|
|
56
|
+
f"Database engine {components['engine']} auto-creates, no action needed"
|
|
57
|
+
)
|
|
58
|
+
return connection_string
|
|
59
|
+
|
|
60
|
+
# Validate database name
|
|
61
|
+
if not self.utils.validate_database_name(components["database"]):
|
|
62
|
+
raise ValueError(f"Invalid database name: {components['database']}")
|
|
63
|
+
|
|
64
|
+
# Check if database exists
|
|
65
|
+
if self._database_exists(components):
|
|
66
|
+
logger.debug(f"Database '{components['database']}' already exists")
|
|
67
|
+
return connection_string
|
|
68
|
+
|
|
69
|
+
# Create database
|
|
70
|
+
self._create_database(components)
|
|
71
|
+
logger.info(f"Successfully created database '{components['database']}'")
|
|
72
|
+
return connection_string
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Database auto-creation failed: {e}")
|
|
76
|
+
# Don't raise exception - let the original connection attempt proceed
|
|
77
|
+
# This allows graceful degradation if user has manual setup
|
|
78
|
+
return connection_string
|
|
79
|
+
|
|
80
|
+
def _database_exists(self, components: Dict[str, str]) -> bool:
|
|
81
|
+
"""Check if target database exists."""
|
|
82
|
+
try:
|
|
83
|
+
engine = components["engine"]
|
|
84
|
+
|
|
85
|
+
if engine == "postgresql":
|
|
86
|
+
return self._postgresql_database_exists(components)
|
|
87
|
+
elif engine == "mysql":
|
|
88
|
+
return self._mysql_database_exists(components)
|
|
89
|
+
else:
|
|
90
|
+
logger.warning(f"Database existence check not supported for {engine}")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Failed to check database existence: {e}")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def _postgresql_database_exists(self, components: Dict[str, str]) -> bool:
|
|
98
|
+
"""Check if PostgreSQL database exists."""
|
|
99
|
+
try:
|
|
100
|
+
# Connect to postgres system database
|
|
101
|
+
engine = create_engine(components["default_url"])
|
|
102
|
+
|
|
103
|
+
with engine.connect() as conn:
|
|
104
|
+
result = conn.execute(
|
|
105
|
+
text("SELECT 1 FROM pg_database WHERE datname = :dbname"),
|
|
106
|
+
{"dbname": components["database"]},
|
|
107
|
+
)
|
|
108
|
+
exists = result.fetchone() is not None
|
|
109
|
+
|
|
110
|
+
engine.dispose()
|
|
111
|
+
return exists
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"PostgreSQL database existence check failed: {e}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def _get_mysql_connect_args(self, original_url: str) -> Dict:
|
|
118
|
+
"""Get MySQL connection arguments with SSL support for system database connections."""
|
|
119
|
+
connect_args = {"charset": "utf8mb4"}
|
|
120
|
+
|
|
121
|
+
# Parse original URL for SSL parameters
|
|
122
|
+
parsed = urlparse(original_url)
|
|
123
|
+
if parsed.query:
|
|
124
|
+
query_params = parse_qs(parsed.query)
|
|
125
|
+
|
|
126
|
+
# Handle SSL parameters for PyMySQL - same logic as sqlalchemy_manager
|
|
127
|
+
if any(key in query_params for key in ["ssl", "ssl_disabled"]):
|
|
128
|
+
if query_params.get("ssl", ["false"])[0].lower() == "true":
|
|
129
|
+
# Enable SSL with secure configuration for required secure transport
|
|
130
|
+
connect_args["ssl"] = {
|
|
131
|
+
"ssl_disabled": False,
|
|
132
|
+
"check_hostname": False,
|
|
133
|
+
"verify_mode": ssl.CERT_NONE,
|
|
134
|
+
}
|
|
135
|
+
# Also add ssl_disabled=False for PyMySQL
|
|
136
|
+
connect_args["ssl_disabled"] = False
|
|
137
|
+
elif query_params.get("ssl_disabled", ["true"])[0].lower() == "false":
|
|
138
|
+
# Enable SSL with secure configuration for required secure transport
|
|
139
|
+
connect_args["ssl"] = {
|
|
140
|
+
"ssl_disabled": False,
|
|
141
|
+
"check_hostname": False,
|
|
142
|
+
"verify_mode": ssl.CERT_NONE,
|
|
143
|
+
}
|
|
144
|
+
# Also add ssl_disabled=False for PyMySQL
|
|
145
|
+
connect_args["ssl_disabled"] = False
|
|
146
|
+
|
|
147
|
+
return connect_args
|
|
148
|
+
|
|
149
|
+
def _mysql_database_exists(self, components: Dict[str, str]) -> bool:
|
|
150
|
+
"""Check if MySQL database exists."""
|
|
151
|
+
try:
|
|
152
|
+
# Connect to mysql system database with SSL support
|
|
153
|
+
connect_args = self._get_mysql_connect_args(components["original_url"])
|
|
154
|
+
engine = create_engine(components["default_url"], connect_args=connect_args)
|
|
155
|
+
|
|
156
|
+
with engine.connect() as conn:
|
|
157
|
+
result = conn.execute(
|
|
158
|
+
text(
|
|
159
|
+
"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = :dbname"
|
|
160
|
+
),
|
|
161
|
+
{"dbname": components["database"]},
|
|
162
|
+
)
|
|
163
|
+
exists = result.fetchone() is not None
|
|
164
|
+
|
|
165
|
+
engine.dispose()
|
|
166
|
+
return exists
|
|
167
|
+
|
|
168
|
+
except ModuleNotFoundError as e:
|
|
169
|
+
if "mysql" in str(e).lower():
|
|
170
|
+
logger.error(f"MySQL database existence check failed: {e}")
|
|
171
|
+
error_msg = (
|
|
172
|
+
"❌ MySQL driver not found for database existence check. Install one of:\n"
|
|
173
|
+
"- pip install mysql-connector-python\n"
|
|
174
|
+
"- pip install PyMySQL\n"
|
|
175
|
+
"- pip install memorisdk[mysql]"
|
|
176
|
+
)
|
|
177
|
+
logger.error(error_msg)
|
|
178
|
+
return False
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f"MySQL database existence check failed: {e}")
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def _create_database(self, components: Dict[str, str]) -> None:
|
|
184
|
+
"""Create the target database."""
|
|
185
|
+
engine = components["engine"]
|
|
186
|
+
|
|
187
|
+
if engine == "postgresql":
|
|
188
|
+
self._create_postgresql_database(components)
|
|
189
|
+
elif engine == "mysql":
|
|
190
|
+
self._create_mysql_database(components)
|
|
191
|
+
else:
|
|
192
|
+
raise ValueError(f"Database creation not supported for {engine}")
|
|
193
|
+
|
|
194
|
+
def _create_postgresql_database(self, components: Dict[str, str]) -> None:
|
|
195
|
+
"""Create PostgreSQL database."""
|
|
196
|
+
try:
|
|
197
|
+
logger.info(f"Creating PostgreSQL database '{components['database']}'...")
|
|
198
|
+
|
|
199
|
+
# Connect to postgres system database
|
|
200
|
+
engine = create_engine(components["default_url"])
|
|
201
|
+
|
|
202
|
+
# PostgreSQL requires autocommit for CREATE DATABASE
|
|
203
|
+
with engine.connect() as conn:
|
|
204
|
+
# Set autocommit mode
|
|
205
|
+
conn = conn.execution_options(isolation_level="AUTOCOMMIT")
|
|
206
|
+
|
|
207
|
+
# Create database (can't use parameters for database name)
|
|
208
|
+
# Database name is already validated, so this is safe
|
|
209
|
+
conn.execute(text(f'CREATE DATABASE "{components["database"]}"'))
|
|
210
|
+
|
|
211
|
+
engine.dispose()
|
|
212
|
+
logger.info(
|
|
213
|
+
f"PostgreSQL database '{components['database']}' created successfully"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
except (OperationalError, ProgrammingError) as e:
|
|
217
|
+
error_msg = str(e)
|
|
218
|
+
if "already exists" in error_msg.lower():
|
|
219
|
+
logger.info(
|
|
220
|
+
f"PostgreSQL database '{components['database']}' already exists"
|
|
221
|
+
)
|
|
222
|
+
return
|
|
223
|
+
elif "permission denied" in error_msg.lower():
|
|
224
|
+
raise PermissionError(
|
|
225
|
+
f"Insufficient permissions to create database '{components['database']}'"
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
raise RuntimeError(f"Failed to create PostgreSQL database: {e}")
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise RuntimeError(f"Unexpected error creating PostgreSQL database: {e}")
|
|
232
|
+
|
|
233
|
+
def _create_mysql_database(self, components: Dict[str, str]) -> None:
|
|
234
|
+
"""Create MySQL database."""
|
|
235
|
+
try:
|
|
236
|
+
logger.info(f"Creating MySQL database '{components['database']}'...")
|
|
237
|
+
|
|
238
|
+
# Connect to mysql system database with SSL support
|
|
239
|
+
connect_args = self._get_mysql_connect_args(components["original_url"])
|
|
240
|
+
engine = create_engine(components["default_url"], connect_args=connect_args)
|
|
241
|
+
|
|
242
|
+
with engine.connect() as conn:
|
|
243
|
+
# Create database (can't use parameters for database name)
|
|
244
|
+
# Database name is already validated, so this is safe
|
|
245
|
+
conn.execute(
|
|
246
|
+
text(
|
|
247
|
+
f'CREATE DATABASE `{components["database"]}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
conn.commit()
|
|
251
|
+
|
|
252
|
+
engine.dispose()
|
|
253
|
+
logger.info(
|
|
254
|
+
f"MySQL database '{components['database']}' created successfully"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
except ModuleNotFoundError as e:
|
|
258
|
+
if "mysql" in str(e).lower():
|
|
259
|
+
error_msg = (
|
|
260
|
+
"❌ MySQL driver not found for database creation. Install one of:\n"
|
|
261
|
+
"- pip install mysql-connector-python\n"
|
|
262
|
+
"- pip install PyMySQL\n"
|
|
263
|
+
"- pip install memorisdk[mysql]"
|
|
264
|
+
)
|
|
265
|
+
logger.error(error_msg)
|
|
266
|
+
raise RuntimeError(error_msg)
|
|
267
|
+
else:
|
|
268
|
+
raise RuntimeError(f"Missing dependency for database creation: {e}")
|
|
269
|
+
except (OperationalError, ProgrammingError) as e:
|
|
270
|
+
error_msg = str(e)
|
|
271
|
+
if "database exists" in error_msg.lower():
|
|
272
|
+
logger.info(f"MySQL database '{components['database']}' already exists")
|
|
273
|
+
return
|
|
274
|
+
elif "access denied" in error_msg.lower():
|
|
275
|
+
raise PermissionError(
|
|
276
|
+
f"Insufficient permissions to create database '{components['database']}'"
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
raise RuntimeError(f"Failed to create MySQL database: {e}")
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
raise RuntimeError(f"Unexpected error creating MySQL database: {e}")
|
|
283
|
+
|
|
284
|
+
def get_database_info(self, connection_string: str) -> Dict[str, str]:
|
|
285
|
+
"""
|
|
286
|
+
Get detailed information about database from connection string.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
connection_string: Database connection URL
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Dictionary with database information
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
components = self.utils.parse_connection_string(connection_string)
|
|
296
|
+
|
|
297
|
+
info = {
|
|
298
|
+
"engine": components["engine"],
|
|
299
|
+
"database": components["database"],
|
|
300
|
+
"host": components["host"],
|
|
301
|
+
"port": components["port"],
|
|
302
|
+
"needs_creation": components["needs_creation"],
|
|
303
|
+
"auto_creation_enabled": self.schema_init,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Add existence check if auto-creation is enabled
|
|
307
|
+
if self.schema_init and components["needs_creation"]:
|
|
308
|
+
info["exists"] = self._database_exists(components)
|
|
309
|
+
|
|
310
|
+
return info
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.error(f"Failed to get database info: {e}")
|
|
314
|
+
return {"error": str(e)}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class DatabaseCreationError(Exception):
|
|
318
|
+
"""Raised when database creation fails"""
|
|
319
|
+
|
|
320
|
+
pass
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Connection Utilities for Auto-Creation System
|
|
3
|
+
|
|
4
|
+
This module handles parsing connection strings, creating databases automatically,
|
|
5
|
+
and managing multi-database scenarios for memori instances.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, Tuple
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DatabaseConnectionUtils:
|
|
16
|
+
"""Utilities for parsing and managing database connections"""
|
|
17
|
+
|
|
18
|
+
# Default system databases for each engine
|
|
19
|
+
DEFAULT_DATABASES = {
|
|
20
|
+
"postgresql": "postgres",
|
|
21
|
+
"mysql": "mysql",
|
|
22
|
+
"sqlite": None, # SQLite doesn't need default DB
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def parse_connection_string(cls, connection_string: str) -> Dict[str, str]:
|
|
27
|
+
"""
|
|
28
|
+
Parse database connection string and extract components.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
connection_string: Database connection URL
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dictionary with parsed components
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
mysql://root:pass@localhost:3306/memori_dev
|
|
38
|
+
-> {
|
|
39
|
+
'engine': 'mysql',
|
|
40
|
+
'user': 'root',
|
|
41
|
+
'password': 'pass',
|
|
42
|
+
'host': 'localhost',
|
|
43
|
+
'port': 3306,
|
|
44
|
+
'database': 'memori_dev',
|
|
45
|
+
'base_url': 'mysql://root:pass@localhost:3306',
|
|
46
|
+
'default_url': 'mysql://root:pass@localhost:3306/mysql'
|
|
47
|
+
}
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
parsed = urlparse(connection_string)
|
|
51
|
+
|
|
52
|
+
# Extract components
|
|
53
|
+
engine = parsed.scheme.split("+")[
|
|
54
|
+
0
|
|
55
|
+
].lower() # Handle postgresql+psycopg2, mysql+pymysql
|
|
56
|
+
driver = parsed.scheme.split("+")[1] if "+" in parsed.scheme else None
|
|
57
|
+
user = parsed.username or ""
|
|
58
|
+
password = parsed.password or ""
|
|
59
|
+
host = parsed.hostname or "localhost"
|
|
60
|
+
port = parsed.port
|
|
61
|
+
database = parsed.path.lstrip("/") if parsed.path else ""
|
|
62
|
+
|
|
63
|
+
# Build base URL without database, preserving the driver
|
|
64
|
+
auth = f"{user}:{password}@" if user or password else ""
|
|
65
|
+
if user and not password:
|
|
66
|
+
auth = f"{user}@"
|
|
67
|
+
|
|
68
|
+
port_str = f":{port}" if port else ""
|
|
69
|
+
scheme = f"{engine}+{driver}" if driver else engine
|
|
70
|
+
base_url = f"{scheme}://{auth}{host}{port_str}"
|
|
71
|
+
|
|
72
|
+
# Create default database URL for system operations
|
|
73
|
+
default_db = cls.DEFAULT_DATABASES.get(engine)
|
|
74
|
+
|
|
75
|
+
# Preserve query parameters (especially SSL settings) for system database connections
|
|
76
|
+
query_string = f"?{parsed.query}" if parsed.query else ""
|
|
77
|
+
default_url = (
|
|
78
|
+
f"{base_url}/{default_db}{query_string}"
|
|
79
|
+
if default_db
|
|
80
|
+
else f"{base_url}{query_string}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"engine": engine,
|
|
85
|
+
"user": user,
|
|
86
|
+
"password": password,
|
|
87
|
+
"host": host,
|
|
88
|
+
"port": port,
|
|
89
|
+
"database": database,
|
|
90
|
+
"base_url": base_url,
|
|
91
|
+
"default_url": default_url,
|
|
92
|
+
"original_url": connection_string,
|
|
93
|
+
"needs_creation": engine
|
|
94
|
+
in ["postgresql", "mysql"], # SQLite auto-creates
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Failed to parse connection string: {e}")
|
|
99
|
+
raise ValueError(f"Invalid connection string format: {connection_string}")
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def build_connection_string(
|
|
103
|
+
cls, components: Dict[str, str], target_database: str
|
|
104
|
+
) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Build connection string with specific database name.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
components: Parsed connection components
|
|
110
|
+
target_database: Database name to connect to
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Complete connection string
|
|
114
|
+
"""
|
|
115
|
+
return f"{components['base_url']}/{target_database}"
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def validate_database_name(cls, database_name: str) -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Validate database name for security and compatibility.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
database_name: Name to validate
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if valid, False otherwise
|
|
127
|
+
"""
|
|
128
|
+
if not database_name:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Basic SQL injection prevention
|
|
132
|
+
if any(
|
|
133
|
+
char in database_name.lower()
|
|
134
|
+
for char in [";", "'", '"', "\\", "/", "*", "?", "<", ">", "|"]
|
|
135
|
+
):
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
# Check length (most databases have limits)
|
|
139
|
+
if len(database_name) > 64: # MySQL limit
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
# Must start with letter or underscore, contain only alphanumeric, underscore, hyphen
|
|
143
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_-]*$", database_name):
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
# Reserved words check (only for database creation, not connection)
|
|
147
|
+
# Note: 'postgres' is a valid system database to connect to in PostgreSQL
|
|
148
|
+
reserved_words = ["mysql", "information_schema", "performance_schema", "sys"]
|
|
149
|
+
if database_name.lower() in reserved_words:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def generate_database_name(
|
|
156
|
+
cls, base_name: str = "memori", suffix: str = None, prefix: str = None
|
|
157
|
+
) -> str:
|
|
158
|
+
"""
|
|
159
|
+
Generate a valid database name with optional prefix/suffix.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
base_name: Base database name
|
|
163
|
+
suffix: Optional suffix (e.g., "dev", "prod", "test")
|
|
164
|
+
prefix: Optional prefix (e.g., "company", "project")
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Generated database name
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
generate_database_name() -> "memori"
|
|
171
|
+
generate_database_name(suffix="dev") -> "memori_dev"
|
|
172
|
+
generate_database_name(prefix="acme", suffix="prod") -> "acme_memori_prod"
|
|
173
|
+
"""
|
|
174
|
+
parts = []
|
|
175
|
+
|
|
176
|
+
if prefix:
|
|
177
|
+
parts.append(prefix)
|
|
178
|
+
|
|
179
|
+
parts.append(base_name)
|
|
180
|
+
|
|
181
|
+
if suffix:
|
|
182
|
+
parts.append(suffix)
|
|
183
|
+
|
|
184
|
+
database_name = "_".join(parts)
|
|
185
|
+
|
|
186
|
+
if not cls.validate_database_name(database_name):
|
|
187
|
+
raise ValueError(f"Generated database name is invalid: {database_name}")
|
|
188
|
+
|
|
189
|
+
return database_name
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def extract_database_info(cls, connection_string: str) -> Tuple[str, str, bool]:
|
|
193
|
+
"""
|
|
194
|
+
Extract database engine, name, and creation requirement.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
connection_string: Database connection URL
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Tuple of (engine, database_name, needs_creation)
|
|
201
|
+
"""
|
|
202
|
+
components = cls.parse_connection_string(connection_string)
|
|
203
|
+
return (
|
|
204
|
+
components["engine"],
|
|
205
|
+
components["database"],
|
|
206
|
+
components["needs_creation"],
|
|
207
|
+
)
|