maleo-database 0.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.
File without changes
@@ -0,0 +1,105 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Generic
3
+ from .additional import AdditionalConfigT, RedisAdditionalConfig
4
+ from .connection import (
5
+ ConnectionConfigT,
6
+ PostgreSQLConnectionConfig,
7
+ MySQLConnectionConfig,
8
+ SQLiteConnectionConfig,
9
+ SQLServerConnectionConfig,
10
+ MongoDBConnectionConfig,
11
+ RedisConnectionConfig,
12
+ ElasticsearchConnectionConfig,
13
+ )
14
+ from .identifier import DatabaseIdentifierConfig
15
+ from .pooling import (
16
+ PoolingConfigT,
17
+ PostgreSQLPoolingConfig,
18
+ MySQLPoolingConfig,
19
+ SQLitePoolingConfig,
20
+ SQLServerPoolingConfig,
21
+ MongoDBPoolingConfig,
22
+ RedisPoolingConfig,
23
+ ElasticsearchPoolingConfig,
24
+ )
25
+
26
+
27
+ class BaseDatabaseConfig(
28
+ BaseModel, Generic[ConnectionConfigT, PoolingConfigT, AdditionalConfigT]
29
+ ):
30
+ """Base configuration for database."""
31
+
32
+ identifier: DatabaseIdentifierConfig = Field(..., description="Identifier config")
33
+ connection: ConnectionConfigT = Field(..., description="Connection config")
34
+ pooling: PoolingConfigT = Field(..., description="Pooling config")
35
+ additional: AdditionalConfigT = Field(..., description="Additional config")
36
+
37
+
38
+ class PostgreSQLDatabaseConfig(
39
+ BaseDatabaseConfig[
40
+ PostgreSQLConnectionConfig,
41
+ PostgreSQLPoolingConfig,
42
+ None,
43
+ ]
44
+ ):
45
+ additional: None = None
46
+
47
+
48
+ class MySQLDatabaseConfig(
49
+ BaseDatabaseConfig[
50
+ MySQLConnectionConfig,
51
+ MySQLPoolingConfig,
52
+ None,
53
+ ]
54
+ ):
55
+ additional: None = None
56
+
57
+
58
+ class SQLiteDatabaseConfig(
59
+ BaseDatabaseConfig[
60
+ SQLiteConnectionConfig,
61
+ SQLitePoolingConfig,
62
+ None,
63
+ ]
64
+ ):
65
+ additional: None = None
66
+
67
+
68
+ class SQLServerDatabaseConfig(
69
+ BaseDatabaseConfig[
70
+ SQLServerConnectionConfig,
71
+ SQLServerPoolingConfig,
72
+ None,
73
+ ]
74
+ ):
75
+ additional: None = None
76
+
77
+
78
+ class MongoDBDatabaseConfig(
79
+ BaseDatabaseConfig[
80
+ MongoDBConnectionConfig,
81
+ MongoDBPoolingConfig,
82
+ None,
83
+ ]
84
+ ):
85
+ additional: None = None
86
+
87
+
88
+ class RedisDatabaseConfig(
89
+ BaseDatabaseConfig[
90
+ RedisConnectionConfig,
91
+ RedisPoolingConfig,
92
+ RedisAdditionalConfig,
93
+ ]
94
+ ):
95
+ additional: RedisAdditionalConfig = Field(..., description="Additional config")
96
+
97
+
98
+ class ElasticsearchDatabaseConfig(
99
+ BaseDatabaseConfig[
100
+ ElasticsearchConnectionConfig,
101
+ ElasticsearchPoolingConfig,
102
+ None,
103
+ ]
104
+ ):
105
+ additional: None = None
@@ -0,0 +1,36 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, TypeVar, Union
3
+ from maleo.enums.cache import Origin, Layer
4
+ from maleo.enums.expiration import Expiration
5
+ from maleo.types.base.string import OptionalString
6
+
7
+
8
+ class RedisCacheNamespaces(BaseModel):
9
+ base: str = Field(..., description="Base Redis's namespace")
10
+
11
+ def create(
12
+ self,
13
+ *ext: str,
14
+ origin: Origin,
15
+ layer: Layer,
16
+ base_override: OptionalString = None,
17
+ ) -> str:
18
+ return ":".join(
19
+ [self.base if base_override is None else base_override, origin, layer, *ext]
20
+ )
21
+
22
+
23
+ class BaseAdditionalConfig(BaseModel):
24
+ """Base additional configuration class for database."""
25
+
26
+
27
+ AdditionalConfigT = TypeVar("AdditionalConfigT", bound=Optional[BaseAdditionalConfig])
28
+
29
+
30
+ class RedisAdditionalConfig(BaseAdditionalConfig):
31
+ ttl: Union[float, int] = Field(
32
+ Expiration.EXP_15MN.value, description="Time to live"
33
+ )
34
+ namespaces: RedisCacheNamespaces = Field(
35
+ ..., description="Redis cache's namepsaces"
36
+ )
@@ -0,0 +1,544 @@
1
+ from pydantic import BaseModel, Field, model_validator
2
+ from typing import Generic, Literal, Optional, TypeVar
3
+ from urllib.parse import quote_plus, urlencode
4
+ from maleo.types.base.boolean import OptionalBoolean
5
+ from maleo.types.base.dict import (
6
+ OptionalStringToAnyDict,
7
+ StringToAnyDict,
8
+ StringToStringDict,
9
+ )
10
+ from maleo.types.base.integer import OptionalInteger
11
+ from maleo.types.base.string import OptionalString
12
+ from ..enums import (
13
+ Connection,
14
+ Driver,
15
+ PostgreSQLSSLMode,
16
+ MySQLCharset,
17
+ MongoDBReadPreference,
18
+ ElasticsearchScheme,
19
+ )
20
+
21
+
22
+ DriverT = TypeVar("DriverT", bound=Driver)
23
+ UsernameT = TypeVar("UsernameT", bound=OptionalString)
24
+ PasswordT = TypeVar("PasswordT", bound=OptionalString)
25
+ HostT = TypeVar("HostT", bound=OptionalString)
26
+ PortT = TypeVar("PortT", bound=OptionalInteger)
27
+ DatabaseT = TypeVar("DatabaseT", int, str)
28
+
29
+
30
+ class BaseConnectionConfig(
31
+ BaseModel, Generic[DriverT, UsernameT, PasswordT, HostT, PortT, DatabaseT]
32
+ ):
33
+ driver: DriverT = Field(..., description="Database's driver")
34
+ username: UsernameT = Field(..., description="Database user's username")
35
+ password: PasswordT = Field(
36
+ ..., min_length=1, description="Database user's password"
37
+ )
38
+ host: HostT = Field(..., description="Database's host")
39
+ port: PortT = Field(..., ge=1, le=65535, description="Database's port")
40
+ database: DatabaseT = Field(..., description="Database's name")
41
+
42
+ # Optional fields for different database types
43
+ auth_source: OptionalString = Field(
44
+ None, description="Authentication database (MongoDB)"
45
+ )
46
+ ssl: OptionalBoolean = Field(None, description="Enable SSL connection")
47
+ options: OptionalStringToAnyDict = Field(
48
+ default_factory=dict, description="Additional connection options"
49
+ )
50
+
51
+ @model_validator(mode="after")
52
+ def validate_required_fields_per_driver(self):
53
+ """Validate that required fields are present based on driver type"""
54
+ if self.driver is Driver.SQLITE:
55
+ if not self.database:
56
+ raise ValueError("SQLite requires database file path")
57
+ elif self.driver in [
58
+ Driver.POSTGRESQL,
59
+ Driver.MYSQL,
60
+ Driver.MSSQL,
61
+ ]:
62
+ if not self.host:
63
+ raise ValueError(f"{self.driver} requires host")
64
+ if not self.username:
65
+ raise ValueError(f"{self.driver} requires username")
66
+ if not self.password:
67
+ raise ValueError(f"{self.driver} requires password")
68
+ elif self.driver is Driver.REDIS:
69
+ if not self.host:
70
+ raise ValueError("Redis requires host")
71
+ elif self.driver is Driver.MONGODB:
72
+ if not self.host:
73
+ raise ValueError("MongoDB requires host")
74
+
75
+ return self
76
+
77
+ def _safe_encode_credential(self, credential: OptionalString) -> str:
78
+ """Safely URL-encode credentials to handle special characters."""
79
+ return quote_plus(credential) if credential else ""
80
+
81
+ def _make_options_string(
82
+ self, additional_options: OptionalStringToAnyDict = None
83
+ ) -> str:
84
+ """Build URL query string from options, removing null values."""
85
+ all_options: StringToAnyDict = dict(self.options) if self.options else {}
86
+ if additional_options:
87
+ all_options.update(additional_options)
88
+
89
+ # Filter out None values
90
+ all_options = {k: v for k, v in all_options.items() if v is not None}
91
+
92
+ if not all_options:
93
+ return ""
94
+
95
+ # Convert boolean values to lowercase strings
96
+ formatted_options = {
97
+ k: str(v).lower() if isinstance(v, bool) else str(v)
98
+ for k, v in all_options.items()
99
+ }
100
+
101
+ return "?" + urlencode(formatted_options)
102
+
103
+ def make_async_url(self) -> str:
104
+ return self.make_url(Connection.ASYNC)
105
+
106
+ def make_sync_url(self) -> str:
107
+ return self.make_url(Connection.SYNC)
108
+
109
+ def make_url(self, connection: Connection = Connection.ASYNC) -> str:
110
+ """Build URL based on driver-specific format with async/sync support."""
111
+ if self.driver is Driver.POSTGRESQL:
112
+ return self._make_postgresql_url(connection)
113
+ elif self.driver is Driver.MYSQL:
114
+ return self._make_mysql_url(connection)
115
+ elif self.driver is Driver.SQLITE:
116
+ return self._make_sqlite_url(connection)
117
+ elif self.driver is Driver.MONGODB:
118
+ return self._make_mongodb_url()
119
+ elif self.driver is Driver.REDIS:
120
+ return self._make_redis_url()
121
+ elif self.driver is Driver.MSSQL:
122
+ return self._make_mssql_url(connection)
123
+ elif self.driver is Driver.ELASTICSEARCH:
124
+ return self._make_elasticsearch_url()
125
+ else:
126
+ # Fallback to standard format
127
+ encoded_user = self._safe_encode_credential(self.username)
128
+ encoded_pass = self._safe_encode_credential(self.password)
129
+ return f"{self.driver}://{encoded_user}:{encoded_pass}@{self.host}:{self.port}/{self.database}"
130
+
131
+ def _make_postgresql_url(self, connection: Connection = Connection.ASYNC) -> str:
132
+ """PostgreSQL database URL format with proper async/sync drivers."""
133
+ raise NotImplementedError()
134
+
135
+ def _make_mysql_url(self, connection: Connection = Connection.ASYNC) -> str:
136
+ """MySQL database URL format with proper async/sync drivers."""
137
+ raise NotImplementedError()
138
+
139
+ def _make_sqlite_url(self, connection: Connection = Connection.ASYNC) -> str:
140
+ """SQLite URL format with async/sync support."""
141
+ raise NotImplementedError()
142
+
143
+ def _make_mongodb_url(self) -> str:
144
+ """MongoDB URL format - MongoDB driver handles async/sync internally."""
145
+ raise NotImplementedError()
146
+
147
+ def _make_redis_url(self) -> str:
148
+ """Redis URL format with async/sync support."""
149
+ raise NotImplementedError()
150
+
151
+ def _make_mssql_url(self, connection: Connection = Connection.ASYNC) -> str:
152
+ """SQL Server URL format with async/sync drivers."""
153
+ raise NotImplementedError()
154
+
155
+ def _make_elasticsearch_url(self) -> str:
156
+ """Elasticsearch URL format - HTTP-based, same for async/sync."""
157
+ raise NotImplementedError()
158
+
159
+ def get_driver_variants(self) -> StringToStringDict:
160
+ """Get available driver variants for this database type."""
161
+ variants = {}
162
+
163
+ if self.driver is Driver.POSTGRESQL:
164
+ variants.update(
165
+ {
166
+ "async": "postgresql+asyncpg",
167
+ "sync": "postgresql+psycopg2",
168
+ "sync_alt": "postgresql+pg8000",
169
+ }
170
+ )
171
+ elif self.driver is Driver.MYSQL:
172
+ variants.update(
173
+ {
174
+ "async": "mysql+aiomysql",
175
+ "sync": "mysql+pymysql",
176
+ "sync_alt": "mysql+mysqlclient",
177
+ }
178
+ )
179
+ elif self.driver is Driver.SQLITE:
180
+ variants.update({"async": "sqlite+aiosqlite", "sync": "sqlite"})
181
+ elif self.driver is Driver.MSSQL:
182
+ variants.update(
183
+ {
184
+ "async": "mssql+aioodbc",
185
+ "sync": "mssql+pyodbc",
186
+ "sync_alt": "mssql+pymssql",
187
+ }
188
+ )
189
+ elif self.driver is Driver.MONGODB:
190
+ variants.update({"async": "mongodb (motor)", "sync": "mongodb (pymongo)"})
191
+ elif self.driver is Driver.REDIS:
192
+ variants.update({"async": "redis (aioredis)", "sync": "redis (redis-py)"})
193
+ elif self.driver is Driver.ELASTICSEARCH:
194
+ variants.update(
195
+ {
196
+ "async": "elasticsearch (elasticsearch-async)",
197
+ "sync": "elasticsearch (elasticsearch-py)",
198
+ }
199
+ )
200
+
201
+ return variants
202
+
203
+ def make_url_with_custom_driver(self, driver_string: str) -> str:
204
+ """Build URL with a custom driver string (useful for specific driver variants)."""
205
+ if self.driver in [Driver.SQLITE]:
206
+ return f"{driver_string}:///{self.database}" + self._make_options_string()
207
+ elif self.driver in [Driver.REDIS]:
208
+ auth_part = ""
209
+ if self.username and self.password:
210
+ encoded_user = self._safe_encode_credential(self.username)
211
+ encoded_pass = self._safe_encode_credential(self.password)
212
+ auth_part = f"{encoded_user}:{encoded_pass}@"
213
+ elif self.password:
214
+ encoded_pass = self._safe_encode_credential(self.password)
215
+ auth_part = f":{encoded_pass}@"
216
+ return (
217
+ f"{driver_string}://{auth_part}{self.host}:{self.port}/{self.database}"
218
+ + self._make_options_string()
219
+ )
220
+ elif self.driver in [Driver.ELASTICSEARCH]:
221
+ scheme = getattr(self, "scheme", "http")
222
+ auth_part = ""
223
+ if self.username and self.password:
224
+ encoded_user = self._safe_encode_credential(self.username)
225
+ encoded_pass = self._safe_encode_credential(self.password)
226
+ auth_part = f"{encoded_user}:{encoded_pass}@"
227
+ return (
228
+ f"{scheme}://{auth_part}{self.host}:{self.port}"
229
+ + self._make_options_string()
230
+ )
231
+ else:
232
+ # Standard SQL databases
233
+ encoded_user = self._safe_encode_credential(self.username)
234
+ encoded_pass = self._safe_encode_credential(self.password)
235
+ return (
236
+ f"{driver_string}://{encoded_user}:{encoded_pass}@{self.host}:{self.port}/{self.database}"
237
+ + self._make_options_string()
238
+ )
239
+
240
+
241
+ ConnectionConfigT = TypeVar("ConnectionConfigT", bound=BaseConnectionConfig)
242
+
243
+
244
+ class PostgreSQLConnectionConfig(
245
+ BaseConnectionConfig[Literal[Driver.POSTGRESQL], str, str, str, int, str]
246
+ ):
247
+ driver: Literal[Driver.POSTGRESQL] = Driver.POSTGRESQL
248
+ port: int = Field(5432, description="PostgreSQL port")
249
+ username: str = Field("postgres", description="PostgreSQL username")
250
+
251
+ # PostgreSQL-specific options
252
+ echo: bool = Field(False, description="Enable SQL statement logging")
253
+ sslmode: Optional[PostgreSQLSSLMode] = Field(None, description="SSL mode")
254
+ application_name: OptionalString = Field(
255
+ None, description="Application name for connection tracking"
256
+ )
257
+
258
+ def _make_postgresql_url(self, connection: Connection = Connection.ASYNC) -> str:
259
+ """PostgreSQL database URL format with proper async/sync drivers."""
260
+ # Choose appropriate driver based on connection type
261
+ if connection is Connection.ASYNC:
262
+ driver_name = "postgresql+asyncpg"
263
+ elif connection is Connection.SYNC:
264
+ driver_name = "postgresql+psycopg2"
265
+ else:
266
+ driver_name = "postgresql" # Default
267
+
268
+ encoded_user = self._safe_encode_credential(self.username)
269
+ encoded_pass = self._safe_encode_credential(self.password)
270
+
271
+ base_url = f"{driver_name}://{encoded_user}:{encoded_pass}@{self.host}:{self.port}/{self.database}"
272
+
273
+ # Add PostgreSQL-specific options
274
+ pg_options = {
275
+ "echo": self.echo,
276
+ "sslmode": self.sslmode,
277
+ "application_name": self.application_name,
278
+ }
279
+
280
+ return base_url + self._make_options_string(pg_options)
281
+
282
+
283
+ class MySQLConnectionConfig(
284
+ BaseConnectionConfig[Literal[Driver.MYSQL], str, str, str, int, str]
285
+ ):
286
+ driver: Literal[Driver.MYSQL] = Driver.MYSQL
287
+ port: int = Field(3306, description="MySQL port")
288
+ username: str = Field("root", description="MySQL username")
289
+
290
+ # MySQL-specific options
291
+ echo: bool = Field(False, description="Enable SQL statement logging")
292
+ charset: MySQLCharset = Field(MySQLCharset.UTF8MB4, description="Character set")
293
+
294
+ def _make_mysql_url(self, connection: Connection = Connection.ASYNC) -> str:
295
+ """MySQL database URL format with proper async/sync drivers."""
296
+ # Choose appropriate driver based on connection type
297
+ if connection is Connection.ASYNC:
298
+ driver_name = "mysql+aiomysql"
299
+ elif connection is Connection.SYNC:
300
+ driver_name = "mysql+pymysql" # or mysqlclient
301
+ else:
302
+ driver_name = "mysql" # Default
303
+
304
+ encoded_user = self._safe_encode_credential(self.username)
305
+ encoded_pass = self._safe_encode_credential(self.password)
306
+
307
+ base_url = f"{driver_name}://{encoded_user}:{encoded_pass}@{self.host}:{self.port}/{self.database}"
308
+
309
+ # Add MySQL-specific options
310
+ mysql_options = {"charset": self.charset}
311
+ return base_url + self._make_options_string(mysql_options)
312
+
313
+
314
+ class SQLiteConnectionConfig(
315
+ BaseConnectionConfig[Literal[Driver.SQLITE], None, None, None, None, str]
316
+ ):
317
+ driver: Literal[Driver.SQLITE] = Driver.SQLITE
318
+ database: str = Field(..., min_length=1, description="SQLite database file path")
319
+
320
+ # SQLite doesn't need these fields
321
+ username: None = Field(None, description="Not used in SQLite")
322
+ password: None = Field(None, description="Not used in SQLite")
323
+ host: None = Field(None, description="Not used in SQLite")
324
+ port: None = Field(None, description="Not used in SQLite")
325
+
326
+ echo: bool = Field(False, description="Enable SQL statement logging")
327
+
328
+ def _make_sqlite_url(self, connection: Connection = Connection.ASYNC) -> str:
329
+ """SQLite URL format with async/sync support."""
330
+ # SQLite async/sync drivers
331
+ if connection is Connection.ASYNC:
332
+ driver_name = "sqlite+aiosqlite"
333
+ elif connection is Connection.SYNC:
334
+ driver_name = "sqlite"
335
+ else:
336
+ driver_name = "sqlite" # Default to sync
337
+
338
+ # For SQLite, database field contains the file path
339
+ base_url = f"{driver_name}:///{self.database}"
340
+ return base_url + self._make_options_string()
341
+
342
+
343
+ class SQLServerConnectionConfig(
344
+ BaseConnectionConfig[Literal[Driver.MSSQL], str, str, str, int, str]
345
+ ):
346
+ driver: Literal[Driver.MSSQL] = Driver.MSSQL
347
+ port: int = Field(1433, description="SQL Server port")
348
+
349
+ # SQL Server-specific options
350
+ echo: bool = Field(False, description="Enable SQL statement logging")
351
+ odbc_driver: str = Field(
352
+ "ODBC Driver 17 for SQL Server", description="ODBC driver name"
353
+ )
354
+ trusted_connection: bool = Field(False, description="Use Windows Authentication")
355
+
356
+ @model_validator(mode="after")
357
+ def validate_auth_method(self):
358
+ """Validate authentication method consistency"""
359
+ if self.trusted_connection:
360
+ # Windows auth doesn't need username/password
361
+ if self.username or self.password:
362
+ raise ValueError(
363
+ "Username/password not needed with trusted_connection=True"
364
+ )
365
+ else:
366
+ # SQL auth requires username/password
367
+ if not self.username or not self.password:
368
+ raise ValueError(
369
+ "Username and password required when trusted_connection=False"
370
+ )
371
+ return self
372
+
373
+ def _make_mssql_url(self, connection: Connection = Connection.ASYNC) -> str:
374
+ """SQL Server URL format with async/sync drivers."""
375
+ # Choose appropriate driver based on connection type
376
+ if connection is Connection.ASYNC:
377
+ driver_name = "mssql+aioodbc" # or mssql+asyncpg for some setups
378
+ elif connection is Connection.SYNC:
379
+ driver_name = "mssql+pyodbc"
380
+ else:
381
+ driver_name = "mssql+pyodbc" # Default to sync
382
+
383
+ encoded_user = self._safe_encode_credential(self.username)
384
+ encoded_pass = self._safe_encode_credential(self.password)
385
+
386
+ base_url = f"{driver_name}://{encoded_user}:{encoded_pass}@{self.host}:{self.port}/{self.database}"
387
+
388
+ # Add SQL Server-specific options
389
+ mssql_options = {
390
+ "driver": self.odbc_driver,
391
+ "trusted_connection": "yes" if self.trusted_connection else "no",
392
+ }
393
+ return base_url + self._make_options_string(mssql_options)
394
+
395
+
396
+ class MongoDBConnectionConfig(
397
+ BaseConnectionConfig[
398
+ Literal[Driver.MONGODB], OptionalString, OptionalString, str, int, str
399
+ ]
400
+ ):
401
+ driver: Literal[Driver.MONGODB] = Driver.MONGODB
402
+ port: int = Field(27017, description="MongoDB port")
403
+ username: OptionalString = Field(None, description="MongoDB username")
404
+ password: OptionalString = Field(None, description="MongoDB password")
405
+
406
+ # MongoDB-specific options
407
+ auth_source: OptionalString = Field("admin", description="Authentication database")
408
+ replica_set: OptionalString = Field(None, description="Replica set name")
409
+ read_preference: Optional[MongoDBReadPreference] = Field(
410
+ MongoDBReadPreference.PRIMARY, description="Read preference"
411
+ )
412
+
413
+ @model_validator(mode="after")
414
+ def validate_auth_consistency(self):
415
+ """Both username and password should be provided together"""
416
+ if (self.username is None) != (self.password is None):
417
+ raise ValueError(
418
+ "Username and password must both be provided or both be None"
419
+ )
420
+ return self
421
+
422
+ def _make_mongodb_url(self) -> str:
423
+ """MongoDB URL format - MongoDB driver handles async/sync internally."""
424
+ # MongoDB uses the same URL format for both async and sync
425
+ # The async/sync behavior is determined by the client library used
426
+
427
+ auth_part = ""
428
+ if self.username and self.password:
429
+ encoded_user = self._safe_encode_credential(self.username)
430
+ encoded_pass = self._safe_encode_credential(self.password)
431
+ auth_part = f"{encoded_user}:{encoded_pass}@"
432
+
433
+ base_url = f"mongodb://{auth_part}{self.host}:{self.port}/{self.database}"
434
+
435
+ # Add MongoDB-specific options
436
+ mongo_options = {
437
+ "authSource": self.auth_source,
438
+ "replicaSet": self.replica_set,
439
+ "readPreference": self.read_preference,
440
+ "ssl": self.ssl,
441
+ }
442
+ return base_url + self._make_options_string(mongo_options)
443
+
444
+
445
+ class RedisConnectionConfig(
446
+ BaseConnectionConfig[
447
+ Literal[Driver.REDIS], OptionalString, OptionalString, str, int, int
448
+ ]
449
+ ):
450
+ driver: Literal[Driver.REDIS] = Driver.REDIS
451
+ port: int = Field(6379, description="Redis port")
452
+ database: int = Field(0, ge=0, le=15, description="Redis database number (0-15)")
453
+ username: OptionalString = Field(None, description="Redis username (Redis 6+)")
454
+ password: OptionalString = Field(None, description="Redis password")
455
+
456
+ # Redis-specific options
457
+ decode_responses: bool = Field(True, description="Decode responses to strings")
458
+ max_connections: Optional[int] = Field(
459
+ None, ge=1, description="Max connections in pool"
460
+ )
461
+
462
+ def _make_redis_url(self) -> str:
463
+ """Redis URL format with async/sync support."""
464
+ # Redis URL format is the same, but different libraries handle async/sync
465
+ # redis-py for sync, aioredis for async
466
+
467
+ auth_part = ""
468
+ if self.username and self.password:
469
+ encoded_user = self._safe_encode_credential(self.username)
470
+ encoded_pass = self._safe_encode_credential(self.password)
471
+ auth_part = f"{encoded_user}:{encoded_pass}@"
472
+ elif self.password:
473
+ encoded_pass = self._safe_encode_credential(self.password)
474
+ auth_part = f":{encoded_pass}@"
475
+
476
+ base_url = f"redis://{auth_part}{self.host}:{self.port}/{self.database}"
477
+
478
+ # Add Redis-specific options
479
+ redis_options = {"decode_responses": self.decode_responses}
480
+ return base_url + self._make_options_string(redis_options)
481
+
482
+
483
+ class ElasticsearchConnectionConfig(
484
+ BaseConnectionConfig[
485
+ Literal[Driver.ELASTICSEARCH], OptionalString, OptionalString, str, int, str
486
+ ]
487
+ ):
488
+ driver: Literal[Driver.ELASTICSEARCH] = Driver.ELASTICSEARCH
489
+ port: int = Field(9200, description="Elasticsearch port")
490
+ username: OptionalString = Field(None, description="Elasticsearch username")
491
+ password: OptionalString = Field(None, description="Elasticsearch password")
492
+ database: str = Field("_all", description="Elasticsearch index pattern")
493
+
494
+ # Elasticsearch-specific options
495
+ scheme: ElasticsearchScheme = Field(
496
+ ElasticsearchScheme.HTTP, description="Connection scheme (http/https)"
497
+ )
498
+
499
+ @model_validator(mode="after")
500
+ def validate_https_consistency(self):
501
+ """Validate HTTPS and SSL consistency"""
502
+ if self.scheme == "https" and self.ssl is False:
503
+ raise ValueError("Cannot use https scheme with ssl=False")
504
+ return self
505
+
506
+ def _make_elasticsearch_url(self) -> str:
507
+ """Elasticsearch URL format - HTTP-based, same for async/sync."""
508
+ # Elasticsearch uses HTTP/HTTPS, async/sync is handled by the client library
509
+
510
+ auth_part = ""
511
+
512
+ if self.username and self.password:
513
+ encoded_user = self._safe_encode_credential(self.username)
514
+ encoded_pass = self._safe_encode_credential(self.password)
515
+ auth_part = f"{encoded_user}:{encoded_pass}@"
516
+
517
+ base_url = f"{self.scheme}://{auth_part}{self.host}:{self.port}"
518
+
519
+ # Elasticsearch doesn't typically use database in URL path like SQL databases
520
+ # The 'database' field might represent default index, but it's usually specified per request
521
+
522
+ # Add Elasticsearch-specific options as URL params if needed
523
+ es_options = {}
524
+ return base_url + self._make_options_string(es_options)
525
+
526
+
527
+ # Factory function with better type hints
528
+ def create_connection_config(driver: Driver, **kwargs) -> BaseConnectionConfig:
529
+ """Factory to create the appropriate connection config based on driver"""
530
+ config_map = {
531
+ Driver.POSTGRESQL: PostgreSQLConnectionConfig,
532
+ Driver.MYSQL: MySQLConnectionConfig,
533
+ Driver.SQLITE: SQLiteConnectionConfig,
534
+ Driver.MONGODB: MongoDBConnectionConfig,
535
+ Driver.REDIS: RedisConnectionConfig,
536
+ Driver.MSSQL: SQLServerConnectionConfig,
537
+ Driver.ELASTICSEARCH: ElasticsearchConnectionConfig,
538
+ }
539
+
540
+ config_class = config_map.get(driver)
541
+ if not config_class:
542
+ raise ValueError(f"Unsupported driver: {driver}")
543
+
544
+ return config_class(driver=driver, **kwargs)