LAD / modules /supabase_client.py
ozeiri's picture
Deploy LAD as a Docker Space
857664f
Raw
History Blame Contribute Delete
13 kB
"""
Supabase integration for artifact storage and caching
"""
import os
import json
import hashlib
import logging
from typing import List, Dict, Optional, Any
from datetime import datetime
import uuid
try:
from supabase import create_client, Client
except ImportError:
print("Supabase not installed. Install with: pip install supabase")
raise
logger = logging.getLogger(__name__)
class SupabaseArtifactManager:
"""Manages artifact storage and retrieval from Supabase database"""
def __init__(self):
# Get Supabase credentials from environment
self.url = os.getenv("SUPABASE_URL")
self.key = os.getenv("SUPABASE_ANON_KEY")
if not self.url or not self.key:
raise ValueError("SUPABASE_URL and SUPABASE_ANON_KEY must be set in environment variables")
self.client: Client = create_client(self.url, self.key)
logger.info("Supabase client initialized successfully")
def _generate_content_hash(self, file_path: str, model: str, processing_params: Dict) -> str:
"""Generate a unique hash for the processing configuration"""
hasher = hashlib.sha256()
# Include file content hash
with open(file_path, 'rb') as f:
file_content = f.read()
hasher.update(file_content)
# Include processing parameters
params_str = json.dumps(processing_params, sort_keys=True)
hasher.update(params_str.encode())
hasher.update(model.encode())
return hasher.hexdigest()
def check_processing_cache(self, file_path: str, model: str, processing_params: Dict) -> Optional[str]:
"""Check if this exact processing has been done before"""
try:
content_hash = self._generate_content_hash(file_path, model, processing_params)
file_name = os.path.basename(file_path)
result = self.client.table("processing_cache").select("*").eq(
"content_hash", content_hash
).eq("file_name", file_name).execute()
if result.data:
cache_entry = result.data[0]
logger.info(f"Found cached processing for {file_name} with hash {content_hash[:8]}...")
return cache_entry["processing_run_id"]
return None
except Exception as e:
logger.error(f"Error checking processing cache: {e}")
return None
def create_processing_run(self, file_path: str, model: str, processing_params: Dict) -> str:
"""Create a new processing run record"""
try:
run_id = str(uuid.uuid4())
content_hash = self._generate_content_hash(file_path, model, processing_params)
processing_run = {
"id": run_id,
"file_name": os.path.basename(file_path),
"file_path": file_path,
"model": model,
"processing_params": processing_params,
"content_hash": content_hash,
"status": "running",
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat()
}
self.client.table("processing_runs").insert(processing_run).execute()
# Also add to cache
cache_entry = {
"content_hash": content_hash,
"file_name": os.path.basename(file_path),
"processing_run_id": run_id,
"created_at": datetime.utcnow().isoformat()
}
self.client.table("processing_cache").insert(cache_entry).execute()
logger.info(f"Created processing run {run_id} for {os.path.basename(file_path)}")
return run_id
except Exception as e:
logger.error(f"Error creating processing run: {e}")
raise
def update_processing_status(self, run_id: str, status: str, error_message: str = None):
"""Update the status of a processing run"""
try:
update_data = {
"status": status,
"updated_at": datetime.utcnow().isoformat()
}
if error_message:
update_data["error_message"] = error_message
self.client.table("processing_runs").update(update_data).eq("id", run_id).execute()
logger.info(f"Updated processing run {run_id} status to {status}")
except Exception as e:
logger.error(f"Error updating processing status: {e}")
def save_artifacts(self, run_id: str, artifacts: List[Dict]) -> List[str]:
"""Save extracted artifacts to database"""
try:
artifact_ids = []
for artifact in artifacts:
artifact_id = str(uuid.uuid4())
artifact_record = {
"id": artifact_id,
"processing_run_id": run_id,
"name_en": artifact.get("Name", artifact.get("Name_EN", "")),
"name_ar": artifact.get("Name_AR", ""),
"name_fr": artifact.get("Name_FR", ""),
"creator": artifact.get("Creator", ""),
"creation_date": artifact.get("Creation Date", ""),
"materials": artifact.get("Materials", ""),
"origin": artifact.get("Origin", ""),
"description": artifact.get("Description", ""),
"category": artifact.get("Category", ""),
"source_page": artifact.get("source_page"),
"source_document": artifact.get("source_document", ""),
"metadata": artifact, # Store full artifact data as JSON
"created_at": datetime.utcnow().isoformat()
}
result = self.client.table("artifacts").insert(artifact_record).execute()
artifact_ids.append(artifact_id)
logger.info(f"Saved {len(artifacts)} artifacts for run {run_id}")
return artifact_ids
except Exception as e:
logger.error(f"Error saving artifacts: {e}")
raise
def get_artifacts_by_run_id(self, run_id: str) -> List[Dict]:
"""Retrieve all artifacts for a processing run"""
try:
result = self.client.table("artifacts").select("*").eq(
"processing_run_id", run_id
).execute()
artifacts = []
for record in result.data:
# Convert back to original format
artifact = record["metadata"] if record["metadata"] else {}
# Ensure basic fields are populated
artifact.update({
"Name": record["name_en"] or artifact.get("Name", ""),
"Name_EN": record["name_en"] or artifact.get("Name_EN", ""),
"Name_AR": record["name_ar"] or artifact.get("Name_AR", ""),
"Name_FR": record["name_fr"] or artifact.get("Name_FR", ""),
"Creator": record["creator"] or artifact.get("Creator", ""),
"Creation Date": record["creation_date"] or artifact.get("Creation Date", ""),
"Materials": record["materials"] or artifact.get("Materials", ""),
"Origin": record["origin"] or artifact.get("Origin", ""),
"Description": record["description"] or artifact.get("Description", ""),
"Category": record["category"] or artifact.get("Category", ""),
"source_page": record["source_page"] or artifact.get("source_page"),
"source_document": record["source_document"] or artifact.get("source_document", "")
})
artifacts.append(artifact)
logger.info(f"Retrieved {len(artifacts)} artifacts for run {run_id}")
return artifacts
except Exception as e:
logger.error(f"Error retrieving artifacts: {e}")
return []
def search_artifacts(self, query: str = None, language: str = None,
category: str = None, creator: str = None,
limit: int = 100) -> List[Dict]:
"""Search artifacts with various filters"""
try:
query_builder = self.client.table("artifacts").select("*")
if query:
# Search in names and descriptions
query_builder = query_builder.or_(f"name_en.ilike.%{query}%,name_ar.ilike.%{query}%,name_fr.ilike.%{query}%,description.ilike.%{query}%")
if category:
query_builder = query_builder.eq("category", category)
if creator:
query_builder = query_builder.ilike("creator", f"%{creator}%")
result = query_builder.limit(limit).execute()
artifacts = []
for record in result.data:
artifact = record["metadata"] if record["metadata"] else {}
artifact.update({
"Name_EN": record["name_en"],
"Name_AR": record["name_ar"],
"Name_FR": record["name_fr"],
"Creator": record["creator"],
"Category": record["category"],
"Description": record["description"]
})
artifacts.append(artifact)
logger.info(f"Found {len(artifacts)} artifacts matching search criteria")
return artifacts
except Exception as e:
logger.error(f"Error searching artifacts: {e}")
return []
def get_processing_runs(self, limit: int = 50) -> List[Dict]:
"""Get recent processing runs"""
try:
result = self.client.table("processing_runs").select("*").order(
"created_at", desc=True
).limit(limit).execute()
return result.data
except Exception as e:
logger.error(f"Error retrieving processing runs: {e}")
return []
def delete_processing_run(self, run_id: str) -> bool:
"""Delete a processing run and all its artifacts"""
try:
# Delete artifacts first (due to foreign key constraint)
self.client.table("artifacts").delete().eq("processing_run_id", run_id).execute()
# Delete from cache
self.client.table("processing_cache").delete().eq("processing_run_id", run_id).execute()
# Delete processing run
self.client.table("processing_runs").delete().eq("id", run_id).execute()
logger.info(f"Deleted processing run {run_id} and all associated data")
return True
except Exception as e:
logger.error(f"Error deleting processing run: {e}")
return False
def get_statistics(self) -> Dict:
"""Get database statistics"""
try:
# Count total artifacts
artifacts_result = self.client.table("artifacts").select("id", count="exact").execute()
total_artifacts = artifacts_result.count
# Count processing runs
runs_result = self.client.table("processing_runs").select("id", count="exact").execute()
total_runs = runs_result.count
# Count by status
completed_runs = self.client.table("processing_runs").select("id", count="exact").eq("status", "completed").execute().count
failed_runs = self.client.table("processing_runs").select("id", count="exact").eq("status", "failed").execute().count
# Count by category
categories_result = self.client.table("artifacts").select("category").execute()
categories = {}
for record in categories_result.data:
cat = record.get("category", "Unknown")
categories[cat] = categories.get(cat, 0) + 1
return {
"total_artifacts": total_artifacts,
"total_processing_runs": total_runs,
"completed_runs": completed_runs,
"failed_runs": failed_runs,
"running_runs": total_runs - completed_runs - failed_runs,
"categories": categories
}
except Exception as e:
logger.error(f"Error getting statistics: {e}")
return {}