api-mocker 0.4.0__py3-none-any.whl → 0.5.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.
@@ -0,0 +1,588 @@
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
+
177
+ def _build_where_clause(self, conditions: List[QueryCondition]) -> Tuple[str, Tuple]:
178
+ """Build WHERE clause from conditions, returning parameterized query and params"""
179
+ if not conditions:
180
+ return "1=1", ()
181
+
182
+ clauses = []
183
+ params = []
184
+
185
+ for i, condition in enumerate(conditions):
186
+ # Only add logical operator if it's not the first condition
187
+ if i > 0:
188
+ # Validate logical operator to prevent injection
189
+ op = condition.logical_operator.upper()
190
+ if op not in ["AND", "OR"]:
191
+ op = "AND"
192
+ clauses.append(op)
193
+
194
+ # Map operators to SQL
195
+ op_map = {
196
+ QueryOperator.EQ: "=",
197
+ QueryOperator.NE: "!=",
198
+ QueryOperator.GT: ">",
199
+ QueryOperator.GTE: ">=",
200
+ QueryOperator.LT: "<",
201
+ QueryOperator.LTE: "<=",
202
+ QueryOperator.LIKE: "LIKE"
203
+ }
204
+
205
+ if condition.operator in op_map:
206
+ clauses.append(f"{condition.field} {op_map[condition.operator]} ?")
207
+ params.append(condition.value)
208
+ elif condition.operator == QueryOperator.IN:
209
+ # Handle IN operator safely
210
+ if not condition.value:
211
+ clauses.append("1=0") # Empty list matches nothing
212
+ else:
213
+ placeholders = ",".join(["?" for _ in condition.value])
214
+ clauses.append(f"{condition.field} IN ({placeholders})")
215
+ params.extend(condition.value)
216
+ elif condition.operator == QueryOperator.NOT_IN:
217
+ if not condition.value:
218
+ clauses.append("1=1") # NOT IN empty list is always true
219
+ else:
220
+ placeholders = ",".join(["?" for _ in condition.value])
221
+ clauses.append(f"{condition.field} NOT IN ({placeholders})")
222
+ params.extend(condition.value)
223
+ elif condition.operator == QueryOperator.IS_NULL:
224
+ clauses.append(f"{condition.field} IS NULL")
225
+ elif condition.operator == QueryOperator.IS_NOT_NULL:
226
+ clauses.append(f"{condition.field} IS NOT NULL")
227
+
228
+ return " ".join(clauses), tuple(params)
229
+
230
+
231
+ class PostgreSQLManager:
232
+ """PostgreSQL database manager"""
233
+
234
+ def __init__(self, config: DatabaseConfig):
235
+ self.config = config
236
+ self.pool = None
237
+
238
+ async def initialize(self) -> None:
239
+ """Initialize the connection pool"""
240
+ self.pool = await asyncpg.create_pool(
241
+ host=self.config.host,
242
+ port=self.config.port,
243
+ database=self.config.database,
244
+ user=self.config.username,
245
+ password=self.config.password,
246
+ min_size=1,
247
+ max_size=self.config.connection_pool_size,
248
+ command_timeout=self.config.timeout
249
+ )
250
+
251
+ async def execute_query(self, query: str, *params) -> List[Dict[str, Any]]:
252
+ """Execute a query and return results"""
253
+ async with self.pool.acquire() as conn:
254
+ rows = await conn.fetch(query, *params)
255
+ return [dict(row) for row in rows]
256
+
257
+ async def execute_update(self, query: str, *params) -> int:
258
+ """Execute an update query and return affected rows"""
259
+ async with self.pool.acquire() as conn:
260
+ result = await conn.execute(query, *params)
261
+ return int(result.split()[-1])
262
+
263
+ async def close(self) -> None:
264
+ """Close the connection pool"""
265
+ if self.pool:
266
+ await self.pool.close()
267
+
268
+
269
+ class MongoDBManager:
270
+ """MongoDB database manager"""
271
+
272
+ def __init__(self, config: DatabaseConfig):
273
+ self.config = config
274
+ self.client = None
275
+ self.database = None
276
+
277
+ async def initialize(self) -> None:
278
+ """Initialize MongoDB connection"""
279
+ connection_string = f"mongodb://{self.config.username}:{self.config.password}@{self.config.host}:{self.config.port}/{self.config.database}"
280
+ self.client = motor.motor_asyncio.AsyncIOMotorClient(connection_string)
281
+ self.database = self.client[self.config.database]
282
+
283
+ async def insert_document(self, collection: str, document: Dict[str, Any]) -> str:
284
+ """Insert a document and return the ID"""
285
+ result = await self.database[collection].insert_one(document)
286
+ return str(result.inserted_id)
287
+
288
+ async def find_documents(self, collection: str, filter_dict: Dict[str, Any] = None,
289
+ limit: int = None, skip: int = None) -> List[Dict[str, Any]]:
290
+ """Find documents in a collection"""
291
+ cursor = self.database[collection].find(filter_dict or {})
292
+
293
+ if skip:
294
+ cursor = cursor.skip(skip)
295
+ if limit:
296
+ cursor = cursor.limit(limit)
297
+
298
+ documents = await cursor.to_list(length=limit or 1000)
299
+ return documents
300
+
301
+ async def update_document(self, collection: str, filter_dict: Dict[str, Any],
302
+ update_dict: Dict[str, Any]) -> int:
303
+ """Update documents in a collection"""
304
+ result = await self.database[collection].update_many(filter_dict, {"$set": update_dict})
305
+ return result.modified_count
306
+
307
+ async def delete_document(self, collection: str, filter_dict: Dict[str, Any]) -> int:
308
+ """Delete documents from a collection"""
309
+ result = await self.database[collection].delete_many(filter_dict)
310
+ return result.deleted_count
311
+
312
+ async def create_index(self, collection: str, index_spec: Dict[str, Any]) -> str:
313
+ """Create an index on a collection"""
314
+ result = await self.database[collection].create_index(list(index_spec.items()))
315
+ return result
316
+
317
+ async def close(self) -> None:
318
+ """Close MongoDB connection"""
319
+ if self.client:
320
+ self.client.close()
321
+
322
+
323
+ class RedisManager:
324
+ """Redis database manager"""
325
+
326
+ def __init__(self, config: DatabaseConfig):
327
+ self.config = config
328
+ self.redis_client = None
329
+
330
+ async def initialize(self) -> None:
331
+ """Initialize Redis connection"""
332
+ self.redis_client = redis.Redis(
333
+ host=self.config.host,
334
+ port=self.config.port,
335
+ db=0,
336
+ decode_responses=True
337
+ )
338
+
339
+ async def set(self, key: str, value: Any, expire: int = None) -> bool:
340
+ """Set a key-value pair"""
341
+ if isinstance(value, (dict, list)):
342
+ value = json.dumps(value)
343
+ return self.redis_client.set(key, value, ex=expire)
344
+
345
+ async def get(self, key: str) -> Any:
346
+ """Get a value by key"""
347
+ value = self.redis_client.get(key)
348
+ if value is None:
349
+ return None
350
+
351
+ try:
352
+ return json.loads(value)
353
+ except json.JSONDecodeError:
354
+ return value
355
+
356
+ async def delete(self, key: str) -> bool:
357
+ """Delete a key"""
358
+ return bool(self.redis_client.delete(key))
359
+
360
+ async def exists(self, key: str) -> bool:
361
+ """Check if a key exists"""
362
+ return bool(self.redis_client.exists(key))
363
+
364
+ async def expire(self, key: str, seconds: int) -> bool:
365
+ """Set expiration for a key"""
366
+ return bool(self.redis_client.expire(key, seconds))
367
+
368
+ async def close(self) -> None:
369
+ """Close Redis connection"""
370
+ if self.redis_client:
371
+ self.redis_client.close()
372
+
373
+
374
+ class DatabaseManager:
375
+ """Main database manager that handles multiple database types"""
376
+
377
+ def __init__(self):
378
+ self.managers: Dict[DatabaseType, Any] = {}
379
+ self.configs: Dict[DatabaseType, DatabaseConfig] = {}
380
+
381
+ def add_database(self, db_type: DatabaseType, config: DatabaseConfig) -> None:
382
+ """Add a database configuration"""
383
+ self.configs[db_type] = config
384
+
385
+ if db_type == DatabaseType.SQLITE:
386
+ self.managers[db_type] = SQLiteManager(config.database)
387
+ elif db_type == DatabaseType.POSTGRESQL:
388
+ self.managers[db_type] = PostgreSQLManager(config)
389
+ elif db_type == DatabaseType.MONGODB:
390
+ self.managers[db_type] = MongoDBManager(config)
391
+ elif db_type == DatabaseType.REDIS:
392
+ self.managers[db_type] = RedisManager(config)
393
+
394
+ async def initialize_all(self) -> None:
395
+ """Initialize all configured databases"""
396
+ for db_type, manager in self.managers.items():
397
+ if hasattr(manager, 'initialize'):
398
+ await manager.initialize()
399
+
400
+ async def close_all(self) -> None:
401
+ """Close all database connections"""
402
+ for manager in self.managers.values():
403
+ if hasattr(manager, 'close'):
404
+ await manager.close()
405
+
406
+ def get_manager(self, db_type: DatabaseType):
407
+ """Get a database manager by type"""
408
+ return self.managers.get(db_type)
409
+
410
+ async def execute_sqlite_query(self, query: str, params: Tuple = ()) -> List[Dict[str, Any]]:
411
+ """Execute a SQLite query"""
412
+ manager = self.get_manager(DatabaseType.SQLITE)
413
+ if manager:
414
+ return await manager.execute_query(query, params)
415
+ return []
416
+
417
+ async def execute_postgresql_query(self, query: str, *params) -> List[Dict[str, Any]]:
418
+ """Execute a PostgreSQL query"""
419
+ manager = self.get_manager(DatabaseType.POSTGRESQL)
420
+ if manager:
421
+ return await manager.execute_query(query, *params)
422
+ return []
423
+
424
+ async def insert_mongodb_document(self, collection: str, document: Dict[str, Any]) -> str:
425
+ """Insert a MongoDB document"""
426
+ manager = self.get_manager(DatabaseType.MONGODB)
427
+ if manager:
428
+ return await manager.insert_document(collection, document)
429
+ return ""
430
+
431
+ async def set_redis_value(self, key: str, value: Any, expire: int = None) -> bool:
432
+ """Set a Redis value"""
433
+ manager = self.get_manager(DatabaseType.REDIS)
434
+ if manager:
435
+ return await manager.set(key, value, expire)
436
+ return False
437
+
438
+ async def get_redis_value(self, key: str) -> Any:
439
+ """Get a Redis value"""
440
+ manager = self.get_manager(DatabaseType.REDIS)
441
+ if manager:
442
+ return await manager.get(key)
443
+ return None
444
+
445
+
446
+ class DatabaseMigration:
447
+ """Database migration system"""
448
+
449
+ def __init__(self, db_manager: DatabaseManager):
450
+ self.db_manager = db_manager
451
+ self.migrations: List[Dict[str, Any]] = []
452
+
453
+ def add_migration(self, version: str, description: str,
454
+ up_sql: str, down_sql: str = None) -> None:
455
+ """Add a migration"""
456
+ migration = {
457
+ "version": version,
458
+ "description": description,
459
+ "up_sql": up_sql,
460
+ "down_sql": down_sql,
461
+ "applied": False
462
+ }
463
+ self.migrations.append(migration)
464
+
465
+ async def run_migrations(self) -> None:
466
+ """Run all pending migrations"""
467
+ # Create migrations table if it doesn't exist
468
+ await self._create_migrations_table()
469
+
470
+ # Get applied migrations
471
+ applied_migrations = await self._get_applied_migrations()
472
+
473
+ # Run pending migrations
474
+ for migration in self.migrations:
475
+ if migration["version"] not in applied_migrations:
476
+ await self._apply_migration(migration)
477
+
478
+ async def _create_migrations_table(self) -> None:
479
+ """Create migrations tracking table"""
480
+ sql = """
481
+ CREATE TABLE IF NOT EXISTS migrations (
482
+ version TEXT PRIMARY KEY,
483
+ description TEXT,
484
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
485
+ )
486
+ """
487
+ await self.db_manager.execute_sqlite_query(sql)
488
+
489
+ async def _get_applied_migrations(self) -> List[str]:
490
+ """Get list of applied migrations"""
491
+ result = await self.db_manager.execute_sqlite_query(
492
+ "SELECT version FROM migrations ORDER BY applied_at"
493
+ )
494
+ return [row["version"] for row in result]
495
+
496
+ async def _apply_migration(self, migration: Dict[str, Any]) -> None:
497
+ """Apply a migration"""
498
+ try:
499
+ # Execute up SQL
500
+ await self.db_manager.execute_sqlite_query(migration["up_sql"])
501
+
502
+ # Record migration
503
+ await self.db_manager.execute_sqlite_query(
504
+ "INSERT INTO migrations (version, description) VALUES (?, ?)",
505
+ (migration["version"], migration["description"])
506
+ )
507
+
508
+ print(f"Applied migration: {migration['version']} - {migration['description']}")
509
+ except Exception as e:
510
+ print(f"Error applying migration {migration['version']}: {e}")
511
+ raise
512
+
513
+
514
+ # Global database manager instance
515
+ db_manager = DatabaseManager()
516
+
517
+
518
+ # Convenience functions
519
+ async def setup_sqlite_database(db_path: str = "api_mocker.db") -> None:
520
+ """Setup SQLite database with default tables"""
521
+ config = DatabaseConfig(
522
+ db_type=DatabaseType.SQLITE,
523
+ database=db_path
524
+ )
525
+ db_manager.add_database(DatabaseType.SQLITE, config)
526
+ await db_manager.initialize_all()
527
+
528
+ # Create default tables
529
+ sqlite_manager = db_manager.get_manager(DatabaseType.SQLITE)
530
+ if sqlite_manager:
531
+ await sqlite_manager.create_table("users", {
532
+ "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
533
+ "username": "TEXT UNIQUE NOT NULL",
534
+ "email": "TEXT UNIQUE NOT NULL",
535
+ "created_at": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
536
+ })
537
+
538
+ await sqlite_manager.create_table("api_requests", {
539
+ "id": "INTEGER PRIMARY KEY AUTOINCREMENT",
540
+ "method": "TEXT NOT NULL",
541
+ "path": "TEXT NOT NULL",
542
+ "headers": "TEXT",
543
+ "body": "TEXT",
544
+ "response_status": "INTEGER",
545
+ "response_body": "TEXT",
546
+ "created_at": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
547
+ })
548
+
549
+
550
+ async def setup_postgresql_database(host: str, port: int, database: str,
551
+ username: str, password: str) -> None:
552
+ """Setup PostgreSQL database"""
553
+ config = DatabaseConfig(
554
+ db_type=DatabaseType.POSTGRESQL,
555
+ host=host,
556
+ port=port,
557
+ database=database,
558
+ username=username,
559
+ password=password
560
+ )
561
+ db_manager.add_database(DatabaseType.POSTGRESQL, config)
562
+ await db_manager.initialize_all()
563
+
564
+
565
+ async def setup_mongodb_database(host: str, port: int, database: str,
566
+ username: str = "", password: str = "") -> None:
567
+ """Setup MongoDB database"""
568
+ config = DatabaseConfig(
569
+ db_type=DatabaseType.MONGODB,
570
+ host=host,
571
+ port=port,
572
+ database=database,
573
+ username=username,
574
+ password=password
575
+ )
576
+ db_manager.add_database(DatabaseType.MONGODB, config)
577
+ await db_manager.initialize_all()
578
+
579
+
580
+ async def setup_redis_database(host: str = "localhost", port: int = 6379) -> None:
581
+ """Setup Redis database"""
582
+ config = DatabaseConfig(
583
+ db_type=DatabaseType.REDIS,
584
+ host=host,
585
+ port=port
586
+ )
587
+ db_manager.add_database(DatabaseType.REDIS, config)
588
+ await db_manager.initialize_all()