diff --git a/migrate.py b/migrate.py index 2c7cd5f..610129a 100644 --- a/migrate.py +++ b/migrate.py @@ -35,41 +35,36 @@ def get_database_url() -> str: def run_migration(db) -> None: """ Run database migrations. - + This creates all required tables if they don't exist. """ print(f"[{datetime.now().isoformat()}] Starting database migration...") - + # Import database module from quantumpytho.modules.database import init_schema - + # Run schema initialization init_schema(db) - + print(f"[{datetime.now().isoformat()}] Migration completed successfully!") def check_migration_status(db) -> None: """Check current migration status.""" print(f"[{datetime.now().isoformat()}] Checking migration status...") - + # Check if tables exist - tables = [ - 'quantum_jobs', - 'vqe_results', - 'user_sessions', - 'api_logs' - ] - + tables = ["quantum_jobs", "vqe_results", "user_sessions", "api_logs"] + for table in tables: result = db.fetch_one( "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = %s)", - (table,) + (table,), ) - exists = result.get('exists', False) if result else False + exists = result.get("exists", False) if result else False status = "✅" if exists else "❌" print(f" {status} {table}") - + print(f"[{datetime.now().isoformat()}] Status check complete!") @@ -77,22 +72,22 @@ def rollback_migration(db) -> None: """Rollback last migration (use with caution!).""" print(f"[{datetime.now().isoformat()}] WARNING: Rolling back migrations...") print(" This will DROP all tables!") - + confirm = input(" Are you sure? Type 'YES' to confirm: ") if confirm != "YES": print(" Rollback cancelled.") return - + # Drop all tables - tables = ['api_logs', 'user_sessions', 'vqe_results', 'quantum_jobs'] - + tables = ["api_logs", "user_sessions", "vqe_results", "quantum_jobs"] + for table in tables: try: db.execute(f"DROP TABLE IF EXISTS {table} CASCADE") print(f" ✅ Dropped table: {table}") except Exception as e: print(f" ❌ Failed to drop {table}: {e}") - + print(f"[{datetime.now().isoformat()}] Rollback complete!") @@ -100,17 +95,15 @@ def main(): """Main entry point.""" parser = argparse.ArgumentParser(description="QPyth Database Migrations") parser.add_argument( - "--check", - action="store_true", - help="Check current migration status" + "--check", action="store_true", help="Check current migration status" ) parser.add_argument( "--rollback", action="store_true", - help="Rollback last migration (DROPS all tables)" + help="Rollback last migration (DROPS all tables)", ) args = parser.parse_args() - + # Get database URL try: database_url = get_database_url() @@ -122,16 +115,16 @@ def main(): print("\nOr create a .env file with:") print(" DATABASE_URL=postgresql://...") sys.exit(1) - + # Import and connect try: from quantumpytho.modules.database import DatabaseConfig, NeonDatabase - + config = DatabaseConfig(database_url=database_url) db = NeonDatabase(config) - - print(f"✅ Connected to database") - + + print("✅ Connected to database") + # Run appropriate command if args.rollback: rollback_migration(db) @@ -139,13 +132,14 @@ def main(): check_migration_status(db) else: run_migration(db) - + # Close connection db.close() - + except Exception as e: print(f"❌ Migration failed: {e}") import traceback + traceback.print_exc() sys.exit(1) diff --git a/pyproject.toml b/pyproject.toml index 2ac2489..e295641 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,11 @@ web = [ "uvicorn[standard]>=0.32.0,<1.0.0", "slowapi>=0.1.9", ] +docs = [ + "mkdocs>=1.5.0,<2.0.0", + "mkdocs-material>=9.0.0,<10.0.0", + "pymdown-extensions>=10.0.0", +] database = [ "psycopg2-binary>=2.9.9", ] @@ -72,6 +77,9 @@ all = [ "fastapi>=0.115.0,<1.0.0", "uvicorn[standard]>=0.32.0,<1.0.0", "slowapi>=0.1.9", + "mkdocs>=1.5.0,<2.0.0", + "mkdocs-material>=9.0.0,<10.0.0", + "pymdown-extensions>=10.0.0", "psycopg2-binary>=2.9.9", ] diff --git a/quantumpytho/modules/database.py b/quantumpytho/modules/database.py index 2c962f3..7cb5894 100644 --- a/quantumpytho/modules/database.py +++ b/quantumpytho/modules/database.py @@ -14,16 +14,17 @@ """ import os +from collections.abc import Generator from contextlib import contextmanager from dataclasses import dataclass, field -from datetime import datetime -from typing import Any, Generator, Optional +from typing import Any # Optional import - database support is optional try: import psycopg2 from psycopg2 import pool from psycopg2.extras import RealDictCursor + PSYCOPG2_AVAILABLE = True except ImportError: PSYCOPG2_AVAILABLE = False @@ -35,12 +36,14 @@ @dataclass class DatabaseConfig: """Database configuration from environment variables.""" - + database_url: str = field(default_factory=lambda: os.getenv("DATABASE_URL", "")) - database_url_unpooled: str = field(default_factory=lambda: os.getenv("DATABASE_URL_UNPOOLED", "")) + database_url_unpooled: str = field( + default_factory=lambda: os.getenv("DATABASE_URL_UNPOOLED", "") + ) pool_min: int = 1 pool_max: int = 10 - + @property def is_configured(self) -> bool: """Check if database is configured.""" @@ -50,31 +53,31 @@ def is_configured(self) -> bool: class NeonDatabase: """ Neon PostgreSQL database connection manager. - + Features: - Connection pooling for serverless efficiency - Automatic reconnection on connection loss - SSL mode enforcement (required by Neon) - Context managers for safe connection handling - + Usage: db = NeonDatabase() - + # Using context manager (recommended) with db.get_connection() as conn: with conn.cursor() as cur: cur.execute("SELECT version()") print(cur.fetchone()) - + # Or use query helper methods db.execute("CREATE TABLE IF NOT EXISTS jobs (...)") results = db.fetch_all("SELECT * FROM jobs WHERE user_id = %s", (user_id,)) """ - - def __init__(self, config: Optional[DatabaseConfig] = None): + + def __init__(self, config: DatabaseConfig | None = None): """ Initialize database connection. - + Args: config: Database configuration. If None, loads from environment. """ @@ -83,47 +86,51 @@ def __init__(self, config: Optional[DatabaseConfig] = None): "psycopg2 is required for database support. " "Install with: pip install psycopg2-binary" ) - + self.config = config or DatabaseConfig() - self._connection_pool: Optional[pool.SimpleConnectionPool] = None - + self._connection_pool: pool.SimpleConnectionPool | None = None + if self.config.is_configured: self._initialize_pool() - + def _initialize_pool(self) -> None: """Initialize connection pool.""" if not PSYCOPG2_AVAILABLE: print("[NeonDB] psycopg2 not available, skipping pool initialization") return - + try: self._connection_pool = pool.SimpleConnectionPool( minconn=self.config.pool_min, maxconn=self.config.pool_max, dsn=self.config.database_url, - cursor_factory=RealDictCursor + cursor_factory=RealDictCursor, + ) + print( + f"[NeonDB] Connection pool initialized ({self.config.pool_min}-{self.config.pool_max} connections)" ) - print(f"[NeonDB] Connection pool initialized ({self.config.pool_min}-{self.config.pool_max} connections)") except Exception as e: print(f"[NeonDB] Failed to initialize connection pool: {e}") self._connection_pool = None - + @contextmanager def get_connection(self) -> Generator[Any, None, None]: """ Get a database connection from the pool. - + Yields: psycopg2 connection object - + Usage: with db.get_connection() as conn: with conn.cursor() as cur: cur.execute("SELECT 1") """ if not self._connection_pool: - raise RuntimeError("Database not initialized. Check DATABASE_URL environment variable.") - + raise RuntimeError( + "Database not initialized. Check DATABASE_URL environment variable." + ) + conn = None try: conn = self._connection_pool.getconn() @@ -131,18 +138,18 @@ def get_connection(self) -> Generator[Any, None, None]: finally: if conn: self._connection_pool.putconn(conn) - + @contextmanager def get_cursor(self, commit: bool = False) -> Generator[Any, None, None]: """ Get a database cursor with optional auto-commit. - + Args: commit: If True, commit transaction on success - + Yields: psycopg2 cursor object - + Usage: with db.get_cursor(commit=True) as cur: cur.execute("INSERT INTO jobs (...) VALUES (...)") @@ -158,15 +165,15 @@ def get_cursor(self, commit: bool = False) -> Generator[Any, None, None]: raise finally: cur.close() - - def execute(self, query: str, params: Optional[tuple] = None) -> None: + + def execute(self, query: str, params: tuple | None = None) -> None: """ Execute a database query (no return value). - + Args: query: SQL query string params: Query parameters - + Usage: db.execute( "INSERT INTO jobs (user_id, circuit) VALUES (%s, %s)", @@ -175,15 +182,15 @@ def execute(self, query: str, params: Optional[tuple] = None) -> None: """ with self.get_cursor(commit=True) as cur: cur.execute(query, params or ()) - - def fetch_one(self, query: str, params: Optional[tuple] = None) -> Optional[dict]: + + def fetch_one(self, query: str, params: tuple | None = None) -> dict | None: """ Fetch a single row from the database. - + Args: query: SQL query string params: Query parameters - + Returns: Dictionary with column names as keys, or None if no results """ @@ -191,15 +198,15 @@ def fetch_one(self, query: str, params: Optional[tuple] = None) -> Optional[dict cur.execute(query, params or ()) result = cur.fetchone() return dict(result) if result else None - - def fetch_all(self, query: str, params: Optional[tuple] = None) -> list[dict]: + + def fetch_all(self, query: str, params: tuple | None = None) -> list[dict]: """ Fetch all rows from the database. - + Args: query: SQL query string params: Query parameters - + Returns: List of dictionaries with column names as keys """ @@ -207,17 +214,17 @@ def fetch_all(self, query: str, params: Optional[tuple] = None) -> list[dict]: cur.execute(query, params or ()) results = cur.fetchall() return [dict(row) for row in results] - + def close(self) -> None: """Close all database connections.""" if self._connection_pool: self._connection_pool.closeall() print("[NeonDB] Connection pool closed") - + def is_healthy(self) -> bool: """ Check database connection health. - + Returns: True if database is accessible, False otherwise """ @@ -230,16 +237,16 @@ def is_healthy(self) -> bool: # Global database instance (lazy initialization) -_db_instance: Optional[NeonDatabase] = None +_db_instance: NeonDatabase | None = None def get_database() -> NeonDatabase: """ Get or create the global database instance. - + Returns: NeonDatabase instance - + Usage: db = get_database() if db.config.is_configured: @@ -249,13 +256,15 @@ def get_database() -> NeonDatabase: global _db_instance if _db_instance is None: if not PSYCOPG2_AVAILABLE: - print("[NeonDB] psycopg2 not available (install with: pip install psycopg2-binary)") + print( + "[NeonDB] psycopg2 not available (install with: pip install psycopg2-binary)" + ) # Create a disabled instance _db_instance = NeonDatabase.__new__(NeonDatabase) _db_instance.config = DatabaseConfig() _db_instance._connection_pool = None return _db_instance - + config = DatabaseConfig() if config.is_configured: _db_instance = NeonDatabase(config) @@ -272,18 +281,18 @@ def get_database() -> NeonDatabase: def init_schema(db: NeonDatabase) -> None: """ Initialize database schema for QPyth. - + Creates tables for: - quantum_jobs: Job execution history - vqe_results: VQE computation results - user_sessions: User session tracking - api_logs: API request logging - + Args: db: NeonDatabase instance """ print("[NeonDB] Initializing schema...") - + # Quantum jobs table db.execute(""" CREATE TABLE IF NOT EXISTS quantum_jobs ( @@ -302,7 +311,7 @@ def init_schema(db: NeonDatabase) -> None: execution_time_ms INTEGER ) """) - + # VQE results table db.execute(""" CREATE TABLE IF NOT EXISTS vqe_results ( @@ -321,7 +330,7 @@ def init_schema(db: NeonDatabase) -> None: user_id VARCHAR(255) ) """) - + # User sessions table db.execute(""" CREATE TABLE IF NOT EXISTS user_sessions ( @@ -335,7 +344,7 @@ def init_schema(db: NeonDatabase) -> None: is_active BOOLEAN DEFAULT TRUE ) """) - + # API logs table db.execute(""" CREATE TABLE IF NOT EXISTS api_logs ( @@ -353,7 +362,7 @@ def init_schema(db: NeonDatabase) -> None: created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ) """) - + # Create indexes for performance db.execute(""" CREATE INDEX IF NOT EXISTS idx_quantum_jobs_user_id ON quantum_jobs(user_id) @@ -376,22 +385,22 @@ def init_schema(db: NeonDatabase) -> None: db.execute(""" CREATE INDEX IF NOT EXISTS idx_api_logs_created_at ON api_logs(created_at) """) - + print("[NeonDB] Schema initialized successfully") # Convenience functions for common operations def log_quantum_job( job_type: str, - circuit_json: Optional[dict] = None, + circuit_json: dict | None = None, shots: int = 1024, backend: str = "simulator", - user_id: Optional[str] = None, - session_id: Optional[str] = None, -) -> Optional[int]: + user_id: str | None = None, + session_id: str | None = None, +) -> int | None: """ Log a quantum job execution. - + Args: job_type: Type of job (e.g., 'bloch', 'bell', 'vqe', 'qrng') circuit_json: Circuit representation as JSON @@ -399,16 +408,17 @@ def log_quantum_job( backend: Backend used for execution user_id: Optional user identifier session_id: Optional session identifier - + Returns: Job ID if successful, None otherwise """ db = get_database() if not db.config.is_configured: return None - + try: import json + db.execute( """ INSERT INTO quantum_jobs @@ -423,7 +433,7 @@ def log_quantum_job( json.dumps(circuit_json) if circuit_json else None, shots, backend, - ) + ), ) result = db.fetch_one("SELECT LASTVAL() as id") return result["id"] if result else None @@ -435,19 +445,19 @@ def log_quantum_job( def save_vqe_result( molecule: str, final_energy: float, - geometry: Optional[str] = None, + geometry: str | None = None, basis: str = "STO-3G", optimizer: str = "COBYLA", - initial_energy: Optional[float] = None, - convergence_iterations: Optional[int] = None, - parameters: Optional[dict] = None, + initial_energy: float | None = None, + convergence_iterations: int | None = None, + parameters: dict | None = None, backend_type: str = "simulator", - noise_profile: Optional[str] = None, - user_id: Optional[str] = None, -) -> Optional[int]: + noise_profile: str | None = None, + user_id: str | None = None, +) -> int | None: """ Save VQE computation result. - + Args: molecule: Molecule name (e.g., 'H2', 'LiH') final_energy: Final ground state energy @@ -460,16 +470,17 @@ def save_vqe_result( backend_type: Type of backend used noise_profile: Noise profile if applicable user_id: Optional user identifier - + Returns: Result ID if successful, None otherwise """ db = get_database() if not db.config.is_configured: return None - + try: import json + db.execute( """ INSERT INTO vqe_results @@ -491,7 +502,7 @@ def save_vqe_result( backend_type, noise_profile, user_id, - ) + ), ) result = db.fetch_one("SELECT LASTVAL() as id") return result["id"] if result else None @@ -505,16 +516,16 @@ def log_api_request( method: str, status_code: int, response_time_ms: int, - user_id: Optional[str] = None, - session_id: Optional[str] = None, - ip_address: Optional[str] = None, - request_data: Optional[dict] = None, - response_data: Optional[dict] = None, - error_message: Optional[str] = None, + user_id: str | None = None, + session_id: str | None = None, + ip_address: str | None = None, + request_data: dict | None = None, + response_data: dict | None = None, + error_message: str | None = None, ) -> None: """ Log an API request. - + Args: endpoint: API endpoint path method: HTTP method @@ -530,9 +541,10 @@ def log_api_request( db = get_database() if not db.config.is_configured: return - + try: import json + db.execute( """ INSERT INTO api_logs @@ -551,7 +563,7 @@ def log_api_request( json.dumps(request_data) if request_data else None, json.dumps(response_data) if response_data else None, error_message, - ) + ), ) except Exception as e: print(f"[NeonDB] Failed to log API request: {e}") diff --git a/server.py b/server.py index 6cc973f..3182921 100644 --- a/server.py +++ b/server.py @@ -52,6 +52,7 @@ log_quantum_job, save_vqe_result, ) + DATABASE_AVAILABLE = True except ImportError: DATABASE_AVAILABLE = False @@ -89,11 +90,11 @@ async def log_requests(request: Request, call_next): """Log API requests to database if configured.""" import time - + start_time = time.time() response = await call_next(request) process_time = int((time.time() - start_time) * 1000) - + # Log to database if available if DATABASE_AVAILABLE: try: @@ -107,9 +108,10 @@ async def log_requests(request: Request, call_next): except Exception as e: # Don't fail the request if logging fails print(f"[Server] Failed to log request: {e}") - + return response + # Initialize quantum engine engine = QuantumEngine(QuantumConfig()) @@ -125,7 +127,9 @@ async def log_requests(request: Request, call_next): except Exception as e: print(f"[Server] Database initialization failed: {e}") else: - print("[Server] Database module not available (install with: pip install QPyth[database])") + print( + "[Server] Database module not available (install with: pip install QPyth[database])" + ) class BlochRequest(BaseModel): diff --git a/test_neon_integration.py b/test_neon_integration.py index 20cedb6..c5474e3 100644 --- a/test_neon_integration.py +++ b/test_neon_integration.py @@ -14,112 +14,113 @@ import os import sys -from datetime import datetime # Add project root to path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + def test_neon_integration(): """Test Neon database integration.""" - + print("=" * 60) print("Neon Database Integration Test") print("=" * 60) print() - + # Test 1: Check environment variables print("1. Checking environment variables...") database_url = os.getenv("DATABASE_URL") database_url_unpooled = os.getenv("DATABASE_URL_UNPOOLED") - + if not database_url: print(" ❌ DATABASE_URL not set") print(" ℹ️ Set environment variable: export DATABASE_URL='postgresql://...'") return False else: - print(f" ✅ DATABASE_URL is set") - print(f" Host: {database_url.split('@')[1].split('/')[0] if '@' in database_url else 'N/A'}") - + print(" ✅ DATABASE_URL is set") + print( + f" Host: {database_url.split('@')[1].split('/')[0] if '@' in database_url else 'N/A'}" + ) + if database_url_unpooled: - print(f" ✅ DATABASE_URL_UNPOOLED is set") + print(" ✅ DATABASE_URL_UNPOOLED is set") else: - print(f" ⚠️ DATABASE_URL_UNPOOLED not set (optional)") - + print(" ⚠️ DATABASE_URL_UNPOOLED not set (optional)") + print() - + # Test 2: Import database module print("2. Testing database module import...") try: from quantumpytho.modules.database import ( DatabaseConfig, NeonDatabase, - get_database, init_schema, log_quantum_job, save_vqe_result, - get_database, ) + print(" ✅ Database module imported successfully") except ImportError as e: print(f" ❌ Failed to import database module: {e}") print(" ℹ️ Install with: pip install QPyth[database]") return False - + print() - + # Test 3: Initialize database connection print("3. Testing database connection...") try: config = DatabaseConfig() - print(f" ✅ Database configuration loaded") + print(" ✅ Database configuration loaded") print(f" Configured: {config.is_configured}") - + if not config.is_configured: print(" ⚠️ Database not configured, skipping connection tests") return True - + db = NeonDatabase(config) - print(f" ✅ Database instance created") - + print(" ✅ Database instance created") + if db._connection_pool: - print(f" ✅ Connection pool initialized") + print(" ✅ Connection pool initialized") print(f" Pool size: {config.pool_min}-{config.pool_max}") else: - print(f" ❌ Connection pool failed to initialize") + print(" ❌ Connection pool failed to initialize") return False - + except Exception as e: print(f" ❌ Database connection failed: {e}") return False - + print() - + # Test 4: Health check print("4. Testing database health...") try: is_healthy = db.is_healthy() if is_healthy: - print(f" ✅ Database connection is healthy") + print(" ✅ Database connection is healthy") else: - print(f" ❌ Database connection is unhealthy") + print(" ❌ Database connection is unhealthy") return False except Exception as e: print(f" ❌ Health check failed: {e}") return False - + print() - + # Test 5: Schema initialization print("5. Testing schema initialization...") try: init_schema(db) - print(f" ✅ Schema initialized successfully") + print(" ✅ Schema initialized successfully") except Exception as e: print(f" ❌ Schema initialization failed: {e}") return False - + print() - + # Test 6: Insert test data print("6. Testing data insertion...") try: @@ -133,9 +134,9 @@ def test_neon_integration(): if job_id: print(f" ✅ Quantum job logged (ID: {job_id})") else: - print(f" ❌ Failed to log quantum job") + print(" ❌ Failed to log quantum job") return False - + # Test VQE result saving result_id = save_vqe_result( molecule="H2_test", @@ -148,15 +149,15 @@ def test_neon_integration(): if result_id: print(f" ✅ VQE result saved (ID: {result_id})") else: - print(f" ❌ Failed to save VQE result") + print(" ❌ Failed to save VQE result") return False - + except Exception as e: print(f" ❌ Data insertion failed: {e}") return False - + print() - + # Test 7: Query test data print("7. Testing data retrieval...") try: @@ -165,48 +166,48 @@ def test_neon_integration(): "SELECT * FROM quantum_jobs WHERE job_type = 'test' ORDER BY created_at DESC LIMIT 5" ) print(f" ✅ Retrieved {len(jobs)} test job(s)") - + # Query VQE results vqe_results = db.fetch_all( "SELECT * FROM vqe_results WHERE molecule = 'H2_test' ORDER BY created_at DESC LIMIT 5" ) print(f" ✅ Retrieved {len(vqe_results)} test VQE result(s)") - + # Display sample data if jobs: - print(f" Sample job data:") + print(" Sample job data:") print(f" - Job ID: {jobs[0]['id']}") print(f" - Type: {jobs[0]['job_type']}") print(f" - Backend: {jobs[0]['backend']}") print(f" - Created: {jobs[0]['created_at']}") - + except Exception as e: print(f" ❌ Data retrieval failed: {e}") return False - + print() - + # Test 8: Cleanup test data print("8. Cleaning up test data...") try: db.execute("DELETE FROM quantum_jobs WHERE job_type = 'test'") db.execute("DELETE FROM vqe_results WHERE molecule = 'H2_test'") - print(f" ✅ Test data cleaned up") + print(" ✅ Test data cleaned up") except Exception as e: print(f" ⚠️ Cleanup failed (non-critical): {e}") - + print() - + # Test 9: Connection pool statistics print("9. Checking connection pool status...") try: if db._connection_pool: - print(f" ✅ Connection pool active") + print(" ✅ Connection pool active") print(f" Min connections: {config.pool_min}") print(f" Max connections: {config.pool_max}") except Exception as e: print(f" ⚠️ Pool status check failed: {e}") - + print() print("=" * 60) print("✅ All tests passed!") @@ -221,7 +222,7 @@ def test_neon_integration(): print() print("Neon database integration is working correctly!") print() - + return True diff --git a/vercel.json b/vercel.json index f33d238..63ac0f7 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { "framework": null, - "installCommand": "python3 -m venv .vercel-venv && ./.vercel-venv/bin/python -m pip install --upgrade pip setuptools wheel && ./.vercel-venv/bin/python -m pip install -e .[web,database]", + "installCommand": "python3 -m venv .vercel-venv && ./.vercel-venv/bin/python -m pip install --upgrade pip setuptools wheel && ./.vercel-venv/bin/python -m pip install -e .[web,database,docs]", "buildCommand": "./.vercel-venv/bin/python -m mkdocs build --clean", "outputDirectory": "site" }