Доска связей на живых данных хаба (viewer-live, #17) #18
@ -1,5 +1,6 @@
|
||||
"""HTTP API хаба (stdlib http.server, поток на запрос, соединение SQLite на запрос).
|
||||
Маршруты /agent/* — агентский токен, остальные — клиентский (specs/hub-api/spec.md)."""
|
||||
Маршруты /agent/* — агентский токен, остальные — клиентский (specs/hub-api/spec.md).
|
||||
Страница доски (GET /, GET /links) — без токена: статический файл без данных."""
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
@ -9,6 +10,7 @@ import time
|
||||
import traceback
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from . import backup, db, mirror, queue, relay, settings
|
||||
@ -20,9 +22,16 @@ from .queue import ClaimedError
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_BODY = 16 * 1024 * 1024 # предел тела запроса, байт
|
||||
PAGE = Path(__file__).parent / "web" / "links.html" # страница доски связей; читается на каждый запрос
|
||||
SOCKET_TIMEOUT = 60 # чтение запроса; ожидание долгого claim идёт уже после чтения тела и сокет не держит
|
||||
|
||||
|
||||
class Raw(NamedTuple):
|
||||
"""Не-JSON ответ обработчика (страница доски)."""
|
||||
content_type: str
|
||||
data: bytes
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
def __init__(self, status, error, detail=None):
|
||||
super().__init__(error)
|
||||
@ -142,6 +151,10 @@ def h_board(hub, conn, m, q, body):
|
||||
return 200, mirror.board(conn, settings.get(conn, "base_jql"))
|
||||
|
||||
|
||||
def h_page(hub, conn, m, q, body):
|
||||
return 200, Raw("text/html; charset=utf-8", PAGE.read_bytes())
|
||||
|
||||
|
||||
def h_note_put(hub, conn, m, q, body):
|
||||
fields = {k: v for k, v in body.items() if k != "updated_by"}
|
||||
try:
|
||||
@ -249,6 +262,7 @@ def h_settings_put(hub, conn, m, q, body):
|
||||
|
||||
|
||||
ROUTES = [
|
||||
("GET", r"^/(links)?$", h_page, None),
|
||||
("POST", r"^/agent/claim$", h_claim, "agent"),
|
||||
("GET", r"^/agent/settings$", h_agent_settings, "agent"),
|
||||
("GET", r"^/agent/commands/(\d+)$", h_agent_command, "agent"),
|
||||
@ -318,7 +332,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
match = rx.match(url.path)
|
||||
if not match:
|
||||
continue
|
||||
self._auth(scope)
|
||||
if scope is not None:
|
||||
self._auth(scope)
|
||||
try:
|
||||
body = json.loads(raw.decode("utf-8")) if raw else {}
|
||||
except ValueError as e: # битый JSON или не UTF-8
|
||||
@ -339,10 +354,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._send(500, {"error": "internal", "detail": str(e)})
|
||||
|
||||
def _send(self, status, payload):
|
||||
data = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
|
||||
if isinstance(payload, Raw):
|
||||
ctype, data, extra = payload.content_type, payload.data, {"Cache-Control": "no-cache"}
|
||||
else:
|
||||
ctype, extra = "application/json; charset=utf-8", {}
|
||||
data = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Type", ctype)
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
for name, value in extra.items():
|
||||
self.send_header(name, value)
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<meta charset="utf-8">
|
||||
<title>Спринт 2 линк</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@500&display=swap">
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import json
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
@ -363,3 +365,23 @@ def test_board_route(hub):
|
||||
with pytest.raises(HubError) as e:
|
||||
client.get("/board")
|
||||
assert e.value.status == 401
|
||||
|
||||
|
||||
def _raw_get(base, path):
|
||||
with urllib.request.urlopen(base + path, timeout=10) as resp:
|
||||
return resp.status, resp.headers, resp.read().decode("utf-8")
|
||||
|
||||
|
||||
def test_page_served_without_token(hub):
|
||||
hub["agent"].post("/agent/state", {"issues": [{"key": "ZZTEST-9001", "summary": "тема из зеркала", "labels": []}],
|
||||
"links": []})
|
||||
for path in ("/", "/links"):
|
||||
status, headers, body = _raw_get(hub["base"], path)
|
||||
assert status == 200
|
||||
assert headers["Content-Type"] == "text/html; charset=utf-8"
|
||||
assert headers["Cache-Control"] == "no-cache"
|
||||
assert body.lstrip().lower().startswith("<!doctype html>")
|
||||
assert "ZZTEST-9001" not in body and "тема из зеркала" not in body
|
||||
with pytest.raises(urllib.error.HTTPError) as e:
|
||||
urllib.request.urlopen(hub["base"] + "/links/x", timeout=10)
|
||||
assert e.value.code == 404
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user