carpet-analysis-api / database_postgres.py
yosedie's picture
Initial deployment: 4 ML models
7a2d708
Raw History Blame Contribute Delete
19.4 kB
"""
🗄️ DATABASE MODULE - PostgreSQL Support
Extended version of database.py with PostgreSQL support for production deployment
"""
import os
import uuid
from datetime import datetime
from typing import Dict, List, Optional, Tuple
import json
# =============================================================================
# DATABASE CONFIGURATION
# =============================================================================
# Get database URL from environment
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///carpet_analysis.db")
# Determine database type
if DATABASE_URL.startswith("postgresql"):
DATABASE_TYPE = "postgresql"
import psycopg2
from psycopg2.extras import RealDictCursor
print(f"🗄️ Using PostgreSQL database")
SCHEMA_PATH = "database_schema_postgres.sql" # Use PostgreSQL schema
elif DATABASE_URL.startswith("sqlite"):
DATABASE_TYPE = "sqlite"
import sqlite3
DATABASE_PATH = DATABASE_URL.replace("sqlite:///", "")
print(f"🗄️ Using SQLite database: {DATABASE_PATH}")
SCHEMA_PATH = "database_schema.sql" # Use SQLite schema
else:
# Default to SQLite
DATABASE_TYPE = "sqlite"
import sqlite3
DATABASE_PATH = "carpet_analysis.db"
print(f"🗄️ Using SQLite database (default): {DATABASE_PATH}")
SCHEMA_PATH = "database_schema.sql" # Use SQLite schema
# =============================================================================
# CONNECTION HELPERS
# =============================================================================
def get_connection():
"""Get database connection based on type"""
if DATABASE_TYPE == "postgresql":
return psycopg2.connect(DATABASE_URL)
else:
return sqlite3.connect(DATABASE_PATH)
def get_cursor(conn):
"""Get cursor with appropriate row factory"""
if DATABASE_TYPE == "postgresql":
return conn.cursor(cursor_factory=RealDictCursor)
else:
conn.row_factory = sqlite3.Row
return conn.cursor()
# =============================================================================
# DATABASE INITIALIZATION
# =============================================================================
def init_database():
"""Initialize database dengan schema dari SQL file"""
print("🗄️ Initializing database...")
conn = get_connection()
cursor = get_cursor(conn)
# Load dan execute schema
if os.path.exists(SCHEMA_PATH):
with open(SCHEMA_PATH, 'r', encoding='utf-8') as f:
schema = f.read()
if DATABASE_TYPE == "postgresql":
cursor.execute(schema)
else:
cursor.executescript(schema)
print(f"✅ Database schema loaded from {SCHEMA_PATH}")
else:
print(f"⚠️ Schema file not found: {SCHEMA_PATH}")
print(" Creating basic schema...")
create_basic_schema(cursor)
conn.commit()
conn.close()
print(f"✅ Database initialized: {DATABASE_TYPE}")
def create_basic_schema(cursor):
"""Create basic schema jika SQL file tidak ada"""
if DATABASE_TYPE == "postgresql":
# PostgreSQL version
cursor.execute("""
CREATE TABLE IF NOT EXISTS analysis_history (
id SERIAL PRIMARY KEY,
analysis_id TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
material TEXT NOT NULL,
material_confidence REAL NOT NULL,
carpet_type TEXT NOT NULL,
type_confidence REAL NOT NULL,
carpet_coverage REAL NOT NULL,
is_authentic BOOLEAN NOT NULL,
confidence_level TEXT,
price_min INTEGER,
price_max INTEGER,
total_processing_time REAL NOT NULL
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS carpet_products (
id SERIAL PRIMARY KEY,
product_name TEXT UNIQUE NOT NULL,
carpet_type TEXT NOT NULL,
material TEXT NOT NULL,
price_min INTEGER NOT NULL,
price_max INTEGER NOT NULL,
stock_status TEXT DEFAULT 'available'
)
""")
else:
# SQLite version (original)
cursor.execute("""
CREATE TABLE IF NOT EXISTS analysis_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
analysis_id TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
material TEXT NOT NULL,
material_confidence REAL NOT NULL,
carpet_type TEXT NOT NULL,
type_confidence REAL NOT NULL,
carpet_coverage REAL NOT NULL,
is_authentic BOOLEAN NOT NULL,
confidence_level TEXT,
price_min INTEGER,
price_max INTEGER,
total_processing_time REAL NOT NULL
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS carpet_products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_name TEXT UNIQUE NOT NULL,
carpet_type TEXT NOT NULL,
material TEXT NOT NULL,
price_min INTEGER NOT NULL,
price_max INTEGER NOT NULL,
stock_status TEXT DEFAULT 'available'
)
""")
# =============================================================================
# PRODUCT OPERATIONS
# =============================================================================
def get_all_products() -> List[Dict]:
"""Get semua produk karpet dari database"""
conn = get_connection()
cursor = get_cursor(conn)
cursor.execute("""
SELECT * FROM carpet_products
WHERE stock_status = 'available'
ORDER BY carpet_type
""")
products = [dict(row) for row in cursor.fetchall()]
conn.close()
return products
def get_product_by_combination(material: str, carpet_type: str) -> Optional[Dict]:
"""Get produk berdasarkan kombinasi material + type"""
conn = get_connection()
cursor = get_cursor(conn)
if DATABASE_TYPE == "postgresql":
cursor.execute("""
SELECT * FROM carpet_products
WHERE material = %s AND carpet_type = %s
AND stock_status = 'available'
""", (material, carpet_type))
else:
cursor.execute("""
SELECT * FROM carpet_products
WHERE material = ? AND carpet_type = ?
AND stock_status = 'available'
""", (material, carpet_type))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_price_range(material: str, carpet_type: str) -> Optional[Tuple[int, int]]:
"""Get price range untuk kombinasi material + type"""
product = get_product_by_combination(material, carpet_type)
if product:
return (product['price_min'], product['price_max'])
return None
# =============================================================================
# ANALYSIS HISTORY OPERATIONS
# =============================================================================
def save_analysis_result(result: Dict) -> str:
"""
Simpan hasil analisis ke database
Returns:
analysis_id: UUID untuk tracking
"""
conn = get_connection()
cursor = get_cursor(conn)
analysis_id = str(uuid.uuid4())
# Prepare query based on database type
if DATABASE_TYPE == "postgresql":
cursor.execute("""
INSERT INTO analysis_history (
analysis_id,
mikro_image_path,
makro_image_path,
material,
material_confidence,
material_processing_time,
carpet_coverage,
segmentation_processing_time,
carpet_type,
type_confidence,
type_processing_time,
is_authentic,
confidence_level,
warning,
reason,
price_min,
price_max,
price_note,
total_processing_time
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
analysis_id,
result.get('mikro_image_path'),
result.get('makro_image_path'),
result['material'],
result['material_confidence'],
result.get('material_processing_time'),
result['carpet_coverage'],
result.get('segmentation_processing_time'),
result['carpet_type'],
result['type_confidence'],
result.get('type_processing_time'),
result['is_authentic'],
result.get('confidence_level'),
result.get('warning'),
result.get('reason'),
result.get('price_min'),
result.get('price_max'),
result.get('price_note'),
result['total_processing_time']
))
else:
cursor.execute("""
INSERT INTO analysis_history (
analysis_id,
mikro_image_path,
makro_image_path,
material,
material_confidence,
material_processing_time,
carpet_coverage,
segmentation_processing_time,
carpet_type,
type_confidence,
type_processing_time,
is_authentic,
confidence_level,
warning,
reason,
price_min,
price_max,
price_note,
total_processing_time
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
analysis_id,
result.get('mikro_image_path'),
result.get('makro_image_path'),
result['material'],
result['material_confidence'],
result.get('material_processing_time'),
result['carpet_coverage'],
result.get('segmentation_processing_time'),
result['carpet_type'],
result['type_confidence'],
result.get('type_processing_time'),
result['is_authentic'],
result.get('confidence_level'),
result.get('warning'),
result.get('reason'),
result.get('price_min'),
result.get('price_max'),
result.get('price_note'),
result['total_processing_time']
))
conn.commit()
conn.close()
print(f"💾 Analysis saved to database: {analysis_id}")
return analysis_id
def get_analysis_by_id(analysis_id: str) -> Optional[Dict]:
"""Get hasil analisis berdasarkan ID"""
conn = get_connection()
cursor = get_cursor(conn)
if DATABASE_TYPE == "postgresql":
cursor.execute("""
SELECT * FROM analysis_history
WHERE analysis_id = %s
""", (analysis_id,))
else:
cursor.execute("""
SELECT * FROM analysis_history
WHERE analysis_id = ?
""", (analysis_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_recent_analyses(limit: int = 20) -> List[Dict]:
"""Get analisis terbaru"""
conn = get_connection()
cursor = get_cursor(conn)
# Try to use view first, fallback to direct query
try:
if DATABASE_TYPE == "postgresql":
cursor.execute("""
SELECT * FROM recent_analyses
LIMIT %s
""", (limit,))
else:
cursor.execute("""
SELECT * FROM recent_analyses
LIMIT ?
""", (limit,))
except:
# Fallback if view doesn't exist
if DATABASE_TYPE == "postgresql":
cursor.execute("""
SELECT * FROM analysis_history
ORDER BY created_at DESC
LIMIT %s
""", (limit,))
else:
cursor.execute("""
SELECT * FROM analysis_history
ORDER BY created_at DESC
LIMIT ?
""", (limit,))
analyses = [dict(row) for row in cursor.fetchall()]
conn.close()
return analyses
# =============================================================================
# STATISTICS & REPORTING
# =============================================================================
def get_material_statistics() -> List[Dict]:
"""Get statistik per material"""
conn = get_connection()
cursor = get_cursor(conn)
try:
cursor.execute("SELECT * FROM material_statistics")
stats = [dict(row) for row in cursor.fetchall()]
except:
# Fallback if view doesn't exist
cursor.execute("""
SELECT
material,
COUNT(*) as total_analyses,
SUM(CASE WHEN is_authentic THEN 1 ELSE 0 END) as authentic_count,
SUM(CASE WHEN NOT is_authentic THEN 1 ELSE 0 END) as fake_count,
ROUND(AVG(material_confidence) * 100, 2) as avg_confidence_pct,
ROUND(AVG(total_processing_time), 3) as avg_processing_time
FROM analysis_history
GROUP BY material
""")
stats = [dict(row) for row in cursor.fetchall()]
conn.close()
return stats
def get_carpet_type_statistics() -> List[Dict]:
"""Get statistik per jenis karpet"""
conn = get_connection()
cursor = get_cursor(conn)
try:
cursor.execute("SELECT * FROM carpet_type_statistics")
stats = [dict(row) for row in cursor.fetchall()]
except:
# Fallback if view doesn't exist
cursor.execute("""
SELECT
carpet_type,
COUNT(*) as total_analyses,
SUM(CASE WHEN is_authentic THEN 1 ELSE 0 END) as authentic_count,
SUM(CASE WHEN NOT is_authentic THEN 1 ELSE 0 END) as fake_count,
ROUND(AVG(type_confidence) * 100, 2) as avg_confidence_pct,
ROUND(AVG(total_processing_time), 3) as avg_processing_time
FROM analysis_history
GROUP BY carpet_type
""")
stats = [dict(row) for row in cursor.fetchall()]
conn.close()
return stats
def get_overall_statistics() -> Dict:
"""Get statistik keseluruhan"""
conn = get_connection()
cursor = get_cursor(conn)
# Total analyses
cursor.execute("SELECT COUNT(*) as total FROM analysis_history")
result = cursor.fetchone()
total = result['total'] if DATABASE_TYPE == "postgresql" else result[0]
# Authentic vs fake
cursor.execute("""
SELECT
COALESCE(SUM(CASE WHEN is_authentic THEN 1 ELSE 0 END), 0) as authentic,
COALESCE(SUM(CASE WHEN NOT is_authentic THEN 1 ELSE 0 END), 0) as fake
FROM analysis_history
""")
result = cursor.fetchone()
auth_fake = (result['authentic'], result['fake']) if DATABASE_TYPE == "postgresql" else (result[0], result[1])
# Average processing time
cursor.execute("SELECT AVG(total_processing_time) as avg_time FROM analysis_history")
result = cursor.fetchone()
avg_time = result['avg_time'] if DATABASE_TYPE == "postgresql" else result[0]
avg_time = avg_time or 0
# Today's analyses
if DATABASE_TYPE == "postgresql":
cursor.execute("""
SELECT COUNT(*) as today FROM analysis_history
WHERE DATE(created_at) = CURRENT_DATE
""")
else:
cursor.execute("""
SELECT COUNT(*) as today FROM analysis_history
WHERE DATE(created_at) = DATE('now')
""")
result = cursor.fetchone()
today = result['today'] if DATABASE_TYPE == "postgresql" else result[0]
conn.close()
return {
'total_analyses': total,
'authentic_count': auth_fake[0] or 0,
'fake_count': auth_fake[1] or 0,
'avg_processing_time': round(avg_time, 3),
'today_count': today
}
def log_suspicious_pattern(analysis_id: str, pattern_type: str,
severity: str, details: str):
"""Log pola mencurigakan untuk fraud detection"""
conn = get_connection()
cursor = get_cursor(conn)
if DATABASE_TYPE == "postgresql":
cursor.execute("""
INSERT INTO suspicious_patterns (
analysis_id, pattern_type, severity, details
) VALUES (%s, %s, %s, %s)
""", (analysis_id, pattern_type, severity, details))
else:
cursor.execute("""
INSERT INTO suspicious_patterns (
analysis_id, pattern_type, severity, details
) VALUES (?, ?, ?, ?)
""", (analysis_id, pattern_type, severity, details))
conn.commit()
conn.close()
print(f"⚠️ Suspicious pattern logged: {pattern_type} ({severity})")
# =============================================================================
# EXPORT FUNCTIONS
# =============================================================================
def export_to_json(analysis_id: str, output_path: str):
"""Export hasil analisis ke JSON file"""
result = get_analysis_by_id(analysis_id)
if result:
# Convert datetime to string
if 'created_at' in result:
result['created_at'] = str(result['created_at'])
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, ensure_ascii=False)
print(f"📄 Exported to {output_path}")
else:
print(f"❌ Analysis not found: {analysis_id}")
# =============================================================================
# MAIN - TEST DATABASE
# =============================================================================
if __name__ == "__main__":
print()
print("="*70)
print("🗄️ TESTING DATABASE MODULE (PostgreSQL Support)")
print("="*70)
print(f"Database Type: {DATABASE_TYPE}")
print(f"Database URL: {DATABASE_URL[:50]}...")
print("="*70)
print()
# Initialize database
init_database()
print()
# Test: Get all products
print("📦 Products in database:")
products = get_all_products()
for p in products:
print(f" • {p['product_name']}: Rp {p['price_min']:,} - Rp {p['price_max']:,}")
print()
# Test: Get price for specific combination
print("💰 Price check:")
price = get_price_range('Polypropylene', 'Asia')
if price:
print(f" Asia + Polypropylene: Rp {price[0]:,} - Rp {price[1]:,}")
print()
# Test: Get statistics
print("📊 Overall Statistics:")
stats = get_overall_statistics()
print(f" Total: {stats['total_analyses']}")
print(f" Authentic: {stats['authentic_count']}")
print(f" Fake: {stats['fake_count']}")
print(f" Avg time: {stats['avg_processing_time']}s")
print()
print("="*70)
print("✅ Database test complete!")
print("="*70)
print()