341 lines
12 KiB
Python
341 lines
12 KiB
Python
import httpx
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from .config import STOCK_URL, MUSIC_LAB_URL, INSTA_LAB_URL, REALESTATE_LAB_URL
|
|
|
|
_client = httpx.AsyncClient(timeout=30.0)
|
|
|
|
async def fetch_stock_news(limit: int = 10, category: str = None) -> List[Dict[str, Any]]:
|
|
params = {"limit": limit}
|
|
if category:
|
|
params["category"] = category
|
|
resp = await _client.get(f"{STOCK_URL}/api/stock/news", params=params)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def fetch_stock_indices() -> Dict[str, Any]:
|
|
resp = await _client.get(f"{STOCK_URL}/api/stock/indices")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def summarize_stock_news(limit: int = 15) -> Dict[str, Any]:
|
|
"""stock의 AI 요약 엔드포인트 호출.
|
|
반환: {"summary": str, "tokens": {...}, "model": str, "duration_ms": int, "article_count": int}
|
|
"""
|
|
# stock 내부 Ollama 호출이 180s까지 가능하므로 여유있게 200s
|
|
async with httpx.AsyncClient(timeout=200.0) as client:
|
|
resp = await client.post(
|
|
f"{STOCK_URL}/api/stock/news/summarize",
|
|
json={"limit": limit},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def refresh_screener_snapshot() -> Dict[str, Any]:
|
|
"""stock의 KRX 일봉 스냅샷 갱신 (스크리너 실행 전 호출).
|
|
|
|
네이버 금융 일괄 다운로드라 보통 30~120s, 여유있게 180s.
|
|
"""
|
|
async with httpx.AsyncClient(timeout=180.0) as client:
|
|
resp = await client.post(f"{STOCK_URL}/api/stock/screener/snapshot/refresh")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def refresh_ai_news_sentiment() -> Dict[str, Any]:
|
|
"""stock의 AI 뉴스 sentiment 분석 트리거 (08:00 cron).
|
|
|
|
네이버 100종목 스크래핑 + Claude Haiku 100콜 병렬 = 약 30-60초.
|
|
여유있게 240s timeout.
|
|
"""
|
|
async with httpx.AsyncClient(timeout=240.0) as client:
|
|
resp = await client.post(
|
|
f"{STOCK_URL}/api/stock/screener/snapshot/refresh-news-sentiment"
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def run_stock_screener(mode: str = "auto") -> Dict[str, Any]:
|
|
"""stock의 스크리너 실행.
|
|
|
|
반환 status:
|
|
- 'skipped_holiday': 공휴일/주말 — telegram_payload 없음
|
|
- 'success': telegram_payload 동봉
|
|
엔진 자체는 수 초 내 끝나지만, 컨텍스트 로드+200종목 처리 여유 180s.
|
|
"""
|
|
async with httpx.AsyncClient(timeout=180.0) as client:
|
|
resp = await client.post(
|
|
f"{STOCK_URL}/api/stock/screener/run",
|
|
json={"mode": mode},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def scrape_stock_news() -> Dict[str, Any]:
|
|
"""stock의 수동 뉴스 스크랩 트리거 — DB에 최신 뉴스 저장.
|
|
|
|
아침 브리핑 직전 호출하여 어제 데이터가 아닌 오늘 새벽 뉴스를 보장한다.
|
|
네이버 금융 단일 요청이라 보통 수 초 내 완료, 여유있게 60s.
|
|
"""
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
resp = await client.post(f"{STOCK_URL}/api/stock/scrap")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def generate_music(payload: dict) -> Dict[str, Any]:
|
|
resp = await _client.post(f"{MUSIC_LAB_URL}/api/music/generate", json=payload)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def get_music_status(task_id: str) -> Dict[str, Any]:
|
|
resp = await _client.get(f"{MUSIC_LAB_URL}/api/music/status/{task_id}")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def get_music_credits() -> Dict[str, Any]:
|
|
resp = await _client.get(f"{MUSIC_LAB_URL}/api/music/credits")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
# --- insta-lab ---
|
|
|
|
async def insta_collect(categories: Optional[list] = None) -> Dict[str, Any]:
|
|
"""뉴스 수집 트리거 → task_id 반환."""
|
|
payload = {"categories": categories} if categories else {}
|
|
resp = await _client.post(f"{INSTA_LAB_URL}/api/insta/news/collect", json=payload)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def insta_extract(categories: Optional[list] = None) -> Dict[str, Any]:
|
|
payload = {"categories": categories} if categories else {}
|
|
resp = await _client.post(f"{INSTA_LAB_URL}/api/insta/keywords/extract", json=payload)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def insta_list_keywords(category: Optional[str] = None,
|
|
used: Optional[bool] = None) -> List[Dict[str, Any]]:
|
|
params: Dict[str, Any] = {}
|
|
if category:
|
|
params["category"] = category
|
|
if used is not None:
|
|
params["used"] = "true" if used else "false"
|
|
resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/keywords", params=params)
|
|
resp.raise_for_status()
|
|
return resp.json().get("items", [])
|
|
|
|
|
|
async def insta_get_keyword(keyword_id: int) -> Optional[Dict[str, Any]]:
|
|
items = await insta_list_keywords()
|
|
for it in items:
|
|
if it["id"] == keyword_id:
|
|
return it
|
|
return None
|
|
|
|
|
|
async def insta_create_slate(keyword: str, category: str, keyword_id: Optional[int] = None) -> Dict[str, Any]:
|
|
resp = await _client.post(
|
|
f"{INSTA_LAB_URL}/api/insta/slates",
|
|
json={"keyword": keyword, "category": category, "keyword_id": keyword_id},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def insta_task_status(task_id: str) -> Dict[str, Any]:
|
|
resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/tasks/{task_id}")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def insta_get_slate(slate_id: int) -> Dict[str, Any]:
|
|
resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/slates/{slate_id}")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def insta_get_asset_bytes(slate_id: int, page: int) -> bytes:
|
|
"""카드 PNG 바이트를 가져와 텔레그램 미디어 그룹에 첨부."""
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
resp = await client.get(f"{INSTA_LAB_URL}/api/insta/slates/{slate_id}/assets/{page}")
|
|
resp.raise_for_status()
|
|
return resp.content
|
|
|
|
|
|
async def insta_collect_trends(categories: Optional[list] = None) -> Dict[str, Any]:
|
|
payload = {"categories": categories} if categories else {}
|
|
resp = await _client.post(f"{INSTA_LAB_URL}/api/insta/trends/collect", json=payload)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def insta_list_trends(source: Optional[str] = None,
|
|
category: Optional[str] = None,
|
|
days: int = 1) -> List[Dict[str, Any]]:
|
|
params: Dict[str, Any] = {"days": days}
|
|
if source:
|
|
params["source"] = source
|
|
if category:
|
|
params["category"] = category
|
|
resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/trends", params=params)
|
|
resp.raise_for_status()
|
|
return resp.json().get("items", [])
|
|
|
|
|
|
async def insta_get_preferences() -> Dict[str, float]:
|
|
resp = await _client.get(f"{INSTA_LAB_URL}/api/insta/preferences")
|
|
resp.raise_for_status()
|
|
return {p["category"]: p["weight"] for p in resp.json().get("categories", [])}
|
|
|
|
|
|
async def insta_put_preferences(weights: Dict[str, float]) -> Dict[str, Any]:
|
|
resp = await _client.put(
|
|
f"{INSTA_LAB_URL}/api/insta/preferences",
|
|
json={"categories": weights},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
# --- realestate-lab ---
|
|
|
|
async def realestate_collect() -> Dict[str, Any]:
|
|
"""청약 공고 수동 수집 트리거"""
|
|
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
resp = await client.post(f"{REALESTATE_LAB_URL}/api/realestate/collect")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def realestate_matches(limit: int = 20) -> List[Dict[str, Any]]:
|
|
"""realestate-lab의 GET /api/realestate/matches 호출."""
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.get(
|
|
f"{REALESTATE_LAB_URL}/api/realestate/matches",
|
|
params={"size": limit},
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
return data.get("items", [])
|
|
|
|
|
|
async def realestate_dashboard() -> Dict[str, Any]:
|
|
resp = await _client.get(f"{REALESTATE_LAB_URL}/api/realestate/dashboard")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def realestate_mark_read(match_id: int) -> Dict[str, Any]:
|
|
resp = await _client.patch(f"{REALESTATE_LAB_URL}/api/realestate/matches/{match_id}/read")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def realestate_bookmark_toggle(announcement_id: int) -> Dict[str, Any]:
|
|
"""realestate-lab의 PATCH /api/realestate/announcements/{id}/bookmark 호출."""
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.patch(
|
|
f"{REALESTATE_LAB_URL}/api/realestate/announcements/{announcement_id}/bookmark"
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
# --- lotto-backend ---
|
|
|
|
async def lotto_candidates(n: int = 20) -> Dict[str, Any]:
|
|
from .config import LOTTO_BACKEND_URL
|
|
resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/curator/candidates", params={"n": n})
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def lotto_context() -> Dict[str, Any]:
|
|
from .config import LOTTO_BACKEND_URL
|
|
resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/curator/context")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def lotto_save_briefing(payload: dict) -> Dict[str, Any]:
|
|
from .config import LOTTO_BACKEND_URL
|
|
resp = await _client.post(f"{LOTTO_BACKEND_URL}/api/lotto/briefing", json=payload)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def lotto_review_latest() -> Optional[Dict[str, Any]]:
|
|
from .config import LOTTO_BACKEND_URL
|
|
resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/review/latest")
|
|
if resp.status_code == 404:
|
|
return None
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def lotto_review_by_draw(draw_no: int) -> Optional[Dict[str, Any]]:
|
|
from .config import LOTTO_BACKEND_URL
|
|
resp = await _client.get(f"{LOTTO_BACKEND_URL}/api/lotto/review/{draw_no}")
|
|
if resp.status_code == 404:
|
|
return None
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def lotto_reviews_history(limit: int = 10) -> List[Dict[str, Any]]:
|
|
from .config import LOTTO_BACKEND_URL
|
|
resp = await _client.get(
|
|
f"{LOTTO_BACKEND_URL}/api/lotto/review/history",
|
|
params={"limit": limit},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json().get("reviews", [])
|
|
|
|
|
|
# --- music-lab pipeline (YouTube publisher orchestration) ---
|
|
|
|
async def list_active_pipelines() -> list[dict]:
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline?status=active")
|
|
resp.raise_for_status()
|
|
return resp.json().get("pipelines", [])
|
|
|
|
|
|
async def get_pipeline(pid: int) -> dict:
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def post_pipeline_feedback(pid: int, step: str, intent: str,
|
|
feedback_text: Optional[str] = None) -> dict:
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.post(
|
|
f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}/feedback",
|
|
json={"step": step, "intent": intent, "feedback_text": feedback_text},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
async def save_pipeline_telegram_msg(pid: int, step: str, msg_id: int) -> None:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
await client.patch(
|
|
f"{MUSIC_LAB_URL}/api/music/pipeline/{pid}/telegram-msg",
|
|
json={"step": step, "message_id": msg_id},
|
|
)
|
|
|
|
|
|
async def lookup_pipeline_by_msg(msg_id: int) -> Optional[dict]:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.get(f"{MUSIC_LAB_URL}/api/music/pipeline/lookup-by-msg/{msg_id}")
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
return None
|