271 lines
9.5 KiB
Python
271 lines
9.5 KiB
Python
"""
|
||
api_client.py — единое место для всех HTTP-запросов к бэкенду.
|
||
Не зависит от Flet. Все функции async, возвращают данные или None/ошибку.
|
||
"""
|
||
import os
|
||
from typing import Any
|
||
from urllib.parse import urlparse
|
||
|
||
import httpx
|
||
|
||
API_URL = os.getenv("API_URL", "http://api:8000")
|
||
_TIMEOUT = 10
|
||
|
||
|
||
def build_radar_download_url(page_url: str, response_id: str) -> str:
|
||
"""
|
||
Строит URL для скачивания SVG-диаграммы через веб-сервер (/radar-image/{id}).
|
||
page_url — значение page.url из Flet (может быть ws:// или wss://).
|
||
"""
|
||
parsed = urlparse(page_url)
|
||
scheme = "https" if parsed.scheme in ("wss", "https") else "http"
|
||
port = parsed.port
|
||
port_str = f":{port}" if port and port not in (80, 443) else ""
|
||
return f"{scheme}://{parsed.hostname}{port_str}/radar-image/{response_id}"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Auth
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def login(username: str, password: str) -> tuple[str | None, str | None]:
|
||
"""
|
||
POST /auth/login
|
||
Возвращает (token, None) при успехе или (None, error_message) при ошибке.
|
||
"""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.post(
|
||
"/auth/login",
|
||
data={"username": username, "password": password},
|
||
)
|
||
if resp.status_code == 200:
|
||
return resp.json()["access_token"], None
|
||
elif resp.status_code == 401:
|
||
return None, "Неверный логин или пароль"
|
||
return None, f"Ошибка сервера: {resp.status_code}"
|
||
except httpx.ConnectError:
|
||
return None, "Не удалось подключиться к серверу"
|
||
except Exception as e:
|
||
return None, f"Ошибка: {e}"
|
||
|
||
|
||
async def get_me(token: str) -> dict | None:
|
||
"""GET /users/me — данные текущего пользователя."""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get(
|
||
"/users/me",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
)
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
async def register(
|
||
username: str,
|
||
password: str,
|
||
first_name: str,
|
||
last_name: str,
|
||
group_id: str | None = None,
|
||
organization_id: str | None = None,
|
||
) -> str | None:
|
||
"""
|
||
POST /auth/register
|
||
Возвращает None при успехе или строку с ошибкой.
|
||
"""
|
||
payload: dict[str, Any] = {
|
||
"username": username,
|
||
"password": password,
|
||
"first_name": first_name,
|
||
"last_name": last_name,
|
||
}
|
||
if group_id:
|
||
payload["group_id"] = group_id
|
||
if organization_id:
|
||
payload["organization_id"] = organization_id
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.post("/auth/register", json=payload)
|
||
if resp.status_code == 201:
|
||
return None
|
||
elif resp.status_code == 400:
|
||
return resp.json().get("detail", "Ошибка регистрации")
|
||
return f"Ошибка сервера: {resp.status_code}"
|
||
except httpx.ConnectError:
|
||
return "Не удалось подключиться к серверу"
|
||
except Exception as e:
|
||
return f"Ошибка: {e}"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Organizations & Groups
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def get_organizations() -> list:
|
||
"""GET /organizations/"""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get("/organizations/")
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return []
|
||
|
||
|
||
async def get_groups() -> list:
|
||
"""GET /groups/"""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get("/groups/")
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return []
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Polls
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def get_polls() -> list:
|
||
"""GET /polls/ — список всех опросов."""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get("/polls/")
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return []
|
||
|
||
|
||
async def get_poll(poll_id: str) -> dict | None:
|
||
"""GET /polls/{poll_id} — опрос с вопросами и вариантами ответов."""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get(f"/polls/{poll_id}")
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
async def get_response(response_id: str) -> dict | None:
|
||
"""GET /polls/responses/{response_id} — детальный ответ с answers."""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get(f"/polls/responses/{response_id}")
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
async def get_radar_result(response_id: str) -> dict | None:
|
||
"""GET /responses/{response_id}/radar — данные для радарной диаграммы."""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get(f"/responses/{response_id}/radar")
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
async def get_radar_svg_bytes(response_id: str) -> bytes | None:
|
||
"""GET /responses/{response_id}/radar/image — SVG-файл с диаграммой."""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get(f"/responses/{response_id}/radar/image")
|
||
if resp.status_code == 200:
|
||
return resp.content
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
async def submit_response(
|
||
poll_id: str,
|
||
answers: list[dict],
|
||
user_id: str | None = None,
|
||
group_id: str | None = None,
|
||
organization_id: str | None = None,
|
||
) -> tuple[str | None, str | None]:
|
||
"""
|
||
POST /polls/{poll_id}/responses
|
||
answers: [{"question_id": ..., "choice_id": ..., "text": ...}, ...]
|
||
Возвращает (response_id, None) или (None, error_message).
|
||
"""
|
||
payload: dict[str, Any] = {"answers": answers}
|
||
if user_id:
|
||
payload["user_id"] = user_id
|
||
if group_id:
|
||
payload["group_id"] = group_id
|
||
if organization_id:
|
||
payload["organization_id"] = organization_id
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.post(f"/polls/{poll_id}/responses", json=payload)
|
||
if resp.status_code == 201:
|
||
return resp.json()["id"], None
|
||
return None, f"Ошибка сервера: {resp.status_code}"
|
||
except httpx.ConnectError:
|
||
return None, "Не удалось подключиться к серверу"
|
||
except Exception as e:
|
||
return None, f"Ошибка: {e}"
|
||
|
||
|
||
async def get_user_responses(user_id: str) -> list:
|
||
"""GET /polls/users/{user_id}/responses — ответы конкретного пользователя."""
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.get(f"/polls/users/{user_id}/responses")
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
except Exception:
|
||
pass
|
||
return []
|
||
|
||
|
||
async def update_user(
|
||
user_id: str,
|
||
token: str,
|
||
first_name: str | None = None,
|
||
last_name: str | None = None,
|
||
group_id: str | None = None,
|
||
organization_id: str | None = None,
|
||
) -> tuple[dict | None, str | None]:
|
||
"""PUT /users/{user_id} — обновить данные пользователя."""
|
||
payload: dict[str, Any] = {}
|
||
if first_name is not None:
|
||
payload["first_name"] = first_name
|
||
if last_name is not None:
|
||
payload["last_name"] = last_name
|
||
if group_id is not None:
|
||
payload["group_id"] = group_id
|
||
if organization_id is not None:
|
||
payload["organization_id"] = organization_id
|
||
try:
|
||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||
resp = await client.put(
|
||
f"/users/{user_id}",
|
||
json=payload,
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
)
|
||
if resp.status_code == 200:
|
||
return resp.json(), None
|
||
return None, f"Ошибка сервера: {resp.status_code}"
|
||
except httpx.ConnectError:
|
||
return None, "Не удалось подключиться к серверу"
|
||
except Exception as e:
|
||
return None, f"Ошибка: {e}"
|