Skip to content

Commit

Permalink
Add built-in features for server-sent events
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertoPrevato authored Jan 16, 2024
1 parent 4fc7a4b commit aa8e7df
Show file tree
Hide file tree
Showing 16 changed files with 437 additions and 16 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.6] - 2024-01-16 :kr: :heart:

- Adds built-in support for [Server-Sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).
- Adds a function to detect when the server process is terminating because it
received a `SIGINT` or a `SIGTERM` command
(`from blacksheep.server.process import is_stopping`).
- Adds support for request handler normalization for methods defined as
asynchronous generators. This feature is enabled by default only for
ServerSentEvents, but can be configured for user defined types.

Refer to the [BlackSheep documentation](https://www.neoteroi.dev/blacksheep/server-sent-events/)
and to the [examples repository](https://github.com/Neoteroi/BlackSheep-Examples/tree/main/server-sent-events) for more information on server-sent events support.

## [2.0.5] - 2024-01-12 :pie:

- Fixes [#466](https://github.com/Neoteroi/BlackSheep/issues/466), regression
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ for more details and examples.
methods](https://www.neoteroi.dev/blacksheep/controllers/)
* [Middlewares](https://www.neoteroi.dev/blacksheep/middlewares/)
* [WebSocket](https://www.neoteroi.dev/blacksheep/websocket/)
* [Server-Sent Events (SSE)](https://www.neoteroi.dev/blacksheep/server-sent-events/)
* [Built-in support for dependency
injection](https://www.neoteroi.dev/blacksheep/dependency-injection/)
* [Support for automatic binding of route and query parameters to request
Expand Down Expand Up @@ -281,6 +282,5 @@ Please refer to the [documentation website](https://www.neoteroi.dev/blacksheep/
[BlackSheep community in Gitter](https://gitter.im/Neoteroi/BlackSheep).

## Branches
The _main_ branch contains the currently developed version, which is version 2
alpha. The _v1_ branch contains version 1 of the web framework, for bugs fixes
The _main_ branch contains the currently developed version, which is version 2. The _v1_ branch contains version 1 of the web framework, for bugs fixes
and maintenance.
2 changes: 1 addition & 1 deletion blacksheep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
used types to reduce the verbosity of the imports statements.
"""
__author__ = "Roberto Prevato <roberto.prevato@gmail.com>"
__version__ = "2.0.5"
__version__ = "2.0.6"

from .contents import Content as Content
from .contents import FormContent as FormContent
Expand Down
9 changes: 8 additions & 1 deletion blacksheep/contents.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,19 @@ cdef class FormPart:
cdef readonly bytes charset


cdef class ServerSentEvent:
cdef readonly object data
cdef readonly str event
cdef readonly str id
cdef readonly int retry
cdef readonly str comment


cdef class MultiPartFormData(Content):
cdef readonly list parts
cdef readonly bytes boundary



cdef dict parse_www_form_urlencoded(str content)


Expand Down
29 changes: 29 additions & 0 deletions blacksheep/contents.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,32 @@ def parse_www_form(content: str) -> Dict[str, Union[str, List[str]]]:
def write_www_form_urlencoded(
data: Union[Dict[str, str], List[Tuple[str, str]]]
) -> bytes: ...

class ServerSentEvent:
"""
Represents a single event of a Server-sent event communication, to be used
in a asynchronous generator.
Attributes:
data: An object that will be transmitted to the client, in JSON.
event: Optional event name.
id: Optional event ID to set the EventSource's last event ID value.
retry: Optional reconnection time, in milliseconds.
If the connection to the server is lost, the browser will wait
for the specified time before attempting to reconnect.
comment: Optional comment.
"""

def __init__(
self,
data: Any,
event: Optional[str] = None,
id: Optional[str] = None,
retry: int = -1,
comment: Optional[str] = None,
):
self.data = data
self.event = event
self.id = id
self.retry = retry
self.comment = comment
36 changes: 36 additions & 0 deletions blacksheep/contents.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,39 @@ cpdef bytes write_multipart_form_data(MultiPartFormData data):
contents.extend(data.boundary)
contents.extend(b'--\r\n')
return bytes(contents)


cdef class ServerSentEvent:
"""
Represents a single event of a Server-sent event communication, to be used
in a asynchronous generator.
Attributes:
data: An object that will be transmitted to the client, in JSON.
event: Optional event name.
id: Optional event ID to set the EventSource's last event ID value.
retry: The reconnection time. If the connection to the server is lost,
the browser will wait for the specified time before attempting
to reconnect.
comment: Optional comment.
"""

def __init__(
self,
object data,
str event = None,
str id = None,
int retry = -1,
str comment = None,
):
"""
Creates an instance of ServerSentEvent
"""
self.data = data
self.event = event
self.id = id
self.retry = retry
self.comment = comment

def __repr__(self):
return f"ServerSentEvent({self.data})"
4 changes: 3 additions & 1 deletion blacksheep/scribe.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# This module is part of BlackSheep and is released under
# the MIT License https://opensource.org/licenses/MIT

from .contents cimport Content
from .contents cimport Content, ServerSentEvent
from .cookies cimport Cookie
from .messages cimport Message, Request, Response

Expand All @@ -28,3 +28,5 @@ cdef bytes write_small_response(Response response)
cdef void set_headers_for_content(Message message)

cdef void set_headers_for_response_content(Response message)

cpdef bytes write_sse(ServerSentEvent event)
3 changes: 2 additions & 1 deletion blacksheep/scribe.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import AsyncIterable, Callable

from blacksheep.contents import Content
from blacksheep.contents import Content, ServerSentEvent
from blacksheep.cookies import Cookie
from blacksheep.messages import Request, Response

Expand All @@ -15,3 +15,4 @@ def write_request(request: Request) -> AsyncIterable[bytes]: ...
def write_response(response: Response) -> AsyncIterable[bytes]: ...
def write_request_body_only(request: Request) -> AsyncIterable[bytes]: ...
def write_response_cookie(cookie: Cookie) -> bytes: ...
def write_sse(event: ServerSentEvent) -> bytes: ...
33 changes: 33 additions & 0 deletions blacksheep/scribe.pyx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import http
import re

from .contents cimport Content, StreamedContent
from .cookies cimport Cookie, write_cookie_for_response
from .messages cimport Request, Response
from .url cimport URL

from blacksheep.settings.json import json_settings


cdef int MAX_RESPONSE_CHUNK_SIZE = 61440 # 64kb

Expand Down Expand Up @@ -360,3 +363,33 @@ async def send_asgi_response(Response response, object send):
'type': 'http.response.body',
'body': b''
})


_NEW_LINES_RX = re.compile("\r\n|\n")


cpdef bytes write_sse(ServerSentEvent event):
"""
Writes a ServerSentEvent object to bytes.
"""
cdef bytearray value = bytearray()

if event.id:
value.extend(b"id: " + _NEW_LINES_RX.sub("", event.id).encode("utf8") + b"\r\n")

if event.comment:
for part in _NEW_LINES_RX.split(event.comment):
value.extend(b": " + part.encode("utf8") + b"\r\n")

if event.event:
value.extend(b"event: " + _NEW_LINES_RX.sub("", event.event).encode("utf8") + b"\r\n")

if event.data:
json_data = json_settings.dumps(event.data)
value.extend(b"data: " + json_data.encode("utf8") + b"\r\n")

if event.retry > -1:
value.extend(b"retry: " + str(event.retry).encode() + b"\r\n")

value.extend(b"\r\n")
return bytes(value)
4 changes: 4 additions & 0 deletions blacksheep/server/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from blacksheep.server.files import DefaultFileOptions
from blacksheep.server.files.dynamic import serve_files_dynamic
from blacksheep.server.normalization import normalize_handler, normalize_middleware
from blacksheep.server.process import use_shutdown_handler
from blacksheep.server.responses import _ensure_bytes
from blacksheep.server.routing import (
MountRegistry,
Expand Down Expand Up @@ -216,6 +217,9 @@ def __init__(
_auto_import_controllers(parent_file)
_auto_import_routes(parent_file)

if env_settings.add_signal_handler:
use_shutdown_handler(self)

@property
def controllers_router(self) -> RoutesRegistry:
return self.router.controllers_routes
Expand Down
2 changes: 2 additions & 0 deletions blacksheep/server/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ class EnvironmentSettings:
show_error_details: bool
mount_auto_events: bool
use_default_router: bool
add_signal_handler: bool

def __init__(self) -> None:
self.env = get_env()
self.show_error_details = truthy(os.environ.get("APP_SHOW_ERROR_DETAILS", ""))
self.mount_auto_events = truthy(os.environ.get("APP_MOUNT_AUTO_EVENTS", "1"))
self.use_default_router = truthy(os.environ.get("APP_DEFAULT_ROUTER", "1"))
self.add_signal_handler = truthy(os.environ.get("APP_SIGNAL_HANDLER", "1"))
Loading

0 comments on commit aa8e7df

Please sign in to comment.