from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from sqlalchemy import or_ from typing import List, Optional import re from .database import get_db from .storage import get_storage, S3Storage from .models import Grove, Tree, Fruit, Graft, Harvest, Consumer from .schemas import ( GroveCreate, GroveResponse, TreeCreate, TreeResponse, FruitResponse, GraftCreate, GraftResponse, CultivateResponse, ConsumerResponse, HealthResponse, ) router = APIRouter() def get_user_id(request: Request) -> str: """Extract user ID from request (simplified for now)""" api_key = request.headers.get("X-Orchard-API-Key") if api_key: return "api-user" auth = request.headers.get("Authorization") if auth: return "bearer-user" return "anonymous" # Health check @router.get("/health", response_model=HealthResponse) def health_check(): return HealthResponse(status="ok") # Grove routes @router.get("/api/v1/groves", response_model=List[GroveResponse]) def list_groves(request: Request, db: Session = Depends(get_db)): user_id = get_user_id(request) groves = db.query(Grove).filter( or_(Grove.is_public == True, Grove.created_by == user_id) ).order_by(Grove.name).all() return groves @router.post("/api/v1/groves", response_model=GroveResponse) def create_grove(grove: GroveCreate, request: Request, db: Session = Depends(get_db)): user_id = get_user_id(request) existing = db.query(Grove).filter(Grove.name == grove.name).first() if existing: raise HTTPException(status_code=400, detail="Grove already exists") db_grove = Grove( name=grove.name, description=grove.description, is_public=grove.is_public, created_by=user_id, ) db.add(db_grove) db.commit() db.refresh(db_grove) return db_grove @router.get("/api/v1/groves/{grove_name}", response_model=GroveResponse) def get_grove(grove_name: str, db: Session = Depends(get_db)): grove = db.query(Grove).filter(Grove.name == grove_name).first() if not grove: raise HTTPException(status_code=404, detail="Grove not found") return grove # Tree routes @router.get("/api/v1/grove/{grove_name}/trees", response_model=List[TreeResponse]) def list_trees(grove_name: str, db: Session = Depends(get_db)): grove = db.query(Grove).filter(Grove.name == grove_name).first() if not grove: raise HTTPException(status_code=404, detail="Grove not found") trees = db.query(Tree).filter(Tree.grove_id == grove.id).order_by(Tree.name).all() return trees @router.post("/api/v1/grove/{grove_name}/trees", response_model=TreeResponse) def create_tree(grove_name: str, tree: TreeCreate, db: Session = Depends(get_db)): grove = db.query(Grove).filter(Grove.name == grove_name).first() if not grove: raise HTTPException(status_code=404, detail="Grove not found") existing = db.query(Tree).filter(Tree.grove_id == grove.id, Tree.name == tree.name).first() if existing: raise HTTPException(status_code=400, detail="Tree already exists in this grove") db_tree = Tree( grove_id=grove.id, name=tree.name, description=tree.description, ) db.add(db_tree) db.commit() db.refresh(db_tree) return db_tree # Cultivate (upload) @router.post("/api/v1/grove/{grove_name}/{tree_name}/cultivate", response_model=CultivateResponse) def cultivate( grove_name: str, tree_name: str, request: Request, file: UploadFile = File(...), tag: Optional[str] = Form(None), db: Session = Depends(get_db), storage: S3Storage = Depends(get_storage), ): user_id = get_user_id(request) # Get grove and tree grove = db.query(Grove).filter(Grove.name == grove_name).first() if not grove: raise HTTPException(status_code=404, detail="Grove not found") tree = db.query(Tree).filter(Tree.grove_id == grove.id, Tree.name == tree_name).first() if not tree: raise HTTPException(status_code=404, detail="Tree not found") # Store file sha256_hash, size, s3_key = storage.store(file.file) # Create or update fruit record fruit = db.query(Fruit).filter(Fruit.id == sha256_hash).first() if fruit: fruit.ref_count += 1 else: fruit = Fruit( id=sha256_hash, size=size, content_type=file.content_type, original_name=file.filename, created_by=user_id, s3_key=s3_key, ) db.add(fruit) # Record harvest harvest = Harvest( fruit_id=sha256_hash, tree_id=tree.id, original_name=file.filename, harvested_by=user_id, source_ip=request.client.host if request.client else None, ) db.add(harvest) # Create tag if provided if tag: existing_graft = db.query(Graft).filter(Graft.tree_id == tree.id, Graft.name == tag).first() if existing_graft: existing_graft.fruit_id = sha256_hash existing_graft.created_by = user_id else: graft = Graft( tree_id=tree.id, name=tag, fruit_id=sha256_hash, created_by=user_id, ) db.add(graft) db.commit() return CultivateResponse( fruit_id=sha256_hash, size=size, grove=grove_name, tree=tree_name, tag=tag, ) # Harvest (download) @router.get("/api/v1/grove/{grove_name}/{tree_name}/+/{ref}") def harvest( grove_name: str, tree_name: str, ref: str, db: Session = Depends(get_db), storage: S3Storage = Depends(get_storage), ): # Get grove and tree grove = db.query(Grove).filter(Grove.name == grove_name).first() if not grove: raise HTTPException(status_code=404, detail="Grove not found") tree = db.query(Tree).filter(Tree.grove_id == grove.id, Tree.name == tree_name).first() if not tree: raise HTTPException(status_code=404, detail="Tree not found") # Resolve reference to fruit fruit = None # Check for explicit prefixes if ref.startswith("fruit:"): fruit_id = ref[6:] fruit = db.query(Fruit).filter(Fruit.id == fruit_id).first() elif ref.startswith("tag:") or ref.startswith("version:"): tag_name = ref.split(":", 1)[1] graft = db.query(Graft).filter(Graft.tree_id == tree.id, Graft.name == tag_name).first() if graft: fruit = db.query(Fruit).filter(Fruit.id == graft.fruit_id).first() else: # Try as tag name first graft = db.query(Graft).filter(Graft.tree_id == tree.id, Graft.name == ref).first() if graft: fruit = db.query(Fruit).filter(Fruit.id == graft.fruit_id).first() else: # Try as direct fruit ID fruit = db.query(Fruit).filter(Fruit.id == ref).first() if not fruit: raise HTTPException(status_code=404, detail="Artifact not found") # Stream from S3 stream = storage.get_stream(fruit.s3_key) filename = fruit.original_name or f"{fruit.id}" return StreamingResponse( stream, media_type=fruit.content_type or "application/octet-stream", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) # Compatibility route @router.get("/grove/{grove_name}/{tree_name}/+/{ref}") def harvest_compat( grove_name: str, tree_name: str, ref: str, db: Session = Depends(get_db), storage: S3Storage = Depends(get_storage), ): return harvest(grove_name, tree_name, ref, db, storage) # Graft routes @router.get("/api/v1/grove/{grove_name}/{tree_name}/grafts", response_model=List[GraftResponse]) def list_grafts(grove_name: str, tree_name: str, db: Session = Depends(get_db)): grove = db.query(Grove).filter(Grove.name == grove_name).first() if not grove: raise HTTPException(status_code=404, detail="Grove not found") tree = db.query(Tree).filter(Tree.grove_id == grove.id, Tree.name == tree_name).first() if not tree: raise HTTPException(status_code=404, detail="Tree not found") grafts = db.query(Graft).filter(Graft.tree_id == tree.id).order_by(Graft.name).all() return grafts @router.post("/api/v1/grove/{grove_name}/{tree_name}/graft", response_model=GraftResponse) def create_graft( grove_name: str, tree_name: str, graft: GraftCreate, request: Request, db: Session = Depends(get_db), ): user_id = get_user_id(request) grove = db.query(Grove).filter(Grove.name == grove_name).first() if not grove: raise HTTPException(status_code=404, detail="Grove not found") tree = db.query(Tree).filter(Tree.grove_id == grove.id, Tree.name == tree_name).first() if not tree: raise HTTPException(status_code=404, detail="Tree not found") # Verify fruit exists fruit = db.query(Fruit).filter(Fruit.id == graft.fruit_id).first() if not fruit: raise HTTPException(status_code=404, detail="Fruit not found") # Create or update graft existing = db.query(Graft).filter(Graft.tree_id == tree.id, Graft.name == graft.name).first() if existing: existing.fruit_id = graft.fruit_id existing.created_by = user_id db.commit() db.refresh(existing) return existing db_graft = Graft( tree_id=tree.id, name=graft.name, fruit_id=graft.fruit_id, created_by=user_id, ) db.add(db_graft) db.commit() db.refresh(db_graft) return db_graft # Consumer routes @router.get("/api/v1/grove/{grove_name}/{tree_name}/consumers", response_model=List[ConsumerResponse]) def get_consumers(grove_name: str, tree_name: str, db: Session = Depends(get_db)): grove = db.query(Grove).filter(Grove.name == grove_name).first() if not grove: raise HTTPException(status_code=404, detail="Grove not found") tree = db.query(Tree).filter(Tree.grove_id == grove.id, Tree.name == tree_name).first() if not tree: raise HTTPException(status_code=404, detail="Tree not found") consumers = db.query(Consumer).filter(Consumer.tree_id == tree.id).order_by(Consumer.last_access.desc()).all() return consumers # Fruit by ID @router.get("/api/v1/fruit/{fruit_id}", response_model=FruitResponse) def get_fruit(fruit_id: str, db: Session = Depends(get_db)): fruit = db.query(Fruit).filter(Fruit.id == fruit_id).first() if not fruit: raise HTTPException(status_code=404, detail="Fruit not found") return fruit