Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
Alc-Alc committed Mar 10, 2024
2 parents 8840de8 + 36324b9 commit d292aac
Show file tree
Hide file tree
Showing 114 changed files with 1,260 additions and 343 deletions.
2 changes: 1 addition & 1 deletion docs/examples/middleware/session/cookie_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
from litestar import Litestar
from litestar.middleware.session.client_side import CookieBackendConfig

session_config = CookieBackendConfig(secret=urandom(16)) # type: ignore[arg-type]
session_config = CookieBackendConfig(secret=urandom(16)) # type: ignore

app = Litestar(middleware=[session_config.middleware])
32 changes: 32 additions & 0 deletions docs/examples/request_data/custom_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from litestar import Litestar, Request, get
from litestar.connection.base import empty_receive, empty_send
from litestar.enums import HttpMethod
from litestar.types import Receive, Scope, Send

KITTEN_NAMES_MAP = {
HttpMethod.GET: "Whiskers",
}


class CustomRequest(Request):
"""Enrich request with the kitten name."""

__slots__ = ("kitten_name",)

def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send) -> None:
"""Initialize CustomRequest class."""
super().__init__(scope=scope, receive=receive, send=send)
self.kitten_name = KITTEN_NAMES_MAP.get(scope["method"], "Mittens")


@get(path="/kitten-name")
def get_kitten_name(request: CustomRequest) -> str:
"""Get kitten name based on the HTTP method."""
return request.kitten_name


app = Litestar(
route_handlers=[get_kitten_name],
request_class=CustomRequest,
debug=True,
)
16 changes: 16 additions & 0 deletions docs/examples/responses/json_suffix_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Any, Dict

import litestar.status_codes
from litestar import Litestar, get


@get("/resources", status_code=litestar.status_codes.HTTP_418_IM_A_TEAPOT, media_type="application/problem+json")
async def retrieve_resource() -> Dict[str, Any]:
return {
"title": "Server thinks it is a teapot",
"type": "Server delusion",
"status": litestar.status_codes.HTTP_418_IM_A_TEAPOT,
}


app = Litestar(route_handlers=[retrieve_resource])
8 changes: 5 additions & 3 deletions docs/examples/testing/test_set_session_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def get_session_data(request: Request) -> Dict[str, Any]:

app = Litestar(route_handlers=[get_session_data], middleware=[session_config.middleware])

with TestClient(app=app, session_config=session_config) as client:
client.set_session_data({"foo": "bar"})
assert client.get("/test").json() == {"foo": "bar"}

def test_get_session_data() -> None:
with TestClient(app=app, session_config=session_config) as client:
client.set_session_data({"foo": "bar"})
assert client.get("/test").json() == {"foo": "bar"}
19 changes: 19 additions & 0 deletions docs/examples/websockets/custom_websocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

from litestar import Litestar, WebSocket, websocket_listener
from litestar.types.asgi_types import WebSocketMode


class CustomWebSocket(WebSocket):
async def receive_data(self, mode: WebSocketMode) -> str | bytes:
"""Return fixed response for every websocket message."""
await super().receive_data(mode=mode)
return "Fixed response"


@websocket_listener("/")
async def handler(data: str) -> str:
return data


app = Litestar([handler], websocket_class=CustomWebSocket)
123 changes: 123 additions & 0 deletions docs/release-notes/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,129 @@
2.x Changelog
=============

.. changelog:: 2.7.0
:date: 2024-03-10

.. change:: missing cors headers in response
:type: bugfix
:pr: 3179
:issue: 3178

Set CORS Middleware headers as per spec.
Addresses issues outlined on https://github.com/litestar-org/litestar/issues/3178

.. change:: sending empty data in sse in js client
:type: bugfix
:pr: 3176

Fix an issue with SSE where JavaScript clients fail to receive an event without data.
The `spec <https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream>`_ is not clear in whether or not an event without data is ok.
Considering the EventSource "client" is not ok with it, and that it's so easy DX-wise to make the mistake not explicitly sending it, this change fixes it by defaulting to the empty-string

.. change:: Support ``ResponseSpec(..., examples=[...])``
:type: feature
:pr: 3100
:issue: 3068

Allow defining custom examples for the responses via ``ResponseSpec``.
The examples set this way are always generated locally, for each response:
Examples that go within the schema definition cannot be set by this.

.. code-block:: json
{
"paths": {
"/": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {},
"examples": "..."}}
}}
}}
}
}
.. change:: support "+json"-suffixed response media types
:type: feature
:pr: 3096
:issue: 3088

