Files
LLM-infa/web/api_client.py
jze9 bf854f2d6e Разделы, фильтры и просмотр текстов в UI; опция «не хранить видео»
- таблица sections (дерево через parent_id), у видео section_id
- API: CRUD /sections, фильтры /videos (поиск, раздел с потомками, метод),
  GET /videos/{id}/text, PATCH раздела, DELETE вместе с файлами
- pipeline: keep_video=false удаляет mp4 после расшифровки, section_id
- UI: панель разделов с вложенностью и счётчиками, поиск, фильтр по методу,
  диалоги полного текста/выжимки, привязка к разделу, удаление,
  переключатель «Сохранять видео на диске»
- compose: bind-mount ./web для правок без пересборки

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 12:07:55 +05:00

119 lines
3.6 KiB
Python

"""Thin httpx wrapper for the FastAPI backend.
The browser never talks to FastAPI directly — Flet's Python process is the
only client of the API. So no CORS, no auth surface exposed publicly.
"""
from __future__ import annotations
import httpx
from web.config import API_BASE_URL, HTTP_TIMEOUT
class ApiError(RuntimeError):
pass
class ApiClient:
def __init__(self, base_url: str = API_BASE_URL, timeout: float = HTTP_TIMEOUT):
self._client = httpx.Client(base_url=base_url, timeout=timeout)
def close(self) -> None:
self._client.close()
# --- LLM ---
def llm_status(self) -> dict:
return self._get("/llm/config")
def llm_models(self, base_url: str, api_key: str) -> list[dict]:
data = self._post(
"/llm/models",
{"base_url": base_url, "api_key": api_key},
)
return data.get("models", [])
def llm_test(self, base_url: str, api_key: str, model: str) -> dict:
return self._post(
"/llm/test",
{"base_url": base_url, "api_key": api_key, "model": model},
)
# --- Pipeline / videos ---
def pipeline_process(self, payload: dict) -> dict:
return self._post("/pipeline/process", payload)
def list_videos(
self,
q: str | None = None,
section_id: int | None = None,
method: str | None = None,
) -> list[dict]:
params: dict = {}
if q:
params["q"] = q
if section_id is not None:
params["section_id"] = section_id
if method:
params["method"] = method
return self._get("/videos/", params=params)
def video_text(self, uuid: str, kind: str) -> dict:
return self._get(f"/videos/{uuid}/text", params={"kind": kind})
def set_video_section(self, uuid: str, section_id: int | None) -> dict:
return self._request("PATCH", f"/videos/{uuid}", {"section_id": section_id})
def delete_video(self, uuid: str) -> dict:
return self._request("DELETE", f"/videos/{uuid}")
# --- Sections ---
def list_sections(self) -> list[dict]:
return self._get("/sections/")
def create_section(self, name: str, parent_id: int | None) -> dict:
return self._post("/sections/", {"name": name, "parent_id": parent_id})
def delete_section(self, section_id: int) -> dict:
return self._request("DELETE", f"/sections/{section_id}")
# --- internals ---
def _get(self, path: str, params: dict | None = None) -> dict | list:
try:
r = self._client.get(path, params=params)
except httpx.HTTPError as e:
raise ApiError(f"Сеть: {e}")
return self._unwrap(r)
def _post(self, path: str, json: dict | None) -> dict:
try:
r = self._client.post(path, json=json)
except httpx.HTTPError as e:
raise ApiError(f"Сеть: {e}")
return self._unwrap(r)
def _request(self, method: str, path: str, json: dict | None = None) -> dict:
try:
r = self._client.request(method, path, json=json)
except httpx.HTTPError as e:
raise ApiError(f"Сеть: {e}")
return self._unwrap(r)
@staticmethod
def _unwrap(r: httpx.Response):
try:
data = r.json()
except Exception:
data = None
if r.is_success:
return data
detail = ""
if isinstance(data, dict):
detail = str(data.get("detail") or data.get("message") or "")
if not detail:
detail = r.text[:300] if r.text else r.reason_phrase
raise ApiError(f"HTTP {r.status_code}: {detail}")