clone

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

main
diagramzip / renderers / python / wireviz-translator / src / entry.py
entry.py 7.9K
1import json2import hashlib3from urllib.parse import urlparse45from workers import Request, Response, WorkerEntrypoint67from protocol import MAX_REQUEST_BYTES, ProtocolError, parse_render_input8from render import WIREVIZ_VERSION, render_dot910UNIT_ID = 'wireviz'11EXPOSED_HEADERS = ', '.join([12 'Cache-Control', 'Content-Type', 'X-Diagram-Cache', 'X-Diagram-Engine',13 'X-Diagram-Engine-Version', 'X-Diagram-Pipeline', 'X-Diagram-Renderer',14 'X-Diagram-Unit', 'X-Renderer-Build',15])161718def _json(value, status=200, cache='no-store'):19 return Response(json.dumps(value, separators=(',', ':')), status=status, headers={20 'Access-Control-Allow-Origin': '*', 'Cache-Control': cache,21 'Content-Type': 'application/json; charset=utf-8',22 })232425def _error(error):26 return _json({'error': {'code': error.code, 'message': str(error)}}, error.status)272829def _cache_url(build, render_input):30 canonical = json.dumps({31 'source': render_input.source, 'options': render_input.options,32 'metadata': [render_input.title, render_input.description],33 'presentation': [render_input.background, render_input.padding, render_input.frame],34 }, sort_keys=True, separators=(',', ':'))35 digest = hashlib.sha256(canonical.encode()).hexdigest()36 return f'https://diagramzip-wireviz-cache.invalid/1/{build}/{digest}'373839def _cache_status(response, status):40 from js import Response as JSResponse41 updated = JSResponse.new(response.body, response.js_object)42 updated.headers.set('X-Diagram-Cache', status)43 return Response(updated)444546async def _body(request):47 declared = request.headers.get('Content-Length')48 if declared is not None:49 try:50 length = int(declared)51 except ValueError as error:52 raise ProtocolError(400, 'invalid_request', 'Content-Length is invalid.') from error53 if length < 0:54 raise ProtocolError(400, 'invalid_request', 'Content-Length is invalid.')55 if length > MAX_REQUEST_BYTES:56 raise ProtocolError(413, 'request_too_large', 'Render request is too large.')57 if request.body is None:58 raise ProtocolError(400, 'invalid_request', 'A JSON request body is required.')59 reader = request.body.getReader()60 chunks, length = [], 061 while True:62 result = await reader.read()63 if result.done:64 break65 chunk = result.value.to_bytes()66 length += len(chunk)67 if length > MAX_REQUEST_BYTES:68 await reader.cancel('request too large')69 raise ProtocolError(413, 'request_too_large', 'Render request is too large.')70 chunks.append(chunk)71 try:72 return b''.join(chunks).decode('utf-8')73 except UnicodeDecodeError as error:74 raise ProtocolError(400, 'invalid_json', 'Render request is not valid UTF-8 JSON.') from error757677class Default(WorkerEntrypoint):78 async def fetch(self, request):79 if request.method == 'OPTIONS':80 return Response(None, status=204, headers={81 'Access-Control-Allow-Headers': 'Accept, Content-Type',82 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',83 'Access-Control-Allow-Origin': '*',84 'Access-Control-Max-Age': '86400',85 })86 path = urlparse(request.url).path87 if request.method == 'GET' and path == '/v1/health':88 return _json({'ok': True, 'unit': UNIT_ID, 'id': UNIT_ID})89 if request.method == 'GET' and path == '/v1/capabilities':90 return _json({91 'unit': UNIT_ID, 'id': UNIT_ID, 'kind': 'translate', 'format': 'svg',92 'runtime': 'edge-python', 'version': WIREVIZ_VERSION,93 'build': self.env.RENDERER_BUILD, 'pipeline': [UNIT_ID, 'graphviz-family', 'graphviz'],94 'license': 'GPL-3.0-only',95 'source': 'https://github.com/FelineStateMachine/diagramzip/tree/main/renderers/python/wireviz-translator',96 'knownLosses': [97 'Only SVG is supported; BOM and HTML sidecars are not exposed.',98 'Filesystem and remote images are rejected.',99 'tweak.append and tweak.override are rejected.',100 'GraphViz layout is provided by the shared GraphViz-Wasm unit; its 15.1.1 output may differ from the compatibility image GraphViz 14.1.3.',101 ],102 }, cache='public, max-age=300')103 if request.method != 'POST' or path != '/v1/svg':104 return _json({'error': {'code': 'not_found', 'message': 'Route not found.'}}, 404)105 try:106 render_input = parse_render_input(await _body(request))107 from js import Request as JSRequest, caches108 from pyodide.ffi import create_proxy109110 cache_key = JSRequest.new(_cache_url(self.env.RENDERER_BUILD, render_input))111 try:112 cached = await caches.default.match(cache_key)113 if cached is not None:114 return _cache_status(Response(cached), 'HIT')115 except Exception as error:116 print(json.dumps({'message': 'WireViz cache read failed', 'unit': UNIT_ID, 'error': str(error)}))117118 dot = render_dot(render_input.source)119 downstream = Request('https://graphviz.render.diagram.zip/v1/svg', method='POST', headers={120 'Accept': 'image/svg+xml', 'Content-Type': 'application/json',121 }, body=json.dumps({122 'source': dot, 'format': 'svg', 'options': {},123 'metadata': {'title': render_input.title, 'description': render_input.description},124 'presentation': {125 'background': render_input.background, 'padding': render_input.padding,126 'frame': render_input.frame,127 },128 })).js_object129 try:130 rendered = await self.env.GRAPHVIZ.fetch(downstream)131 except Exception as error:132 print(json.dumps({'message': 'WireViz GraphViz binding failed', 'unit': UNIT_ID, 'error': str(error)}))133 raise ProtocolError(503, 'renderer_unavailable', 'The GraphViz renderer is unavailable.') from error134 if rendered.status < 300:135 headers = {136 'Cache-Control': rendered.headers.get('Cache-Control') or 'public, max-age=1800',137 'Content-Type': 'image/svg+xml; charset=utf-8',138 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; sandbox",139 'X-Content-Type-Options': 'nosniff',140 'Access-Control-Allow-Origin': '*', 'Access-Control-Expose-Headers': EXPOSED_HEADERS,141 'X-Diagram-Engine': UNIT_ID, 'X-Diagram-Engine-Version': WIREVIZ_VERSION,142 'X-Diagram-Pipeline': 'wireviz,graphviz-family,graphviz', 'X-Diagram-Renderer': 'edge-python',143 'X-Diagram-Unit': UNIT_ID, 'X-Renderer-Build': self.env.RENDERER_BUILD,144 'X-Diagram-Cache': 'MISS',145 }146 response = Response(rendered.body, status=rendered.status, headers=headers)147 try:148 self.ctx.waitUntil(create_proxy(caches.default.put(cache_key, response.js_object.clone())))149 except Exception as error:150 print(json.dumps({'message': 'WireViz cache write failed', 'unit': UNIT_ID, 'error': str(error)}))151 return response152 return Response(rendered.body, status=rendered.status, headers={153 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store',154 'Content-Type': rendered.headers.get('Content-Type') or 'application/json',155 })156 except ProtocolError as error:157 return _error(error)158 except Exception as error:159 print(json.dumps({'message': 'WireViz translator failed', 'unit': UNIT_ID, 'error': str(error)}))160 return _json({'error': {'code': 'render_failed', 'message': 'The WireViz source could not be rendered.'}}, 422)