Automatically encode responses with media type of the form "application/<something>+json" as json.

.. change:: Allow reusable ``Router`` instances
:type: feature
:pr: 3103
:issue: 3012

It was not possible to re-attach a router instance once it was attached. This
makes that possible.

The router instance now gets deecopied when it's registered to another router.

The application startup performance gets a hit here, but the same approach is
already used for controllers and handlers, so this only harmonizes the
implementation.

.. change:: only display path in ``ValidationException``\ s
:type: feature
:pr: 3064
:issue: 3061

Fix an issue where ``ValidationException`` exposes the full URL in the error response, leaking internal IP(s) or other similar infra related information.

.. change:: expose ``request_class`` to other layers
:type: feature
:pr: 3125

Expose ``request_class`` to other layers

.. change:: expose ``websocket_class``
:type: feature
:pr: 3152

Expose ``websocket_class`` to other layers

.. change:: Add ``type_decoders`` to Router and route handlers
:type: feature
:pr: 3153

Add ``type_decoders`` to ``__init__`` method for handler, routers and decorators to keep consistency with ``type_encoders`` parameter

.. change:: Pass ``type_decoders`` in ``WebsocketListenerRouteHandler``
:type: feature
:pr: 3162

Pass ``type_decoders`` to parent's ``__init__`` in ``WebsocketListenerRouteHandler`` init, otherwise ``type_decoders`` will be ``None``
replace params order in docs, ``__init__`` (`decoders` before `encoders`)

.. change:: 3116 enhancement session middleware
:type: feature
:pr: 3127
:issue: 3116

For server side sessions, the session id is now generated before the route handler. Thus, on first visit, a session id will be available inside the route handler's scope instead of afterwards
A new abstract method ``get_session_id`` was added to ``BaseSessionBackend`` since this method will be called for both ClientSideSessions and ServerSideSessions. Only for ServerSideSessions it will return an actual id.
Using ``request.set_session(...)`` will return the session id for ServerSideSessions and None for ClientSideSessions
The session auth MiddlewareWrapper now refers to the Session Middleware via the configured backend, instead of it being hardcoded

.. change:: make random seed for openapi example generation configurable
:type: feature
:pr: 3166

Allow random seed used for generating the examples in the OpenAPI schema (when ``create_examples`` is set to ``True``) to be configured by the user.
This is related to https://github.com/litestar-org/litestar/issues/3059 however whether this change is enough to close that issue or not is not confirmed.

.. change:: generate openapi components schemas in a deterministic order
:type: feature
:pr: 3172

Ensure that the insertion into the ``Components.schemas`` dictionary of the OpenAPI spec will be in alphabetical order (based on the normalized name of the ``Schema``).


.. changelog:: 2.6.3
:date: 2024-03-04

Expand Down
22 changes: 22 additions & 0 deletions docs/usage/requests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,25 @@ for ``Body`` , by using :class:`RequestEncodingType.MESSAGEPACK <.enums.RequestE
.. literalinclude:: /examples/request_data/msgpack_request.py
:caption: msgpack_request.py
:language: python


Custom Request
--------------

.. versionadded:: 2.7.0

Litestar supports custom ``request_class`` instances, which can be used to further configure the default :class:`Request`.
The example below illustrates how to implement custom request class for the whole application.

.. dropdown:: Example of a custom request at the application level

.. literalinclude:: /examples/request_data/custom_request.py
:language: python

.. admonition:: Layered architecture

Request classes are part of Litestar's layered architecture, which means you can
set a request class on every layer of the application. If you have set a request
class on multiple layers, the layer closest to the route handler will take precedence.

You can read more about this in the :ref:`usage/applications:layered architecture` section
11 changes: 11 additions & 0 deletions docs/usage/responses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ As previously mentioned, the default ``media_type`` is ``MediaType.JSON``. which
If you need to return other values and would like to extend serialization you can do
this :ref:`custom responses <usage/responses:Custom Responses>`.

You can also set an application media type string with the ``+json`` suffix
defined in `RFC 6839 <https://datatracker.ietf.org/doc/html/rfc6839#section-3.1>`_
as the ``media_type`` and it will be recognized and serialized as json.
For example, you can use ``application/problem+json``
(see `RFC 7807 <https://datatracker.ietf.org/doc/html/rfc7807#section-6.1>`_)
and it will work just like json but have the appropriate content-type header
and show up in the generated OpenAPI schema.

