From 91caddb4b2a04e722be0be721f17471200644b38 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 25 May 2026 18:32:37 +0900 Subject: [PATCH] =?UTF-8?q?feat(tarot-lab):=20main.py=20+=205=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20(=EC=B4=9D?= =?UTF-8?q?=2021=20tests=20=ED=86=B5=EA=B3=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tarot-lab/app/main.py | 90 ++++++++++++++++++++++++++++++++++ tarot-lab/tests/test_routes.py | 71 +++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 tarot-lab/app/main.py create mode 100644 tarot-lab/tests/test_routes.py diff --git a/tarot-lab/app/main.py b/tarot-lab/app/main.py new file mode 100644 index 0000000..396129a --- /dev/null +++ b/tarot-lab/app/main.py @@ -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} diff --git a/tarot-lab/tests/test_routes.py b/tarot-lab/tests/test_routes.py new file mode 100644 index 0000000..d9eac2c --- /dev/null +++ b/tarot-lab/tests/test_routes.py @@ -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