Source code for falcon.asgi.structures
from __future__ import annotations
from typing import Optional
from falcon.constants import MEDIA_JSON
from falcon.media import BaseHandler
from falcon.media.json import _DEFAULT_JSON_HANDLER
__all__ = ('SSEvent',)
[docs]
class SSEvent:
"""Represents a Server-Sent Event (SSE).
Instances of this class can be yielded by an async generator in order to
send a series of `Server-Sent Events`_ to the user agent.
(See also: :attr:`falcon.asgi.Response.sse`)
Keyword Args:
data (bytes): Raw byte string to use as the ``data`` field for the
event message. Takes precedence over both `text` and `json`.
text (str): String to use for the ``data`` field in the message. Will
be encoded as UTF-8 in the event. Takes precedence over `json`.
json (object): JSON-serializable object to be converted to JSON and
used as the ``data`` field in the event message.
event (str): A string identifying the event type (AKA event name).
event_id (str): The event ID that the User Agent should use for
the `EventSource` object's last event ID value.
retry (int): The reconnection time to use when attempting to send the
event. This must be an integer, specifying the reconnection time
in milliseconds.
comment (str): Comment to include in the event message; this is
normally ignored by the user agent, but is useful when composing
a periodic "ping" message to keep the connection alive. Since this
is a common use case, a default "ping" comment will be included
in any event that would otherwise be blank (i.e., one that does
not specify any fields when initializing the `SSEvent` instance.)
.. _Server-Sent Events:
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
"""
__slots__ = [
'data',
'text',
'json',
'event',
'event_id',
'retry',
'comment',
]
data: Optional[bytes]
"""Raw byte string to use as the ``data`` field for the event message.
Takes precedence over both `text` and `json`.
"""
text: Optional[str]
"""String to use for the ``data`` field in the message.
Will be encoded as UTF-8 in the event. Takes precedence over `json`.
"""
json: object
"""JSON-serializable object to be converted to JSON and used as the ``data``
field in the event message.
"""
event: Optional[str]
"""A string identifying the event type (AKA event name)."""
event_id: Optional[str]
"""The event ID that the User Agent should use for the `EventSource` object's
last event ID value.
"""
retry: Optional[int]
"""The reconnection time to use when attempting to send the event.
This must be an integer, specifying the reconnection time in milliseconds.
"""
comment: Optional[str]
"""Comment to include in the event message.
This is normally ignored by the user agent, but is useful when composing a periodic
"ping" message to keep the connection alive. Since this is a common use case, a
default "ping" comment will be included in any event that would otherwise be blank
(i.e., one that does not specify any of the fields when initializing the
:class:`SSEvent` instance.)
"""
def __init__(
self,
data: Optional[bytes] = None,
text: Optional[str] = None,
json: Optional[object] = None,
event: Optional[str] = None,
event_id: Optional[str] = None,
retry: Optional[int] = None,
comment: Optional[str] = None,
) -> None:
# NOTE(kgriffs): Check up front since this makes it a lot easier
# to debug the source of the problem in the app vs. waiting for
# an error to be raised from the framework when it calls serialize()
# after the fact.
if data is not None and not isinstance(data, bytes):
raise TypeError('data must be a byte string')
if text is not None and not isinstance(text, str):
raise TypeError('text must be a string')
if event is not None and not isinstance(event, str):
raise TypeError('event name must be a string')
if event_id is not None and not isinstance(event_id, str):
raise TypeError('event_id must be a string')
if comment is not None and not isinstance(comment, str):
raise TypeError('comment must be a string')
if retry is not None and not isinstance(retry, int):
raise TypeError('retry must be an int')
self.data = data
self.text = text
self.json = json
self.event = event
self.event_id = event_id
self.retry = retry
self.comment = comment
[docs]
def serialize(self, handler: Optional[BaseHandler] = None) -> bytes:
"""Serialize this event to string.
Args:
handler: Handler object that will be used to serialize the ``json``
attribute to string. When not provided, a default handler using
the builtin JSON library will be used (default ``None``).
Returns:
bytes: string representation of this event.
"""
if self.comment is not None:
block = f': {self.comment}\n'
else:
block = ''
if self.event is not None:
block += f'event: {self.event}\n'
if self.event_id is not None:
# NOTE(kgriffs): f-strings are a tiny bit faster than str().
block += f'id: {self.event_id}\n'
if self.retry is not None:
block += f'retry: {self.retry}\n'
if self.data is not None:
# NOTE(kgriffs): While this decode() may seem unnecessary, it
# does provide a check to ensure it is valid UTF-8. I'm also
# assuming for the moment that most people will not use this
# attribute, but rather the text and json ones instead. If that
# is true, it makes sense to construct the entire string
# first, then encode it all in one go at the end.
block += f'data: {self.data.decode()}\n'
elif self.text is not None:
block += f'data: {self.text}\n'
elif self.json is not None:
if handler is None:
handler = _DEFAULT_JSON_HANDLER
serialized = handler.serialize(self.json, MEDIA_JSON)
block += 'data: '
return block.encode() + serialized + b'\n\n'
if not block:
return b': ping\n\n'
return (block + '\n').encode()