Files
iobj/iobj/app.py
Henrik Jess Nielsen 8caa9f3071
All checks were successful
Build and Deploy iObj / build-image (push) Successful in 1h17m41s
iObj: initial service — FastAPI + TinyDB memory object store
- Month-sharded TinyDB storage with search/filter (type, project, tag, date range, free-text)
- FastAPI app with bearer-token auth, CRUD + search endpoints
- 27 passing pytest tests (storage + API)
- Dockerfile + Nomad job spec + Gitea Actions deploy workflow (mirrors devops-mcp pattern)
2026-07-13 19:24:31 +02:00

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)