"""FastAPI application exposing the iObj memory-object API.""" from __future__ import annotations import logging import os from datetime import date from typing import Optional from fastapi import Depends, FastAPI, HTTPException, Query, Security, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from iobj.models import MemoryObject, ObjectCreate, ObjectType, ObjectUpdate, SearchQuery from iobj.storage import ObjectStore logging.basicConfig( level=os.environ.get("IOBJ_LOG_LEVEL", "INFO"), format='{"time":"%(asctime)s","level":"%(levelname)s","msg":%(message)r}', ) logger = logging.getLogger("iobj") DATA_DIR = os.environ.get("IOBJ_DATA_DIR", "data") API_TOKEN = os.environ.get("IOBJ_API_TOKEN") app = FastAPI(title="iObj", version="0.1.0", description="Personal JSON object / memory bank service") store = ObjectStore(DATA_DIR) _bearer = HTTPBearer(auto_error=False) def require_auth(creds: Optional[HTTPAuthorizationCredentials] = Security(_bearer)) -> None: """Validate the bearer token against IOBJ_API_TOKEN. If IOBJ_API_TOKEN is unset, auth is disabled (useful for local dev only — always set it in any networked deployment). """ if not API_TOKEN: return if creds is None or creds.credentials != API_TOKEN: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing bearer token") @app.get("/health") def health() -> dict: return {"status": "ok"} @app.post("/objects", response_model=MemoryObject, dependencies=[Depends(require_auth)]) def create_object(payload: ObjectCreate) -> MemoryObject: obj = store.create(payload) logger.info("created object id=%s type=%s", obj.id, obj.type) return obj @app.get("/objects", response_model=list[MemoryObject], dependencies=[Depends(require_auth)]) def search_objects( type: Optional[ObjectType] = None, project: Optional[str] = None, tag: Optional[str] = None, date_from: Optional[date] = None, date_to: Optional[date] = None, q: Optional[str] = None, limit: int = Query(50, ge=1, le=1000), offset: int = Query(0, ge=0), ) -> list[MemoryObject]: query = SearchQuery( type=type, project=project, tag=tag, date_from=date_from, date_to=date_to, q=q, limit=limit, offset=offset, ) return store.search(query) @app.get("/objects/date/{object_date}", response_model=list[MemoryObject], dependencies=[Depends(require_auth)]) def objects_for_date(object_date: date) -> list[MemoryObject]: query = SearchQuery(date_from=object_date, date_to=object_date, limit=1000) return store.search(query) @app.get("/objects/{object_id}", response_model=MemoryObject, dependencies=[Depends(require_auth)]) def get_object(object_id: str) -> MemoryObject: obj = store.get(object_id) if obj is None: raise HTTPException(status_code=404, detail="Object not found") return obj @app.patch("/objects/{object_id}", response_model=MemoryObject, dependencies=[Depends(require_auth)]) def update_object(object_id: str, payload: ObjectUpdate) -> MemoryObject: obj = store.update(object_id, payload) if obj is None: raise HTTPException(status_code=404, detail="Object not found") logger.info("updated object id=%s", obj.id) return obj @app.delete("/objects/{object_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_auth)]) def delete_object(object_id: str) -> None: deleted = store.delete(object_id) if not deleted: raise HTTPException(status_code=404, detail="Object not found") logger.info("deleted object id=%s", object_id)