.. literalinclude:: /examples/responses/json_suffix_responses.py
:language: python

MessagePack responses
+++++++++++++++++++++

Expand Down
22 changes: 22 additions & 0 deletions docs/usage/websockets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,25 @@ encapsulate more complex logic.

.. literalinclude:: /examples/websockets/listener_class_based_async.py
:language: python


Custom WebSocket
----------------

.. versionadded:: 2.7.0

Litestar supports custom ``websocket_class`` instances, which can be used to further configure the default :class:`WebSocket`.
The example below illustrates how to implement custom websocket class for the whole application.

.. dropdown:: Example of a custom websocket at the application level

.. literalinclude:: /examples/websockets/custom_websocket.py
:language: python

.. admonition:: Layered architecture

WebSocket classes are part of Litestar's layered architecture, which means you can
set a websocket class on every layer of the application. If you have set a webscoket
class on multiple layers, the layer closest to the route handler will take precedence.

You can read more about this in the :ref:`usage/applications:layered architecture` section
2 changes: 1 addition & 1 deletion litestar/_asgi/routing_trie/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def build_route_middleware_stack(
handler, kwargs = cast("tuple[Any, dict[str, Any]]", middleware)
asgi_handler = handler(app=asgi_handler, **kwargs)
else:
asgi_handler = middleware(app=asgi_handler) # type: ignore
asgi_handler = middleware(app=asgi_handler) # type: ignore[call-arg]

# we wrap the entire stack again in ExceptionHandlerMiddleware
return wrap_in_exception_handler(
Expand Down
9 changes: 7 additions & 2 deletions litestar/_kwargs/extractors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from litestar.datastructures import Headers
from litestar.datastructures.upload_file import UploadFile
from litestar.datastructures.url import URL
from litestar.enums import ParamType, RequestEncodingType
from litestar.exceptions import ValidationException
from litestar.params import BodyKwarg
Expand Down Expand Up @@ -106,8 +107,12 @@ def extractor(values: dict[str, Any], connection: ASGIConnection) -> None:
values.update(connection_mapping)
except KeyError as e:
param = alias_to_params[e.args[0]]
path = URL.from_components(
path=connection.url.path,
query=connection.url.query,
)
raise ValidationException(
f"Missing required {param.param_type.value} parameter {param.field_alias!r} for url {connection.url}"
f"Missing required {param.param_type.value} parameter {param.field_alias!r} for path {path}"
) from e

return extractor
Expand All @@ -130,7 +135,7 @@ def create_query_default_dict(

for k, v in parsed_query:
if k in sequence_query_parameter_names:
output[k].append(v) # type: ignore
output[k].append(v) # type: ignore[union-attr]
else:
output[k] = v

Expand Down
2 changes: 1 addition & 1 deletion litestar/_kwargs/kwargs_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def _create_extractors(self) -> list[Callable[[dict[str, Any], ASGIConnection],
"headers": headers_extractor,
"cookies": cookies_extractor,
"query": query_extractor,
"body": body_extractor, # type: ignore
"body": body_extractor, # type: ignore[dict-item]
}

extractors: list[Callable[[dict[str, Any], ASGIConnection], None]] = [
Expand Down
3 changes: 2 additions & 1 deletion litestar/_openapi/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ def generate_components_schemas(self) -> dict[str, Schema]:
self.set_reference_paths(name_, registered_schema)
components_schemas[name_] = registered_schema.schema

return components_schemas
# Sort them by name to ensure they're always generated in the same order.
return {name: components_schemas[name] for name in sorted(components_schemas.keys())}


class OpenAPIContext:
Expand Down
11 changes: 9 additions & 2 deletions litestar/_openapi/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ def __init__(self, app: Litestar) -> None:
self._openapi_schema: OpenAPI | None = None

def _build_openapi_schema(self) -> OpenAPI:
openapi = self.openapi_config.to_openapi_schema()
context = OpenAPIContext(openapi_config=self.openapi_config, plugins=self.app.plugins.openapi)
openapi_config = self.openapi_config

if openapi_config.create_examples:
from litestar._openapi.schema_generation.examples import ExampleFactory

ExampleFactory.seed_random(openapi_config.random_seed)

openapi = openapi_config.to_openapi_schema()
context = OpenAPIContext(openapi_config=openapi_config, plugins=self.app.plugins.openapi)
openapi.paths = {
route.path_format or "/": create_path_item_for_route(context, route)
for route in self.included_routes.values()
Expand Down
Loading

0 comments on commit d292aac

Please sign in to comment.