107 lines
3.6 KiB
Python
107 lines
3.6 KiB
Python
|
|
"""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)
|