Source code for photonforge.live_viewer

import asyncio as _a
import pathlib as _p
import queue as _q
import threading as _t
import time as _tm
import typing as _typ

import fastapi as _f
import uvicorn as _uc
from fastapi.middleware.cors import CORSMiddleware as _cors
from fastapi.responses import FileResponse as _fr
from fastapi.responses import StreamingResponse as _sr
from fastapi.staticfiles import StaticFiles as _sf
from fastapi.templating import Jinja2Templates as _j2

import photonforge as _pf


[docs] class LiveViewer: """Live viewer for PhotonForge objects. Args: port: Port number used by the viewer server. start: If ``True``, the viewer server is automatically started. Example: >>> from photonforge.live_viewer import LiveViewer >>> viewer = LiveViewer() >>> component = pf.parametric.straight(port_spec="Strip", length=3) >>> viewer(component) >>> terminal = pf.Terminal("METAL", pf.Circle(2)) >>> viewer(terminal) """ def __init__(self, port: int = 0, start: bool = True): self.app = _f.FastAPI( title="LiveViewer server", description="PhotonForge LiveViewer server", version=_pf.__version__, ) self.app.add_middleware( _cors, allow_origins=[], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) root = _p.Path(__file__).parent self.app.mount("/static", _sf(directory=root / "static"), name="static") self.templates = _j2(directory=root / "templates") self.port = port self.current_data = "" self.server = None @self.app.get("/") async def home(request: _f.Request): return self.templates.TemplateResponse(name="index.html", request=request) @self.app.get("/events", include_in_schema=False) async def events(): return _sr(self.generate(), media_type="text/event-stream") @self.app.get("/favicon.ico", include_in_schema=False) async def favicon(): return _fr(root / "static" / "icons" / "photonforge.svg") if start: self.start() async def generate(self): while self.server is not None: try: while not self.queue.empty(): self.current_data = self.queue.get_nowait() except _q.Empty: pass if self.current_data == "shutdown": return if self.current_data: yield f"data: {self.current_data}\n\n" else: yield "data: Waiting for data…\n\n" await _a.sleep(0.25)
[docs] def start(self) -> "LiveViewer": """Start the server.""" config = _uc.Config(app=self.app, host="0.0.0.0", port=self.port, log_level="error") self.server = _uc.Server(config=config) self.server_thread = _t.Thread(target=self.server.run, daemon=False) self.server_thread.start() self.queue = _q.SimpleQueue() while not self.server.started: _tm.sleep(0.1) for s in self.server.servers: for socket in s.sockets: self.port = socket.getsockname()[1] break print(f"LiveViewer started at http://localhost:{self.port}") return self
[docs] def stop(self): """Stop the server.""" if self.server is not None: self.queue.put("shutdown") self.server.should_exit = True self.server_thread.join() self.server = None print("LiveViewer stopped.")
def __call__(self, item: _typ.Any) -> _typ.Any: """Display an item with an SVG representation. Args: item: Item to be displayed. Returns: 'item'. """ if self.server is not None and hasattr(item, "_repr_svg_"): self.queue.put(item._repr_svg_()) return item
[docs] def display(self, item: _typ.Any) -> _typ.Any: """Display an item with an SVG representation. Args: item: Item to be displayed. Returns: 'item'. """ return self(item)
def _repr_html_(self) -> str: """Returns a clickable link for Jupyter.""" if self.server is None: return "LiveViewer not started." return ( f'Live viewer at <a href="http://localhost:{self.port}" target="_blank">' f"http://localhost:{self.port}</a>" ) def __str__(self) -> str: if self.server is None: return "LiveViewer not started." return f"Live viewer at http://localhost:{self.port}"