api-mocker 0.4.0__py3-none-any.whl → 0.5.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.
@@ -0,0 +1,566 @@
1
+ """
2
+ Database Integration System
3
+
4
+ This module provides comprehensive database integration capabilities including:
5
+ - SQLite, PostgreSQL, and MongoDB support
6
+ - Connection pooling and management
7
+ - Query builders and ORM-like functionality
8
+ - Database migrations and schema management
9
+ - Transaction support
10
+ - Caching and performance optimization
11
+ """
12
+
13
+ import sqlite3
14
+ import asyncio
15
+ import json
16
+ from typing import Any, Dict, List, Optional, Union, Callable, Tuple
17
+ from dataclasses import dataclass, field
18
+ from enum import Enum
19
+ from datetime import datetime
20
+ import threading
21
+ from contextlib import asynccontextmanager
22
+ import aiosqlite
23
+ import asyncpg
24
+ import motor.motor_asyncio
25
+ from pymongo import MongoClient
26
+ import redis
27
+ import pickle
28
+
29
+
30
+ class DatabaseType(Enum):
31
+ """Database types"""
32
+ SQLITE = "sqlite"
33
+ POSTGRESQL = "postgresql"
34
+ MONGODB = "mongodb"
35
+ REDIS = "redis"
36
+
37
+
38
+ class QueryOperator(Enum):
39
+ """Query operators"""
40
+ EQ = "="
41
+ NE = "!="
42
+ GT = ">"
43
+ GTE = ">="
44
+ LT = "<"
45
+ LTE = "<="
46
+ LIKE = "LIKE"
47
+ IN = "IN"
48
+ NOT_IN = "NOT IN"
49
+ IS_NULL = "IS NULL"
50
+ IS_NOT_NULL = "IS NOT NULL"
51
+
52
+
53
+ @dataclass
54
+ class DatabaseConfig:
55
+ """Database configuration"""
56
+ db_type: DatabaseType
57
+ host: str = "localhost"
58
+ port: int = 5432
59
+ database: str = "api_mocker"
60
+ username: str = ""
61
+ password: str = ""
62
+ connection_pool_size: int = 10
63
+ max_connections: int = 100
64
+ timeout: int = 30
65
+ ssl_mode: str = "prefer"
66
+ additional_params: Dict[str, Any] = field(default_factory=dict)
67
+
68
+
69
+ @dataclass
70
+ class QueryCondition:
71
+ """Query condition"""
72
+ field: str
73
+ operator: QueryOperator
74
+ value: Any
75
+ logical_operator: str = "AND" # AND, OR
76
+
77
+
78
+ @dataclass
79
+ class QueryBuilder:
80
+ """Query builder for database operations"""
81
+ table: str
82
+ conditions: List[QueryCondition] = field(default_factory=list)
83
+ fields: List[str] = field(default_factory=list)
84
+ order_by: List[str] = field(default_factory=list)
85
+ limit: Optional[int] = None
86
+ offset: Optional[int] = None
87
+ joins: List[Dict[str, str]] = field(default_factory=list)
88
+ group_by: List[str] = field(default_factory=list)
89
+ having: List[QueryCondition] = field(default_factory=list)
90
+
91
+
92
+ class SQLiteManager:
93
+ """SQLite database manager"""
94
+
95
+ def __init__(self, db_path: str = "api_mocker.db"):
96
+ self.db_path = db_path
97
+ self.connection_pool = []
98
+ self.pool_lock = threading.Lock()
99
+ self.max_connections = 10
100
+
101
+ async def get_connection(self) -> aiosqlite.Connection:
102
+ """Get a database connection from the pool"""
103
+ with self.pool_lock:
104
+ if self.connection_pool:
105
+ return self.connection_pool.pop()
106
+ else:
107
+ return await aiosqlite.connect(self.db_path)
108
+
109
+ async def return_connection(self, conn: aiosqlite.Connection) -> None:
110
+ """Return a connection to the pool"""
111
+ with self.pool_lock:
112
+ if len(self.connection_pool) < self.max_connections:
113
+ self.connection_pool.append(conn)
114
+ else:
115
+ await conn.close()
116
+
117
+ async def execute_query(self, query: str, params: Tuple = ()) -> List[Dict[str, Any]]:
118
+ """Execute a query and return results"""
119
+ conn = await self.get_connection()
120
+ try:
121
+ cursor = await conn.execute(query, params)
122
+ rows = await cursor.fetchall()
123
+ columns = [description[0] for description in cursor.description] if cursor.description else []
124
+ return [dict(zip(columns, row)) for row in rows]
125
+ finally:
126
+ await self.return_connection(conn)
127
+
128
+ async def execute_update(self, query: str, params: Tuple = ()) -> int:
129
+ """Execute an update query and return affected rows"""
130
+ conn = await self.get_connection()
131
+ try:
132
+ cursor = await conn.execute(query, params)
133
+ await conn.commit()
134
+ return cursor.rowcount
135
+ finally:
136
+ await self.return_connection(conn)
137
+
138
+ async def create_table(self, table_name: str, schema: Dict[str, str]) -> None:
139
+ """Create a table with the given schema"""
140
+ columns = [f"{name} {type_def}" for name, type_def in schema.items()]
141
+ query = f"CREATE TABLE IF NOT EXISTS {table_name} ({', '.join(columns)})"
142
+ await self.execute_update(query)
143
+
144
+ async def insert_record(self, table_name: str, data: Dict[str, Any]) -> int:
145
+ """Insert a record and return the ID"""
146
+ fields = list(data.keys())
147
+ placeholders = ["?" for _ in fields]
148
+ query = f"INSERT INTO {table_name} ({', '.join(fields)}) VALUES ({', '.join(placeholders)})"
149
+ params = tuple(data.values())
150
+
151
+ conn = await self.get_connection()
152
+ try:
153
+ cursor = await conn.execute(query, params)
154
+ await conn.commit()
155
+ return cursor.lastrowid
156
+ finally:
157
+ await self.return_connection(conn)
158
+
159
+ async def update_record(self, table_name: str, data: Dict[str, Any],
160
+ conditions: List[QueryCondition]) -> int:
161
+ """Update records based on conditions"""
162
+ set_clause = ", ".join([f"{field} = ?" for field in data.keys()])
163
+ where_clause, params = self._build_where_clause(conditions)
164
+
165
+ query = f"UPDATE {table_name} SET {set_clause} WHERE {where_clause}"
166
+ all_params = tuple(data.values()) + params
167
+
168
+ return await self.execute_update(query, all_params)
169
+
170
+ async def delete_record(self, table_name: str, conditions: List[QueryCondition]) -> int:
171
+ """Delete records based on conditions"""
172
+ where_clause, params = self._build_where_clause(conditions)
173
+ query = f"DELETE FROM {table_name} WHERE {where_clause}"
174
+ return await self.execute_update(query, params)
175
+
176
+ def _build_where_clause(self, conditions: List[QueryCondition]) -> Tuple[str, Tuple]:
177
+ """Build WHERE clause from conditions"""
178
+ if not conditions:
179
+ return "1=1", ()
180
+
181
+ clauses = []
182
+ params = []
183
+
184
+ for i, condition in enumerate(conditions):
185
+ if i > 0:
186
+ clauses.append(condition.logical_operator)
187
+
188
+ if condition.operator == QueryOperator.EQ:
189
+ clauses.append(f"{condition.field} = ?")
190
+ params.append(condition.value)
191
+ elif condition.operator == QueryOperator.NE:
192
+ clauses.append(f"{condition.field} != ?")
193
+ params.append(condition.value)
194
+ elif condition.operator == QueryOperator.LIKE:
195
+ clauses.append(f"{condition.field} LIKE ?")
196
+ params.append(condition.value)
197
+ elif condition.operator == QueryOperator.IN:
198
+ placeholders = ",".join(["?" for _ in condition.value])
199
+ clauses.append(f"{condition.field} IN ({placeholders})")
200
+ params.extend(condition.value)
201
+ elif condition.operator == QueryOperator.IS_NULL:
202
+ clauses.append(f"{condition.field} IS NULL")
203
+ elif condition.operator == QueryOperator.IS_NOT_NULL:
204
+ clauses.append(f"{condition.field} IS NOT NULL")
205
+
206
+ return " ".join(clauses), tuple(params)
207
+
208
+
209
+ class PostgreSQLManager:
210
+ """PostgreSQL database manager"""
211
+
212
+ def __init__(self, config: DatabaseConfig):
213
+ self.config = config
214
+ self.pool = None
215
+
216
+ async def initialize(self) -> None:
217
+ """Initialize the connection pool"""
218
+ self.pool = await asyncpg.create_pool(
219
+ host=self.config.host,
220
+ port=self.config.port,
221
+ database=self.config.database,
222
+ user=self.config.username,
223
+ password=self.config.password,
224
+ min_size=1,
225
+ max_size=self.config.connection_pool_size,
226
+ command_timeout=self.config.timeout
227
+ )
228
+
229
+ async def execute_query(self, query: str, *params) -> List[Dict[str, Any]]:
230
+ """Execute a query and return results"""
231
+ async with self.pool.acquire() as conn:
232
+ rows = await conn.fetch(query, *params)
233
+ return [dict(row) for row in rows]
234
+
235
+ async def execute_update(self, query: str, *params) -> int:
236
+ """Execute an update query and return affected rows"""
237
+ async with self.pool.acquire() as conn:
238
+ result = await conn.execute(query, *params)
239
+ return int(result.split()[-1])
240
+
241
+ async def close(self) -> None:
242
+ """Close the connection pool"""
243
+ if self.pool:
244
+ await self.pool.close()
245
+
246
+
247
+ class MongoDBManager:
248
+ """MongoDB database manager"""
249
+
250
+ def __init__(self, config: DatabaseConfig):
251
+ self.config = config
252
+ self.client = None
253
+ self.database = None
254
+
255
+ async def initialize(self) -> None:
256
+ """Initialize MongoDB connection"""
257
+ connection_string = f"mongodb://{self.config.username}:{self.config.password}@{self.config.host}:{self.config.port}/{self.config.database}"
258
+ self.client = motor.motor_asyncio.AsyncIOMotorClient(connection_string)
259
+ self.database = self.client[self.config.database]
260
+
261
+ async def insert_document(self, collection: str, document: Dict[str, Any]) -> str:
262
+ """Insert a document and return the ID"""
263
+ result = await self.database[collection].insert_one(document)
264
+ return str(result.inserted_id)
265
+
266
+ async def find_documents(self, collection: str, filter_dict: Dict[str, Any] = None,
267
+ limit: int = None, skip: int = None) -> List[Dict[str, Any]]:
268
+ """Find documents in a collection"""
269
+ cursor = self.database[collection].find(filter_dict or {})
270
+
271
+ if skip:
272
+ cursor = cursor.skip(skip)
273
+ if limit:
274
+ cursor = cursor.limit(limit)
275
+
276
+ documents = await cursor.to_list(length=limit or 1000)
277
+ return documents
278
+
279
+ async def update_document(self, collection: str, filter_dict: Dict[str, Any],
280
+ update_dict: Dict[str, Any]) -> int:
281
+ """Update documents in a collection"""
282
+ result = await self.database[collection].update_many(filter_dict, {"$set": update_dict})
283
+ return result.modified_count
284
+
285
+ async def delete_document(self, collection: str, filter_dict: Dict[str, Any]) -> int:
286
+ """Delete documents from a collection"""
287
+ result = await self.database[collection].delete_many(filter_dict)
288
+ return result.deleted_count
289
+
290
+ async def create_index(self, collection: str, index_spec: Dict[str, Any]) -> str:
291
+ """Create an index on a collection"""
292
+ result = await self.database[collection].create_index(list(index_spec.items()))
293
+ return result
294
+
295
+ async def close(self) -> None:
296
+ """Close MongoDB connection"""
297
+ if self.client:
298
+ self.client.close()
299
+
300
+
301
+ class RedisManager:
302
+ """Redis database manager"""
303
+
304
+ def __init__(self, config: DatabaseConfig):
305
+ self.config = config
306
+ self.redis_client = None
307
+
308
+ async def initialize(self) -> None:
309
+ """Initialize Redis connection"""
310
+ self.redis_client = redis.Redis(
311
+ host=self.config.host,
312
+ port=self.config.port,
313
+ db=0,
314
+ decode_responses=True
315
+ )
316
+
317
+ async def set(self, key: str, value: Any, expire: int = None) -> bool:
318
+ """Set a key-value pair"""
319
+ if isinstance(value, (dict, list)):
320
+ value = json.dumps(value)
321
+ return self.redis_client.set(key, value, ex=expire)
322
+
323
+ async def get(self, key: str) -> Any:
324
+ """Get a value by key"""
325
+ value = self.redis_client.get(key)
326
+ if value is None:
327
+ return None
328
+
329
+ try:
330
+ return json.loads(value)
331
+ except json.JSONDecodeError:
332
+ return value
333
+
334
+ async def delete(self, key: str) -> bool:
335
+ """Delete a key"""
336
+ return bool(self.redis_client.delete(key))
337
+
338
+ async def exists(self, key: str) -> bool:
339
+ """Check if a key exists"""
340
+ return bool(self.redis_client.exists(key))
341
+
342
+ async def expire(self, key: str, seconds: int) -> bool:
343
+ """Set expiration for a key"""
344
+ return bool(self.redis_client.expire(key, seconds))
345
+
346
+ async def close(self) -> None:
347
+ """Close Redis connection"""
348
+ if self.redis_client:
349
+ self.redis_client.close()
350
+
351
+
352
+ class DatabaseManager:
353
+ """Main database manager that handles multiple database types"""
354
+
355
+ def __init__(self):
356
+ self.managers: Dict[DatabaseType, Any] = {}
357
+ self.configs: Dict[DatabaseType, DatabaseConfig] = {}
358
+
359
+ def add_database(self, db_type: DatabaseType, config: DatabaseConfig) -> None:
360
+ """Add a database configuration"""
361
+ self.configs[db_type] = config
362
+
363
+ if db_type == DatabaseType.SQLITE:
364
+ self.managers[db_type] = SQLiteManager(config.database)
365
+ elif db_type == DatabaseType.POSTGRESQL:
366
+ self.managers[db_type] = PostgreSQLManager(config)
367
+ elif db_type == DatabaseType.MONGODB:
368
+ self.managers[db_type] = MongoDBManager(config)
369
+ elif db_type == DatabaseType.REDIS:
370
+ self.managers[db_type] = RedisManager(config)
371
+
372
+ async def initialize_all(self) -> None:
373
+ """Initialize all configured databases"""
374
+ for db_type, manager in self.managers.items():
375
+ if hasattr(manager, 'initialize'):
376
+ await manager.initialize()
377
+
378
+ async def close_all(self) -> None:
379
+ """Close all database connections"""
380
+ for manager in self.managers.values():
381
+ if hasattr(manager, 'close'):
382
+ await manager.close()
383
+
384
+ def get_manager(self, db_type: DatabaseType):
385
+ """Get a database manager by type"""
386
+ return self.managers.get(db_type)
387
+
388
+ async def execute_sqlite_query(self, query: str, params: Tuple = ()) -> List[Dict[str, Any]]:
389
+ """Execute a SQLite query"""
390
+ manager = self.get_manager(DatabaseType.SQLITE)
391
+ if manager:
392
+ return await manager.execute_query(query, params)
393
+ return []
394
+
395
+ async def execute_postgresql_query(self, query: str, *params) -> List[Dict[str, Any]]:
396
+ """Execute a PostgreSQL query"""
397
+ manager = self.get_manager(DatabaseType.POSTGRESQL)
398
+ if manager:
399
+ return await manager.execute_query(query, *params)
400
+ return []
401
+
402
+ async def insert_mongodb_document(self, collection: str, document: Dict[str, Any]) -> str:
403
+ """Insert a MongoDB document"""
404
+ manager = self.get_manager(DatabaseType.MONGODB)
405
+ if manager:
406
+ return await manager.insert_document(collection, document)
407
+ return ""
408
+
409
+ async def set_redis_value(self, key: str, value: Any, expire: int = None) -> bool:
410
+ """Set a Redis value"""
411
+ manager = self.get_manager(DatabaseType.REDIS)
412
+ if manager:
413
+ return await manager.set(key, value, expire)
414
+ return False
415
+
416
+ async def get_redis_value(self, key: str) -> Any:
417
+ """Get a Redis value"""
418
+ manager = self.get_manager(DatabaseType.REDIS)
419
+ if manager:
420
+ return await manager.get(key)
421
+ return None
422
+
423
+
424
+ class DatabaseMigration:
425
+ """Database migration system"""
426
+
427
+ def __init__(self, db_manager: DatabaseManager):
428
+ self.db_manager = db_manager
429
+ self.migrations: List[Dict[str, Any]] = []
430
+
431
+ def add_migration(self, version: str, description: str,
432
+ up_sql: str, down_sql: str = None) -> None:
433
+ """Add a migration"""
434
+ migration = {
435
+ "version": version,
436
+ "description": description,
437
+ "up_sql": up_sql,
438
+ "down_sql": down_sql,
439
+ "applied": False
440
+ }
441
+ self.migrations.append(migration)
442
+
443
+ async def run_migrations(self) -> None:
444
+ """Run all pending migrations"""
445
+ # Create migrations table if it doesn't exist
446
+ await self._create_migrations_table()
447
+
448
+ # Get applied migrations
449
+ applied_migrations = await self._get_applied_migrations()
450
+
451
+ # Run pending migrations
452
+ for migration in self.migrations:
453
+ if migration["version"] not in applied_migrations:
454
+ await self._apply_migration(migration)
455
+
456
+ async def _create_migrations_table(self) -> None:
457
+ """Create migrations tracking table"""
458
+ sql = """
459
+ CREATE TABLE IF NOT EXISTS migrations (
460
+ version TEXT PRIMARY KEY,
461
+ description TEXT,
462
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
463
+ )
464
+ """
465
+ await self.db_manager.execute_sqlite_query(sql)
466
+
467
+ async def _get_applied_migrations(self) -> List[str]:
468
+ """Get list of applied migrations"""
469
+ result = await self.db_manager.execute_sqlite_query(
470
+ "SELECT version FROM migrations ORDER BY applied_at"
471
+ )
472
+ return [row["version"] for row in result]
473
+
474
+ async def _apply_migration(self, migration: Dict[str, Any]) -> None:
475
+ """Apply a migration"""
476
+ try:
477
+ # Execute up SQL
478
+ await self.db_manager.execute_sqlite_query(migration["up_sql"])
479
+
480
+ # Record migration
481
+ await self.db_manager.execute_sqlite_query(
482
+ "INSERT INTO migrations (version, description) VALUES (?, ?)",
483
+ (migration["version"], migration["description"])
484
+ )
485
+
486
+ print(f"Applied migration: {migration['version']} - {migration['description']}")
487
+ except Exception as e:
488
+ print(f"Error applying migration {migration['version']}: {e}")
489
+ raise
490
+
491
+
492
+ # Global database manager instance
493
+ db_manager = DatabaseManager()
494
+
495
+
496
+ # Convenience functions
497
+ async def setup_sqlite_database(db_path: str = "api_mocker.db") -> None:
498
+ """Setup SQLite database with default tables"""
499
+ config = DatabaseConfig(
500
+ db_type=DatabaseType.SQLITE,
501
+ database=db_path
502
+ )
503
+ db_manager.add_database(DatabaseType.SQLITE, config)
504
+ await db_manager.initialize_all()
505
+
506
+ # Create default tables
507
+ sqlite_manager = db_manager.get_manager(DatabaseType.SQLITE)
508
+ if sqlite_manager:
509
+ await sqlite_manager.create_table("users", {
510
+ "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
511
+ "username": "TEXT UNIQUE NOT NULL",
512
+ "email": "TEXT UNIQUE NOT NULL",
513
+ "created_at": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
514
+ })
515
+
516
+ await sqlite_manager.create_table("api_requests", {
517
+ "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
518
+ "method": "TEXT NOT NULL",
519
+ "path": "TEXT NOT NULL",
520
+ "headers": "TEXT",
521
+ "body": "TEXT",
522
+ "response_status": "INTEGER",
523
+ "response_body": "TEXT",
524
+ "created_at": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
525
+ })
526
+
527
+
528
+ async def setup_postgresql_database(host: str, port: int, database: str,
529
+ username: str, password: str) -> None:
530
+ """Setup PostgreSQL database"""
531
+ config = DatabaseConfig(
532
+ db_type=DatabaseType.POSTGRESQL,
533
+ host=host,
534
+ port=port,
535
+ database=database,
536
+ username=username,
537
+ password=password
538
+ )
539
+ db_manager.add_database(DatabaseType.POSTGRESQL, config)
540
+ await db_manager.initialize_all()
541
+
542
+
543
+ async def setup_mongodb_database(host: str, port: int, database: str,
544
+ username: str = "", password: str = "") -> None:
545
+ """Setup MongoDB database"""
546
+ config = DatabaseConfig(
547
+ db_type=DatabaseType.MONGODB,
548
+ host=host,
549
+ port=port,
550
+ database=database,
551
+ username=username,
552
+ password=password
553
+ )
554
+ db_manager.add_database(DatabaseType.MONGODB, config)
555
+ await db_manager.initialize_all()
556
+
557
+
558
+ async def setup_redis_database(host: str = "localhost", port: int = 6379) -> None:
559
+ """Setup Redis database"""
560
+ config = DatabaseConfig(
561
+ db_type=DatabaseType.REDIS,
562
+ host=host,
563
+ port=port
564
+ )
565
+ db_manager.add_database(DatabaseType.REDIS, config)
566
+ await db_manager.initialize_all()