Download app.py from dipan004/Proofly: direct link, hf CLI and curl.
- Browser
- Download file 19.4 kB
-
https://huggingface.co/spaces/dipan004/Proofly/resolve/main/app.py
- Command line
-
hf download hf://spaces/dipan004/Proofly/app.py
-
curl -L -o app.py https://huggingface.co/spaces/dipan004/Proofly/resolve/main/app.py
19.4 kB
| """ | |
| FastAPI Application - Thin HTTP translation layer | |
| Stateless server that delegates all business logic to the orchestrator. | |
| Design principles: | |
| - No business logic in this file | |
| - Orchestrator handles all operations | |
| - Clean error handling with typed exceptions | |
| - Graceful degradation for optional features (OCR, AI) | |
| """ | |
| from fastapi import FastAPI, File, UploadFile, HTTPException, Form | |
| from fastapi.responses import JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| from typing import Optional | |
| import logging | |
| from contextlib import asynccontextmanager | |
| import os | |
| # Load environment variables | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from core.orchestrator import Orchestrator | |
| from core.errors import ( | |
| ProofSystemError, | |
| ValidationError, | |
| ProofNotFoundError, | |
| OCRError | |
| ) | |
| from config.settings import settings | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # ============================================================================ | |
| # APPLICATION LIFECYCLE | |
| # ============================================================================ | |
| # Global orchestrator instance (initialized at startup) | |
| orchestrator: Optional[Orchestrator] = None | |
| async def lifespan(app: FastAPI): | |
| """ | |
| Application lifespan handler. | |
| Initialize orchestrator at startup, cleanup at shutdown. | |
| """ | |
| global orchestrator | |
| logger.info("Starting Proof-of-Existence API") | |
| logger.info(f"OCR Enabled: {settings.OCR_ENABLED}") | |
| logger.info(f"AI Enabled: {settings.AI_ENABLED}") | |
| # Initialize orchestrator | |
| try: | |
| orchestrator = Orchestrator() | |
| logger.info("Orchestrator initialized successfully") | |
| except Exception as e: | |
| logger.error(f"Failed to initialize orchestrator: {str(e)}") | |
| raise | |
| yield | |
| # Cleanup (if needed) | |
| logger.info("Shutting down Proof-of-Existence API") | |
| # ============================================================================ | |
| # FASTAPI APP INITIALIZATION | |
| # ============================================================================ | |
| app = FastAPI( | |
| title="Proof-of-Existence API", | |
| description="Deterministic proof generation and verification with optional OCR and AI assistance", | |
| version="0.4.0", | |
| lifespan=lifespan | |
| ) | |
| # CORS middleware - PERMISSIVE for Flutter/Dreamflow | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # Allow all origins (Flutter web, HF spaces, localhost) | |
| allow_credentials=True, | |
| allow_methods=["*"], # Allow all HTTP methods | |
| allow_headers=["*"], # Allow all headers | |
| expose_headers=["*"], # Expose all response headers | |
| max_age=3600, # Cache preflight for 1 hour | |
| ) | |
| # ============================================================================ | |
| # REQUEST/RESPONSE MODELS | |
| # ============================================================================ | |
| class TextProofRequest(BaseModel): | |
| """Request model for creating proof from text content.""" | |
| content: str = Field(..., description="Text content to create proof for") | |
| metadata: Optional[dict] = Field(None, description="Optional metadata") | |
| class VerifyProofRequest(BaseModel): | |
| """Request model for verifying a proof.""" | |
| proof_id: str = Field(..., description="Unique proof identifier") | |
| content: str = Field(..., description="Content to verify against proof") | |
| class AssistantRequest(BaseModel): | |
| """Request model for AI assistant queries.""" | |
| question: str = Field(..., description="Question to ask the AI assistant") | |
| proof_id: Optional[str] = Field(None, description="Optional proof ID for context") | |
| class ProofResponse(BaseModel): | |
| """Standard proof creation response.""" | |
| success: bool | |
| proof_id: Optional[str] = None | |
| hash: Optional[str] = None | |
| timestamp: Optional[str] = None | |
| message: str | |
| assistant: Optional[dict] = None | |
| class VerificationResponse(BaseModel): | |
| """Standard verification response.""" | |
| success: bool | |
| is_valid: Optional[bool] = None | |
| message: str | |
| assistant: Optional[dict] = None | |
| # ============================================================================ | |
| # CORS PREFLIGHT HANDLER | |
| # ============================================================================ | |
| async def preflight_handler(rest_of_path: str): | |
| """Handle CORS preflight requests.""" | |
| return JSONResponse( | |
| content={"message": "OK"}, | |
| headers={ | |
| "Access-Control-Allow-Origin": "*", | |
| "Access-Control-Allow-Methods": "*", | |
| "Access-Control-Allow-Headers": "*", | |
| } | |
| ) | |
| # ============================================================================ | |
| # ERROR HANDLERS | |
| # ============================================================================ | |
| async def validation_error_handler(request, exc: ValidationError): | |
| """Handle validation errors with 400 Bad Request.""" | |
| return JSONResponse( | |
| status_code=400, | |
| content={ | |
| "success": False, | |
| "error": "validation_error", | |
| "message": str(exc) | |
| } | |
| ) | |
| async def proof_not_found_handler(request, exc: ProofNotFoundError): | |
| """Handle proof not found with 404.""" | |
| return JSONResponse( | |
| status_code=404, | |
| content={ | |
| "success": False, | |
| "error": "proof_not_found", | |
| "message": str(exc) | |
| } | |
| ) | |
| async def proof_system_error_handler(request, exc: ProofSystemError): | |
| """Handle general proof system errors with 500.""" | |
| logger.error(f"Proof system error: {str(exc)}") | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "success": False, | |
| "error": "system_error", | |
| "message": str(exc) | |
| } | |
| ) | |
| async def general_exception_handler(request, exc: Exception): | |
| """Handle unexpected errors with 500.""" | |
| logger.error(f"Unexpected error: {str(exc)}", exc_info=True) | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "success": False, | |
| "error": "internal_error", | |
| "message": "An unexpected error occurred" | |
| } | |
| ) | |
| # ============================================================================ | |
| # HEALTH CHECK | |
| # ============================================================================ | |
| async def health_check(): | |
| """ | |
| Health check endpoint. | |
| Returns system status and feature availability. | |
| """ | |
| return { | |
| "status": "healthy", | |
| "version": "0.4.0", | |
| "features": { | |
| "ocr": settings.OCR_ENABLED, | |
| "ai_assistant": settings.AI_ENABLED and orchestrator.ai_sidecar.enabled if orchestrator else False | |
| } | |
| } | |
| # ============================================================================ | |
| # PROOF ENDPOINTS | |
| # ============================================================================ | |
| async def create_proof_from_text(request: TextProofRequest): | |
| """ | |
| Create a cryptographic proof from text content. | |
| This endpoint: | |
| 1. Validates text input | |
| 2. Generates SHA-256 hash | |
| 3. Creates proof with timestamp | |
| 4. Stores proof in database | |
| 5. Optionally provides AI explanation | |
| Returns: | |
| Proof ID, hash, timestamp, and optional AI explanation | |
| """ | |
| if not orchestrator: | |
| raise HTTPException(status_code=503, detail="Service not initialized") | |
| logger.info(f"Creating proof from text ({len(request.content)} chars)") | |
| result = orchestrator.create_proof({ | |
| "type": "text", | |
| "content": request.content | |
| }) | |
| if not result["success"]: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=result.get("message", "Failed to create proof") | |
| ) | |
| proof = result["proof"] | |
| return ProofResponse( | |
| success=True, | |
| proof_id=proof.proof_id, | |
| hash=proof.content_hash, | |
| timestamp=proof.timestamp, | |
| message="Proof created successfully", | |
| assistant=result.get("assistant") | |
| ) | |
| async def create_proof_from_file( | |
| file: UploadFile = File(...), | |
| metadata: Optional[str] = Form(None) | |
| ): | |
| """ | |
| Create a cryptographic proof from uploaded file. | |
| Supports: | |
| - Text files | |
| - Images (with optional OCR) | |
| - PDFs | |
| - Binary files | |
| Returns: | |
| Proof ID, hash, timestamp, OCR results (if applicable), and optional AI explanation | |
| """ | |
| if not orchestrator: | |
| raise HTTPException(status_code=503, detail="Service not initialized") | |
| logger.info(f"Creating proof from file: {file.filename} ({file.content_type})") | |
| # Read file content | |
| content = await file.read() | |
| if len(content) == 0: | |
| raise ValidationError("File is empty") | |
| result = orchestrator.create_proof({ | |
| "type": "file", | |
| "content": content, | |
| "filename": file.filename | |
| }) | |
| if not result["success"]: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=result.get("message", "Failed to create proof") | |
| ) | |
| proof = result["proof"] | |
| return { | |
| "success": True, | |
| "proof_id": proof.proof_id, | |
| "hash": proof.content_hash, | |
| "timestamp": proof.timestamp, | |
| "content_type": proof.content_type, | |
| "size": proof.content_size, | |
| "ocr_status": proof.ocr_status, | |
| "extracted_text": proof.extracted_text[:200] + "..." if proof.extracted_text and len(proof.extracted_text) > 200 else proof.extracted_text, | |
| "message": "Proof created successfully", | |
| "assistant": result.get("assistant") | |
| } | |
| async def get_proof(proof_id: str): | |
| """ | |
| Retrieve a stored proof by ID. | |
| Args: | |
| proof_id: Unique proof identifier | |
| Returns: | |
| Complete proof object with all metadata | |
| """ | |
| if not orchestrator: | |
| raise HTTPException(status_code=503, detail="Service not initialized") | |
| logger.info(f"Retrieving proof: {proof_id}") | |
| result = orchestrator.get_proof(proof_id) | |
| if not result["success"]: | |
| raise ProofNotFoundError(f"Proof not found: {proof_id}") | |
| proof = result["proof"] | |
| return { | |
| "success": True, | |
| "proof": proof.to_dict(), | |
| "message": "Proof retrieved successfully" | |
| } | |
| async def verify_proof(request: VerifyProofRequest): | |
| """ | |
| Verify a proof against original content. | |
| This endpoint: | |
| 1. Retrieves original proof | |
| 2. Recomputes hash from provided content | |
| 3. Compares hashes | |
| 4. Returns verification result | |
| 5. Optionally provides AI explanation | |
| Returns: | |
| Verification status (valid/invalid) with explanation | |
| """ | |
| if not orchestrator: | |
| raise HTTPException(status_code=503, detail="Service not initialized") | |
| logger.info(f"Verifying proof: {request.proof_id}") | |
| # Convert content to bytes | |
| content_bytes = request.content.encode('utf-8') | |
| result = orchestrator.verify_proof(request.proof_id, content_bytes) | |
| if not result["success"]: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=result.get("message", "Verification failed") | |
| ) | |
| verification_result = result["verification_result"] | |
| return VerificationResponse( | |
| success=True, | |
| is_valid=verification_result.is_valid, | |
| message=verification_result.message, | |
| assistant=result.get("assistant") | |
| ) | |
| async def verify_proof_with_file( | |
| proof_id: str = Form(...), | |
| file: UploadFile = File(...) | |
| ): | |
| """ | |
| Verify a proof by uploading the original file. | |
| This endpoint: | |
| 1. Accepts proof ID + file upload | |
| 2. Retrieves original proof from storage | |
| 3. Recomputes hash from uploaded file | |
| 4. Compares hashes | |
| 5. Returns verification result with original timestamp | |
| Args: | |
| proof_id: Unique proof identifier | |
| file: Original file to verify | |
| Returns: | |
| - is_valid: True if hashes match, False if tampered | |
| - original_hash: Hash from original proof | |
| - computed_hash: Hash from uploaded file | |
| - original_timestamp: When proof was created | |
| - message: Human-readable result | |
| """ | |
| if not orchestrator: | |
| raise HTTPException(status_code=503, detail="Service not initialized") | |
| logger.info(f"Verifying proof {proof_id} with file: {file.filename}") | |
| # Read uploaded file | |
| content = await file.read() | |
| if len(content) == 0: | |
| raise ValidationError("Uploaded file is empty") | |
| # Verify proof | |
| result = orchestrator.verify_proof(proof_id, content) | |
| if not result["success"]: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=result.get("message", "Verification failed") | |
| ) | |
| verification_result = result["verification_result"] | |
| # Get original proof for timestamp | |
| proof_result = orchestrator.get_proof(proof_id) | |
| original_proof = proof_result.get("proof") if proof_result["success"] else None | |
| return { | |
| "success": True, | |
| "is_valid": verification_result.is_valid, | |
| "proof_id": proof_id, | |
| "original_hash": verification_result.original_hash, | |
| "computed_hash": verification_result.computed_hash, | |
| "original_timestamp": original_proof.timestamp if original_proof else None, | |
| "original_filename": original_proof.metadata.get("filename") if original_proof else None, | |
| "file_size": len(content), | |
| "message": verification_result.message, | |
| "assistant": result.get("assistant") | |
| } | |
| async def get_proof_details(proof_id: str): | |
| """ | |
| Get detailed information about a proof by its ID. | |
| Shows everything: hash, timestamp, file info, OCR results. | |
| Args: | |
| proof_id: Unique proof identifier | |
| Returns: | |
| Complete proof details including: | |
| - Hash | |
| - Original timestamp | |
| - File name and size | |
| - OCR results (if applicable) | |
| - Content type | |
| """ | |
| if not orchestrator: | |
| raise HTTPException(status_code=503, detail="Service not initialized") | |
| logger.info(f"Getting details for proof: {proof_id}") | |
| result = orchestrator.get_proof(proof_id) | |
| if not result["success"]: | |
| raise ProofNotFoundError(f"Proof not found: {proof_id}") | |
| proof = result["proof"] | |
| return { | |
| "success": True, | |
| "proof_id": proof.proof_id, | |
| "hash": proof.content_hash, | |
| "hash_algorithm": proof.hash_algorithm, | |
| "timestamp": proof.timestamp, | |
| "content_type": proof.content_type, | |
| "file_size": proof.content_size, | |
| "filename": proof.metadata.get("filename"), | |
| "ocr_status": proof.ocr_status, | |
| "extracted_text": proof.extracted_text, | |
| "message": "Proof details retrieved successfully" | |
| } | |
| async def verify_by_hash( | |
| proof_id: str = Form(...), | |
| hash_to_verify: str = Form(...) | |
| ): | |
| """ | |
| Verify a proof by comparing hashes directly. | |
| Useful when you've computed the hash separately. | |
| Args: | |
| proof_id: Unique proof identifier | |
| hash_to_verify: SHA-256 hash to compare | |
| Returns: | |
| Verification result with timestamp info | |
| """ | |
| if not orchestrator: | |
| raise HTTPException(status_code=503, detail="Service not initialized") | |
| logger.info(f"Verifying proof {proof_id} with hash: {hash_to_verify[:16]}...") | |
| # Get original proof | |
| result = orchestrator.get_proof(proof_id) | |
| if not result["success"]: | |
| raise ProofNotFoundError(f"Proof not found: {proof_id}") | |
| proof = result["proof"] | |
| # Compare hashes | |
| is_valid = proof.content_hash.lower() == hash_to_verify.lower() | |
| return { | |
| "success": True, | |
| "is_valid": is_valid, | |
| "proof_id": proof_id, | |
| "original_hash": proof.content_hash, | |
| "provided_hash": hash_to_verify, | |
| "original_timestamp": proof.timestamp, | |
| "original_filename": proof.metadata.get("filename"), | |
| "message": "Hash match: proof is valid" if is_valid else "Hash mismatch: content has been modified" | |
| } | |
| # ============================================================================ | |
| # AI ASSISTANT ENDPOINTS (OPTIONAL) | |
| # ============================================================================ | |
| async def ask_assistant(request: AssistantRequest): | |
| """ | |
| Ask the AI assistant a question about proofs. | |
| This is an OPTIONAL feature that provides explanations and guidance. | |
| The assistant is non-authoritative and never affects proof validity. | |
| Returns: | |
| AI-generated explanation (clearly marked as non-authoritative) | |
| """ | |
| if not orchestrator: | |
| raise HTTPException(status_code=503, detail="Service not initialized") | |
| if not settings.AI_ENABLED or not orchestrator.ai_sidecar.enabled: | |
| return { | |
| "success": False, | |
| "message": "AI assistant is not enabled. Set AI_ENABLED=true and configure GEMINI_API_KEY." | |
| } | |
| logger.info(f"AI assistant query: {request.question[:50]}...") | |
| result = orchestrator.ask_assistant(request.question, request.proof_id) | |
| return result | |
| # ============================================================================ | |
| # DOCUMENTATION ENDPOINTS | |
| # ============================================================================ | |
| async def root(): | |
| """ | |
| API root with basic information and links. | |
| """ | |
| return { | |
| "name": "Proof-of-Existence API", | |
| "version": "0.4.0", | |
| "description": "Deterministic cryptographic proof generation and verification", | |
| "docs": "/docs", | |
| "health": "/health", | |
| "endpoints": { | |
| "create_text_proof": "POST /proof/create/text", | |
| "create_file_proof": "POST /proof/create/file", | |
| "get_proof": "GET /proof/{proof_id}", | |
| "get_proof_details": "GET /proof/{proof_id}/details", | |
| "verify_proof": "POST /proof/verify", | |
| "verify_proof_file": "POST /proof/verify/file", | |
| "verify_proof_hash": "POST /proof/verify/hash", | |
| "ask_assistant": "POST /assistant/ask (optional)" | |
| } | |
| } | |
| # ============================================================================ | |
| # DEVELOPMENT SERVER | |
| # ============================================================================ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # Run development server | |
| uvicorn.run( | |
| "app:app", | |
| host="0.0.0.0", | |
| port=7860, | |
| reload=True, # Enable auto-reload in development | |
| log_level="info" | |
| ) |