"""A Websocket Handler for emitting Jupyter server events. .. versionadded:: 2.0 """ from __future__ import annotations import json from datetime import datetime from typing import TYPE_CHECKING, Any, Dict, Optional, cast from jupyter_core.utils import ensure_async from tornado import web, websocket from jupyter_server.auth.decorator import authorized, ws_authenticated from jupyter_server.base.handlers import JupyterHandler from ...base.handlers import APIHandler AUTH_RESOURCE = "events" if TYPE_CHECKING: import jupyter_events.logger class SubscribeWebsocket( JupyterHandler, websocket.WebSocketHandler, ): """Websocket handler for subscribing to events""" auth_resource = AUTH_RESOURCE async def pre_get(self): """Handles authorization when attempting to subscribe to events emitted by Jupyter Server's eventbus. """ user = self.current_user # authorize the user. authorized = await ensure_async( self.authorizer.is_authorized(self, user, "execute", "events") ) if not authorized: raise web.HTTPError(403) @ws_authenticated async def get(self, *args, **kwargs): """Get an event socket.""" await ensure_async(self.pre_get()) res = super().get(*args, **kwargs) if res is not None: await res async def event_listener( self, logger: jupyter_events.logger.EventLogger, schema_id: str, data: dict[str, Any] ) -> None: """Write an event message.""" capsule = dict(schema_id=schema_id, **data) self.write_message(json.dumps(capsule)) def open(self): """Routes events that are emitted by Jupyter Server's EventBus to a WebSocket client in the browser. """ self.event_logger.add_listener(listener=self.event_listener) def on_close(self): """Handle a socket close.""" self.event_logger.remove_listener(listener=self.event_listener) def validate_model(data: dict[str, Any]) -> None: """Validates for required fields in the JSON request body""" required_keys = {"schema_id", "version", "data"} for key in required_keys: if key not in data: raise web.HTTPError(400, f"Missing `{key}` in the JSON request body.") def get_timestamp(data: dict[str, Any]) -> Optional[datetime]: """Parses timestamp from the JSON request body""" try: if "timestamp" in data: timestamp = datetime.strptime(data["timestamp"], "%Y-%m-%dT%H:%M:%S%zZ") else: timestamp = None except Exception as e: raise web.HTTPError( 400, """Failed to parse timestamp from JSON request body, an ISO format datetime string with UTC offset is expected, for example, 2022-05-26T13:50:00+05:00Z""", ) from e return timestamp class EventHandler(APIHandler): """REST api handler for events""" auth_resource = AUTH_RESOURCE @web.authenticated @authorized async def post(self): """Emit an event.""" payload = self.get_json_body() if payload is None: raise web.HTTPError(400, "No JSON data provided") try: validate_model(payload) self.event_logger.emit( schema_id=cast(str, payload.get("schema_id")), data=cast("Dict[str, Any]", payload.get("data")), timestamp_override=get_timestamp(payload), ) self.set_status(204) self.finish() except web.HTTPError: raise except Exception as e: raise web.HTTPError(500, str(e)) from e default_handlers = [ (r"/api/events", EventHandler), (r"/api/events/subscribe", SubscribeWebsocket), ]