feat(insta-lab): main.py — trends + preferences endpoints
- POST /api/insta/trends/collect — background trend collection via trend_collector.collect_all - GET /api/insta/trends — list external trends with source/category/days filters - GET /api/insta/preferences — return category weights (defaults seeded on init_db) - PUT /api/insta/preferences — upsert category weights - Modified GET /api/insta/keywords to accept source= filter (source present → list_trends, else existing list_trending_keywords, backward compatible) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ from .config import (
|
|||||||
CORS_ALLOW_ORIGINS, NAVER_CLIENT_ID, ANTHROPIC_API_KEY,
|
CORS_ALLOW_ORIGINS, NAVER_CLIENT_ID, ANTHROPIC_API_KEY,
|
||||||
INSTA_DATA_PATH, DB_PATH, DEFAULT_CATEGORY_SEEDS, KEYWORDS_PER_CATEGORY,
|
INSTA_DATA_PATH, DB_PATH, DEFAULT_CATEGORY_SEEDS, KEYWORDS_PER_CATEGORY,
|
||||||
)
|
)
|
||||||
from . import db, news_collector, keyword_extractor, card_writer, card_renderer
|
from . import db, news_collector, keyword_extractor, card_writer, card_renderer, trend_collector
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -119,7 +119,13 @@ def extract_keywords(req: ExtractRequest, bg: BackgroundTasks):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/insta/keywords")
|
@app.get("/api/insta/keywords")
|
||||||
def list_keywords(category: Optional[str] = None, used: Optional[bool] = None):
|
def list_keywords(
|
||||||
|
category: Optional[str] = None,
|
||||||
|
used: Optional[bool] = None,
|
||||||
|
source: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if source:
|
||||||
|
return {"items": db.list_trends(source=source, category=category, days=30)}
|
||||||
return {"items": db.list_trending_keywords(category=category, used=used)}
|
return {"items": db.list_trending_keywords(category=category, used=used)}
|
||||||
|
|
||||||
|
|
||||||
@@ -243,3 +249,52 @@ def get_prompt(name: str):
|
|||||||
def upsert_prompt(name: str, body: TemplateBody):
|
def upsert_prompt(name: str, body: TemplateBody):
|
||||||
db.upsert_prompt_template(name, body.template, body.description)
|
db.upsert_prompt_template(name, body.template, body.description)
|
||||||
return db.get_prompt_template(name)
|
return db.get_prompt_template(name)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Trends ───────────────────────────────────────────────────────
|
||||||
|
class TrendsCollectRequest(BaseModel):
|
||||||
|
categories: Optional[list[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def _bg_collect_trends(task_id: str, categories: list[str]):
|
||||||
|
try:
|
||||||
|
db.update_task(task_id, "processing", 10, "외부 트렌드 수집 중")
|
||||||
|
result = trend_collector.collect_all(categories)
|
||||||
|
msg = f"naver:{result['naver_popular']}, google:{result['google_trends']}"
|
||||||
|
db.update_task(task_id, "succeeded", 100, msg, result_id=sum(result.values()))
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("trends collect failed")
|
||||||
|
db.update_task(task_id, "failed", 0, "", error=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/insta/trends/collect")
|
||||||
|
def collect_trends(req: TrendsCollectRequest, bg: BackgroundTasks):
|
||||||
|
cats = req.categories or list(DEFAULT_CATEGORY_SEEDS.keys())
|
||||||
|
tid = db.create_task("trends_collect", {"categories": cats})
|
||||||
|
bg.add_task(_bg_collect_trends, tid, cats)
|
||||||
|
return {"task_id": tid, "categories": cats}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/insta/trends")
|
||||||
|
def list_trends_endpoint(
|
||||||
|
source: Optional[str] = None,
|
||||||
|
category: Optional[str] = None,
|
||||||
|
days: int = Query(1, ge=1, le=90),
|
||||||
|
):
|
||||||
|
return {"items": db.list_trends(source=source, category=category, days=days)}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Preferences ──────────────────────────────────────────────────
|
||||||
|
class PreferencesBody(BaseModel):
|
||||||
|
categories: dict[str, float]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/insta/preferences")
|
||||||
|
def get_preferences_endpoint():
|
||||||
|
return {"categories": db.get_preferences()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/insta/preferences")
|
||||||
|
def put_preferences_endpoint(body: PreferencesBody):
|
||||||
|
db.upsert_preferences(body.categories)
|
||||||
|
return {"categories": db.get_preferences()}
|
||||||
|
|||||||
83
insta-lab/tests/test_main_trends.py
Normal file
83
insta-lab/tests/test_main_trends.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import os
|
||||||
|
import gc
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app import db as db_module
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(monkeypatch):
|
||||||
|
fd, path = tempfile.mkstemp(suffix=".db")
|
||||||
|
os.close(fd)
|
||||||
|
monkeypatch.setattr(db_module, "DB_PATH", path)
|
||||||
|
db_module.init_db()
|
||||||
|
from app import main
|
||||||
|
monkeypatch.setattr(main, "DB_PATH", path)
|
||||||
|
with TestClient(main.app) as c:
|
||||||
|
yield c
|
||||||
|
gc.collect()
|
||||||
|
for ext in ("", "-wal", "-shm"):
|
||||||
|
try:
|
||||||
|
os.remove(path + ext)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_preferences_returns_defaults(client):
|
||||||
|
resp = client.get("/api/insta/preferences")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
cats = {p["category"]: p["weight"] for p in resp.json()["categories"]}
|
||||||
|
assert cats == {"economy": 1.0, "psychology": 1.0, "celebrity": 1.0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_preferences_upsert(client):
|
||||||
|
resp = client.put("/api/insta/preferences",
|
||||||
|
json={"categories": {"economy": 0.7, "psychology": 0.2, "tech": 0.5}})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
cats = {p["category"]: p["weight"] for p in resp.json()["categories"]}
|
||||||
|
assert cats["economy"] == 0.7
|
||||||
|
assert cats["tech"] == 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_trends_filter(client):
|
||||||
|
db_module.add_external_trend({"keyword": "A", "category": "economy",
|
||||||
|
"source": "naver_popular", "score": 1.0})
|
||||||
|
db_module.add_external_trend({"keyword": "B", "category": "celebrity",
|
||||||
|
"source": "google_trends", "score": 0.8})
|
||||||
|
resp = client.get("/api/insta/trends?source=naver_popular")
|
||||||
|
items = resp.json()["items"]
|
||||||
|
assert {it["keyword"] for it in items} == {"A"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_trends_kicks_background(client, monkeypatch):
|
||||||
|
from app import main, trend_collector
|
||||||
|
|
||||||
|
captured = {"called": False}
|
||||||
|
|
||||||
|
def fake_collect_all(cats):
|
||||||
|
captured["called"] = True
|
||||||
|
return {"naver_popular": 3, "google_trends": 2}
|
||||||
|
|
||||||
|
monkeypatch.setattr(trend_collector, "collect_all", fake_collect_all)
|
||||||
|
resp = client.post("/api/insta/trends/collect", json={})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
task_id = resp.json()["task_id"]
|
||||||
|
for _ in range(20):
|
||||||
|
st = client.get(f"/api/insta/tasks/{task_id}").json()
|
||||||
|
if st["status"] in ("succeeded", "failed"):
|
||||||
|
break
|
||||||
|
assert st["status"] == "succeeded"
|
||||||
|
assert captured["called"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_keywords_filters_by_source(client):
|
||||||
|
db_module.add_trending_keyword({"keyword": "M", "category": "economy",
|
||||||
|
"score": 0.4, "articles_count": 1, "source": "manual"})
|
||||||
|
db_module.add_external_trend({"keyword": "N", "category": "economy",
|
||||||
|
"source": "naver_popular", "score": 0.9})
|
||||||
|
resp = client.get("/api/insta/keywords?source=manual")
|
||||||
|
items = resp.json()["items"]
|
||||||
|
assert {it["keyword"] for it in items} == {"M"}
|
||||||
Reference in New Issue
Block a user