#!/usr/bin/env python3 """ UCXL Integration API for WHOOSH API endpoints for distributed artifact storage, retrieval, and temporal navigation """ from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File from typing import Dict, List, Optional, Any, Union from pydantic import BaseModel, Field from datetime import datetime from ..services.ucxl_integration_service import ucxl_service, UCXLAddress from ..core.auth_deps import get_current_user from ..models.user import User router = APIRouter(prefix="/api/ucxl", tags=["UCXL Integration"]) # Pydantic models for API requests/responses class StoreArtifactRequest(BaseModel): project: str = Field(..., description="Project name") component: str = Field(..., description="Component name") path: str = Field(..., description="Artifact path") content: str = Field(..., description="Artifact content") content_type: str = Field("text/plain", description="Content MIME type") metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") class StoreArtifactResponse(BaseModel): address: str success: bool message: str class ArtifactInfo(BaseModel): address: str content_hash: str content_type: str size: int created_at: str modified_at: str metadata: Dict[str, Any] cached: Optional[bool] = None class CreateProjectContextRequest(BaseModel): project_name: str = Field(..., description="Project name") description: str = Field(..., description="Project description") components: List[str] = Field(..., description="List of project components") metadata: Optional[Dict[str, Any]] = Field(None, description="Additional project metadata") class LinkArtifactsRequest(BaseModel): source_address: str = Field(..., description="Source UCXL address") target_address: str = Field(..., description="Target UCXL address") relationship: str = Field(..., description="Relationship type (e.g., 'depends_on', 'implements', 'tests')") metadata: Optional[Dict[str, Any]] = Field(None, description="Link metadata") class SystemStatusResponse(BaseModel): ucxl_endpoints: int dht_nodes: int bzzz_gateways: int cached_artifacts: int cache_limit: int system_health: float last_update: str @router.get("/status", response_model=SystemStatusResponse) async def get_ucxl_status( current_user: User = Depends(get_current_user) ) -> SystemStatusResponse: """Get UCXL integration system status""" try: status = await ucxl_service.get_system_status() return SystemStatusResponse(**status) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get UCXL status: {str(e)}") @router.post("/artifacts", response_model=StoreArtifactResponse) async def store_artifact( request: StoreArtifactRequest, current_user: User = Depends(get_current_user) ) -> StoreArtifactResponse: """ Store an artifact in the distributed UCXL system """ try: address = await ucxl_service.store_artifact( project=request.project, component=request.component, path=request.path, content=request.content, content_type=request.content_type, metadata=request.metadata ) if address: return StoreArtifactResponse( address=address, success=True, message="Artifact stored successfully" ) else: raise HTTPException(status_code=500, detail="Failed to store artifact") except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to store artifact: {str(e)}") @router.post("/artifacts/upload", response_model=StoreArtifactResponse) async def upload_artifact( project: str, component: str, path: str, file: UploadFile = File(...), metadata: Optional[str] = None, current_user: User = Depends(get_current_user) ) -> StoreArtifactResponse: """ Upload and store a file artifact in the distributed UCXL system """ try: # Read file content content = await file.read() # Parse metadata if provided file_metadata = {} if metadata: import json file_metadata = json.loads(metadata) # Add file info to metadata file_metadata.update({ "original_filename": file.filename, "uploaded_by": current_user.username, "upload_timestamp": datetime.utcnow().isoformat() }) address = await ucxl_service.store_artifact( project=project, component=component, path=path, content=content, content_type=file.content_type or "application/octet-stream", metadata=file_metadata ) if address: return StoreArtifactResponse( address=address, success=True, message=f"File '{file.filename}' uploaded successfully" ) else: raise HTTPException(status_code=500, detail="Failed to upload file") except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to upload file: {str(e)}") @router.get("/artifacts/{address:path}", response_model=Optional[ArtifactInfo]) async def retrieve_artifact( address: str, current_user: User = Depends(get_current_user) ) -> Optional[ArtifactInfo]: """ Retrieve an artifact from the distributed UCXL system """ try: # Decode URL-encoded address import urllib.parse decoded_address = urllib.parse.unquote(address) data = await ucxl_service.retrieve_artifact(decoded_address) if data: return ArtifactInfo(**data) else: raise HTTPException(status_code=404, detail=f"Artifact not found: {decoded_address}") except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to retrieve artifact: {str(e)}") @router.get("/artifacts", response_model=List[ArtifactInfo]) async def list_artifacts( project: Optional[str] = Query(None, description="Filter by project"), component: Optional[str] = Query(None, description="Filter by component"), limit: int = Query(100, ge=1, le=1000, description="Maximum number of artifacts to return"), current_user: User = Depends(get_current_user) ) -> List[ArtifactInfo]: """ List artifacts from the distributed UCXL system """ try: artifacts = await ucxl_service.list_artifacts( project=project, component=component, limit=limit ) return [ArtifactInfo(**artifact) for artifact in artifacts] except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to list artifacts: {str(e)}") @router.get("/artifacts/{address:path}/temporal", response_model=Optional[ArtifactInfo]) async def resolve_temporal_artifact( address: str, timestamp: Optional[str] = Query(None, description="ISO timestamp for temporal resolution"), current_user: User = Depends(get_current_user) ) -> Optional[ArtifactInfo]: """ Resolve a UCXL address at a specific point in time using temporal navigation """ try: # Decode URL-encoded address import urllib.parse decoded_address = urllib.parse.unquote(address) # Parse timestamp if provided target_time = None if timestamp: target_time = datetime.fromisoformat(timestamp) data = await ucxl_service.resolve_temporal_address(decoded_address, target_time) if data: return ArtifactInfo(**data) else: raise HTTPException(status_code=404, detail=f"Artifact not found at specified time: {decoded_address}") except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to resolve temporal artifact: {str(e)}") @router.post("/projects", response_model=Dict[str, str]) async def create_project_context( request: CreateProjectContextRequest, current_user: User = Depends(get_current_user) ) -> Dict[str, str]: """ Create a project context in the UCXL system """ try: address = await ucxl_service.create_project_context( project_name=request.project_name, description=request.description, components=request.components, metadata=request.metadata ) if address: return { "address": address, "project_name": request.project_name, "status": "created" } else: raise HTTPException(status_code=500, detail="Failed to create project context") except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to create project context: {str(e)}") @router.post("/links", response_model=Dict[str, str]) async def link_artifacts( request: LinkArtifactsRequest, current_user: User = Depends(get_current_user) ) -> Dict[str, str]: """ Create a relationship link between two UCXL artifacts """ try: success = await ucxl_service.link_artifacts( source_address=request.source_address, target_address=request.target_address, relationship=request.relationship, metadata=request.metadata ) if success: return { "status": "linked", "source": request.source_address, "target": request.target_address, "relationship": request.relationship } else: raise HTTPException(status_code=500, detail="Failed to create artifact link") except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to link artifacts: {str(e)}") @router.get("/artifacts/{address:path}/links", response_model=List[Dict[str, Any]]) async def get_artifact_links( address: str, current_user: User = Depends(get_current_user) ) -> List[Dict[str, Any]]: """ Get all links involving a specific artifact """ try: # Decode URL-encoded address import urllib.parse decoded_address = urllib.parse.unquote(address) links = await ucxl_service.get_artifact_links(decoded_address) return links except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get artifact links: {str(e)}") @router.get("/addresses/parse", response_model=Dict[str, Any]) async def parse_ucxl_address( address: str = Query(..., description="UCXL address to parse"), current_user: User = Depends(get_current_user) ) -> Dict[str, Any]: """ Parse a UCXL address into its components """ try: ucxl_addr = UCXLAddress.parse(address) return { "original": address, "protocol": ucxl_addr.protocol.value, "user": ucxl_addr.user, "password": "***" if ucxl_addr.password else None, # Hide password "project": ucxl_addr.project, "component": ucxl_addr.component, "path": ucxl_addr.path, "reconstructed": ucxl_addr.to_string() } except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid UCXL address: {str(e)}") @router.get("/addresses/generate", response_model=Dict[str, str]) async def generate_ucxl_address( project: str = Query(..., description="Project name"), component: str = Query(..., description="Component name"), path: str = Query(..., description="Artifact path"), user: Optional[str] = Query(None, description="User name"), secure: bool = Query(False, description="Use secure protocol (ucxls)"), current_user: User = Depends(get_current_user) ) -> Dict[str, str]: """ Generate a UCXL address from components """ try: from ..services.ucxl_integration_service import UCXLProtocol ucxl_addr = UCXLAddress( protocol=UCXLProtocol.UCXL_SECURE if secure else UCXLProtocol.UCXL, user=user, project=project, component=component, path=path ) return { "address": ucxl_addr.to_string(), "project": project, "component": component, "path": path } except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to generate address: {str(e)}") @router.get("/health") async def ucxl_health_check() -> Dict[str, Any]: """UCXL integration health check endpoint""" try: status = await ucxl_service.get_system_status() health_status = "healthy" if status.get("system_health", 0) < 0.5: health_status = "degraded" if status.get("dht_nodes", 0) == 0: health_status = "offline" return { "status": health_status, "ucxl_endpoints": status.get("ucxl_endpoints", 0), "dht_nodes": status.get("dht_nodes", 0), "bzzz_gateways": status.get("bzzz_gateways", 0), "cached_artifacts": status.get("cached_artifacts", 0), "system_health": status.get("system_health", 0), "timestamp": datetime.utcnow().isoformat() } except Exception as e: return { "status": "error", "error": str(e), "timestamp": datetime.utcnow().isoformat() } # Note: Exception handlers are registered at the app level, not router level