Files
SolitaryThinker 21e59268d8 [misc] v2: full pre-commit clean (ruff UP038/SIM/UP031 + mypy annotations)
Sweep all of v2/ through pre-commit (was previously only run on changed
files). Fixes surfaced across untouched modules:

- ruff: isinstance-tuple -> X | Y (UP038), try/except/pass ->
  contextlib.suppress (SIM105), negated-return (SIM103), %-format ->
  f-string (UP031).
- mypy: add annotations for no-untyped-call + var-annotated across recipes,
  training methods, torch/toy backends, and serving.
- yapf reflow of the SF-Wan KV-cache call sites (semantics unchanged).

yapf/ruff/codespell/mypy all pass; v2 tests 237 passed / 5 pre-existing
(bucket_c torch-absent on CPU venv).
2026-06-19 22:16:28 +00:00

161 lines
5.8 KiB
Python

"""A tiny framework-free async HTTP/1.1 server (stdlib asyncio only).
No FastAPI/uvicorn dependency — the production OpenAI *fleet* frontend is Dynamo's job; this is the
per-engine worker server. Supports JSON and streamed (SSE) responses. Deliberately minimal: one
request per connection (Connection: close), enough to be real and curl-able, not a hardened server.
"""
from __future__ import annotations
import asyncio
import contextlib
import json
from dataclasses import dataclass, field
from typing import Any
from collections.abc import AsyncIterator, Awaitable, Callable
@dataclass
class Request:
method: str
path: str
headers: dict[str, str]
body: bytes
query: dict[str, str] = field(default_factory=dict)
def json(self) -> dict:
return json.loads(self.body or b"{}")
@dataclass
class Response:
status: int = 200
body: bytes = b""
content_type: str = "application/json"
stream: AsyncIterator | None = None # if set, body is ignored and chunks are streamed
@classmethod
def json(cls, obj: Any, status: int = 200) -> Response:
return cls(status=status, body=json.dumps(obj).encode(), content_type="application/json")
@classmethod
def sse(cls, gen: AsyncIterator) -> Response:
return cls(status=200, content_type="text/event-stream", stream=gen)
_STATUS = {
200: "OK",
201: "Created",
202: "Accepted",
400: "Bad Request",
404: "Not Found",
408: "Request Timeout",
413: "Payload Too Large",
500: "Internal Server Error"
}
Handler = Callable[[Request], Awaitable[Response]]
class _BadRequest(Exception):
pass
class _TooLarge(Exception):
pass
async def _read_request(reader: asyncio.StreamReader, max_body: int) -> Request:
head = await reader.readuntil(b"\r\n\r\n")
lines = head.decode("latin1").split("\r\n")
parts = lines[0].split(" ", 2)
if len(parts) < 2:
raise _BadRequest("malformed request line")
method, target = parts[0], parts[1]
path, _, qs = target.partition("?")
query = dict(kv.split("=", 1) for kv in qs.split("&") if "=" in kv)
headers: dict[str, str] = {}
for ln in lines[1:]:
if ":" in ln:
k, v = ln.split(":", 1)
headers[k.strip().lower()] = v.strip()
raw = headers.get("content-length", "0") or "0"
try:
clen = int(raw)
except ValueError:
raise _BadRequest(f"invalid Content-Length {raw!r}") from None
if clen < 0 or clen > max_body:
raise _TooLarge(f"Content-Length {clen} exceeds limit {max_body}")
body = await reader.readexactly(clen) if clen else b""
return Request(method, path, headers, body, query)
async def _write(writer: asyncio.StreamWriter, resp: Response) -> None:
if resp.stream is not None:
head = (f"HTTP/1.1 {resp.status} {_STATUS.get(resp.status, 'OK')}\r\n"
f"Content-Type: {resp.content_type}\r\nCache-Control: no-cache\r\n"
f"Connection: close\r\n\r\n")
writer.write(head.encode())
await writer.drain()
async for chunk in resp.stream:
writer.write(chunk.encode() if isinstance(chunk, str) else chunk)
await writer.drain()
else:
head = (f"HTTP/1.1 {resp.status} {_STATUS.get(resp.status, 'OK')}\r\n"
f"Content-Type: {resp.content_type}\r\nContent-Length: {len(resp.body)}\r\n"
f"Connection: close\r\n\r\n")
writer.write(head.encode())
writer.write(resp.body)
await writer.drain()
writer.close()
class HttpServer:
def __init__(self,
dispatch: Handler,
*,
read_timeout: float = 30.0,
max_body_bytes: int = 64 << 20,
header_limit: int = 1 << 20):
self.dispatch = dispatch
self.read_timeout = read_timeout # slowloris guard
self.max_body_bytes = max_body_bytes # body-size cap → 413
self.header_limit = header_limit # StreamReader buffer for the header block
self._server: asyncio.AbstractServer | None = None
async def _conn(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
try:
req = await asyncio.wait_for(_read_request(reader, self.max_body_bytes), self.read_timeout)
resp = await self.dispatch(req)
except asyncio.TimeoutError:
resp = Response.json({"error": "request read timeout"}, status=408)
except _BadRequest as e:
resp = Response.json({"error": str(e)}, status=400)
except _TooLarge as e:
resp = Response.json({"error": str(e)}, status=413)
except (asyncio.IncompleteReadError, asyncio.LimitOverrunError):
resp = Response.json({"error": "malformed or oversized request"}, status=400)
except Exception as e: # noqa: BLE001 — never let one connection kill the server
resp = Response.json({"error": str(e)}, status=500)
try:
await _write(writer, resp)
except Exception:
if resp.stream is not None: # client gone mid-SSE → close the generator
# async-gen close -> GeneratorExit -> submit() cancels the driver
aclose = getattr(resp.stream, "aclose", None)
if aclose is not None:
with contextlib.suppress(Exception):
await aclose()
with contextlib.suppress(Exception):
writer.close()
async def start(self, host: str = "127.0.0.1", port: int = 0) -> tuple[str, int]:
self._server = await asyncio.start_server(self._conn, host, port, limit=self.header_limit)
sock = self._server.sockets[0].getsockname()
return sock[0], sock[1]
async def close(self) -> None:
if self._server is not None:
self._server.close()
await self._server.wait_closed()