iObj: initial service — FastAPI + TinyDB memory object store
All checks were successful
Build and Deploy iObj / build-image (push) Successful in 1h17m41s
All checks were successful
Build and Deploy iObj / build-image (push) Successful in 1h17m41s
- 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)
This commit is contained in:
106
iobj/app.py
Normal file
106
iobj/app.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user