feat(tarot-lab): main.py + 5 라우트 테스트 (총 21 tests 통과)
This commit is contained in:
90
tarot-lab/app/main.py
Normal file
90
tarot-lab/app/main.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""tarot-lab FastAPI app — /api/tarot/* 6 endpoints."""
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from .config import CORS_ALLOW_ORIGINS
|
||||||
|
from .models import (
|
||||||
|
TarotInterpretRequest,
|
||||||
|
TarotInterpretResponse,
|
||||||
|
TarotSaveRequest,
|
||||||
|
TarotPatchRequest,
|
||||||
|
)
|
||||||
|
from . import pipeline, db as db_module
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="tarot-lab")
|
||||||
|
|
||||||
|
_origins = [o.strip() for o in CORS_ALLOW_ORIGINS.split(",") if o.strip()]
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=_origins,
|
||||||
|
allow_credentials=False,
|
||||||
|
allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
||||||
|
allow_headers=["Content-Type"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
def _init():
|
||||||
|
db_module.init_db()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/tarot/interpret", response_model=TarotInterpretResponse)
|
||||||
|
async def interpret_endpoint(req: TarotInterpretRequest):
|
||||||
|
try:
|
||||||
|
result = await pipeline.interpret(req)
|
||||||
|
except pipeline.TarotError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/tarot/readings")
|
||||||
|
async def save_reading(req: TarotSaveRequest):
|
||||||
|
rid = db_module.save_tarot_reading(req.model_dump())
|
||||||
|
row = db_module.get_tarot_reading(rid)
|
||||||
|
return {"id": rid, "created_at": row["created_at"]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/tarot/readings")
|
||||||
|
async def list_readings(
|
||||||
|
page: int = 1,
|
||||||
|
size: int = 20,
|
||||||
|
favorite: bool | None = None,
|
||||||
|
spread_type: str | None = None,
|
||||||
|
category: str | None = None,
|
||||||
|
):
|
||||||
|
return db_module.list_tarot_readings(
|
||||||
|
page=page, size=size,
|
||||||
|
favorite=favorite, spread_type=spread_type, category=category,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/tarot/readings/{reading_id}")
|
||||||
|
async def get_reading(reading_id: int):
|
||||||
|
row = db_module.get_tarot_reading(reading_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="reading not found")
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch("/api/tarot/readings/{reading_id}")
|
||||||
|
async def patch_reading(reading_id: int, req: TarotPatchRequest):
|
||||||
|
row = db_module.get_tarot_reading(reading_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="reading not found")
|
||||||
|
db_module.update_tarot_reading(reading_id, **req.model_dump(exclude_none=True))
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/tarot/readings/{reading_id}")
|
||||||
|
async def delete_reading(reading_id: int):
|
||||||
|
row = db_module.get_tarot_reading(reading_id)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="reading not found")
|
||||||
|
db_module.delete_tarot_reading(reading_id)
|
||||||
|
return {"ok": True}
|
||||||
71
tarot-lab/tests/test_routes.py
Normal file
71
tarot-lab/tests/test_routes.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
from app import db as db_module
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def fresh_db(monkeypatch, tmp_path):
|
||||||
|
db_file = tmp_path / "test_tarot.db"
|
||||||
|
monkeypatch.setattr(db_module, "DB_PATH", str(db_file))
|
||||||
|
db_module.init_db()
|
||||||
|
yield
|
||||||
|
try:
|
||||||
|
if db_file.exists():
|
||||||
|
db_file.unlink()
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _save_payload():
|
||||||
|
return {
|
||||||
|
"spread_type": "three_card",
|
||||||
|
"category": "연애",
|
||||||
|
"question": "Q",
|
||||||
|
"cards": [{"position": "과거", "card_id": "the-fool", "reversed": False}],
|
||||||
|
"interpretation_json": {"summary": "S", "cards": [], "interactions": [], "advice": "A", "warning": None, "confidence": "medium"},
|
||||||
|
"model": "claude-sonnet-4-6",
|
||||||
|
"tokens_in": 100, "tokens_out": 200, "cost_usd": 0.005,
|
||||||
|
"confidence": "medium",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_health():
|
||||||
|
with TestClient(app) as c:
|
||||||
|
r = c.get("/health")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json() == {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_list_get_cycle():
|
||||||
|
with TestClient(app) as c:
|
||||||
|
r = c.post("/api/tarot/readings", json=_save_payload())
|
||||||
|
assert r.status_code == 200
|
||||||
|
rid = r.json()["id"]
|
||||||
|
r = c.get("/api/tarot/readings")
|
||||||
|
assert r.json()["total"] == 1
|
||||||
|
r = c.get(f"/api/tarot/readings/{rid}")
|
||||||
|
assert r.json()["category"] == "연애"
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_favorite_and_note():
|
||||||
|
with TestClient(app) as c:
|
||||||
|
rid = c.post("/api/tarot/readings", json=_save_payload()).json()["id"]
|
||||||
|
r = c.patch(f"/api/tarot/readings/{rid}", json={"favorite": True, "note": "n"})
|
||||||
|
assert r.status_code == 200
|
||||||
|
row = c.get(f"/api/tarot/readings/{rid}").json()
|
||||||
|
assert row["favorite"] == 1
|
||||||
|
assert row["note"] == "n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete():
|
||||||
|
with TestClient(app) as c:
|
||||||
|
rid = c.post("/api/tarot/readings", json=_save_payload()).json()["id"]
|
||||||
|
assert c.delete(f"/api/tarot/readings/{rid}").status_code == 200
|
||||||
|
assert c.get(f"/api/tarot/readings/{rid}").status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_404():
|
||||||
|
with TestClient(app) as c:
|
||||||
|
assert c.get("/api/tarot/readings/9999").status_code == 404
|
||||||
Reference in New Issue
Block a user