Skip to content

EventHandler

hololinked.server.http.handlers.EventHandler

Bases: BaseHandler

handles events emitted by Thing and tunnels them as HTTP SSE.

Source code in repo/hololinked/hololinked/server/http/handlers.py
class EventHandler(BaseHandler):
    """handles events emitted by `Thing` and tunnels them as HTTP SSE."""

    resource: EventAffordance | PropertyAffordance
    """an event, or the observable property whose change event is streamed - both name an event to subscribe to"""

    def initialize(
        self,
        resource: InteractionAffordance | EventAffordance,
        config: RuntimeConfig,
        logger: structlog.stdlib.BoundLogger,
        thing: Thing,
        metadata: HandlerMetadata | None = None,
    ) -> None:
        """
        Set up the handler to stream events with a plain SSE data header.

        Raises
        ------
        RuntimeError
            If the `Thing` is not served by an event loop.
        """
        super().initialize(resource, config, logger, thing, metadata)
        self.data_header = b"data: %s\n\n"
        if self.thing.eventloop is None:  # type gaurd, not a real logic
            raise RuntimeError("Thing is not served by an event loop")

    def set_custom_default_headers(self) -> None:
        """
        Sets default headers for event handling. The general headers are listed as follows:

        ```yaml
        Content-Type: text/event-stream
        Cache-Control: no-cache
        Connection: keep-alive
        Access-Control-Allow-Credentials: true # Possibly for cookie auth
        Access-Control-Allow-Origin: <client> # if CORS is enabled
        ```
        """  # noqa: D400
        self.set_header("Content-Type", "text/event-stream")
        self.set_header("Cache-Control", "no-cache")
        self.set_header("Connection", "keep-alive")
        super().set_custom_default_headers()

    async def get(self):
        """Events are support only with GET method."""
        if await self.has_access_control():
            self.set_custom_default_headers()
            await self.handle_datastream()
        self.finish()

    async def options(self):  # ty: ignore[invalid-method-override]
        """Options for the resource."""
        if await self.has_access_control():
            self.set_status(204)
            self.set_custom_default_headers()
            self.set_access_control_allow_headers()
            self.set_header("Access-Control-Allow-Methods", "GET")
        self.finish()

    async def handle_datastream(self) -> None:
        """Called by GET method and handles the event publishing."""  # noqa: DOC501
        if not self.thing.eventloop:  # type gaurd, not a real logic.
            raise RuntimeError("Thing is not served by an event loop")
        try:
            subscription = EventSubscription(
                self.thing.eventloop.event_bus,
                self.resource.event_unique_identifier,
            )
            self.set_status(200)
        except Exception as ex:
            self.logger.error(f"error while subscribing to event - {str(ex)}")
            self.set_status(500, f"could not subscribe to event source from thing - {str(ex)}")
            self.write(Serializers.json.dumps({"exception": format_exception_as_json(ex)}))
            return

        # Send the header right away. One needs to flush to even send headers.
        # This confirms that a subscription happened. If clients end with too short timeout even without receiving
        # headers, then they think that the subscription did not go through.
        await self.flush()

        try:
            while True:
                try:
                    data = await subscription.receive(timeout=10)
                    body, content_type = subscription.encode(data)
                    # TODO use content_type and get rid of other event handlers
                    self.write(self.data_header % body)
                    self.logger.debug(f"new data scheduled to flush - {self.resource.name}")
                    # flushes and handles heartbeat - raises StreamClosedError if the client left
                    await self.flush()
                except TimeoutError:
                    self.logger.debug(f"found no new data - {self.resource.name}")
                except StreamClosedError:
                    break
                except Exception as ex:
                    self.logger.error(f"error while pushing event - {str(ex)}")
                    self.write(self.data_header % Serializers.json.dumps({"exception": format_exception_as_json(ex)}))
        finally:
            subscription.unsubscribe()

Attributes

resource instance-attribute

resource: EventAffordance | PropertyAffordance

an event, or the observable property whose change event is streamed - both name an event to subscribe to

Functions

initialize

initialize(resource: InteractionAffordance | EventAffordance, config: RuntimeConfig, logger: BoundLogger, thing: Thing, metadata: HandlerMetadata | None = None) -> None

Set up the handler to stream events with a plain SSE data header.

Raises:

Type Description
RuntimeError

If the Thing is not served by an event loop.

Source code in repo/hololinked/hololinked/server/http/handlers.py
def initialize(
    self,
    resource: InteractionAffordance | EventAffordance,
    config: RuntimeConfig,
    logger: structlog.stdlib.BoundLogger,
    thing: Thing,
    metadata: HandlerMetadata | None = None,
) -> None:
    """
    Set up the handler to stream events with a plain SSE data header.

    Raises
    ------
    RuntimeError
        If the `Thing` is not served by an event loop.
    """
    super().initialize(resource, config, logger, thing, metadata)
    self.data_header = b"data: %s\n\n"
    if self.thing.eventloop is None:  # type gaurd, not a real logic
        raise RuntimeError("Thing is not served by an event loop")

set_custom_default_headers

set_custom_default_headers() -> None

Sets default headers for event handling. The general headers are listed as follows:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Access-Control-Allow-Credentials: true # Possibly for cookie auth
Access-Control-Allow-Origin: <client> # if CORS is enabled
Source code in repo/hololinked/hololinked/server/http/handlers.py
def set_custom_default_headers(self) -> None:
    """
    Sets default headers for event handling. The general headers are listed as follows:

    ```yaml
    Content-Type: text/event-stream
    Cache-Control: no-cache
    Connection: keep-alive
    Access-Control-Allow-Credentials: true # Possibly for cookie auth
    Access-Control-Allow-Origin: <client> # if CORS is enabled
    ```
    """  # noqa: D400
    self.set_header("Content-Type", "text/event-stream")
    self.set_header("Cache-Control", "no-cache")
    self.set_header("Connection", "keep-alive")
    super().set_custom_default_headers()

