clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / renderers / python / symbolator / src / entry.py
entry.py 5.9K
1from __future__ import annotations23import json4from urllib.parse import urlparse56from workers import Request, Response, WorkerEntrypoint78from protocol import MAX_REQUEST_BYTES, ProtocolError, parse_render_input9from render import render_svg101112UNIT_ID = "symbolator"13EXPOSED_HEADERS = ", ".join([14 "Cache-Control",15 "Content-Type",16 "X-Diagram-Cache",17 "X-Diagram-Engine",18 "X-Diagram-Engine-Version",19 "X-Diagram-Pipeline",20 "X-Diagram-Renderer",21 "X-Diagram-Unit",22 "X-Renderer-Build",23])242526def _json(value, status=200, cache_control="no-store") -> Response:27 return Response(json.dumps(value, separators=(",", ":")), status=status, headers={28 "Access-Control-Allow-Origin": "*",29 "Cache-Control": cache_control,30 "Content-Type": "application/json; charset=utf-8",31 })323334def _error(error: ProtocolError) -> Response:35 return _json({"error": {"code": error.code, "message": str(error)}}, status=error.status)363738async def _bounded_text(request: Request) -> str:39 declared = request.headers.get("Content-Length")40 if declared is not None:41 try:42 length = int(declared)43 except ValueError as error:44 raise ProtocolError(400, "invalid_request", "Content-Length is invalid.") from error45 if length < 0:46 raise ProtocolError(400, "invalid_request", "Content-Length is invalid.")47 if length > MAX_REQUEST_BYTES:48 raise ProtocolError(413, "request_too_large", "Render request is too large.")49 if request.body is None:50 raise ProtocolError(400, "invalid_request", "A JSON request body is required.")51 reader = request.body.getReader()52 chunks: list[bytes] = []53 length = 054 try:55 while True:56 result = await reader.read()57 if result.done:58 break59 chunk = result.value.to_bytes()60 length += len(chunk)61 if length > MAX_REQUEST_BYTES:62 await reader.cancel("request too large")63 raise ProtocolError(413, "request_too_large", "Render request is too large.")64 chunks.append(chunk)65 finally:66 # A complete stream only needs its reader lock released. If an error67 # occurred before cancellation, best-effort cancellation avoids keeping68 # a request body live after the response.69 try:70 reader.releaseLock()71 except Exception:72 pass73 try:74 return b"".join(chunks).decode("utf-8")75 except UnicodeDecodeError as error:76 raise ProtocolError(400, "invalid_request", "Request body must be UTF-8 JSON.") from error777879class Default(WorkerEntrypoint):80 async def fetch(self, request: Request):81 path = urlparse(request.url).path82 if request.method == "OPTIONS":83 return Response(None, status=204, headers={84 "Access-Control-Allow-Headers": "Accept, Content-Type",85 "Access-Control-Allow-Methods": "GET, POST, OPTIONS",86 "Access-Control-Allow-Origin": "*",87 "Access-Control-Max-Age": "86400",88 })89 if request.method == "GET" and path == "/v1/health":90 return _json({"ok": True, "unit": UNIT_ID, "id": UNIT_ID})91 if request.method == "GET" and path == "/v1/capabilities":92 return _json({93 "unit": UNIT_ID,94 "id": UNIT_ID,95 "kind": "render",96 "format": "svg",97 "runtime": "edge-python",98 "version": "1.2.2-compatible-translation",99 "build": self.env.RENDERER_BUILD,100 "pipeline": [UNIT_ID],101 "knownLosses": [102 "Native Pango font metrics and shaping are replaced by browser-font SVG estimates.",103 "One request contains one HDL source; filesystem library scanning and persisted type caches are unavailable.",104 "Only SVG output is supported.",105 ],106 }, cache_control="public, max-age=300")107 if request.method != "POST" or path != "/v1/svg":108 return _json({"error": {"code": "not_found", "message": "Route not found."}}, status=404)109 if not (request.headers.get("Content-Type") or "").lower().startswith("application/json"):110 return _error(ProtocolError(415, "unsupported_media_type", "Render requests must use application/json."))111 try:112 request_input = parse_render_input(await _bounded_text(request))113 svg, version = render_svg(114 request_input.source,115 request_input.options,116 title_text=request_input.title,117 description=request_input.description,118 presentation_background=request_input.background,119 presentation_padding=request_input.padding,120 presentation_frame=request_input.frame,121 )122 except ProtocolError as error:123 return _error(error)124 except Exception as error:125 print(json.dumps({"message": "Symbolator renderer failed", "unit": UNIT_ID, "error": str(error)}))126 return _json({"error": {"code": "render_failed", "message": "The HDL source could not be rendered."}}, status=422)127 return Response(svg, headers={128 "Access-Control-Allow-Origin": "*",129 "Access-Control-Expose-Headers": EXPOSED_HEADERS,130 "Cache-Control": "public, max-age=1800",131 "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",132 "Content-Type": "image/svg+xml; charset=utf-8",133 "X-Content-Type-Options": "nosniff",134 "X-Diagram-Cache": "MISS",135 "X-Diagram-Engine": UNIT_ID,136 "X-Diagram-Engine-Version": version,137 "X-Diagram-Pipeline": UNIT_ID,138 "X-Diagram-Renderer": "edge-python",139 "X-Diagram-Unit": UNIT_ID,140 "X-Renderer-Build": self.env.RENDERER_BUILD,141 })