get async

get()

Events are support only with GET method.

Source code in repo/hololinked/hololinked/server/http/handlers.py
async def get(self):
    """Events are support only with GET method."""
    if await self.has_access_control():
        self.set_custom_default_headers()
        await self.handle_datastream()
    self.finish()

options async

options()

Options for the resource.

Source code in repo/hololinked/hololinked/server/http/handlers.py
async def options(self):  # ty: ignore[invalid-method-override]
    """Options for the resource."""
    if await self.has_access_control():
        self.set_status(204)
        self.set_custom_default_headers()
        self.set_access_control_allow_headers()
        self.set_header("Access-Control-Allow-Methods", "GET")
    self.finish()

handle_datastream async

handle_datastream() -> None

Called by GET method and handles the event publishing.

Source code in repo/hololinked/hololinked/server/http/handlers.py
async def handle_datastream(self) -> None:
    """Called by GET method and handles the event publishing."""  # noqa: DOC501
    if not self.thing.eventloop:  # type gaurd, not a real logic.
        raise RuntimeError("Thing is not served by an event loop")
    try:
        subscription = EventSubscription(
            self.thing.eventloop.event_bus,
            self.resource.event_unique_identifier,
        )
        self.set_status(200)
    except Exception as ex:
        self.logger.error(f"error while subscribing to event - {str(ex)}")
        self.set_status(500, f"could not subscribe to event source from thing - {str(ex)}")
        self.write(Serializers.json.dumps({"exception": format_exception_as_json(ex)}))
        return

    # Send the header right away. One needs to flush to even send headers.
    # This confirms that a subscription happened. If clients end with too short timeout even without receiving
    # headers, then they think that the subscription did not go through.
    await self.flush()

    try:
        while True:
            try:
                data = await subscription.receive(timeout=10)
                body, content_type = subscription.encode(data)
                # TODO use content_type and get rid of other event handlers
                self.write(self.data_header % body)
                self.logger.debug(f"new data scheduled to flush - {self.resource.name}")
                # flushes and handles heartbeat - raises StreamClosedError if the client left
                await self.flush()
            except TimeoutError:
                self.logger.debug(f"found no new data - {self.resource.name}")
            except StreamClosedError:
                break
            except Exception as ex:
                self.logger.error(f"error while pushing event - {str(ex)}")
                self.write(self.data_header % Serializers.json.dumps({"exception": format_exception_as_json(ex)}))
    finally:
        subscription.unsubscribe()

JPEGImageEventHandler

Bases: EventHandler

handles events with images with JPEG image data header.

Source code in repo/hololinked/hololinked/server/http/handlers.py
class JPEGImageEventHandler(EventHandler):
    """handles events with images with JPEG image data header."""

    def initialize(
        self,
        resource: InteractionAffordance | EventAffordance,
        config: RuntimeConfig,
        logger: structlog.stdlib.BoundLogger,
        thing: Thing,
        metadata: HandlerMetadata | None = None,
    ) -> None:
        """Set up the handler to stream events with a base64 JPEG image SSE data header."""
        super().initialize(resource, config, logger, thing, metadata)
        self.data_header = b"data:image/jpeg;base64,%s\n\n"

Functions

initialize

initialize(resource: InteractionAffordance | EventAffordance, config: RuntimeConfig, logger: BoundLogger, thing: Thing, metadata: HandlerMetadata | None = None) -> None

Set up the handler to stream events with a base64 JPEG image SSE data header.

Source code in repo/hololinked/hololinked/server/http/handlers.py
def initialize(
    self,
    resource: InteractionAffordance | EventAffordance,
    config: RuntimeConfig,
    logger: structlog.stdlib.BoundLogger,
    thing: Thing,
    metadata: HandlerMetadata | None = None,
) -> None:
    """Set up the handler to stream events with a base64 JPEG image SSE data header."""
    super().initialize(resource, config, logger, thing, metadata)
    self.data_header = b"data:image/jpeg;base64,%s\n\n"

PNGImageEventHandler

Bases: EventHandler

handles events with images with PNG image data header.

Source code in repo/hololinked/hololinked/server/http/handlers.py
class PNGImageEventHandler(EventHandler):
    """handles events with images with PNG image data header."""

    def initialize(
        self,
        resource: InteractionAffordance | EventAffordance,
        config: RuntimeConfig,
        logger: structlog.stdlib.BoundLogger,
        thing: Thing,
        metadata: HandlerMetadata | None = None,
    ) -> None:
        """Set up the handler to stream events with a base64 PNG image SSE data header."""
        super().initialize(resource, config, logger, thing, metadata)
        self.data_header = b"data:image/png;base64,%s\n\n"

Functions

initialize

initialize(resource: InteractionAffordance | EventAffordance, config: RuntimeConfig, logger: BoundLogger, thing: Thing, metadata: HandlerMetadata | None = None) -> None

Set up the handler to stream events with a base64 PNG image SSE data header.

Source code in repo/hololinked/hololinked/server/http/handlers.py
def initialize(
    self,
    resource: InteractionAffordance | EventAffordance,
    config: RuntimeConfig,
    logger: structlog.stdlib.BoundLogger,
    thing: Thing,
    metadata: HandlerMetadata | None = None,
) -> None:
    """Set up the handler to stream events with a base64 PNG image SSE data header."""
    super().initialize(resource, config, logger, thing, metadata)
    self.data_header = b"data:image/png;base64,%s\n\n"