diff --git a/docs/_static/style.css b/docs/_static/style.css
index ed74eb4754..86ab5bdd46 100644
--- a/docs/_static/style.css
+++ b/docs/_static/style.css
@@ -25,9 +25,9 @@
color: #f55353;
}
-p {
- font-size: 1.1em;
-}
+/*p {*/
+/* font-size: 1.1em;*/
+/*}*/
html[data-theme="dark"] .mermaid svg {
background-color: white;
diff --git a/docs/_static/tables/framework-comparison.csv b/docs/_static/tables/framework-comparison.csv
new file mode 100644
index 0000000000..1039f25292
--- /dev/null
+++ b/docs/_static/tables/framework-comparison.csv
@@ -0,0 +1,16 @@
+Feature,Litestar,FastAPI,Starlette,Sanic,Quart
+OpenAPI,:octicon:`check`,:octicon:`check`,-,-,-
+Automatic API documentation,"Swagger, ReDoc, Stoplight Elements","Swagger, ReDoc",-,-,-
+Data validation,:octicon:`check`,:octicon:`check`,-,-,-
+Dependency Injection,:octicon:`check`,:octicon:`check`,-,:octicon:`check`,-
+Class based routing,:octicon:`check`,Extension,:octicon:`check`,:octicon:`check`,:octicon:`check`
+ORM integration,"SQLAlchemy, Tortoise, Piccolo",-,-,-,Extension
+Templating,"Jinja, Mako",Jinja,Jinja,Jinja,Jinja
+MessagePack,:octicon:`check`,-,-,-,-
+CORS,:octicon:`check`,:octicon:`check`,:octicon:`check`,:octicon:`check`,Extension
+CSRF,:octicon:`check`,-,-,-,-
+Rate-limiting,:octicon:`check`,-,-,Extension,-
+JWT,:octicon:`check`,-,-,-,-
+Sessions,:octicon:`check`,Client-side,Client-side,-,Client-side
+Authentication,JWT / Session based,-,-,-,-
+Caching,:octicon:`check`,-,-,-,-
diff --git a/docs/_static/versioning.js b/docs/_static/versioning.js
index 819a83e92b..d02fa5e282 100644
--- a/docs/_static/versioning.js
+++ b/docs/_static/versioning.js
@@ -21,13 +21,15 @@ const addVersionWarning = (currentVersion, latestVersion) => {
const container = document.createElement("div");
container.id = "version-warning";
- const warningText = document.createElement("span");
- warningText.textContent = `You are viewing the documentation for ${
- currentVersion === "dev" ||
- parseInt(currentVersion) > parseInt(latestVersion)
- ? "a preview"
- : "an outdated"
- } version of Litestar.`;
+ const isPreviewVersion = (version) => {
+ const previewVersions = ['dev', 'develop', 'main', '3-dev'];
+ return previewVersions.includes(version) || parseInt(version) > parseInt(latestVersion);
+ };
+
+ const warningText = document.createElement("span");
+ warningText.textContent = `You are viewing the documentation for ${
+ isPreviewVersion(currentVersion) ? "a preview" : "an outdated"
+ } version of Litestar.`;
container.appendChild(warningText);
const latestLink = document.createElement("a");
diff --git a/docs/conf.py b/docs/conf.py
index bcfa4aadd7..e3357bd727 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -4,6 +4,7 @@
import os
import re
import warnings
+from datetime import datetime
from functools import partial
from typing import Any
@@ -22,10 +23,16 @@
PY_OBJ = "py:obj"
PY_FUNC = "py:func"
+current_year = datetime.now().year
project = "Litestar"
-copyright = "2024, Litestar-Org"
-author = "Litestar-Org"
+copyright = f"{current_year}, Litestar Organization"
+author = "Litestar Organization"
release = os.getenv("_LITESTAR_DOCS_BUILD_VERSION", importlib.metadata.version("litestar").rsplit(".")[0])
+environment = os.getenv("_LITESTAR_DOCS_BUILD_ENVIRONMENT", "local")
+
+rst_epilog = f"""
+.. |version| replace:: {release}
+"""
extensions = [
"sphinx.ext.intersphinx",
@@ -39,6 +46,7 @@
"sphinxcontrib.mermaid",
"sphinx_click",
"sphinx_paramlinks",
+ "sphinx_togglebutton",
]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
@@ -130,7 +138,6 @@
(PY_CLASS, "litestar.template.base.TemplateType_co"),
(PY_CLASS, "litestar.template.base.ContextType_co"),
(PY_CLASS, "litestar.template.base.R"),
- (PY_ATTR, "litestar.openapi.controller.OpenAPIController.swagger_ui_init_oauth"),
# intentionally undocumented
(PY_CLASS, "BacklogStrategy"),
(PY_CLASS, "ExceptionT"),
@@ -175,6 +182,8 @@
("py:exc", "HTTPExceptions"),
(PY_CLASS, "litestar.template.Template"),
(PY_CLASS, "litestar.middleware.compression.gzip_facade.GzipCompression"),
+ (PY_CLASS, "litestar.openapi.OpenAPIController"),
+ (PY_CLASS, "openapi.controller.OpenAPIController"),
(PY_CLASS, "litestar.handlers.http_handlers.decorators._subclass_warning"),
(PY_CLASS, "litestar.background_tasks.P"),
(PY_CLASS, "P.args"),
@@ -219,6 +228,7 @@
(PY_RE, r"advanced_alchemy\.config.common\.EngineT"),
(PY_RE, r"advanced_alchemy\.config.common\.SessionT"),
(PY_RE, r".*R"),
+ (PY_RE, r".*ScopeT"),
(PY_OBJ, r"litestar.security.jwt.auth.TokenT"),
(PY_CLASS, "ExceptionToProblemDetailMapType"),
(PY_CLASS, "litestar.security.jwt.token.JWTDecodeOptions"),
@@ -232,7 +242,6 @@
# No idea what autodoc is doing here. Possibly unfixable on our end
"litestar.template.base.TemplateEngineProtocol.get_template": {"litestar.template.base.T_co"},
"litestar.template": {"litestar.template.base.T_co"},
- "litestar.openapi.OpenAPIController.security": {"SecurityRequirement"},
"litestar.response.file.async_file_iterator": {"FileSystemAdapter"},
re.compile("litestar.response.redirect.*"): {"RedirectStatusType"},
re.compile(r"litestar\.plugins.*"): re.compile(".*ModelT"),
@@ -256,7 +265,7 @@
]
auto_pytabs_min_version = (3, 8)
-auto_pytabs_max_version = (3, 11)
+auto_pytabs_max_version = (3, 12)
auto_pytabs_compat_mode = True
autosectionlabel_prefix_document = True
@@ -266,78 +275,134 @@
"ref.python", # TODO: remove when https://github.com/sphinx-doc/sphinx/issues/4961 is fixed
]
+# -- Style configuration -----------------------------------------------------
html_theme = "litestar_sphinx_theme"
+html_title = "Litestar Framework"
+pygments_style = "lightbulb"
+
html_static_path = ["_static"]
+templates_path = ["_templates"]
html_js_files = ["versioning.js"]
html_css_files = ["style.css"]
-html_show_sourcelink = False
-html_title = "Litestar Framework"
+
+html_show_sourcelink = True # TODO: this doesn't work :(
+html_copy_source = True
+
+html_context = {
+ "source_type": "github",
+ "source_user": "litestar-org",
+ "source_repo": "litestar",
+ # "source_version": "main", # TODO: We should set this with an envvar depending on which branch we are building?
+ "current_version": "latest", # TODO: Version dropdown only show caret and now text
+ "versions": [ # TODO(provinzkraut): this needs to use versions.json but im not 100% on how to do this yet
+ ("latest", "/latest"),
+ ("development", "/main"),
+ ("v3", "/3-dev"),
+ ("v2", "/2"),
+ ("v1", "/1"),
+ ],
+ "version": release,
+}
html_theme_options = {
- "use_page_nav": False,
+ "logo_target": "/",
"github_repo_name": "litestar",
- "logo": {
- "link": "https://litestar.dev",
- },
- "pygment_light_style": "xcode",
- "pygment_dark_style": "lightbulb",
"navigation_with_keys": True,
- "extra_navbar_items": {
- "Documentation": "index",
- "Community": {
- "Contributing": {
- "description": "Learn how to contribute to the Litestar project",
- "link": "https://docs.litestar.dev/latest/contribution-guide.html",
- "icon": "contributing",
- },
- "Code of Conduct": {
- "description": "Review the etiquette for interacting with the Litestar community",
- "link": "https://github.com/litestar-org/.github?tab=coc-ov-file",
- "icon": "coc",
- },
- "Security": {
- "description": "Overview of Litestar's security protocols",
- "link": "https://github.com/litestar-org/.github?tab=coc-ov-file#security-ov-file",
- "icon": "coc",
- },
+ "nav_links": [ # TODO(provinzkraut): I need a guide on extra_navbar_items and its magic :P
+ {"title": "Home", "url": "index"},
+ {
+ "title": "Community",
+ "children": [
+ {
+ "title": "Contributing",
+ "summary": "Learn how to contribute to the Litestar project",
+ "url": "contribution-guide",
+ "icon": "contributing",
+ },
+ {
+ "title": "Code of Conduct",
+ "summary": "Review the etiquette for interacting with the Litestar community",
+ "url": "https://github.com/litestar-org/.github?tab=coc-ov-file",
+ "icon": "coc",
+ },
+ {
+ "title": "Security",
+ "summary": "Overview of Litestar's security protocols",
+ "url": "https://github.com/litestar-org/.github?tab=coc-ov-file#security-ov-file",
+ "icon": "coc",
+ },
+ ],
},
- "About": {
- "Litestar Organization": {
- "description": "Details about the Litestar organization",
- "link": "https://litestar.dev/about/organization",
- "icon": "org",
- },
- "Releases": {
- "description": "Explore the release process, versioning, and deprecation policy for Litestar",
- "link": "https://litestar.dev/about/litestar-releases",
- "icon": "releases",
- },
+ {
+ "title": "About",
+ "children": [
+ {
+ "title": "Litestar Organization",
+ "summary": "Details about the Litestar organization",
+ "url": "https://litestar.dev/about/organization",
+ "icon": "org",
+ },
+ {
+ "title": "Releases",
+ "summary": "Explore the release process, versioning, and deprecation policy for Litestar",
+ "url": "https://litestar.dev/about/litestar-releases",
+ "icon": "releases",
+ },
+ ],
},
- "Release notes": {
- "What's new in 2.0": "release-notes/whats-new-2",
- "2.x Changelog": "https://docs.litestar.dev/2/release-notes/changelog.html",
- "1.x Changelog": "https://docs.litestar.dev/1/release-notes/changelog.html",
+ {
+ "title": "Release notes",
+ "children": [
+ {
+ "title": "What's new in 3.0",
+ "url": "release-notes/whats-new-3",
+ "summary": "Explore the new features in Litestar 3.0",
+ },
+ {
+ "title": "3.x Changelog",
+ "url": "release-notes/changelog",
+ "summary": "All changes in the 3.x series",
+ },
+ {
+ "title": "2.x Changelog",
+ "url": "https://docs.litestar.dev/2/release-notes/changelog.html",
+ "summary": "All changes in the 2.x series",
+ },
+ ],
},
- "Help": {
- "Discord Help Forum": {
- "description": "Dedicated Discord help forum",
- "link": "https://discord.gg/litestar",
- "icon": "coc",
- },
- "GitHub Discussions": {
- "description": "GitHub Discussions ",
- "link": "https://github.com/orgs/litestar-org/discussions",
- "icon": "coc",
- },
- "Stack Overflow": {
- "description": "We monitor the litestar tag on Stack Overflow",
- "link": "https://stackoverflow.com/questions/tagged/litestar",
- "icon": "coc",
- },
+ {
+ "title": "Help",
+ "children": [
+ {
+ "title": "Discord Help Forum",
+ "summary": "Dedicated Discord help forum",
+ "url": "https://discord.gg/litestar",
+ "icon": "coc",
+ },
+ {
+ "title": "GitHub Discussions",
+ "summary": "GitHub Discussions",
+ "url": "https://github.com/orgs/litestar-org/discussions",
+ "icon": "coc",
+ },
+ {
+ "title": "Stack Overflow",
+ "summary": "We monitor the litestar tag on Stack Overflow",
+ "url": "https://stackoverflow.com/questions/tagged/litestar",
+ "icon": "coc",
+ },
+ ],
},
- },
+ {"title": "Sponsor", "url": "https://github.com/sponsors/Litestar-Org", "icon": "heart"},
+ ],
}
+if environment != "latest": # TODO(provinzkraut): it'd be awesome to be able to use the builtin announcement banner
+ html_theme_options["announcement"] = (
+ f"You are viewing the {environment} version of the documentation. "
+ f"Click here to go to the latest version."
+ )
+
def update_html_context(
app: Sphinx, pagename: str, templatename: str, context: dict[str, Any], doctree: document
@@ -347,20 +412,18 @@ def update_html_context(
def delayed_setup(app: Sphinx) -> None:
"""
- When running linkcheck pydata_sphinx_theme causes a build failure, and checking
+ When running linkcheck Shibuya causes a build failure, and checking
the builder in the initial `setup` function call is not possible, so the check
and extension setup has to be delayed until the builder is initialized.
"""
if app.builder.name == "linkcheck":
return
- app.setup_extension("pydata_sphinx_theme")
- app.connect("html-page-context", update_html_context) # type: ignore
+ app.setup_extension("shibuya")
+ # app.connect("html-page-context", update_html_context) # TODO(provinkraut): fix
def setup(app: Sphinx) -> dict[str, bool]:
app.connect("builder-inited", delayed_setup, priority=0) # type: ignore
-
app.setup_extension("litestar_sphinx_theme")
-
return {"parallel_read_safe": True, "parallel_write_safe": True}
diff --git a/docs/examples/pagination/using_cursor_pagination.py b/docs/examples/pagination/using_cursor_pagination.py
index 289103c944..986ea320d3 100644
--- a/docs/examples/pagination/using_cursor_pagination.py
+++ b/docs/examples/pagination/using_cursor_pagination.py
@@ -34,7 +34,7 @@ def get_items(self, cursor: Optional[str], results_per_page: int) -> Tuple[List[
# we now create a regular handler. The handler will receive a single query parameter - 'cursor', which
# we will pass to the paginator.
@get("/people", sync_to_thread=False)
-def people_handler(cursor: Optional[str], results_per_page: int) -> CursorPagination[str, Person]:
+def people_handler(*, cursor: Optional[str] = None, results_per_page: int) -> CursorPagination[str, Person]:
return paginator(cursor=cursor, results_per_page=results_per_page)
diff --git a/docs/examples/static_files/upgrade_from_static_1.py b/docs/examples/static_files/upgrade_from_static_1.py
deleted file mode 100644
index ad0b8aa61d..0000000000
--- a/docs/examples/static_files/upgrade_from_static_1.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from litestar import Litestar
-from litestar.static_files.config import StaticFilesConfig
-
-app = Litestar(
- static_files_config=[
- StaticFilesConfig(directories=["assets"], path="/static"),
- ],
-)
diff --git a/docs/examples/static_files/upgrade_from_static_2.py b/docs/examples/static_files/upgrade_from_static_2.py
deleted file mode 100644
index af4578f333..0000000000
--- a/docs/examples/static_files/upgrade_from_static_2.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from litestar import Litestar
-from litestar.static_files import create_static_files_router
-
-app = Litestar(
- route_handlers=[
- create_static_files_router(directories=["assets"], path="/static"),
- ],
-)
diff --git a/docs/getting-started.rst b/docs/getting-started.rst
new file mode 100644
index 0000000000..ade87c2742
--- /dev/null
+++ b/docs/getting-started.rst
@@ -0,0 +1,259 @@
+===============
+Getting Started
+===============
+
+Installation
+------------
+
+.. code-block:: shell
+
+ pip install litestar
+
+.. tip:: ``litestar[standard]`` includes commonly used extras like ``uvicorn`` and ``jinja2`` (for templating).
+
+.. dropdown:: Extras
+ :icon: star
+
+ `Pydantic `_
+ :code:`pip install litestar[pydantic]`
+
+ `Attrs `_
+ :code:`pip install litestar[attrs]`
+
+ :ref:`Brotli Compression Middleware `:
+ :code:`pip install litestar[brotli]`
+
+ :ref:`Cookie Based Sessions `
+ :code:`pip install litestar[cryptography]`
+
+ :doc:`JWT `
+ :code:`pip install litestar[jwt]`
+
+ :doc:`RedisStore `
+ :code:`pip install litestar[redis]`
+
+ :ref:`Picologging `
+ :code:`pip install litestar[picologging]`
+
+ :ref:`StructLog `
+ :code:`pip install litestar[structlog]`
+
+ :doc:`Prometheus Instrumentation `
+ :code:`pip install litestar[prometheus]`
+
+ :doc:`Open Telemetry Instrumentation `
+ :code:`pip install litestar[opentelemetry]`
+
+ :doc:`SQLAlchemy `
+ :code:`pip install litestar[sqlalchemy]`
+
+ :doc:`CLI `
+ .. deprecated:: 2.1.1
+ The ``litestar`` base installation now includes the CLI dependencies and this group is no longer required
+ to use the CLI.
+ If you need the optional CLI dependencies, install the ``standard`` group instead.
+ **Will be removed in 3.0**
+
+ :code:`pip install litestar[cli]`
+
+ :doc:`Jinja Templating `
+ :code:`pip install litestar[jinja]`
+
+ :doc:`Mako Templating `
+ :code:`pip install litestar[mako]`
+
+ Standard Installation (includes Uvicorn and Jinja2 templating):
+ :code:`pip install litestar[standard]`
+
+ All Extras:
+ :code:`pip install litestar[full]`
+
+ .. note:: The full extras is not recommended because it will add a lot of unnecessary extras.
+
+.. _minimal_example:
+
+Minimal Example
+---------------
+
+At a minimum, make sure you have installed ``litestar[standard]``, which includes uvicorn.
+
+First, create a file named ``app.py`` with the following contents:
+
+.. code-block:: python
+ :caption: This is a caption
+
+ from litestar import Litestar, get
+
+
+ @get("/")
+ async def index() -> str:
+ return "Hello, world!"
+
+
+ @get("/books/{book_id:int}")
+ async def get_book(book_id: int) -> dict[str, int]:
+ return {"book_id": book_id}
+
+
+ app = Litestar([index, get_book])
+
+Then, run the following command:
+
+.. code-block:: shell
+
+ litestar run
+ # Or you can run Uvicorn directly:
+ uvicorn app:app --reload
+
+You can now visit ``http://localhost:8000/`` and ``http://localhost:8000/books/1`` in your browser and
+you should see the responses of your two endpoints:
+
+.. code-block:: text
+
+ "Hello, world!"
+
+and
+
+.. code-block:: json
+
+ {"book_id": 1}
+
+.. tip:: You can also check out the automatically generated OpenAPI-based documentation at:
+
+ * ``http://localhost:8000/schema`` (for `ReDoc `_),
+ * ``http://localhost:8000/schema/swagger`` (for `Swagger UI `_),
+ * ``http://localhost:8000/schema/elements`` (for `Stoplight Elements `_)
+ * ``http://localhost:8000/schema/rapidoc`` (for `RapiDoc `_)
+
+You can check out a more in-depth tutorial in the :doc:`/tutorials/todo-app/index` section!
+
+Expanded Example
+----------------
+
+**Define your data model** using pydantic or any library based on it (for example ormar, beanie, SQLModel):
+
+.. code-block:: python
+
+ from pydantic import BaseModel, UUID4
+
+
+ class User(BaseModel):
+ first_name: str
+ last_name: str
+ id: UUID4
+
+
+
+
+You can also use dataclasses (standard library and Pydantic),
+:class:`typing.TypedDict`, or :class:`msgspec.Struct`.
+
+.. code-block:: python
+
+ from uuid import UUID
+
+ from dataclasses import dataclass
+ from litestar.dto import DTOConfig, DataclassDTO
+
+
+ @dataclass
+ class User:
+ first_name: str
+ last_name: str
+ id: UUID
+
+
+ class PartialUserDTO(DataclassDTO[User]):
+ config = DTOConfig(exclude={"id"}, partial=True)
+
+**Define a Controller for your data model:**
+
+.. code-block:: python
+
+ from typing import List
+
+ from litestar import Controller, get, post, put, patch, delete
+ from litestar.dto import DTOData
+ from pydantic import UUID4
+
+ from my_app.models import User, PartialUserDTO
+
+
+ class UserController(Controller):
+ path = "/users"
+
+ @post()
+ async def create_user(self, data: User) -> User: ...
+
+ @get()
+ async def list_users(self) -> List[User]: ...
+
+ @patch(path="/{user_id:uuid}", dto=PartialUserDTO)
+ async def partial_update_user(
+ self, user_id: UUID4, data: DTOData[User]
+ ) -> User: ...
+
+ @put(path="/{user_id:uuid}")
+ async def update_user(self, user_id: UUID4, data: User) -> User: ...
+
+ @get(path="/{user_id:uuid}")
+ async def get_user(self, user_id: UUID4) -> User: ...
+
+ @delete(path="/{user_id:uuid}")
+ async def delete_user(self, user_id: UUID4) -> None: ...
+
+
+When instantiating your app, import your *controller* into your application's
+entry-point and pass it to Litestar:
+
+.. code-block:: python
+
+ from litestar import Litestar
+
+ from my_app.controllers.user import UserController
+
+ app = Litestar(route_handlers=[UserController])
+
+To **run your application**, use an ASGI server such as `uvicorn `_ :
+
+.. code-block:: shell
+
+ uvicorn my_app.main:app --reload
+
+
+Philosophy
+----------
+
+- Litestar is a community-driven project. This means not a single author,
+ but rather a core team of maintainers is leading the project, supported by a community
+ of contributors. Litestar currently has 5 maintainers and is being very actively developed.
+- Litestar draws inspiration from `NestJS `_ - a contemporary TypeScript framework - which places
+ opinions and patterns at its core.
+- While still allowing for **function-based endpoints**, Litestar seeks to build on Python's powerful and versatile
+ OOP, by placing **class-based controllers** at its core.
+- Litestar is **not** a microframework. Unlike frameworks such as FastAPI, Starlette, or Flask, Litestar includes a lot
+ of functionalities out of the box needed for a typical modern web application, such as ORM integration,
+ client- and server-side sessions, caching, OpenTelemetry integration, and many more. It's not aiming to be "the next
+ Django" (for example, it will never feature its own ORM), but its scope is not micro either.
+
+
+Feature comparison with similar frameworks
+------------------------------------------
+
+.. csv-table:: Litestar vs. other frameworks
+ :file: _static/tables/framework-comparison.csv
+ :widths: 5, 35, 15, 15, 15, 15
+ :header-rows: 1
+
+Example Applications
+--------------------
+
+* `litestar-pg-redis-docker `_ : In addition to Litestar, this
+ demonstrates a pattern of application modularity, SQLAlchemy 2.0 ORM, Redis cache connectivity, and more. Like all
+ Litestar projects, this application is open to contributions, big and small.
+* `litestar-fullstack `_ : A fully-capable, production-ready fullstack
+ Litestar web application configured with best practices. It includes SQLAlchemy 2.0, ReactJS, `Vite `_,
+ `SAQ job queue `_, ``Jinja`` templates and more.
+ `Read more `_.
+* `litestar-hello-world `_: A bare-minimum application setup.
+ Great for testing and POC work.
diff --git a/docs/release-notes/2.x-changelog.rst b/docs/release-notes/2.x-changelog.rst
new file mode 100644
index 0000000000..3bd45b7a6c
--- /dev/null
+++ b/docs/release-notes/2.x-changelog.rst
@@ -0,0 +1,4286 @@
+:orphan:
+
+2.x Changelog
+=============
+
+.. changelog:: 2.7.1
+ :date: 2024-03-22
+
+ .. change:: add default encoders for `Enums` and `EnumMeta`
+ :type: bugfix
+ :pr: 3193
+
+ This addresses an issue when serializing ``Enums`` that was reported in discord.
+
+ .. change:: replace TestClient.__enter__ return type with Self
+ :type: bugfix
+ :pr: 3194
+
+ ``TestClient.__enter__`` and ``AsyncTestClient.__enter__`` return ``Self``.
+ If you inherit ``TestClient``, its ``__enter__`` method should return derived class's instance
+ unless override the method. ``Self`` is a more flexible return type.
+
+ .. change:: use the full path for fetching openapi.json
+ :type: bugfix
+ :pr: 3196
+ :issue: 3047
+
+ This specifies the ``spec-url`` and ``apiDescriptionUrl`` of Rapidoc, and Stoplight Elements as absolute
+ paths relative to the root of the site.
+
+ This ensures that both of the send the request for the JSON of the OpenAPI schema to the right endpoint.
+
+ .. change:: JSON schema ``examples`` were OpenAPI formatted
+ :type: bugfix
+ :pr: 3224
+ :issue: 2849
+
+ The generated ``examples`` in *JSON schema* objects were formatted as:
+
+ .. code-block:: json
+
+ "examples": {
+ "some-id": {
+ "description": "Lorem ipsum",
+ "value": "the real beef"
+ }
+ }
+
+ However, above is OpenAPI example format, and must not be used in JSON schema
+ objects. Schema objects follow different formatting:
+
+ .. code-block:: json
+
+ "examples": [
+ "the real beef"
+ ]
+
+ * Explained in `APIs You Won't Hate blog post `_.
+ * `Schema objects spec `_
+ * `OpenAPI example format spec `_.
+
+ This is referenced at least from parameters, media types and components.
+
+ The technical change here is to define ``Schema.examples`` as ``list[Any]`` instead
+ of ``list[Example]``. Examples can and must still be defined as ``list[Example]``
+ for OpenAPI objects (e.g. ``Parameter``, ``Body``) but for JSON schema ``examples``
+ the code now internally generates/converts ``list[Any]`` format instead.
+
+ Extra confusion here comes from the OpenAPI 3.0 vs OpenAPI 3.1 difference.
+ OpenAPI 3.0 only allowed ``example`` (singular) field in schema objects.
+ OpenAPI 3.1 supports the full JSON schema 2020-12 spec and so ``examples`` array
+ in schema objects.
+
+ Both ``example`` and ``examples`` seem to be supported, though the former is marked
+ as deprecated in the latest specs.
+
+ This can be tested over at https://editor-next.swagger.io by loading up the
+ OpenAPI 3.1 Pet store example. Then add ``examples`` in ``components.schemas.Pet``
+ using the both ways and see the Swagger UI only render the example once it's
+ properly formatted (it ignores is otherwise).
+
+ .. change:: queue_listener handler for Python >= 3.12
+ :type: bugfix
+ :pr: 3185
+ :issue: 2954
+
+ - Fix the ``queue_listener`` handler for Python 3.12
+
+ Python 3.12 introduced a new way to configure ``QueueHandler`` and ``QueueListener`` via
+ ``logging.config.dictConfig()``. As described in the
+ `logging documentation `_.
+
+ The listener still needs to be started & stopped, as previously.
+ To do so, we've introduced ``LoggingQueueListener``.
+
+ And as stated in the doc:
+ * Any custom queue handler and listener classes will need to be defined with the same initialization signatures
+ as `QueueHandler `_ and
+ `QueueListener `_.
+
+ .. change:: extend openapi meta collected from domain models
+ :type: bugfix
+ :pr: 3237
+ :issue: 3232
+
+ :class:`~litestar.typing.FieldDefinition` s pack any OpenAPI metadata onto a ``KwargDefinition`` instance when
+ types are parsed from domain models.
+
+ When we produce a DTO type, we transfer this meta from the `KwargDefinition` to a `msgspec.Meta` instance,
+ however so far this has only included constraints, not attributes such as descriptions, examples and title.
+
+ This change ensures that we transfer the openapi meta for the complete intersection of fields that exist on b
+ oth `KwargDefinition` and `Meta`.
+
+ .. change:: kwarg ambiguity exc msg for path params
+ :type: bugfix
+ :pr: 3261
+
+ Fixes the way we construct the exception message when there is a kwarg ambiguity detected for path parameters.
+
+.. 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 `_ 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/+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 deepcopied 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
+
+ .. change:: Pydantic V1 schema generation for PrivateAttr in GenericModel
+ :type: bugfix
+ :pr: 3161
+ :issue: 3150
+
+ Fixes a bug that caused a ``NameError`` when a Pydantic V1 ``GenericModel`` has a private attribute of which the type annotation cannot be resolved at the time of schema generation.
+
+
+.. changelog:: 2.6.2
+ :date: 2024/03/02
+
+ .. change:: DTO msgspec meta constraints not being included in transfer model
+ :type: bugfix
+ :pr: 3113
+ :issue: 3026
+
+ Fix an issue where msgspec constraints set in ``msgspec.Meta`` would not be
+ honoured by the DTO.
+
+ In the given example, the ``min_length=3`` constraint would be ignored by the
+ model generated by ``MsgspecDTO``.
+
+ .. code-block:: python
+
+ from typing import Annotated
+
+ import msgspec
+ from litestar import post, Litestar
+ from litestar.dto import MsgspecDTO
+
+ class Request(msgspec.Struct):
+ foo: Annotated[str, msgspec.Meta(min_length=3)]
+
+ @post("/example/", dto=MsgspecDTO[Request])
+ async def example(data: Request) -> Request:
+ return data
+
+ Constraints like these are now transferred.
+
+ Two things to note are:
+
+ - For DTOs with ``DTOConfig(partial=True)`` we cannot transfer the length
+ constraints as they are only supported on fields that as subtypes of ``str``,
+ ``bytes`` or a collection type, but ``partial=True`` sets all fields as
+ ``T | UNSET``
+ - For the ``PiccoloDTO``, fields which are not required will also drop the
+ length constraints. A warning about this will be raised here.
+
+ .. change:: Missing control header for static files
+ :type: bugfix
+ :pr: 3131
+ :issue: 3129
+
+ Fix an issue where a ``cache_control`` that is set on a router created by
+ ``create_static_files_router`` wasn't passed to the generated handler
+
+ .. change:: Fix OpenAPI schema generation for Pydantic v2 constrained ``Secret`` types
+ :type: bugfix
+ :pr: 3149
+ :issue: 3148
+
+ Fix schema generation for ``pydantic.SecretStr`` and ``pydantic.SecretBytes``
+ which, when constrained, would not be recognised as such with Pydantic V2 since
+ they're not subtypes of their respective bases anymore.
+
+ .. change:: Fix OpenAPI schema generation for Pydantic private attributes
+ :type: bugfix
+ :pr: 3151
+ :issue: 3150
+
+ Fix a bug that caused a :exc:`NameError` when trying to resolve forward
+ references in Pydantic private fields.
+
+ Although private fields were respected excluded from the schema, it was still
+ attempted to extract their type annotation. This was fixed by not relying on
+ ``typing.get_type_hints`` to get the type information, but instead using
+ Pydantic's own APIs, allowing us to only extract information about the types of
+ relevant fields.
+
+ .. change:: OpenAPI description not set for UUID based path parameters in OpenAPI
+ :type: bugfix
+ :pr: 3118
+ :issue: 2967
+
+ Resolved a bug where the description was not set for UUID-based path
+ parameters in OpenAPI due to the reason mentioned in the issue.
+
+ .. change:: Fix ``RedisStore`` client created with ``with_client`` unclosed
+ :type: bugfix
+ :pr: 3111
+ :issue: 3083
+
+ Fix a bug where, when a :class:`~litestar.stores.redis.RedisStore` was created
+ with the :meth:`~litestar.stores.redis.RedisStore.with_client` method, that
+ client wasn't closed explicitly
+
+
+.. changelog:: 2.6.1
+ :date: 2024/02/14
+
+ .. change:: SQLAlchemy: Use `IntegrityError` instead of deprecated `ConflictError`
+ :type: bugfix
+ :pr: 3094
+
+ Updated the repository to return ``IntegrityError`` instead of the now
+ deprecated ``ConflictError``
+
+ .. change:: Remove usage of deprecated `static_files` property
+ :type: bugfix
+ :pr: 3087
+
+ Remove the usage of the deprecated ``Litestar.static_files_config`` in
+ ``Litestar.__init__``.
+
+ .. change:: Sessions: Fix cookie naming for short cookies
+ :type: bugfix
+ :pr: 3095
+ :issue: 3090
+
+ Previously, cookie names always had a suffix of the form ``"-{i}"`` appended to
+ them. With this change, the suffix is omitted if the cookie is short enough
+ (< 4 KB) to not be split into multiple chunks.
+
+ .. change:: Static files: Fix path resolution for windows
+ :type: bugfix
+ :pr: 3102
+
+ Fix an issue with the path resolution on Windows introduced in
+ https://github.com/litestar-org/litestar/pull/2960 that would lead to 404s
+
+ .. change:: Fix logging middleware with structlog causes application to return a ``500`` when request body is malformed
+ :type: bugfix
+ :pr: 3109
+ :issue: 3063
+
+ Gracefully handle malformed request bodies during parsing when using structlog;
+ Instead of erroring out and returning a ``500``, the raw body is now being used
+ when an error occurs during parsing
+
+ .. change:: OpenAPI: Generate correct response schema for ``ResponseSpec(None)``
+ :type: bugfix
+ :pr: 3098
+ :issue: 3069
+
+ Explicitly declaring ``responses={...: ResponseSpec(None)}`` used to generate
+ OpenAPI a ``content`` property, when it should be omitted.
+
+ .. change:: Prevent exception handlers from extracting details from non-Litestar exceptions
+ :type: bugfix
+ :pr: 3106
+ :issue: 3082
+
+ Fix a bug where exception classes that had a ``status_code`` attribute would be
+ treated as Litestar exceptions and details from them would be extracted and
+ added to the exception response.
+
+.. changelog:: 2.6.0
+ :date: 2024/02/06
+
+ .. change:: Enable disabling configuring ``root`` logger within ``LoggingConfig``
+ :type: feature
+ :pr: 2969
+
+ The option :attr:`~litestar.logging.config.LoggingConfig.configure_root_logger` was
+ added to :class:`~litestar.logging.config.LoggingConfig` attribute. It is enabled by
+ default to not implement a breaking change.
+
+ When set to ``False`` the ``root`` logger will not be modified for ``logging``
+ or ``picologging`` loggers.
+
+ .. change:: Simplified static file handling and enhancements
+ :type: feature
+ :pr: 2960
+ :issue: 2629
+
+ Static file serving has been implemented with regular route handlers instead of
+ a specialised ASGI app. At the moment, this is complementary to the usage of
+ `litestar.static_files.StaticFilesConfig`` to maintain backwards
+ compatibility.
+
+ This achieves a few things:
+
+ - Fixes https://github.com/litestar-org/litestar/issues/2629
+ - Circumvents special casing needed in the routing logic for the static files app
+ - Removes the need for a ``static_files_config`` attribute on the app
+ - Removes the need for a special ``litestar.app.Litestar.url_for_static_asset``
+ method on the app since `route_reverse` can be used instead
+
+ Additionally:
+
+ - Most router options can now be passed to the
+ :func:`~litestar.static_files.create_static_files_router`, allowing further
+ customisation
+ - A new ``resolve_symlinks`` flag has been added, defaulting to ``True`` to keep
+ backwards compatibility
+
+ **Usage**
+
+ Instead of
+
+ .. code-block:: python
+
+ app = Litestar(
+ static_files_config=[StaticFilesConfig(path="/static", directories=["some_dir"])]
+ )
+
+
+ You can now simply use
+
+ .. code-block:: python
+
+ app = Litestar(
+ route_handlers=[
+ create_static_files_router(path="/static", directories=["some_dir"])
+ ]
+ )
+
+ .. seealso::
+ :doc:`/usage/static-files`
+
+
+ .. change:: Exclude Piccolo ORM columns with ``secret=True`` from ``PydanticDTO`` output
+ :type: feature
+ :pr: 3030
+
+ For Piccolo columns with ``secret=True`` set, corresponding ``PydanticDTO``
+ attributes will be marked as ``WRITE_ONLY`` to prevent the column being included
+ in ``return_dto``
+
+
+ .. change:: Allow discovering registered plugins by their fully qualified name
+ :type: feature
+ :pr: 3027
+
+ `PluginRegistryPluginRegistry`` now supports retrieving a plugin by its fully
+ qualified name.
+
+
+ .. change:: Support externally typed classes as dependency providers
+ :type: feature
+ :pr: 3066
+ :issue: 2979
+
+ - Implement a new :class:`~litestar.plugins.DIPlugin` class that allows the
+ generation of signatures for arbitrary types where their signature cannot be
+ extracted from the type's ``__init__`` method
+ - Implement ``DIPlugin``\ s for Pydantic and Msgspec to allow using their
+ respective modelled types as dependency providers. These plugins will be
+ registered by default
+
+ .. change:: Add structlog plugin
+ :type: feature
+ :pr: 2943
+
+ A Structlog plugin to make it easier to configure structlog in a single place.
+
+ The plugin:
+
+ - Detects if a logger has ``setLevel`` before calling
+ - Set even message name to be init-cap
+ - Add ``set_level`` interface to config
+ - Allows structlog printer to detect if console is TTY enabled. If so, a
+ Struglog color formatter with Rich traceback printer is used
+ - Auto-configures stdlib logger to use the structlog logger
+
+ .. change:: Add reload-include and reload-exclude to CLI run command
+ :type: feature
+ :pr: 2973
+ :issue: 2875
+
+ The options ``reload-exclude`` and ``reload-include`` were added to the CLI
+ ``run`` command to explicitly in-/exclude specific paths from the reloading
+ watcher.
+
+
+.. changelog:: 2.5.5
+ :date: 2024/02/04
+
+ .. change:: Fix scope ``state`` key handling
+ :type: bugfix
+ :pr: 3070
+
+ Fix a regression introduced in #2751 that would wrongfully assume the ``state``
+ key is always present within the ASGI Scope. This is *only* the case when the
+ Litestar root application is invoked first, since we enforce such a key there,
+ but the presence of that key is not actually guaranteed by the ASGI spec and
+ some servers, such as hypercorn, do not provide it.
+
+
+.. changelog:: 2.5.4
+ :date: 2024/01/31
+
+ .. change:: Handle ``KeyError`` when `root_path` is not present in ASGI scope
+ :type: bugfix
+ :pr: 3051
+
+ Nginx Unit ASGI server does not set "root_path" in the ASGI scope, which is
+ expected as part of the changes done in #3039. This PR fixes the assumption that
+ the key is always present and instead tries to optionally retrieve it.
+
+ .. code-block::
+
+ KeyError on GET /
+ 'root_path'
+
+ .. change:: ServerSentEvent typing error
+ :type: bugfix
+ :pr: 3048
+
+ fixes small typing error:
+
+ .. code-block::
+
+ error: Argument 1 to "ServerSentEvent" has incompatible type "AsyncIterable[ServerSentEventMessage]"; expected "str | bytes | Iterable[str | bytes] | Iterator[str | bytes] | AsyncIterable[str | bytes] | AsyncIterator[str | bytes]" [arg-type]
+
+ inside ``test_sse`` there was a ``Any`` I changed to trigger the test then solved it.
+
+
+.. changelog:: 2.5.3
+ :date: 2024/01/29
+
+ .. change:: Handle diverging ASGI ``root_path`` behaviour
+ :type: bugfix
+ :pr: 3039
+ :issue: 3041
+
+ Uvicorn `0.26.0 `_
+ introduced a breaking change in its handling of the ASGI ``root_path`` behaviour,
+ which, while adhering to the spec, diverges from the interpretation of other
+ ASGI servers of this aspect of the spec (e.g. hypercorn and daphne do not follow
+ uvicorn's interpretation as of today). A fix was introduced that ensures
+ consistent behaviour of applications in any case.
+
+.. changelog:: 2.5.2
+ :date: 2024/01/27
+
+ .. change:: Ensure ``MultiDict`` and ``ImmutableMultiDict`` copy methods return the instance's type
+ :type: bugfix
+ :pr: 3009
+ :issue: 2549
+
+ Ensure :class:`~litestar.datastructures.MultiDict` and
+ :class:`~litestar.datastructures.ImmutableMultiDict` copy methods return a new
+ instance of ``MultiDict`` and ``ImmutableMultiDict``. Previously, these would
+ return a :class:`multidict.MultiDict` instance.
+
+ .. change:: Ensure ``exceptiongroup`` is installed on Python 3.11
+ :type: bugfix
+ :pr: 3035
+ :issue: 3029
+
+ Add the `exceptiongroup `_ package
+ as a required dependency on Python ``<3.11`` (previously ``<3.10``) as a
+ backport of `Exception Groups `_
+
+
+.. changelog:: 2.5.1
+ :date: 2024/01/18
+
+ .. change:: Fix OpenAPI schema generation for Union of multiple ``msgspec.Struct``\ s and ``None``
+ :type: bugfix
+ :pr: 2982
+ :issue: 2971
+
+ The following code would raise a :exc:`TypeError`
+
+ .. code-block:: python
+
+ import msgspec
+
+ from litestar import get
+ from litestar.testing import create_test_client
+
+
+ class StructA(msgspec.Struct):
+ pass
+
+
+ class StructB(msgspec.Struct):
+ pass
+
+
+ @get("/")
+ async def handler() -> StructA | StructB | None:
+ return StructA()
+
+
+ .. change:: Fix misleading error message for missing dependencies provide by a package extra
+ :type: bugfix
+ :pr: 2921
+
+ Ensure that :exc:`MissingDependencyException` includes the correct name of the
+ package to install if the package name differs from the Litestar package extra.
+ (e.g. ``pip install litestar[jinja]`` vs ``pip install jinja2``). Previously the
+ exception assumed the same name for both the package and package-extra name.
+
+
+ .. change:: Fix OpenAPI schema file upload schema types for swagger
+ :type: bugfix
+ :pr: 2745
+ :issue: 2628
+
+ - Always set ``format`` as ``binary``
+ - Fix schema for swagger with multiple files, which requires the type of the
+ request body schema to be ``object`` with ``properties`` instead of a schema
+ of type ``array`` and ``items``.
+
+
+
+.. changelog:: 2.5.0
+ :date: 2024/01/06
+
+ .. change:: Fix serialization of custom types in exception responses
+ :type: bugfix
+ :issue: 2867
+ :pr: 2941
+
+ Fix a bug that would lead to a :exc:`SerializationException` when custom types
+ were present in an exception response handled by the built-in exception
+ handlers.
+
+ .. code-block:: python
+
+ class Foo:
+ pass
+
+
+ @get()
+ def handler() -> None:
+ raise ValidationException(extra={"foo": Foo("bar")})
+
+
+ app = Litestar(route_handlers=[handler], type_encoders={Foo: lambda foo: "foo"})
+
+ The cause was that, in examples like the one shown above, ``type_encoders``
+ were not resolved properly from all layers by the exception handling middleware,
+ causing the serializer to throw an exception for an unknown type.
+
+ .. change:: Fix SSE reverting to default ``event_type`` after 1st message
+ :type: bugfix
+ :pr: 2888
+ :issue: 2877
+
+ The ``event_type`` set within an SSE returned from a handler would revert back
+ to a default after the first message sent:
+
+ .. code-block:: python
+
+ @get("/stream")
+ async def stream(self) -> ServerSentEvent:
+ async def gen() -> AsyncGenerator[str, None]:
+ c = 0
+ while True:
+ yield f"
{c}
\n"
+ c += 1
+
+ return ServerSentEvent(gen(), event_type="my_event")
+
+ In this example, the event type would only be ``my_event`` for the first
+ message, and fall back to a default afterwards. The implementation has been
+ fixed and will now continue sending the set event type for all messages.
+
+ .. change:: Correctly handle single file upload validation when multiple files are specified
+ :type: bugfix
+ :pr: 2950
+ :issue: 2939
+
+ Uploading a single file when the validation target allowed multiple would cause
+ a :exc:`ValidationException`:
+
+ .. code-block:: python
+
+ class FileUpload(Struct):
+ files: list[UploadFile]
+
+
+ @post(path="/")
+ async def upload_files_object(
+ data: Annotated[FileUpload, Body(media_type=RequestEncodingType.MULTI_PART)]
+ ) -> list[str]:
+ pass
+
+
+ This could would only allow for 2 or more files to be sent, and otherwise throw
+ an exception.
+
+ .. change:: Fix trailing messages after unsubscribe in channels
+ :type: bugfix
+ :pr: 2894
+
+ Fix a bug that would allow some channels backend to receive messages from a
+ channel it just unsubscribed from, for a short period of time, due to how the
+ different brokers handle unsubscribes.
+
+ .. code-block:: python
+
+ await backend.subscribe(["foo", "bar"]) # subscribe to two channels
+ await backend.publish(
+ b"something", ["foo"]
+ ) # publish a message to a channel we're subscribed to
+
+ # start the stream after publishing. Depending on the backend
+ # the previously published message might be in the stream
+ event_generator = backend.stream_events()
+
+ # unsubscribe from the channel we previously published to
+ await backend.unsubscribe(["foo"])
+
+ # this should block, as we expect messages from channels
+ # we unsubscribed from to not appear in the stream anymore
+ print(anext(event_generator))
+
+ Backends affected by this were in-memory, Redis PubSub and asyncpg. The Redis
+ stream and psycopg backends were not affected.
+
+ .. change:: Postgres channels backends
+ :type: feature
+ :pr: 2803
+
+ Two new channel backends were added to bring Postgres support:
+
+ :class:`~litestar.channels.backends.asyncpg.AsyncPgChannelsBackend`, using the
+ `asyncpg `_ driver and
+ :class:`~litestar.channels.backends.psycopg.PsycoPgChannelsBackend` using the
+ `psycopg3 `_ async driver.
+
+ .. seealso::
+ :doc:`/usage/channels`
+
+
+ .. change:: Add ``--schema`` and ``--exclude`` option to ``litestar route`` CLI command
+ :type: feature
+ :pr: 2886
+
+ Two new options were added to the ``litestar route`` CLI command:
+
+ - ``--schema``, to include the routes serving OpenAPI schema and docs
+ - ``--exclude`` to exclude routes matching a specified pattern
+
+ .. seealso::
+ :ref:`usage/cli:routes`
+
+ .. change:: Improve performance of threaded synchronous execution
+ :type: misc
+ :pr: 2937
+
+ Performance of threaded synchronous code was improved by using the async
+ library's native threading helpers instead of anyio. On asyncio,
+ :meth:`asyncio.loop.run_in_executor` is now used and on trio
+ :func:`trio.to_thread.run_sync`.
+
+ Beneficiaries of these performance improvements are:
+
+ - Synchronous route handlers making use of ``sync_to_thread=True``
+ - Synchronous dependency providers making use of ``sync_to_thread=True``
+ - Synchronous SSE generators
+ - :class:`~litestar.stores.file.FileStore`
+ - Large file uploads where the ``max_spool_size`` is exceeded and the spooled
+ temporary file has been rolled to disk
+ - :class:`~litestar.response.file.File` and
+ :class:`~litestar.response.file.ASGIFileResponse`
+
+
+.. changelog:: 2.4.5
+ :date: 2023/12/23
+
+ .. change:: Fix validation of empty payload data with default values
+ :type: bugfix
+ :issue: 2902
+ :pr: 2903
+
+ Prior to this fix, a handler like:
+
+ .. code-block:: python
+
+ @post(path="/", sync_to_thread=False)
+ def test(data: str = "abc") -> dict:
+ return {"foo": data}
+
+ ``$ curl localhost:8000 -X POST``
+
+ would return a client error like:
+
+ .. code-block:: bash
+
+ {"status_code":400,"detail":"Validation failed for POST http://localhost:8000/","extra":[{"message":"Expected `str`, got `null`","key":"data","source":"body"}]}
+
+ .. change:: Support for returning ``Response[None]`` with a ``204`` status code from a handler
+ :type: bugfix
+ :pr: 2915
+ :issue: 2914
+
+ Returning a ``Response[None]`` from a route handler for a response with a
+ ``204`` now works as expected without resulting in an
+ :exc:`ImproperlyConfiguredException`
+
+ .. change:: Fix error message of ``get_logger_placeholder()``
+ :type: bugfix
+ :pr: 2919
+
+ Using a method on
+ :attr:`Request.logger ` when not
+ setting a ``logging_config`` on the application would result in a non-descriptive
+ :exc:`TypeError`. An :exc:`ImproperlyConfiguredException` with an explanation is
+ now raised instead.
+
+
+.. changelog:: 2.4.4
+ :date: 2023/12/13
+
+ .. change:: Support non-valid identifier as serialization target name
+ :type: bugfix
+ :pr: 2850
+ :issue: 2845
+
+ Fix a bug where DTOs would raise a ``TypeError: __slots__ must be identifiers``
+ during serialization, if a non-valid identifier (such as ``field-name``)was used
+ for field renaming.
+
+ .. change:: Fix regression signature validation for DTO validated types
+ :type: bugfix
+ :pr: 2854
+ :issue: 2149
+
+ Fix a regression introduced in ``2.0.0rc1`` that would cause data validated by
+ the DTO to be validated again by the signature model.
+
+ .. change:: Fix regression in OpenAPI schema key names
+ :type: bugfix
+ :pr: 2841
+ :issue: 2804
+
+ Fix a regression introduced in ``2.4.0`` regarding the naming of OpenAPI schema
+ keys, in which a change was introduced to the way that keys for the OpenAPI
+ components/schemas objects were calculated to address the possibility of name
+ collisions.
+
+ This behaviour was reverted for the case where a name has no collision, and now
+ only introduces extended keys for the case where there are multiple objects with
+ the same name, a case which would previously result in an exception.
+
+ .. change:: Fix regression in OpenAPI handling of routes with multiple handlers
+ :type: bugfix
+ :pr: 2864
+ :issue: 2863
+
+ Fix a regression introduced in ``2.4.3`` causing two routes registered with the
+ same path, but different methods to break OpenAPI schema generation due to both
+ of them having the same value for operation ID.
+
+ .. change:: Fix OpenAPI schema generation for recursive models
+ :type: bugfix
+ :pr: 2869
+ :issue: 2429
+
+ Fix an issue that would lead to a :exc:`RecursionError` when including nested
+ models in the OpenAPI schema.
+
+
+.. changelog:: 2.4.3
+ :date: 2023/12/07
+
+ .. change:: Fix OpenAPI schema for ``Literal | None`` unions
+ :type: bugfix
+ :issue: 2812
+ :pr: 2818
+
+ Fix a bug where an incorrect OpenAPI schema was generated generated when any
+ ``Literal | None``-union was present in an annotation.
+
+ For example
+
+ .. code-block:: python
+
+ type: Literal["sink", "source"] | None
+
+ would generate
+
+ .. code-block:: json
+
+ {
+ "name": "type",
+ "in": "query",
+ "schema": {
+ "type": "string",
+ "enum": [ "sink", "source", null ]
+ }
+ }
+
+ .. change:: Fix advanced-alchemy 0.6.0 compatibility issue with ``touch_updated_timestamp``
+ :type: bugfix
+ :pr: 2843
+
+ Fix an incorrect import for ``touch_updated_timestamp`` of Advanced Alchemy,
+ introduced in Advanced-Alchemy version 0.6.0.
+
+.. changelog:: 2.4.2
+ :date: 2023/12/02
+
+ .. change:: Fix OpenAPI handling of parameters with duplicated names
+ :type: bugfix
+ :issue: 2662
+ :pr: 2788
+
+ Fix a bug where schema generation would consider two parameters with the same
+ name but declared in different places (eg., header, cookie) as an error.
+
+ .. change:: Fix late failure where ``DTOData`` is used without a DTO
+ :type: bugfix
+ :issue: 2779
+ :pr: 2789
+
+ Fix an issue where a handler would be allowed to be registered with a
+ ``DTOData`` annotation without having a DTO defined, which would result in a
+ runtime exception. In cases like these, a configuration error is now raised
+ during startup.
+
+ .. change:: Correctly propagate camelCase names on OpenAPI schema
+ :type: bugfix
+ :pr: 2800
+
+ Fix a bug where OpenAPI schema fields would be inappropriately propagated as
+ camelCase where they should have been snake_case
+
+ .. change:: Fix error handling in event handler stream
+ :type: bugfix
+ :pr: 2810, 2814
+
+ Fix a class of errors that could result in the event listener stream being
+ terminated when an exception occurred within an event listener. Errors in
+ event listeners are now not propagated anymore but handled by the backend and
+ logged instead.
+
+ .. change:: Fix OpenAPI schema for pydantic computed fields
+ :type: bugfix
+ :pr: 2797
+ :issue: 2792
+
+ Add support for including computed fields in schemas generated from pydantic
+ models.
+
+.. changelog:: 2.4.1
+ :date: 2023/11/28
+
+ .. change:: Fix circular import when importing from ``litestar.security.jwt``
+ :type: bugfix
+ :pr: 2784
+ :issue: 2782
+
+ An :exc:`ImportError` was raised when trying to import from ``litestar.security.jwt``. This was fixed
+ by removing the imports from the deprecated ``litestar.contrib.jwt`` within ``litesetar.security.jwt``.
+
+ .. change:: Raise config error when generator dependencies are cached
+ :type: bugfix
+ :pr: 2780
+ :issue: 2771
+
+ Previously, an :exc:`InternalServerError` was raised when attempting to use
+ `use_cache=True` with generator dependencies. This will now raise a configuration
+ error during application startup.
+
+.. changelog:: 2.4.0
+ :date: 2023/11/27
+
+ .. change:: Fix ``HTTPException`` handling during concurrent dependency resolving
+ :type: bugfix
+ :pr: 2596
+ :issue: 2594
+
+ An issue was fixed that would lead to :exc:`HTTPExceptions` not being re-raised
+ properly when they occurred within the resolution of nested dependencies during
+ the request lifecycle.
+
+ .. change:: Fix OpenAPI examples format
+ :type: bugfix
+ :pr: 2660
+ :issue: 2272
+
+ Fix the OpenAPI examples format by removing the wrapping object.
+
+ Before the change, for a given model
+
+ .. code-block:: python
+
+ @dataclass
+ class Foo:
+ foo: int
+
+ The following example would be generated:
+
+ .. code-block:: json
+
+ {
+ "description": "Example value",
+ "value": {
+ "foo": 7906
+ }
+ }
+
+ After the fix, this is now:
+
+ .. code-block:: json
+
+ {
+ "foo": 7906
+ }
+
+ .. change:: Fix CLI plugin commands not showing up in command list
+ :type: bugfix
+ :pr: 2441
+
+ Fix a bug where commands registered by CLI plugins were available, but would not
+ show up in the commands list
+
+ .. change:: Fix missing ``write-only`` mark in ``dto_field()`` signature
+ :type: bugfix
+ :pr: 2684
+
+ Fix the missing ``write-only`` string literal in the ``mark`` parameter of
+ :func:`~litestar.dto.field.dto_field`
+
+ .. change:: Fix OpenAPI schemas incorrectly flagged as duplicates
+ :type: bugfix
+ :pr: 2475
+ :issue: 2471
+
+ Fix an issue that would lead to OpenAPI schemas being incorrectly considered
+ duplicates, resulting in an :exc:`ImproperlyConfiguredException` being raised.
+
+ .. change:: Fix Pydantic URL type support in OpenAPI and serialization
+ :type: bugfix
+ :pr: 2701
+ :issue: 2664
+
+ Add missing support for Pydantic's URL types (``AnyUrl`` and its descendants)
+ for both serialization and OpenAPI schema generation. These types were only
+ partially supported previously; Serialization support was lacking for v1 and v2,
+ and OpenAPI support was missing for v2.
+
+ .. change:: Fix incorrect ``ValidationException`` message when multiple errors were encountered
+ :type: bugfix
+ :pr: 2716
+ :issue: 2714
+
+ Fix a bug where :exc:`ValidationException` could contain duplicated messages in
+ ``extra`` field, when multiple errors were encountered during validation
+
+ .. change:: Fix DTO renaming renames all fields of the same name in nested DTOs
+ :type: bugfix
+ :pr: 2764
+ :issue: 2721
+
+ Fix an issue with nested field renaming in DTOs that would lead to all fields
+ with a given name to be renamed in a nested structure.
+
+ In the below example, both ``Foo.id`` and ``Bar.id`` would have been renamed to
+ ``foo_id``
+
+ .. code-block:: python
+
+ from dataclasses import dataclass
+
+
+ @dataclass
+ class Bar:
+ id: str
+
+
+ @dataclass
+ class Foo:
+ id: str
+ bar: Bar
+
+
+ FooDTO = DataclassDTO[Annotated[Foo, DTOConfig(rename_fields={"id": "foo_id"})]]
+
+ .. change:: Fix handling of DTO objects nested in mappings
+ :type: bugfix
+ :pr: 2775
+ :issue: 2737
+
+ Fix a bug where DTOs nested in a :class:`~typing.Mapping` type would fail to
+ serialize correctly.
+
+ .. change:: Fix inconsistent sequence union parameter errors
+ :type: bugfix
+ :pr: 2776
+ :issue: 2600
+
+ Fix a bug where unions of collection types would result in different errors
+ depending on whether the union included :obj:`None` or not.
+
+ .. change:: Fix graceful handling of WebSocket disconnect in channels WebSockets handlers
+ :type: bugfix
+ :pr: 2691
+
+ Fix the behaviour of WebSocket disconnect handling within the WebSocket handlers
+ provided by :doc:`channels `, that would sometimes lead to
+ a ``RuntimeError: Unexpected ASGI message 'websocket.close', after sending 'websocket.close'.``
+ exception being raised upon the closing of a WebSocket connection.
+
+
+ .. change:: Add ``server_lifespan`` hook
+ :type: feature
+ :pr: 2658
+
+ A new ``server_lifespan`` hook is now available on :class:`~litestar.app.Litestar`.
+ This hook works similar to the regular ``lifespan`` context manager, with the
+ difference being is that it is only called once for the entire server lifespan,
+ not for each application startup phase. Note that these only differ when running
+ with an ASGI server that's using multiple worker processes.
+
+ .. change:: Allow rendering templates directly from strings
+ :type: feature
+ :pr: 2689
+ :issue: 2687
+
+ A new ``template_string`` parameter was added to :class:`~litestar.template.Template`,
+ allowing to render templates directly from strings.
+
+ .. seealso::
+ :ref:`usage/templating:Template Files vs. Strings`
+
+ .. change:: Support nested DTO field renaming
+ :type: feature
+ :pr: 2764
+ :issue: 2721
+
+ Using similar semantics as for exclusion/inclusion, nested DTO fields can now
+ also be renamed:
+
+ .. code-block:: python
+
+ from dataclasses import dataclass
+
+
+ @dataclass
+ class Bar:
+ id: str
+
+
+ @dataclass
+ class Foo:
+ id: str
+ bars: list[Bar]
+
+
+ FooDTO = DataclassDTO[Annotated[Foo, DTOConfig(rename_fields={"bars.0.id": "bar_id"})]]
+
+
+.. changelog:: 2.3.2
+ :date: 2023/11/06
+
+ .. change:: Fix recursion error when re-using the path of a route handler for static files
+ :type: bugfix
+ :pr: 2630
+ :issue: 2629
+
+ A regression was fixed that would cause a recursion error when the path of a
+ static files host was reused for a route handler with a different HTTP method.
+
+ .. code-block:: python
+
+ from litestar import Litestar
+ from litestar import post
+ from litestar.static_files import StaticFilesConfig
+
+
+ @post("/uploads")
+ async def handler() -> None:
+ pass
+
+
+ app = Litestar(
+ [handler],
+ static_files_config=[
+ StaticFilesConfig(directories=["uploads"], path="/uploads"),
+ ],
+ )
+
+
+.. changelog:: 2.3.1
+ :date: 2023/11/04
+
+ .. change:: CLI: Fix not providing SSL certfiles breaks uvicorn command when using reload or multiple workers
+ :type: bugfix
+ :pr: 2616
+ :issue: 2613
+
+ Fix an issue where not providing the ``--ssl-certfile`` and ``--ssl-keyfile``
+ options to the ``litestar run`` command would cause a :exc:`FileNotFoundError`
+ in uvicorn, when used together with the ``--reload``, ``--web-concurrency``
+ options.
+
+
+.. changelog:: 2.3.0
+ :date: 2023/11/02
+
+ .. change:: Python 3.12 support
+ :type: feature
+ :pr: 2396
+ :issue: 1862
+
+ Python 3.12 is now fully supported and tested.
+
+ .. change:: New layered parameter ``signature_types``
+ :type: feature
+ :pr: 2422
+
+ Types in this collection are added to ``signature_namespace`` using the type's
+ ``__name__`` attribute.
+ This provides a nicer interface when adding names to the signature namespace
+ w ithout modifying the type name, e.g.: ``signature_namespace={"Model": Model}``
+ is equivalent to ``signature_types=[Model]``.
+
+ The implementation makes it an error to supply a type in ``signature_types``
+ that has a value for ``__name__`` already in the signature namespace.
+
+ It will also throw an error if an item in ``signature_types`` has no
+ ``__name__`` attribute.
+
+ .. change:: Added RapiDoc for OpenAPI schema visualisation
+ :type: feature
+ :pr: 2522
+
+ Add support for using `RapiDoc `_ for
+ OpenAPI schema visualisation.
+
+ .. change:: Support Pydantic 1 & 2 within the same application
+ :type: feature
+ :pr: 2487
+
+ Added support for Pydantic 1 & 2 within the same application by integrating with
+ Pydantic's backwards compatibility layer:
+
+ .. code-block:: python
+
+ from litestar import get
+ from pydantic.v1 import BaseModel as BaseModelV1
+ from pydantic import BaseModel
+
+
+ class V1Foo(BaseModelV1):
+ bar: str
+
+
+ class V2Foo(BaseModel):
+ bar: str
+
+
+ @get("/1")
+ def foo_v1(data: V1Foo) -> V1Foo:
+ return data
+
+
+ @get("/2")
+ def foo_v2(data: V2Foo) -> V2Foo:
+ return data
+
+ .. change:: Add ``ResponseCacheConfig.cache_response_filter`` to allow filtering responses eligible for caching
+ :type: feature
+ :pr: 2537
+ :issue: 2501
+
+ ``ResponseCacheConfig.cache_response_filter`` is predicate called by the
+ response cache middleware that discriminates whether a response should be
+ cached, or not.
+
+
+ .. change:: SSL support and self-signed certificates for CLI
+ :type: feature
+ :pr: 2554
+ :issue: 2335
+
+ Add support for SSL and generating self-signed certificates to the CLI.
+
+ For this, three new arguments were added to the CLI's ``run`` command:
+
+ - ``--ssl-certfile``
+ - ``--ssl-keyfile``
+ - ``--create-self-signed-cert``
+
+ The ``--ssl-certfile`` and `--ssl-keyfile` flags are passed to uvicorn when
+ using ``litestar run``. Uvicorn requires both to be passed (or neither) but
+ additional validation was added to generate a more user friendly CLI errors.
+
+ The other SSL-related flags (like password or CA) were not added (yet). See
+ `uvicorn CLI docs `_
+
+ **Generating of a self-signed certificate**
+
+ One more CLI flag was added (``--create-devcert``) that uses the
+ ``cryptography`` module to generate a self-signed development certificate. Both
+ of the previous flags must be passed when using this flag. Then the following
+ logic is used:
+
+ - If both files already exists, they are used and nothing is generated
+ - If neither file exists, the dev cert and key are generated
+ - If only one file exists, it is ambiguous what to do so an exception is raised
+
+ .. change:: Use custom request class when given during exception handling
+ :type: bugfix
+ :pr: 2444
+ :issue: 2399
+
+ When a custom ``request_class`` is provided, it will now be used while returning
+ an error response
+
+ .. change:: Fix missing OpenAPI schema for generic response type annotations
+ :type: bugfix
+ :pr: 2463
+ :issue: 2383
+
+ OpenAPI schemas are now correctly generated when a response type annotation
+ contains a generic type such as
+
+ .. code-block:: python
+
+ from msgspec import Struct
+ from litestar import Litestar, get, Response
+ from typing import TypeVar, Generic, Optional
+
+ T = TypeVar("T")
+
+
+ class ResponseStruct(Struct, Generic[T]):
+ code: int
+ data: Optional[T]
+
+
+ @get("/")
+ def test_handler() -> Response[ResponseStruct[str]]:
+ return Response(
+ ResponseStruct(code=200, data="Hello World"),
+ )
+
+ .. change:: Fix rendering of OpenAPI examples
+ :type: bugfix
+ :pr: 2509
+ :issue: 2494
+
+ An issue was fixed where OpenAPI examples would be rendered as
+
+ .. code-block:: json
+
+ {
+ "parameters": [
+ {
+ "schema": {
+ "type": "string",
+ "examples": [
+ {
+ "summary": "example summary",
+ "value": "example value"
+ }
+ ]
+ }
+ }
+ ]
+ }
+
+ instead of
+
+ .. code-block:: json
+
+ {
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "examples": {
+ "example1": {
+ "summary": "example summary"
+ "value": "example value"
+ }
+ }
+ }
+ ]
+ }
+
+ .. change:: Fix non UTF-8 handling when logging requests
+ :type: bugfix
+ :issue: 2529
+ :pr: 2530
+
+ When structlog is not installed, the request body would not get parsed and shown
+ as a byte sequence. Instead, it was serialized into a string with the assumption
+ that it is valid UTF-8. This was fixed by decoding the bytes with
+ ``backslashreplace`` before displaying them.
+
+ .. change:: Fix ``ExceptionHandler`` typing to properly support ``Exception`` subclasses
+ :type: bugfix
+ :issue: 2520
+ :pr: 2533
+
+ Fix the typing for ``ExceptionHandler`` to support subclasses of ``Exception``,
+ such that code like this will type check properly:
+
+ .. code-block:: python
+
+ from litestar import Litestar, Request, Response
+
+
+ class CustomException(Exception): ...
+
+
+ def handle_exc(req: Request, exc: CustomException) -> Response: ...
+
+ .. change:: Fix OpenAPI schema generation for variable length tuples
+ :type: bugfix
+ :issue: 2460
+ :pr: 2552
+
+ Fix a bug where an annotation such as ``tuple[str, ...]`` would cause a
+ ``TypeError: '<' not supported between instances of 'NoneType' and 'OpenAPIType')``.
+
+ .. change:: Fix channels performance issue when polling with no subscribers in ``arbitrary_channels_allowed`` mode
+ :type: bugfix
+ :pr: 2547
+
+ Fix a bug that would cause high CPU loads while idling when using a
+ ``ChannelsPlugin`` with the ``arbitrary_channels_allowed`` enabled and while no
+ subscriptions for any channel were active.
+
+ .. change:: Fix CLI schema export for non-serializable types when using ``create_examples=True``
+ :type: bugfix
+ :pr: 2581
+ :issue: 2575
+
+ When trying to export a schema via the
+ ``litestar schema openapi --output schema.json`` making use of a non-JSON
+ serializable type, would result in an encoding error because the standard
+ library JSON serializer was used. This has been fixed by using Litestar's own
+ JSON encoder, enabling the serialization of all types supplied by the schema.
+
+ .. change:: Fix OpenAPI schema generation for ``Literal`` and ``Enum`` unions with ``None``
+ :type: bugfix
+ :pr: 2550
+ :issue: 2546
+
+ Existing behavior was to make the schema for every type that is a union with
+ ``None`` a ``"one_of"`` schema, that includes ``OpenAPIType.NULL`` in the
+ ``"one_of"`` types.
+
+ When a ``Literal`` or ``Enum`` type is in a union with ``None``, this behavior
+ is not desirable, as we want to have ``null`` available in the list of available
+ options on the type's schema.
+
+ This was fixed by modifying ``Literal`` and ``Enum`` schema generation so that i
+ t can be identified that the types are in a union with ``None``, allowing
+ ``null`` to be included in ``Schema.enum`` values.
+
+ .. change:: Fix cache overrides when using same route with different handlers
+ :type: bugfix
+ :pr: 2592
+ :issue: 2573, 2588
+
+ A bug was fixed that would cause the cache for routes being overwritten by a
+ route handler on that same route with a different HTTP method.
+
+
+
+.. changelog:: 2.2.0
+ :date: 2023/10/12
+
+ .. change:: Fix implicit conversion of objects to ``bool`` in debug response
+ :type: bugfix
+ :pr: 2384
+ :issue: 2381
+
+ The exception handler middleware would, when in debug mode, implicitly call an
+ object's :meth:`__bool__ `, which would lead to errors if that
+ object overloaded the operator, for example if the object in question was a
+ SQLAlchemy element.
+
+ .. change:: Correctly re-export filters and exceptions from ``advanced-alchemy``
+ :type: bugfix
+ :pr: 2360
+ :issue: 2358
+
+ Some re-exports of filter and exception types from ``advanced-alchemy`` were
+ missing, causing various issues when ``advanced-alchemy`` was installed, but
+ Litestar would still use its own version of these classes.
+
+ .. change:: Re-add ``create_engine`` method to SQLAlchemy configs
+ :type: bugfix
+ :pr: 2382
+
+ The ``create_engine`` method was removed in an ``advanced-alchemy`` releases.
+ This was addresses by re-adding it to the versions provided by Litestar.
+
+ .. change:: Fix ``before_request`` modifies route handler signature
+ :type: bugfix
+ :pr: 2391
+ :issue: 2368
+
+ The ``before_request`` would modify the return annotation of associated
+ route handlers to conform with its own return type annotation, which would cause
+ issues and unexpected behaviour when that annotation was not compatible with the
+ original one.
+
+ This was fixed by not having the ``before_request`` handler modify the
+ route handler's signature. Users are now expected to ensure that values returned
+ from a ``before_request`` handler conform to the return type annotation of the
+ route handler.
+
+ .. change:: Ensure compression is applied before caching when using compression middleware
+ :type: bugfix
+ :pr: 2393
+ :issue: 1301
+
+ A previous limitation was removed that would apply compression from the
+ :class:`~litestar.middleware.compression.CompressionMiddleware` only *after* a
+ response was restored from the cache, resulting in unnecessary repeated
+ computation and increased size of the stored response.
+
+ This was due to caching being handled on the response layer, where a response
+ object would be pickled, restored upon a cache hit and then re-sent, including
+ all middlewares.
+
+ The new implementation now instead applies caching on the ASGI level; Individual
+ messages sent to the ``send`` callable are cached, and later re-sent. This
+ process ensures that the compression middleware has been applied before, and
+ will be skipped when re-sending a cached response.
+
+ In addition, this increases performance and reduces storage size even in cases
+ where no compression is applied because the slow and inefficient pickle format
+ can be avoided.
+
+ .. change:: Fix implicit JSON parsing of URL encoded data
+ :type: bugfix
+ :pr: 2394
+
+ A process was removed where Litestar would implicitly attempt to parse parts of
+ URL encoded data as JSON. This was originally added to provide some performance
+ boosts when that data was in fact meant to be JSON, but turned out to be too
+ fragile.
+
+ Regular data conversion / validation is unaffected by this.
+
+ .. change:: CLI enabled by default
+ :type: feature
+ :pr: 2346
+ :issue: 2318
+
+ The CLI and all its dependencies are now included by default, to enable a better
+ and more consistent developer experience out of the box.
+
+ The previous ``litestar[cli]`` extra is still available for backwards
+ compatibility, but as of ``2.2.0`` it is without effect.
+
+ .. change:: Customization of Pydantic integration via ``PydanticPlugin``
+ :type: feature
+ :pr: 2404
+ :issue: 2373
+
+ A new :class:`~litestar.contrib.pydantic.PydanticPlugin` has been added, which
+ can be used to configure Pydantic behaviour. Currently it supports setting a
+ ``prefer_alias`` option, which will pass the ``by_alias=True`` flag to Pydantic
+ when exporting models, as well as generate schemas accordingly.
+
+ .. change:: Add ``/schema/openapi.yml`` to the available schema paths
+ :type: feature
+ :pr: 2411
+
+ The YAML version of the OpenAPI schema is now available under
+ ``/schema/openapi.yml`` in addition to ``/schema/openapi.yaml``.
+
+ .. change:: Add experimental DTO codegen backend
+ :type: feature
+ :pr: 2388
+
+ A new DTO backend was introduced which speeds up the transfer process by
+ generating optimized Python code ahead of time. Testing shows that the new
+ backend is between 2.5 and 5 times faster depending on the operation and data
+ provided.
+
+ The new backend can be enabled globally for all DTOs by passing the appropriate
+ feature flag to the Litestar application:
+
+ .. code-block:: python
+
+ from litestar import Litestar
+ from litestar.config.app import ExperimentalFeatures
+
+ app = Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN])
+
+ .. seealso::
+ For more information see
+ :ref:`usage/dto/0-basic-use:Improving performance with the codegen backend`
+
+
+ .. change:: Improved error messages for missing required parameters
+ :type: feature
+ :pr: 2418
+
+ Error messages for missing required parameters will now also contain the source
+ of the expected parameter:
+
+ Before:
+
+ .. code-block:: json
+
+ {
+ "status_code": 400,
+ "detail": "Missing required parameter foo for url http://testerver.local"
+ }
+
+
+ After:
+
+ .. code-block:: json
+
+ {
+ "status_code": 400,
+ "detail": "Missing required header parameter 'foo' for url http://testerver.local"
+ }
+
+
+.. changelog:: 2.1.1
+ :date: 2023/09/24
+
+ .. change:: Fix ``DeprecationWarning`` raised by ``Response.to_asgi_response``
+ :type: bugfix
+ :pr: 2364
+
+ :meth:`~litestar.response.Response.to_asgi_response` was passing a
+ non-:obj:`None` default value (``[]``) to ``ASGIResponse`` for
+ ``encoded_headers``, resulting in a :exc:`DeprecationWarning` being raised.
+ This was fixed by leaving the default value as :obj:`None`.
+
+
+.. changelog:: 2.1.0
+ :date: 2023/09/23
+
+ `View the full changelog `_
+
+ .. change:: Make ``302`` the default ``status_code`` for redirect responses
+ :type: feature
+ :pr: 2189
+ :issue: 2138
+
+ Make ``302`` the default ``status_code`` for redirect responses
+
+ .. change:: Add :meth:`include_in_schema` option for all layers
+ :type: feature
+ :pr: 2295
+ :issue: 2267
+
+ Adds the :meth:`include_in_schema` option to all layers, allowing to include/exclude
+ specific routes from the generated OpenAPI schema.
+
+ .. change:: Deprecate parameter ``app`` of ``Response.to_asgi_response``
+ :type: feature
+ :pr: 2268
+ :issue: 2217
+
+ Adds deprecation warning for unused ``app`` parameter of ``to_asgi_response`` as
+ it is unused and redundant due to ``request.app`` being available.
+
+ .. change:: Authentication: Add parameters to set the JWT ``extras`` field
+ :type: feature
+ :pr: 2313
+
+ Adds ``token_extras`` to both :func:`BaseJWTAuth.login` and :meth:`BaseJWTAuth.create_token` methods,
+ to allow the definition of the ``extras`` JWT field.
+
+ .. change:: Templating: Add possibility to customize Jinja environment
+ :type: feature
+ :pr: 2195
+ :issue: 965
+
+ Adds the ability to pass a custom Jinja2 ``Environment`` or Mako ``TemplateLookup`` by providing a
+ dedicated class method.
+
+ .. change:: Add support for `minjinja `_
+ :type: feature
+ :pr: 2250
+
+ Adds support for MiniJinja, a minimal Jinja2 implementation.
+
+ .. seealso:: :doc:`/usage/templating`
+
+ .. change:: SQLAlchemy: Exclude implicit fields for SQLAlchemy DTO
+ :type: feature
+ :pr: 2170
+
+ :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be
+ configured using a separate config object. This can be set using both
+ class inheritance and `Annotated `_:
+
+ .. code-block:: python
+ :caption: :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be configured using a separate config object using ``config`` object.
+
+ class MyModelDTO(SQLAlchemyDTO[MyModel]):
+ config = SQLAlchemyDTOConfig()
+
+ or
+
+ .. code-block:: python
+ :caption: :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be configured using a separate config object using ``Annotated``.
+
+ MyModelDTO = SQLAlchemyDTO[Annotated[MyModel, SQLAlchemyDTOConfig()]]
+
+ The new configuration currently accepts a single attribute which is ``include_implicit_fields`` that has
+ a default value of ``True``. If set to to ``False``, all implicitly mapped columns will be hidden
+ from the ``DTO``. If set to ``hybrid-only``, then hybrid properties will be shown but not other
+ implicit columns.
+
+ Finally, implicit columns that are marked with ``Mark.READ_ONLY`` or ``Mark.WRITE_ONLY``
+ will always be shown regardless of the value of ``include_implicit_fields``.
+
+ .. change:: SQLAlchemy: Allow repository functions to be filtered by expressions
+ :type: feature
+ :pr: 2265
+
+ Enhances the SQLALchemy repository so that you can more easily pass in complex ``where`` expressions into the repository functions.
+
+ .. tip:: Without this, you have to override the ``statement`` parameter and it separates the where conditions from the filters and the ``kwargs``.
+
+ Allows usage of this syntax:
+
+ .. code-block:: python
+
+ locations, total_count = await model_service.list_and_count(
+ ST_DWithin(UniqueLocation.location, geog, 1000), account_id=str(account_id)
+ )
+
+ instead of the previous method of overriding the ``statement``:
+
+ .. code-block:: python
+
+ locations, total_count = await model_service.list_and_count(
+ statement=select(Model).where(ST_DWithin(UniqueLocation.location, geog, 1000)),
+ account_id=str(account_id),
+ )
+
+ .. change:: SQLAlchemy: Use :func:`lambda_stmt ` in the repository
+ :type: feature
+ :pr: 2179
+
+ Converts the repository to use :func:`lambda_stmt `
+ instead of the normal ``select``
+
+ .. change:: SQLAlchemy: Swap to the `advanced_alchemy `_ implementations
+ :type: feature
+ :pr: 2312
+
+ Swaps the internal SQLAlchemy repository to use the external
+ `advanced_alchemy `_ library implementations
+
+ .. change:: Remove usages of deprecated ``ExceptionHandlerMiddleware`` ``debug`` parameter
+ :type: bugfix
+ :pr: 2192
+
+ Removes leftover usages of deprecated ``ExceptionHandlerMiddleware`` debug parameter.
+
+ .. change:: DTOs: Raise :class:`ValidationException` when Pydantic validation fails
+ :type: bugfix
+ :pr: 2204
+ :issue: 2190
+
+ Ensures that when the Pydantic validation fails in the Pydantic DTO,
+ a :class:`ValidationException` is raised with the extras set to the errors given by Pydantic.
+
+ .. change:: Set the max width of the console to 80
+ :type: bugfix
+ :pr: 2244
+
+ Sets the max width of the console to 80, to prevent the output from being
+ wrapped.
+
+ .. change:: Handling of optional path parameters
+ :type: bugfix
+ :pr: 2224
+ :issue: 2222
+
+ Resolves an issue where optional path parameters caused a 500 error to be raised.
+
+ .. change:: Use os.replace instead of shutil.move for renaming files
+ :type: bugfix
+ :pr: 2223
+
+ Change to using :func:`os.replace` instead of :func:`shutil.move` for renaming files, to
+ ensure atomicity.
+
+ .. change:: Exception detail attribute
+ :type: bugfix
+ :pr: 2231
+
+ Set correctly the detail attribute on :class:`LitestarException` and :class:`HTTPException`
+ regardless of whether it's passed positionally or by name.
+
+ .. change:: Filters not available in ``exists()``
+ :type: bugfix
+ :pr: 2228
+ :issue: 2221
+
+ Fixes :meth:`exists` method for SQLAlchemy sync and async.
+
+ .. change:: Add Pydantic types to SQLAlchemy registry only if Pydantic is installed
+ :type: bugfix
+ :pr: 2252
+
+ Allows importing from ``litestar.contrib.sqlalchemy.base`` even if Pydantic is not installed.
+
+ .. change:: Don't add content type for responses that don't have a body
+ :type: bugfix
+ :pr: 2263
+ :issue: 2106
+
+ Ensures that the ``content-type`` header is not added for responses that do not have a
+ body such as responses with status code ``204 (No Content)``.
+
+ .. change:: ``SQLAlchemyPlugin`` refactored
+ :type: bugfix
+ :pr: 2269
+
+ Changes the way the ``SQLAlchemyPlugin`` to now append the other plugins instead of the
+ inheritance that was previously used. This makes using the ``plugins.get`` function work as expected.
+
+ .. change:: Ensure ``app-dir`` is appended to path during autodiscovery
+ :type: bugfix
+ :pr: 2277
+ :issue: 2266
+
+ Fixes a bug which caused the ``--app-dir`` option to the Litestar CLI to not be propagated during autodiscovery.
+
+ .. change:: Set content length header by default
+ :type: bugfix
+ :pr: 2271
+
+ Sets the ``content-length`` header by default even if the length of the body is ``0``.
+
+ .. change:: Incorrect handling of mutable headers in :class:`ASGIResponse`
+ :type: bugfix
+ :pr: 2308
+ :issue: 2196
+
+ Update :class:`ASGIResponse`, :class:`Response` and friends to address a few issues related to headers:
+
+ - If ``encoded_headers`` were passed in at any point, they were mutated within responses, leading to a growing list of headers with every response
+ - While mutating ``encoded_headers``, the checks performed to assert a value was (not) already present, headers were not treated case-insensitive
+ - Unnecessary work was performed while converting cookies / headers into an encoded headers list
+
+ This was fixed by:
+
+ - Removing the use of and deprecate ``encoded_headers``
+ - Handling headers on :class:`ASGIResponse` with :class:`MutableScopeHeaders`, which allows for case-insensitive membership tests, ``.setdefault`` operations, etc.
+
+ .. change:: Adds missing ORM registry export
+ :type: bugfix
+ :pr: 2316
+
+ Adds an export that was overlooked for the base repo
+
+ .. change:: Discrepancy in ``attrs``, ``msgspec`` and ``Pydantic`` for multi-part forms
+ :type: bugfix
+ :pr: 2280
+ :issue: 2278
+
+ Resolves issue in ``attrs``, ``msgspec`` and Pydantic for multi-part forms
+
+ .. change:: Set proper default for ``exclude_http_methods`` in auth middleware
+ :type: bugfix
+ :pr: 2325
+ :issue: 2205
+
+ Sets ``OPTIONS`` as the default value for ``exclude_http_methods`` in the base authentication middleware class.
+
+.. changelog:: 2.0.0
+ :date: 2023/08/19
+
+ .. change:: Regression | Missing ``media_type`` information to error responses
+ :type: bugfix
+ :pr: 2131
+ :issue: 2024
+
+ Fixed a regression that caused error responses to be sent using a mismatched
+ media type, e.g. an error response from a ``text/html`` endpoint would be sent
+ as JSON.
+
+ .. change:: Regression | ``Litestar.debug`` does not propagate to exception handling middleware
+ :type: bugfix
+ :pr: 2153
+ :issue: 2147
+
+ Fixed a regression where setting ``Litestar.debug`` would not propagate to the
+ exception handler middleware, resulting in exception responses always being sent
+ using the initial debug value.
+
+ .. change:: Static files not being served if a route handler with the same base path was registered
+ :type: bugfix
+ :pr: 2154
+
+ Fixed a bug that would result in a ``404 - Not Found`` when requesting a static
+ file where the ``litestar.static_files.StaticFilesConfig.path`` was also
+ used by a route handler.
+
+ .. change:: HTMX: Missing default values for ``receive`` and ``send`` parameters of ``HTMXRequest``
+ :type: bugfix
+ :pr: 2145
+
+ Add missing default values for the ``receive`` and ``send`` parameters of
+ :class:`~litestar.contrib.htmx.request.HTMXRequest`.
+
+ .. change:: DTO: Excluded attributes accessed during transfer
+ :type: bugfix
+ :pr: 2127
+ :issue: 2125
+
+ Fix the behaviour of DTOs such that they will no longer access fields that have
+ been included. This behaviour would previously cause issues when these
+ attributes were either costly or impossible to access (e.g. lazy loaded
+ relationships of a SQLAlchemy model).
+
+ .. change:: DTO | Regression: ``DTOData.create_instance`` ignores renaming
+ :type: bugfix
+ :pr: 2144
+
+ Fix a regression where calling
+ :meth:`~litestar.dto.data_structures.DTOData.create_instance` would ignore the
+ renaming settings of fields.
+
+ .. change:: OpenAPI | Regression: Response schema for files and streams set ``application/octet-stream`` as ``contentEncoding`` instead of ``contentMediaType``
+ :type: bugfix
+ :pr: 2130
+
+ Fix a regression that would set ``application/octet-stream`` as the ``contentEncoding``
+ instead of ``contentMediaType`` in the response schema of
+ :class:`~litestar.response.File` :class:`~litestar.response.Stream`.
+
+ .. change:: OpenAPI | Regression: Response schema diverges from ``prefer_alias`` setting for Pydantic models
+ :type: bugfix
+ :pr: 2150
+
+ Fix a regression that made the response schema use ``prefer_alias=True``,
+ diverging from how Pydantic models are exported by default.
+
+ .. change:: OpenAPI | Regression: Examples not being generated deterministically
+ :type: bugfix
+ :pr: 2161
+
+ Fix a regression that made generated examples non-deterministic, caused by a
+ misconfiguration of the random seeding.
+
+ .. change:: SQLAlchemy repository: Handling of dialects not supporting JSON
+ :type: bugfix
+ :pr: 2139
+ :issue: 2137
+
+ Fix a bug where SQLAlchemy would raise a :exc:`TypeError` when using a dialect
+ that does not support JSON with the SQLAlchemy repositories.
+
+ .. change:: JWT | Regression: ``OPTIONS`` and ``HEAD`` being authenticated by default
+ :type: bugfix
+ :pr: 2160
+
+ Fix a regression that would make
+ ``litestar.contrib.jwt.JWTAuthenticationMiddleware`` authenticate
+ ``OPTIONS`` and ``HEAD`` requests by default.
+
+ .. change:: SessionAuth | Regression: ``OPTIONS`` and ``HEAD`` being authenticated by default
+ :type: bugfix
+ :pr: 2182
+
+ Fix a regression that would make
+ :class:`~litestar.security.session_auth.middleware.SessionAuthMiddleware` authenticate
+ ``OPTIONS`` and ``HEAD`` requests by default.
+
+.. changelog:: 2.0.0rc1
+ :date: 2023/08/05
+
+ .. change:: Support for server-sent-events
+ :type: feature
+ :pr: 2035
+ :issue: 1185
+
+ Support for `Server-sent events ` has been
+ added with the :class:`ServerSentEvent <.response.ServerSentEvent>`:
+
+ .. code-block:: python
+
+ async def my_generator() -> AsyncGenerator[bytes, None]:
+ count = 0
+ while count < 10:
+ await sleep(0.01)
+ count += 1
+ yield str(count)
+
+
+ @get(path="/count")
+ def sse_handler() -> ServerSentEvent:
+ return ServerSentEvent(my_generator())
+
+ .. seealso::
+ :ref:`Server Sent Events `
+
+
+ .. change:: SQLAlchemy repository: allow specifying ``id_attribute`` per method
+ :type: feature
+ :pr: 2052
+
+ The following methods now accept an ``id_attribute`` argument, allowing to
+ specify an alternative value to the models primary key:
+
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete_many``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.update``
+
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete_many``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.get``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.update``
+
+ .. change:: SQLAlchemy repository: New ``upsert_many`` method
+ :type: feature
+ :pr: 2056
+
+ A new method ``upsert_many`` has been added to the SQLAlchemy repositories,
+ providing equivalent functionality to the ``upsert`` method for multiple
+ model instances.
+
+ .. seealso::
+ ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.upsert_many``
+ ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.upsert_many``
+
+ .. change:: SQLAlchemy repository: New filters: ``OnBeforeAfter``, ``NotInCollectionFilter`` and ``NotInSearchFilter``
+ :type: feature
+ :pr: 2057
+
+ The following filters have been added to the SQLAlchemy repositories:
+
+ ``litestar.contrib.repository.filters.OnBeforeAfter``
+
+ Allowing to filter :class:`datetime.datetime` columns
+
+ ``litestar.contrib.repository.filters.NotInCollectionFilter``
+
+ Allowing to filter using a ``WHERE ... NOT IN (...)`` clause
+
+ ``litestar.contrib.repository.filters.NotInSearchFilter``
+
+ Allowing to filter using a `WHERE field_name NOT LIKE '%' || :value || '%'`` clause
+
+ .. change:: SQLAlchemy repository: Configurable chunk sizing for ``delete_many``
+ :type: feature
+ :pr: 2061
+
+ The repository now accepts a ``chunk_size`` parameter, determining the maximum
+ amount of parameters in an ``IN`` statement before it gets chunked.
+
+ This is currently only used in the ``delete_many`` method.
+
+
+ .. change:: SQLAlchemy repository: Support InstrumentedAttribute for attribute columns
+ :type: feature
+ :pr: 2054
+
+ Support :class:`~sqlalchemy.orm.InstrumentedAttribute` for in the repository's
+ ``id_attribute``, and the following methods:
+
+
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete_many``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.update``
+
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete_many``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.get``
+ - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.update``
+
+ .. change:: OpenAPI: Support callable ``operation_id`` on route handlers
+ :type: feature
+ :pr: 2078
+
+ Route handlers may be passed a callable to ``operation_id`` to create the
+ OpenAPI operation ID.
+
+ .. change:: Run event listeners concurrently
+ :type: feature
+ :pr: 2096
+
+ :doc:`/usage/events` now run concurrently inside a task group.
+
+ .. change:: Support extending the CLI with plugins
+ :type: feature
+ :pr: 2066
+
+ A new plugin protocol :class:`~litestar.plugins.CLIPluginProtocol` has been
+ added that can be used to extend the Litestar CLI.
+
+ .. seealso::
+ :ref:`usage/cli:Using a plugin`
+
+ .. change:: DTO: Support renamed fields in ``DTOData`` and ``create_instance``
+ :type: bugfix
+ :pr: 2065
+
+ A bug was fixed that would cause field renaming to be skipped within
+ :class:`~litestar.dto.data_structures.DTOData` and
+ :meth:`~litestar.dto.data_structures.DTOData.create_instance`.
+
+ .. change:: SQLAlchemy repository: Fix ``health_check`` for oracle
+ :type: bugfix
+ :pr: 2060
+
+ The emitted statement for oracle has been changed to ``SELECT 1 FROM DUAL``.
+
+ .. change:: Fix serialization of empty strings in multipart form
+ :type: bugfix
+ :pr: 2044
+
+ A bug was fixed that would cause a validation error to be raised for empty
+ strings during multipart form decoding.
+
+ .. change:: Use debug mode by default in test clients
+ :type: misc
+ :pr: 2113
+
+ The test clients will now default to ``debug=True`` instead of ``debug=None``.
+
+ .. change:: Removal of deprecated ``partial`` module
+ :type: misc
+ :pr: 2113
+ :breaking:
+
+ The deprecated ``litestar.partial`` has been removed. It can be replaced with
+ DTOs, making use of the :class:`~litestar.dto.config.DTOConfig` option
+ ``partial=True``.
+
+ .. change:: Removal of deprecated ``dto/factory`` module
+ :type: misc
+ :pr: 2114
+ :breaking:
+
+ The deprecated module ``litestar.dto.factory`` has been removed.
+
+ .. change:: Removal of deprecated ``contrib/msgspec`` module
+ :type: misc
+ :pr: 2114
+ :breaking:
+
+ The deprecated module ``litestar.contrib.msgspec`` has been removed.
+
+
+.. changelog:: 2.0.0beta4
+ :date: 2023/07/21
+
+ .. change:: Fix extra package dependencies
+ :type: bugfix
+ :pr: 2029
+
+ A workaround for a
+ `bug in poetry `_ that
+ caused development / extra dependencies to be installed alongside the package
+ has been added.
+
+.. changelog:: 2.0.0beta3
+ :date: 2023/07/20
+
+ .. change:: :class:`SQLAlchemyDTO (Advanced Alchemy) `: column/relationship type inference
+ :type: feature
+ :pr: 1879
+ :issue: 1853
+
+ If type annotations aren't available for a given column/relationship, they may
+ be inferred from the mapped object.
+
+ For columns, the :attr:`~sqlalchemy.engine.interfaces.ReflectedColumn.type`\ 's
+ :attr:`~sqlalchemy.types.TypeEngine.python_type` will be used as the type of the
+ column, and the :attr:`~sqlalchemy.engine.interfaces.ReflectedColumn.nullable`
+ property to determine if the field should have a :obj:`None` union.
+
+ For relationships, where the ``RelationshipProperty.direction`` is
+ :attr:`~sqlalchemy.orm.RelationshipDirection.ONETOMANY` or
+ :attr:`~sqlalchemy.orm.RelationshipDirection.MANYTOMANY`,
+ ``RelationshipProperty.collection_class`` and
+ ``RelationshipProperty.mapper.class_`` are used to construct an annotation for
+ the collection.
+
+ For one-to-one relationships, ``RelationshipProperty.mapper.class_`` is used to
+ get the type annotation, and will be made a union with :obj:`None` if all of the
+ foreign key columns are nullable.
+
+ .. change:: DTO: Piccolo ORM
+ :type: feature
+ :pr: 1896
+
+ Add support for piccolo ORM with the
+ :class:`~litestar.contrib.piccolo.PiccoloDTO`.
+
+ .. change:: OpenAPI: Allow setting ``OpenAPIController.path`` from ```OpenAPIConfig``
+ :type: feature
+ :pr: 1886
+
+ :attr:`~litestar.openapi.OpenAPIConfig.path` has been added, which can be used
+ to set the ``path`` for :class:`~litestar.openapi.OpenAPIController` directly,
+ without needing to create a custom instance of it.
+
+ If ``path`` is set in both :class:`~litestar.openapi.OpenAPIConfig` and
+ :class:`~litestar.openapi.OpenAPIController`, the path set on the controller
+ will take precedence.
+
+ .. change:: SQLAlchemy repository: ``auto_commit``, ``auto_expunge`` and ``auto_refresh`` options
+ :type: feature
+ :pr: 1900
+
+ .. currentmodule:: litestar.contrib.sqlalchemy.repository
+
+ Three new parameters have been added to the repository and various methods:
+
+ ``auto_commit``
+ When this :obj:`True`, the session will
+ :meth:`~sqlalchemy.orm.Session.commit` instead of
+ :meth:`~sqlalchemy.orm.Session.flush` before returning.
+
+ Available in:
+
+ - ``~SQLAlchemyAsyncRepository.add``
+ - ``~SQLAlchemyAsyncRepository.add_many``
+ - ``~SQLAlchemyAsyncRepository.delete``
+ - ``~SQLAlchemyAsyncRepository.delete_many``
+ - ``~SQLAlchemyAsyncRepository.get_or_create``
+ - ``~SQLAlchemyAsyncRepository.update``
+ - ``~SQLAlchemyAsyncRepository.update_many``
+ - ``~SQLAlchemyAsyncRepository.upsert``
+
+ (and their sync equivalents)
+
+ ``auto_refresh``
+ When :obj:`True`, the session will execute
+ :meth:`~sqlalchemy.orm.Session.refresh` objects before returning.
+
+ Available in:
+
+ - ``~SQLAlchemyAsyncRepository.add``
+ - ``~SQLAlchemyAsyncRepository.get_or_create``
+ - ``~SQLAlchemyAsyncRepository.update``
+ - ``~SQLAlchemyAsyncRepository.upsert``
+
+ (and their sync equivalents)
+
+
+ ``auto_expunge``
+ When this is :obj:`True`, the session will execute
+ :meth:`~sqlalchemy.orm.Session.expunge` all objects before returning.
+
+ Available in:
+
+ - ``~SQLAlchemyAsyncRepository.add``
+ - ``~SQLAlchemyAsyncRepository.add_many``
+ - ``~SQLAlchemyAsyncRepository.delete``
+ - ``~SQLAlchemyAsyncRepository.delete_many``
+ - ``~SQLAlchemyAsyncRepository.get``
+ - ``~SQLAlchemyAsyncRepository.get_one``
+ - ``~SQLAlchemyAsyncRepository.get_one_or_none``
+ - ``~SQLAlchemyAsyncRepository.get_or_create``
+ - ``~SQLAlchemyAsyncRepository.update``
+ - ``~SQLAlchemyAsyncRepository.update_many``
+ - ``~SQLAlchemyAsyncRepository.list``
+ - ``~SQLAlchemyAsyncRepository.upsert``
+
+ (and their sync equivalents)
+
+ .. change:: Include path name in ``ImproperlyConfiguredException`` message for missing param types
+ :type: feature
+ :pr: 1935
+
+ The message of a :exc:`ImproperlyConfiguredException` raised when a path
+ parameter is missing a type now contains the name of the path.
+
+ .. change:: DTO: New ``include`` parameter added to ``DTOConfig``
+ :type: feature
+ :pr: 1950
+
+ :attr:`~litestar.dto.config.DTOConfig.include` has been added to
+ :class:`~litestar.dto.config.DTOConfig`, providing a counterpart to
+ :attr:`~litestar.dto.config.DTOConfig.exclude`.
+
+ If ``include`` is provided, only those fields specified within it will be
+ included.
+
+ .. change:: ``AbstractDTOFactory`` moved to ``dto.factory.base``
+ :type: misc
+ :breaking:
+ :pr: 1950
+
+ :class:`~litestar.dto.base_factory.AbstractDTOFactory` has moved from
+ ``litestar.dto.factory.abc`` to ``litestar.dto.factory.base``.
+
+ .. change:: SQLAlchemy repository: Rename ``_sentinel`` column to ``sa_orm_sentinel``
+ :type: misc
+ :breaking:
+ :pr: 1933
+
+
+ The ``_sentinel`` column of
+ ``~litestar.contrib.sqlalchemy.base.UUIDPrimaryKey`` has been renamed to
+ ``sa_orm_sentinel``, to support Spanner, which does not support tables starting
+ with ``_``.
+
+ .. change:: SQLAlchemy repository: Fix audit columns defaulting to app startup time
+ :type: bugfix
+ :pr: 1894
+
+ A bug was fixed where
+ ``~litestar.contrib.sqlalchemy.base.AuditColumns.created_at`` and
+ ``~litestar.contrib.sqlalchemy.base.AuditColumns.updated_at`` would default
+ to the :class:`~datetime.datetime` at initialization time, instead of the time
+ of the update.
+
+ .. change:: :class:`SQLAlchemyDTO (Advanced Alchemy) `: Fix handling of ``Sequence`` with defaults
+ :type: bugfix
+ :pr: 1883
+ :issue: 1851
+
+ Fixes handling of columns defined with
+ `Sequence `_
+ default values.
+
+ The SQLAlchemy default value for a :class:`~sqlalchemy.schema.Column` will be
+ ignored when it is a :class:`~sqlalchemy.schema.Sequence` object. This is
+ because the SQLAlchemy sequence types represent server generated values, and
+ there is no way for us to generate a reasonable default value for that field
+ from it without making a database query, which is not possible deserialization.
+
+ .. change:: Allow JSON as redirect response
+ :type: bugfix
+ :pr: 1908
+
+ Enables using redirect responses with a JSON media type.
+
+ .. change:: DTO / OpenAPI: Fix detection of required fields for Pydantic and msgspec DTOs
+ :type: bugfix
+ :pr: 1946
+
+ A bug was fixed that would lead to fields of a Pydantic model or msgspec Structs
+ being marked as "not required" in the generated OpenAPI schema when used with
+ DTOs.
+
+ .. change:: Replace ``Header``, ``CacheControlHeader`` and ``ETag`` Pydantic models with dataclasses
+ :type: misc
+ :pr: 1917
+ :breaking:
+
+ As part of the removal of Pydantic as a hard dependency, the header models
+ :class:`~litestar.datastructures.Header`,
+ :class:`~litestar.datastructures.CacheControlHeader` and
+ :class:`~litestar.datastructures.ETag` have been replaced with dataclasses.
+
+
+ .. note::
+ Although marked breaking, this change should not affect usage unless you
+ relied on these being Pydantic models in some way.
+
+ .. change:: Pydantic as an optional dependency
+ :breaking:
+ :pr: 1963
+ :type: misc
+
+ As of this release, Pydantic is no longer a required dependency of Litestar.
+ It is still supported in the same capacity as before, but Litestar itself does
+ not depend on it anymore in its internals.
+
+ .. change:: Pydantic 2 support
+ :type: feature
+ :pr: 1956
+
+ Pydantic 2 is now supported alongside Pydantic 1.
+
+ .. change:: Deprecation of ``partial`` module
+ :type: misc
+ :pr: 2002
+
+ The ``litestar.partial`` and ``litestar.partial.Partial`` have been
+ deprecated and will be removed in a future release. Users are advised to upgrade
+ to DTOs, making use of the :class:`~litestar.dto.config.DTOConfig` option
+ ``partial=True``.
+
+
+.. changelog:: 2.0.0beta2
+ :date: 2023/06/24
+
+ .. change:: Support ``annotated-types``
+ :type: feature
+ :pr: 1847
+
+ Extended support for the
+ `annotated-types `_ library is now
+ available.
+
+ .. change:: Increased verbosity of validation error response keys
+ :type: feature
+ :pr: 1774
+ :breaking:
+
+ The keys in validation error responses now include the full path to the field
+ where the originated.
+
+ An optional ``source`` key has been added, signifying whether the value is from
+ the body, a cookie, a header, or a query param.
+
+ .. code-block:: json
+ :caption: before
+
+ {
+ "status_code": 400,
+ "detail": "Validation failed for POST http://localhost:8000/some-route",
+ "extra": [
+ {"key": "int_param", "message": "value is not a valid integer"},
+ {"key": "int_header", "message": "value is not a valid integer"},
+ {"key": "int_cookie", "message": "value is not a valid integer"},
+ {"key": "my_value", "message": "value is not a valid integer"}
+ ]
+ }
+
+ .. code-block:: json
+ :caption: after
+
+ {
+ "status_code": 400,
+ "detail": "Validation failed for POST http://localhost:8000/some-route",
+ "extra": [
+ {"key": "child.my_value", "message": "value is not a valid integer", "source": "body"},
+ {"key": "int_param", "message": "value is not a valid integer", "source": "query"},
+ {"key": "int_header", "message": "value is not a valid integer", "source": "header"},
+ {"key": "int_cookie", "message": "value is not a valid integer", "source": "cookie"},
+ ]
+ }
+
+ .. change:: ``TestClient`` default timeout
+ :type: feature
+ :pr: 1840
+ :breaking:
+
+ A ``timeout`` parameter was added to
+
+ - :class:`~litestar.testing.TestClient`
+ - :class:`~litestar.testing.AsyncTestClient`
+ - :class:`~litestar.testing.create_test_client`
+ - :class:`~litestar.testing.create_async_test_client`
+
+ The value is passed down to the underlying HTTPX client and serves as a default
+ timeout for all requests.
+
+ .. change:: SQLAlchemy DTO: Explicit error messages when type annotations for a column are missing
+ :type: feature
+ :pr: 1852
+
+ Replace the nondescript :exc:`KeyError` raised when a SQLAlchemy DTO is
+ constructed from a model that is missing a type annotation for an included
+ column with an :exc:`ImproperlyConfiguredException`, including an explicit error
+ message, pointing at the potential cause.
+
+ .. change:: Remove exception details from Internal Server Error responses
+ :type: bugfix
+ :pr: 1857
+ :issue: 1856
+
+ Error responses with a ``500`` status code will now always use
+ `"Internal Server Error"` as default detail.
+
+ .. change:: Pydantic v1 regex validation
+ :type: bugfix
+ :pr: 1865
+ :issue: 1860
+
+ A regression has been fixed in the pydantic signature model logic, which was
+ caused by the renaming of ``regex`` to ``pattern``, which would lead to the
+ :attr:`~litestar.params.KwargDefinition.pattern` not being validated.
+
+
+.. changelog:: 2.0.0beta1
+ :date: 2023/06/16
+
+ .. change:: Expose ``ParsedType`` as public API
+ :type: feature
+ :pr: 1677 1567
+
+ Expose the previously private :class:`litestar.typing.ParsedType`. This is
+ mainly indented for usage with
+ :meth:`litestar.plugins.SerializationPluginProtocol.supports_type`
+
+ .. change:: Improved debugging capabilities
+ :type: feature
+ :pr: 1742
+
+ - A new ``pdb_on_exception`` parameter was added to
+ :class:`~litestar.app.Litestar`. When set to ``True``, Litestar will drop into
+ a the Python debugger when an exception occurs. It defaults to ``None``
+ - When ``pdb_on_exception`` is ``None``, setting the environment variable
+ ``LITESTAR_PDB=1`` can be used to enable this behaviour
+ - When using the CLI, passing the ``--pdb`` flag to the ``run`` command will
+ temporarily set the environment variable ``LITESTAR_PDB=1``
+
+ .. change:: OpenAPI: Add `operation_class` argument to HTTP route handlers
+ :type: feature
+ :pr: 1732
+
+ The ``operation_class`` argument was added to
+ :class:`~litestar.handlers.HTTPRouteHandler` and the corresponding decorators,
+ allowing to override the :class:`~litestar.openapi.spec.Operation` class, to
+ enable further customization of the generated OpenAPI schema.
+
+ .. change:: OpenAPI: Support nested ``Literal`` annotations
+ :type: feature
+ :pr: 1829
+
+ Support nested :class:`typing.Literal` annotations by flattening them into
+ a single ``Literal``.
+
+ .. change:: CLI: Add ``--reload-dir`` option to ``run`` command
+ :type: feature
+ :pr: 1689
+
+ A new ``--reload-dir`` option was added to the ``litestar run`` command. When
+ used, ``--reload`` is implied, and the server will watch for changes in the
+ given directory.
+
+ .. change:: Allow extra attributes on JWTs via ``extras`` attribute
+ :type: feature
+ :pr: 1695
+
+ Add the ``litestar.contrib.jwt.Token.extras`` attribute, containing extra
+ attributes found on the JWT.
+
+ .. change:: Add default modes for ``Websocket.iter_json`` and ``WebSocket.iter_data``
+ :type: feature
+ :pr: 1733
+
+ Add a default ``mode`` for :meth:`~litestar.connection.WebSocket.iter_json` and
+ :meth:`~litestar.connection.WebSocket.iter_data`, with a value of ``text``.
+
+ .. change:: SQLAlchemy repository: Synchronous repositories
+ :type: feature
+ :pr: 1683
+
+ Add a new synchronous repository base class:
+ ``litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository``,
+ which offer the same functionality as its asynchronous counterpart while
+ operating on a synchronous :class:`sqlalchemy.orm.Session`.
+
+ .. change:: SQLAlchemy repository: Oracle Database support
+ :type: feature
+ :pr: 1694
+
+ Add support for Oracle Database via
+ `oracledb `_.
+
+ .. change:: SQLAlchemy repository: DuckDB support
+ :type: feature
+ :pr: 1744
+
+ Add support for `DuckDB `_.
+
+ .. change:: SQLAlchemy repository: Google Spanner support
+ :type: feature
+ :pr: 1744
+
+ Add support for `Google Spanner `_.
+
+ .. change:: SQLAlchemy repository: JSON check constraint for Oracle Database
+ :type: feature
+ :pr: 1780
+
+ When using the :class:`litestar.contrib.sqlalchemy.types.JsonB` type with an
+ Oracle Database engine, a JSON check constraint will be created for that
+ column.
+
+ .. change:: SQLAlchemy repository: Remove ``created`` and ``updated`` columns
+ :type: feature
+ :pr: 1816
+ :breaking:
+
+ The ``created`` and ``updated`` columns have been superseded by
+ ``created_at`` and ``updated_at`` respectively, to prevent name clashes.
+
+
+ .. change:: SQLAlchemy repository: Add timezone aware type
+ :type: feature
+ :pr: 1816
+ :breaking:
+
+ A new timezone aware type ``litestar.contrib.sqlalchemy.types.DateTimeUTC``
+ has been added, which enforces UTC timestamps stored in the database.
+
+ .. change:: SQLAlchemy repository: Exclude unloaded columns in ``to_dict``
+ :type: feature
+ :pr: 1802
+
+ When exporting models using the
+ ``~litestar.contrib.sqlalchemy.base.CommonTableAttributes.to_dict`` method,
+ unloaded columns will now always be excluded. This prevents implicit I/O via
+ lazy loading, and errors when using an asynchronous session.
+
+ .. change:: DTOs: Nested keyword arguments in ``.create_instance()``
+ :type: feature
+ :pr: 1741
+ :issue: 1727
+
+ The
+ :meth:`DTOData.create_instance `
+ method now supports providing values for arbitrarily nested data via kwargs
+ using a double-underscore syntax, for example
+ ``data.create_instance(foo__bar="baz")``.
+
+ .. seealso::
+ :ref:`usage/dto/1-abstract-dto:Providing values for nested data`
+
+ .. change:: DTOs: Hybrid properties and association proxies in
+ :class:`SQLAlchemyDTO (Advanced Alchemy) `
+ :type: feature
+ :pr: 1754 1776
+
+ The :class:`SQLAlchemyDTO (Advanced Alchemy) `
+ now supports `hybrid attribute `_
+ and `associationproxy `_.
+
+ The generated field will be marked read-only.
+
+ .. change:: DTOs: Transfer to generic collection types
+ :type: feature
+ :pr: 1764
+ :issue: 1763
+
+ DTOs can now be wrapped in generic collection types such as
+ :class:`typing.Sequence`. These will be substituted with a concrete and
+ instantiable type at run time, e.g. in the case of ``Sequence`` a :class:`list`.
+
+ .. change:: DTOs: Data transfer for non-generic builtin collection annotations
+ :type: feature
+ :pr: 1799
+
+ Non-parametrized generics in annotations (e.g. ``a: dict``) will now be inferred
+ as being parametrized with ``Any``. ``a: dict`` is then equivalent to
+ ``a: dict[Any, Any]``.
+
+ .. change:: DTOs: Exclude leading underscore fields by default
+ :type: feature
+ :pr: 1777
+ :issue: 1768
+ :breaking:
+
+ Leading underscore fields will not be excluded by default. This behaviour can be
+ configured with the newly introduced
+ :attr:`~litestar.dto.factory.DTOConfig.underscore_fields_private` configuration
+ value, which defaults to ``True``.
+
+ .. change:: DTOs: Msgspec and Pydantic DTO factory implementation
+ :type: feature
+ :pr: 1712
+ :issue: 1531, 1532
+
+ DTO factories for `msgspec `_ and
+ `Pydantic `_ have been added:
+
+ - :class:`~litestar.contrib.msgspec.MsgspecDTO`
+ - :class:`~litestar.contrib.pydantic.PydanticDTO`
+
+ .. change:: DTOs: Arbitrary generic wrappers
+ :pr: 1801
+ :issue: 1631, 1798
+
+ When a handler returns a type that is not supported by the DTO, but:
+
+ - the return type is generic
+ - it has a generic type argument that is supported by the dto
+ - the type argument maps to an attribute on the return type
+
+ the DTO operations will be performed on the data retrieved from that attribute
+ of the instance returned from the handler, and return the instance.
+
+ The constraints are:
+
+ - the type returned from the handler must be a type that litestar can
+ natively encode
+ - the annotation of the attribute that holds the data must be a type that DTOs
+ can otherwise manage
+
+ .. code-block:: python
+
+ from dataclasses import dataclass
+ from typing import Generic, List, TypeVar
+
+ from typing_extensions import Annotated
+
+ from litestar import Litestar, get
+ from litestar.dto import DTOConfig
+ from litestar.dto.factory.dataclass_factory import DataclassDTO
+
+
+ @dataclass
+ class User:
+ name: str
+ age: int
+
+
+ T = TypeVar("T")
+ V = TypeVar("V")
+
+
+ @dataclass
+ class Wrapped(Generic[T, V]):
+ data: List[T]
+ other: V
+
+
+ @get(dto=DataclassDTO[Annotated[User, DTOConfig(exclude={"age"})]])
+ def handler() -> Wrapped[User, int]:
+ return Wrapped(
+ data=[User(name="John", age=42), User(name="Jane", age=43)],
+ other=2,
+ )
+
+
+ app = Litestar(route_handlers=[handler])
+
+ # GET "/": {"data": [{"name": "John"}, {"name": "Jane"}], "other": 2}
+
+ .. change:: Store and reuse state `deep_copy` directive when copying state
+ :type: bugfix
+ :issue: 1674
+ :pr: 1678
+
+ App state can be created using ``deep_copy=False``, however state would still be
+ deep copied for dependency injection.
+
+ This was fixed memoizing the value of ``deep_copy`` when state is created, and
+ reusing it on subsequent copies.
+
+ .. change:: ``ParsedType.is_subclass_of(X)`` ``True`` for union if all union types are subtypes of ``X``
+ :type: bugfix
+ :pr: 1690
+ :issue: 1652
+
+ When :class:`~litestar.typing.ParsedType` was introduced,
+ :meth:`~litestar.typing.ParsedType.is_subclass_of` any union was deliberately
+ left to return ``False`` with the intention of waiting for some use-cases to
+ arrive.
+
+ This behaviour was changed to address an issue where a handler may be typed to
+ return a union of multiple response types; If all response types are
+ :class:`~litestar.response.Response` subtypes then the correct response handler
+ will now be applied.
+
+ .. change:: Inconsistent template autoescape behavior
+ :type: bugfix
+ :pr: 1718
+ :issue: 1699
+
+ The mako template engine now defaults to autoescaping expressions, making it
+ consistent with config of Jinja template engine.
+
+ .. change:: Missing ``ChannelsPlugin`` in signature namespace population
+ :type: bugfix
+ :pr: 1719
+ :issue: 1691
+
+ The :class:`~litestar.channels.plugin.ChannelsPlugin` has been added to the
+ signature namespace, fixing an issue where using
+ ``from __future__ import annotations`` or stringized annotations would lead to
+ a :exc:`NameError`, if the plugin was not added to the signatured namespace
+ manually.
+
+ .. change:: Gzip middleware not sending small streaming responses
+ :type: bugfix
+ :pr: 1723
+ :issue: 1681
+
+ A bug was fixed that would cause smaller streaming responses to not be sent at
+ all when the :class:`~litestar.middleware.compression.CompressionMiddleware` was
+ used with ``gzip``.
+
+ .. change:: Premature transfer to nested models with `DTOData`
+ :type: bugfix
+ :pr: 1731
+ :issue: 1726
+
+ An issue was fixed where data that should be transferred to builtin types on
+ instantiation of :class:`~litestar.dto.factory.DTOData` was being instantiated
+ into a model type for nested models.
+
+ .. change:: Incorrect ``sync_to_thread`` usage warnings for generator dependencies
+ :type: bugfix
+ :pr: 1716 1740
+ :issue: 1711
+
+ A bug was fixed that caused incorrect warnings about missing ``sync_to_thread``
+ usage were issues when asynchronous generators were being used as dependencies.
+
+ .. change:: Dependency injection custom dependencies in ``WebSocketListener``
+ :type: bugfix
+ :pr: 1807
+ :issue: 1762
+
+ An issue was resolved that would cause failures when dependency injection was
+ being used with custom dependencies (that is, injection of things other than
+ ``state``, ``query``, path parameters, etc.) within a
+ :class:`~litestar.handlers.WebsocketListener`.
+
+ .. change:: OpenAPI schema for ``Dict[K, V]`` ignores generic
+ :type: bugfix
+ :pr: 1828
+ :issue: 1795
+
+ An issue with the OpenAPI schema generation was fixed that would lead to generic
+ arguments to :class:`dict` being ignored.
+
+ An type like ``dict[str, int]`` now correctly renders as
+ ``{"type": "object", "additionalProperties": { "type": "integer" }}``.
+
+ .. change:: ``WebSocketTestSession`` not timing out without when connection is not accepted
+ :type: bugfix
+ :pr: 1696
+
+ A bug was fixed that caused :class:`~litestar.testing.WebSocketTestSession` to
+ block indefinitely when if :meth:`~litestar.connection.WebSocket.accept` was
+ never called, ignoring the ``timeout`` parameter.
+
+ .. change:: SQLAlchemy repository: Fix alembic migrations generated for models using ``GUID``
+ :type: bugfix
+ :pr: 1676
+
+ Migrations generated for models with a
+ ``~litestar.contrib.sqlalchemy.types.GUID`` type would erroneously add a
+ ``length=16`` on the input. Since this parameter is not defined in the type's
+ the ``__init__`` method. This was fixed by adding the appropriate parameter to
+ the type's signature.
+
+ .. change:: Remove ``state`` parameter from ``AfterExceptionHookHandler`` and ``BeforeMessageSendHookHandler``
+ :type: misc
+ :pr: 1739
+ :breaking:
+
+ Remove the ``state`` parameter from ``AfterExceptionHookHandler`` and
+ ``BeforeMessageSendHookHandler``.
+
+ ``AfterExceptionHookHandler``\ s will have to be updated from
+
+ .. code-block:: python
+
+ async def after_exception_handler(
+ exc: Exception, scope: Scope, state: State
+ ) -> None: ...
+
+ to
+
+ .. code-block:: python
+
+ async def after_exception_handler(exc: Exception, scope: Scope) -> None: ...
+
+ The state can still be accessed like so:
+
+ .. code-block:: python
+
+ async def after_exception_handler(exc: Exception, scope: Scope) -> None:
+ state = scope["app"].state
+
+
+ ``BeforeMessageSendHookHandler``\ s will have to be updated from
+
+ .. code-block:: python
+
+ async def before_send_hook_handler(
+ message: Message, state: State, scope: Scope
+ ) -> None: ...
+
+
+ to
+
+ .. code-block:: python
+
+ async def before_send_hook_handler(message: Message, scope: Scope) -> None: ...
+
+ where state can be accessed in the same manner:
+
+ .. code-block:: python
+
+ async def before_send_hook_handler(message: Message, scope: Scope) -> None:
+ state = scope["app"].state
+
+ .. change:: Removal of ``dto.exceptions`` module
+ :pr: 1773
+ :breaking:
+
+ The module ``dto.exceptions`` has been removed, since it was not used anymore
+ internally by the DTO implementations, and superseded by standard exceptions.
+
+
+ .. change:: ``BaseRouteHandler`` no longer generic
+ :pr: 1819
+ :breaking:
+
+ :class:`~litestar.handlers.BaseRouteHandler` was originally made generic to
+ support proper typing of the ``ownership_layers`` property, but the same effect
+ can now be achieved using :class:`typing.Self`.
+
+ .. change:: Deprecation of ``Litestar`` parameter ``preferred_validation_backend``
+ :pr: 1810
+ :breaking:
+
+ The following changes have been made regarding the
+ ``preferred_validation_backend``:
+
+ - The ``preferred_validation_backend`` parameter of
+ :class:`~litestar.app.Litestar` has been renamed to
+ ``_preferred_validation_backend`` and deprecated. It will be removed
+ completely in a future version.
+ - The ``Litestar.preferred_validation_backend`` attribute has been made private
+ - The ``preferred_validation_backend`` attribute has been removed from
+ :class:`~litestar.config.app.AppConfig`
+
+ In addition, the logic for selecting a signature validation backend has been
+ simplified as follows: If the preferred backend is set to ``attrs``, or the
+ signature contains attrs types, ``attrs`` is selected. In all other cases,
+ Pydantic will be used.
+
+ .. change:: ``Response.get_serializer`` moved to ``serialization.get_serializer``
+ :pr: 1820
+ :breaking:
+
+
+ The ``Response.get_serializer()`` method has been removed in favor of the
+ :func:`~litestar.serialization.get_serializer` function.
+
+ In the previous :class:`~litestar.response.Response` implementation,
+ ``get_serializer()`` was called on the response inside the response's
+ ``__init__``, and the merging of class-level ``type_encoders`` with the
+ ``Response``\ 's ``type_encoders`` occurred inside its ``get_serializer``
+ method.
+
+ In the current version of ``Response``, the response body is not encoded until
+ after the response object has been returned from the handler, and it is
+ converted into a low-level :class:`~litestar.response.base.ASGIResponse` object.
+ Due to this, there is still opportunity for the handler layer resolved
+ ``type_encoders`` object to be merged with the ``Response`` defined
+ ``type_encoders``, making the merge inside the ``Response`` no longer necessary.
+
+ In addition, the separate ``get_serializer`` function greatly simplifies the
+ interaction between middlewares and serializers, allowing to retrieve one
+ independently from a ``Response``.
+
+ .. change:: Remove response containers and introduce ``ASGIResponse``
+ :pr: 1790
+ :breaking:
+
+ Response Containers were wrapper classes used to indicate the type of response
+ returned by a handler, for example ``File``, ``Redirect``, ``Template`` and
+ ``Stream`` types. These types abstracted the interface of responses from the
+ underlying response itself.
+
+ Response containers have been removed and their functionality largely merged with
+ that of :class:`~litestar.response.Response`. The predefined response containers
+ still exist functionally, as subclasses of
+ :class:`Response <.response.Response>` and are now located within the
+ :mod:`litestar.response` module.
+ In addition to the functionality of Response containers, they now also feature
+ all of the response's functionality, such as methods to add headers and cookies.
+
+ The :class:`~litestar.response.Response` class now only serves as a wrapper and
+ context object, and does not handle the data sending part, which has been
+ delegated to a newly introduced
+ :class:`ASGIResponse <.response.base.ASGIResponse>`. This type (and its
+ subclasses) represent the response as an immutable object and are used
+ internally by Litestar to perform the I/O operations of the response. These can
+ be created and returned from handlers like any other ASGI application, however
+ they are low-level, and lack the utility of the higher-level response types.
+
+
+
+.. changelog:: 2.0.0alpha7
+ :date: 2023/05/14
+
+ .. change:: Warn about sync callables in route handlers and dependencies without an explicit ``sync_to_thread`` value
+ :type: feature
+ :pr: 1648 1655
+
+ A warning will now be raised when a synchronous callable is being used in an
+ :class:`~.handlers.HTTPRouteHandler` or :class:`~.di.Provide`, without setting
+ ``sync_to_thread``. This is to ensure that synchronous callables are handled
+ properly, and to prevent accidentally using callables which might block the main
+ thread.
+
+ This warning can be turned off globally by setting the environment variable
+ ``LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0``.
+
+ .. seealso::
+ :doc:`/topics/sync-vs-async`
+
+
+ .. change:: Warn about ``sync_to_thread`` with async callables
+ :type: feature
+ :pr: 1664
+
+ A warning will be raised when ``sync_to_thread`` is being used in
+ :class:`~.handlers.HTTPRouteHandler` or :class:`~.di.Provide` with an
+ asynchronous callable, as this will have no effect.
+
+ This warning can be turned off globally by setting the environment variable
+ ``LITESTAR_WARN_SYNC_TO_THREAD_WITH_ASYNC=0``.
+
+
+ .. change:: WebSockets: Dependencies in listener hooks
+ :type: feature
+ :pr: 1647
+
+ Dependencies can now be used in the
+ :func:`~litestar.handlers.websocket_listener` hooks
+ ``on_accept``, ``on_disconnect`` and the ``connection_lifespan`` context
+ manager. The ``socket`` parameter is therefore also not mandatory anymore in
+ those callables.
+
+ .. change:: Declaring dependencies without ``Provide``
+ :type: feature
+ :pr: 1647
+
+ Dependencies can now be declared without using :class:`~litestar.di.Provide`.
+ The callables can be passed directly to the ``dependencies`` dictionary.
+
+
+ .. change:: Add ``DTOData`` to receive unstructured but validated DTO data
+ :type: feature
+ :pr: 1650
+
+ :class:`~litestar.dto.factory.DTOData` is a datastructure for interacting with
+ DTO validated data in its unstructured form.
+
+ This utility is to support the case where the amount of data that is available
+ from the client request is not complete enough to instantiate an instance of the
+ model that would otherwise be injected.
+
+
+ .. change:: Partial DTOs
+ :type: feature
+ :pr: 1651
+
+ Add a ``partial`` flag to :class:`~litestar.dto.factory.DTOConfig`, making all
+ DTO fields options. Subsequently, any unset values will be filtered when
+ extracting data from transfer models.
+
+ This allows for example to use a to handle PATCH requests more easily.
+
+
+ .. change:: SQLAlchemy repository: ``psycopg`` asyncio support
+ :type: feature
+ :pr: 1657
+
+ Async `psycopg `_ is now officially supported and
+ tested for the SQLAlchemy repository.
+
+ .. change:: SQLAlchemy repository: ``BigIntPrimaryKey`` mixin
+ :type: feature
+ :pr: 1657
+
+ ``~litestar.contrib.sqlalchemy.base.BigIntPrimaryKey`` mixin, providing a
+ ``BigInt`` primary key column, with a fallback to ``Integer`` for sqlite.
+
+ .. change:: SQLAlchemy repository: Store GUIDs as binary on databases that don't have a native GUID type
+ :type: feature
+ :pr: 1657
+
+ On databases without native support for GUIDs,
+ ``~litestar.contrib.sqlalchemy.types.GUID`` will now fall back to
+ ``BINARY(16)``.
+
+ .. change:: Application lifespan context managers
+ :type: feature
+ :pr: 1635
+
+ A new ``lifespan`` argument has been added to :class:`~litestar.app.Litestar`,
+ accepting an asynchronous context manager, wrapping the lifespan of the
+ application. It will be entered with the startup phase and exited on shutdown,
+ providing functionality equal to the ``on_startup`` and ``on_shutdown`` hooks.
+
+ .. change:: Unify application lifespan hooks: Remove ``before_`` and ``after_``
+ :breaking:
+ :type: feature
+ :pr: 1663
+
+ The following application lifespan hooks have been removed:
+
+ - ``before_startup``
+ - ``after_startup``
+ - ``before_shutdown``
+ - ``after_shutdown``
+
+ The remaining hooks ``on_startup`` and ``on_shutdown`` will now receive as their
+ optional first argument the :class:`~litestar.app.Litestar` application instead
+ of the application's state.
+
+ .. change:: Trio-compatible event emitter
+ :type: feature
+ :pr: 1666
+
+ The default :class:`~litestar.events.emitter.SimpleEventEmitter` is now
+ compatible with `trio `_.
+
+
+ .. change:: OpenAPI: Support ``msgspec.Meta``
+ :type: feature
+ :pr: 1669
+
+ :class:`msgspec.Meta` is now fully supported for OpenAPI schema generation.
+
+ .. change:: OpenAPI: Support Pydantic ``FieldInfo``
+ :type: feature
+ :pr: 1670
+ :issue: 1541
+
+ Pydantic's ``FieldInfo`` (``regex``, ``gt``, ``title``, etc.) now have full
+ support for OpenAPI schema generation.
+
+ .. change:: OpenAPI: Fix name collision in DTO models
+ :type: bugfix
+ :pr: 1649
+ :issue: 1643
+
+ A bug was fixed that would lead to name collisions in the OpenAPI schema when
+ using DTOs with the same class name. DTOs now include a short 8 byte random
+ string in their generated name to prevent this.
+
+ .. change:: Fix validated attrs model being injected as a dictionary
+ :type: bugfix
+ :pr: 1668
+ :issue: 1643
+
+ A bug was fixed that would lead to an attrs model used to validate a route
+ handler's ``data`` not being injected itself but as a dictionary representation.
+
+
+ .. change:: Validate unknown media types
+ :breaking:
+ :type: bugfix
+ :pr: 1671
+ :issue: 1446
+
+ An unknown media type in places where Litestar can't infer the type from the
+ return annotation, an :exc:`ImproperlyConfiguredException` will now be raised.
+
+
+.. changelog:: 2.0.0alpha6
+ :date: 2023/05/09
+
+ .. change:: Relax typing of ``**kwargs`` in ``ASGIConnection.url_for``
+ :type: bugfix
+ :pr: 1610
+
+ Change the typing of the ``**kwargs`` in
+ :meth:`ASGIConnection.url_for ` from
+ ``dict[str, Any]`` to ``Any``
+
+
+ .. change:: Fix: Using ``websocket_listener`` in controller causes ``TypeError``
+ :type: bugfix
+ :pr: 1627
+ :issue: 1615
+
+ A bug was fixed that would cause a type error when using a
+ :func:`websocket_listener `
+ in a ``Controller``
+
+ .. change:: Add ``connection_accept_handler`` to ``websocket_listener``
+ :type: feature
+ :pr: 1572
+ :issue: 1571
+
+ Add a new ``connection_accept_handler`` parameter to
+ :func:`websocket_listener `,
+ which can be used to customize how a connection is accepted, for example to
+ add headers or subprotocols
+
+ .. change:: Testing: Add ``block`` and ``timeout`` parameters to ``WebSocketTestSession`` receive methods
+ :type: feature
+ :pr: 1593
+
+ Two parameters, ``block`` and ``timeout`` have been added to the following methods:
+
+ - :meth:`receive `
+ - :meth:`receive_text `
+ - :meth:`receive_bytes `
+ - :meth:`receive_json `
+
+ .. change:: CLI: Add ``--app-dir`` option to root command
+ :type: feature
+ :pr: 1506
+
+ The ``--app-dir`` option was added to the root CLI command, allowing to set the
+ run applications from a path that's not the current working directory.
+
+
+ .. change:: WebSockets: Data iterators
+ :type: feature
+ :pr: 1626
+
+ Two new methods were added to the :class:`WebSocket `
+ connection, which allow to continuously receive data and iterate over it:
+
+ - :meth:`iter_data `
+ - :meth:`iter_json `
+
+
+ .. change:: WebSockets: MessagePack support
+ :type: feature
+ :pr: 1626
+
+ Add support for `MessagePack `_ to the
+ :class:`WebSocket ` connection.
+
+ Three new methods have been added for handling MessagePack:
+
+ - :meth:`send_msgpack `
+ - :meth:`receive_msgpack `
+ - :meth:`iter_msgpack `
+
+ In addition, two MessagePack related methods were added to
+ :class:`WebSocketTestSession `:
+
+ - :meth:`send_msgpack `
+ - :meth:`receive_msgpack `
+
+ .. change:: SQLAlchemy repository: Add support for sentinel column
+ :type: feature
+ :pr: 1603
+
+ This change adds support for ``sentinel column`` feature added in ``sqlalchemy``
+ 2.0.10. Without it, there are certain cases where ``add_many`` raises an
+ exception.
+
+ The ``_sentinel`` value added to the declarative base should be excluded from
+ normal select operations automatically and is excluded in the ``to_dict``
+ methods.
+
+ .. change:: DTO: Alias generator for field names
+ :type: feature
+ :pr: 1590
+
+ A new argument ``rename_strategy`` has been added to the :class:`DTOConfig `,
+ allowing to remap field names with strategies such as "camelize".
+
+ .. change:: DTO: Nested field exclusion
+ :type: feature
+ :pr: 1596
+ :issue: 1197
+
+ This feature adds support for excluding nested model fields using dot-notation,
+ e.g., ``"a.b"`` excludes field ``b`` from nested model field ``a``
+
+ .. change:: WebSockets: Managing a socket's lifespan using a context manager in websocket listeners
+ :type: feature
+ :pr: 1625
+
+ Changes the way a socket's lifespan - accepting the connection and calling the
+ appropriate event hooks - to use a context manager.
+
+ The ``connection_lifespan`` argument was added to the
+ :func:`WebSocketListener `, which accepts
+ an asynchronous context manager, which can be used to handle the lifespan of
+ the socket.
+
+ .. change:: New module: Channels
+ :type: feature
+ :pr: 1587
+
+ A new module :doc:`channels ` has been added: A general purpose
+ event streaming library, which can for example be used to broadcast messages
+ via WebSockets.
+
+ .. change:: DTO: Undocumented ``dto.factory.backends`` has been made private
+ :breaking:
+ :type: misc
+ :pr: 1589
+
+ The undocumented ``dto.factory.backends`` module has been made private
+
+
+
+.. changelog:: 2.0.0alpha5
+
+ .. change:: Pass template context to HTMX template response
+ :type: feature
+ :pr: 1488
+
+ Pass the template context to the :class:`Template ` returned by
+ :class:`htmx.Response `.
+
+
+ .. change:: OpenAPI support for attrs and msgspec classes
+ :type: feature
+ :pr: 1487
+
+ Support OpenAPI schema generation for `attrs `_ classes and
+ `msgspec `_ ``Struct``\ s.
+
+ .. change:: SQLAlchemy repository: Add ``ModelProtocol``
+ :type: feature
+ :pr: 1503
+
+ Add a new class ``contrib.sqlalchemy.base.ModelProtocol``, serving as a generic model base type, allowing to
+ specify custom base classes while preserving typing information
+
+ .. change:: SQLAlchemy repository: Support MySQL/MariaDB
+ :type: feature
+ :pr: 1345
+
+ Add support for MySQL/MariaDB to the SQLAlchemy repository, using the
+ `asyncmy `_ driver.
+
+ .. change:: SQLAlchemy repository: Support MySQL/MariaDB
+ :type: feature
+ :pr: 1345
+
+ Add support for MySQL/MariaDB to the SQLAlchemy repository, using the
+ `asyncmy `_ driver.
+
+ .. change:: SQLAlchemy repository: Add matching logic to ``get_or_create``
+ :type: feature
+ :pr: 1345
+
+ Add a ``match_fields`` argument to
+ ``litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get_or_create``.
+ This lets you lookup a model using a subset of the kwargs you've provided. If the remaining kwargs are different
+ from the retrieved model's stored values, an update is performed.
+
+ .. change:: Repository: Extend filter types
+ :type: feature
+ :pr: 1345
+
+ Add new filters ``litestar.contrib.repository.filters.OrderBy`` and
+ ``litestar.contrib.repository.filters.SearchFilter``, providing ``ORDER BY ...`` and
+ ``LIKE ...`` / ``ILIKE ...`` clauses respectively
+
+ .. change:: SQLAlchemy repository: Rename ``SQLAlchemyRepository`` > ``SQLAlchemyAsyncRepository``
+ :breaking:
+ :type: misc
+ :pr: 1345
+
+ ``SQLAlchemyRepository`` has been renamed to
+ ``litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository``.
+
+
+ .. change:: DTO: Add ``AbstractDTOFactory`` and backends
+ :type: feature
+ :pr: 1461
+
+ An all-new DTO implementation was added, using ``AbstractDTOFactory`` as a base class, providing Pydantic and
+ msgspec backends to facilitate (de)serialization and validation.
+
+ .. change:: DTO: Remove ``from_connection`` / extend ``from_data``
+ :breaking:
+ :type: misc
+ :pr: 1500
+
+ The method ``DTOInterface.from_connection`` has been removed and replaced by ``DTOInterface.from_bytes``, which
+ receives both the raw bytes from the connection, and the connection instance. Since ``from_bytes`` now does not
+ handle connections anymore, it can also be a synchronous method, improving symmetry with
+ ``DTOInterface.from_bytes``.
+
+ The signature of ``from_data`` has been changed to also accept the connection, matching ``from_bytes``'
+ signature.
+
+ As a result of these changes,
+ :meth:`DTOInterface.from_bytes ` no longer needs to
+ receive the connection instance, so the ``request`` parameter has been dropped.
+
+ .. change:: WebSockets: Support DTOs in listeners
+ :type: feature
+ :pr: 1518
+
+ Support for DTOs has been added to :class:`WebSocketListener ` and
+ :func:`WebSocketListener `. A ``dto`` and ``return_dto`` parameter has
+ been added, providing the same functionality as their route handler counterparts.
+
+ .. change:: DTO based serialization plugin
+ :breaking:
+ :type: feature
+ :pr: 1501
+
+ :class:`SerializationPluginProtocol ` has been re-implemented,
+ leveraging the new :class:`DTOInterface `.
+
+ If a handler defines a plugin supported type as either the ``data`` kwarg type annotation, or as the return
+ annotation for a handler function, and no DTO has otherwise been resolved to handle the type, the protocol
+ creates a DTO implementation to represent that type which is then used to de-serialize into, and serialize from
+ instances of that supported type.
+
+ .. important::
+ The `Piccolo ORM `_ and `Tortoise ORM `_ plugins have
+ been removed by this change, but will be re-implemented using the new patterns in a future release leading
+ up to the 2.0 release.
+
+ .. change:: SQLAlchemy 1 contrib module removed
+ :breaking:
+ :type: misc
+ :pr: 1501
+
+ As a result of the changes introduced in `#1501 `_,
+ SQLAlchemy 1 support has been dropped.
+
+ .. note::
+ If you rely on SQLAlchemy 1, you can stick to Starlite *1.51* for now. In the future, a SQLAlchemy 1 plugin
+ may be released as a standalone package.
+
+ .. change:: Fix inconsistent parsing of unix timestamp between pydantic and cattrs
+ :type: bugfix
+ :pr: 1492
+ :issue: 1491
+
+ Timestamps parsed as :class:`date ` with pydantic return a UTC date, while cattrs implementation
+ return a date with the local timezone.
+
+ This was corrected by forcing dates to UTC when being parsed by attrs.
+
+ .. change:: Fix: Retrieve type hints from class with no ``__init__`` method causes error
+ :type: bugfix
+ :pr: 1505
+ :issue: 1504
+
+ An error would occur when using a callable without an :meth:`object.__init__` method was used in a placed that
+ would cause it to be inspected (such as a route handler's signature).
+
+ This was caused by trying to access the ``__module__`` attribute of :meth:`object.__init__`, which would fail
+ with
+
+ .. code-block::
+
+ 'wrapper_descriptor' object has no attribute '__module__'
+
+ .. change:: Fix error raised for partially installed attrs dependencies
+ :type: bugfix
+ :pr: 1543
+
+ An error was fixed that would cause a :exc:`MissingDependencyException` to be raised when dependencies for
+ `attrs `_ were partially installed. This was fixed by being more specific about the
+ missing dependencies in the error messages.
+
+ .. change:: Change ``MissingDependencyException`` to be a subclass of ``ImportError``
+ :type: misc
+ :pr: 1557
+
+ :exc:`MissingDependencyException` is now a subclass of :exc:`ImportError`, to make handling cases where both
+ of them might be raised easier.
+
+ .. change:: Remove bool coercion in URL parsing
+ :breaking:
+ :type: bugfix
+ :pr: 1550
+ :issue: 1547
+
+ When defining a query parameter as ``param: str``, and passing it a string value of ``"true"``, the value
+ received by the route handler was the string ``"True"``, having been title cased. The same was true for the value
+ of ``"false"``.
+
+ This has been fixed by removing the coercing of boolean-like values during URL parsing and leaving it up to
+ the parsing utilities of the receiving side (i.e. the handler's signature model) to handle these values
+ according to the associated type annotations.
+
+ .. change:: Update ``standard`` and ``full`` package extras
+ :type: misc
+ :pr: 1494
+
+ - Add SQLAlchemy, uvicorn, attrs and structlog to the ``full`` extra
+ - Add uvicorn to the ``standard`` extra
+ - Add ``uvicorn[standard]`` as an optional dependency to be used in the extras
+
+ .. change:: Remove support for declaring DTOs as handler types
+ :breaking:
+ :type: misc
+ :pr: 1534
+
+ Prior to this, a DTO type could be declared implicitly using type annotations. With the addition of the ``dto``
+ and ``return_dto`` parameters, this feature has become superfluous and, in the spirit of offering only one clear
+ way of doing things, has been removed.
+
+ .. change:: Fix missing ``content-encoding`` headers on gzip/brotli compressed files
+ :type: bugfix
+ :pr: 1577
+ :issue: 1576
+
+ Fixed a bug that would cause static files served via ``StaticFilesConfig`` that have been compressed with gripz
+ or brotli to miss the appropriate ``content-encoding`` header.
+
+ .. change:: DTO: Simplify ``DTOConfig``
+ :type: misc
+ :breaking:
+ :pr: 1580
+
+ - The ``include`` parameter has been removed, to provide a more accessible interface and avoid overly complex
+ interplay with ``exclude`` and its support for dotted attributes
+ - ``field_mapping`` has been renamed to ``rename_fields`` and support to remap field types has been dropped
+ - experimental ``field_definitions`` has been removed. It may be replaced with a "ComputedField" in a future
+ release that will allow multiple field definitions to be added to the model, and a callable that transforms
+ them into a value for a model field. See
+
+
+.. changelog:: 2.0.0alpha4
+
+ .. change:: ``attrs`` and ``msgspec`` support in :class:`Partial `
+ :type: feature
+ :pr: 1462
+
+ :class:`Partial ` now supports constructing partial models for attrs and msgspec
+
+ .. change:: :class:`Annotated ` support for route handler and dependency annotations
+ :type: feature
+ :pr: 1462
+
+ :class:`Annotated ` can now be used in route handler and dependencies to specify additional
+ information about the fields.
+
+ .. code-block:: python
+
+ @get("/")
+ def index(param: int = Parameter(gt=5)) -> dict[str, int]: ...
+
+ .. code-block:: python
+
+ @get("/")
+ def index(param: Annotated[int, Parameter(gt=5)]) -> dict[str, int]: ...
+
+ .. change:: Support ``text/html`` Media-Type in ``Redirect`` response container
+ :type: bugfix
+ :issue: 1451
+ :pr: 1474
+
+ The media type in :class:`Redirect ` won't be forced to ``text/plain`` anymore and
+ now supports setting arbitrary media types.
+
+
+ .. change:: Fix global namespace for type resolution
+ :type: bugfix
+ :pr: 1477
+ :issue: 1472
+
+ Fix a bug where certain annotations would cause a :exc:`NameError`
+
+
+ .. change:: Add uvicorn to ``cli`` extra
+ :type: bugfix
+ :issue: 1478
+ :pr: 1480
+
+ Add the ``uvicorn`` package to the ``cli`` extra, as it is required unconditionally
+
+
+ .. change:: Update logging levels when setting ``Litestar.debug`` dynamically
+ :type: bugfix
+ :issue: 1476
+ :pr: 1482
+
+ When passing ``debug=True`` to :class:`Litestar `, the ``litestar`` logger would be set
+ up in debug mode, but changing the ``debug`` attribute after the class had been instantiated did not update the
+ logger accordingly.
+
+ This lead to a regression where the ``--debug`` flag to the CLI's ``run`` command would no longer have the
+ desired affect, as loggers would still be on the ``INFO`` level.
+
+
+.. changelog:: 2.0.0alpha3
+
+ .. change:: SQLAlchemy 2.0 Plugin
+ :type: feature
+ :pr: 1395
+
+ A :class:`SQLAlchemyInitPlugin ` was added,
+ providing support for managed synchronous and asynchronous sessions.
+
+ .. seealso::
+ :doc:`/usage/databases/sqlalchemy/index`
+
+ .. change:: Attrs signature modelling
+ :type: feature
+ :pr: 1382
+
+ Added support to model route handler signatures with attrs instead of Pydantic
+
+ .. change:: Support setting status codes in ``Redirect`` container
+ :type: feature
+ :pr: 1412
+ :issue: 1371
+
+ Add support for manually setting status codes in the
+ :class:`RedirectResponse ` response container.
+ This was previously only possible by setting the ``status_code`` parameter on
+ the corresponding route handler, making dynamic redirect status codes and
+ conditional redirects using this container hard to implement.
+
+ .. change:: Sentinel value to support caching responses indefinitely
+ :type: feature
+ :pr: 1414
+ :issue: 1365
+
+ Add the :class:`CACHE_FOREVER ` sentinel value, that, when passed
+ to a route handlers ``cache argument``, will cause it to be cached forever, skipping the default expiration.
+
+ Additionally, add support for setting
+ :attr:`ResponseCacheConfig.default_expiration ` to ``None``,
+ allowing to cache values indefinitely by default when setting ``cache=True`` on a route handler.
+
+ .. change:: `Accept`-header parsing and content negotiation
+ :type: feature
+ :pr: 1317
+
+ Add an :attr:`accept ` property to
+ :class:`Request `, returning the newly added
+ :class:`Accept ` header wrapper, representing the requests ``Accept``
+ HTTP header, offering basic content negotiation.
+
+ .. seealso::
+ :ref:`usage/responses:Content Negotiation`
+
+ .. change:: Enhanced WebSockets support
+ :type: feature
+ :pr: 1402
+
+ Add a new set of features for handling WebSockets, including automatic connection handling, (de)serialization
+ of incoming and outgoing data analogous to route handlers and OOP based event dispatching.
+
+ .. seealso::
+ :doc:`/usage/websockets`
+
+ .. change:: SQLAlchemy 1 plugin mutates app state destructively
+ :type: bugfix
+ :pr: 1391
+ :issue: 1368
+
+ When using the SQLAlchemy 1 plugin, repeatedly running through the application lifecycle (as done when testing
+ an application not provided by a factory function), would result in a :exc:`KeyError` on the second pass.
+
+ This was caused be the plugin's ``on_shutdown`` handler deleting the ``engine_app_state_key`` from the
+ application's state on application shutdown, but only adding it on application init.
+
+ This was fixed by adding performing the necessary setup actions on application startup rather than init.
+
+ .. change:: Fix SQLAlchemy 1 Plugin - ``'Request' object has no attribute 'dict'``
+ :type: bugfix
+ :pr: 1389
+ :issue: 1388
+
+ An annotation such as
+
+ .. code-block:: python
+
+ async def provide_user(request: Request[User, Token, Any]) -> User: ...
+
+ would result in the error ``'Request' object has no attribute 'dict'``.
+
+ This was fixed by changing how ``get_plugin_for_value`` interacts with :func:`typing.get_args`
+
+ .. change:: Support OpenAPI schema generation with stringized return annotation
+ :type: bugfix
+ :pr: 1410
+ :issue: 1409
+
+ The following code would result in non-specific and incorrect information being generated for the OpenAPI schema:
+
+ .. code-block:: python
+
+ from __future__ import annotations
+
+ from starlite import Starlite, get
+
+
+ @get("/")
+ def hello_world() -> dict[str, str]:
+ return {"hello": "world"}
+
+ This could be alleviated by removing ``from __future__ import annotations``. Stringized annotations in any form
+ are now fully supported.
+
+ .. change:: Fix OpenAPI schema generation crashes for models with ``Annotated`` type attribute
+ :type: bugfix
+ :issue: 1372
+ :pr: 1400
+
+ When using a model that includes a type annotation with :class:`typing.Annotated` in a route handler, the
+ interactive documentation would raise an error when accessed. This has been fixed and :class:`typing.Annotated`
+ is now fully supported.
+
+ .. change:: Support empty ``data`` in ``RequestFactory``
+ :type: bugfix
+ :issue: 1419
+ :pr: 1420
+
+ Add support for passing an empty ``data`` parameter to a
+ :class:`RequestFactory `, which would previously lead to an error.
+
+ .. change:: ``create_test_client`` and ``crate_async_test_client`` signatures and docstrings to to match ``Litestar``
+ :type: misc
+ :pr: 1417
+
+ Add missing parameters to :func:`create_test_client ` and
+ :func:`create_test_client `. The following parameters were added:
+
+ - ``cache_control``
+ - ``debug``
+ - ``etag``
+ - ``opt``
+ - ``response_cache_config``
+ - ``response_cookies``
+ - ``response_headers``
+ - ``security``
+ - ``stores``
+ - ``tags``
+ - ``type_encoders``
+
+
+
+.. changelog:: 2.0.0alpha2
+
+ .. change:: Repository contrib & SQLAlchemy repository
+ :type: feature
+ :pr: 1254
+
+ Add a a ``repository`` module to ``contrib``, providing abstract base classes
+ to implement the repository pattern. Also added was the ``contrib.repository.sqlalchemy``
+ module, implementing a SQLAlchemy repository, offering hand-tuned abstractions
+ over commonly used tasks, such as handling of object sessions, inserting,
+ updating and upserting individual models or collections.
+
+ .. change:: Data stores & registry
+ :type: feature
+ :pr: 1330
+ :breaking:
+
+ The ``starlite.storage`` module added in the previous version has been
+ renamed ``starlite.stores`` to reduce ambiguity, and a new feature, the
+ ``starlite.stores.registry.StoreRegistry`` has been introduced;
+ It serves as a central place to manage stores and reduces the amount of
+ configuration needed for various integrations.
+
+ - Add ``stores`` kwarg to ``Starlite`` and ``AppConfig`` to allow seeding of the ``StoreRegistry``
+ - Add ``Starlite.stores`` attribute, containing a ``StoreRegistry``
+ - Change ``RateLimitMiddleware`` to use ``app.stores``
+ - Change request caching to use ``app.stores``
+ - Change server side sessions to use ``app.stores``
+ - Move ``starlite.config.cache.CacheConfig`` to ``starlite.config.response_cache.ResponseCacheConfig``
+ - Rename ``Starlite.cache_config`` > ``Starlite.response_cache_config``
+ - Rename ``AppConfig.cache_config`` > ``response_cache_config``
+ - Remove ``starlite/cache`` module
+ - Remove ``ASGIConnection.cache`` property
+ - Remove ``Starlite.cache`` attribute
+
+ .. attention::
+ ``starlite.middleware.rate_limit.RateLimitMiddleware``,
+ ``starlite.config.response_cache.ResponseCacheConfig``,
+ and ``starlite.middleware.session.server_side.ServerSideSessionConfig``
+ instead of accepting a ``storage`` argument that could be passed a ``Storage`` instance now have to be
+ configured via the ``store`` attribute, accepting a string key for the store to be used from the registry.
+ The ``store`` attribute has a unique default set, guaranteeing a unique
+ ``starlite.stores.memory.MemoryStore`` instance is acquired for every one of them from the
+ registry by default
+
+ .. seealso::
+
+ :doc:`/usage/stores`
+
+
+ .. change:: Add ``starlite.__version__``
+ :type: feature
+ :pr: 1277
+
+ Add a ``__version__`` constant to the ``starlite`` namespace, containing a
+ :class:`NamedTuple `, holding information about the currently
+ installed version of Starlite
+
+
+ .. change:: Add ``starlite version`` command to CLI
+ :type: feature
+ :pr: 1322
+
+ Add a new ``version`` command to the CLI which displays the currently installed
+ version of Starlite
+
+
+ .. change:: Enhance CLI autodiscovery logic
+ :type: feature
+ :breaking:
+ :pr: 1322
+
+ Update the CLI :ref:`usage/cli:autodiscovery` to only consider canonical modules app and application, but every
+ ``starlite.app.Starlite`` instance or application factory able to return a ``Starlite`` instance within
+ those or one of their submodules, giving priority to the canonical names app and application for application
+ objects and submodules containing them.
+
+ .. seealso::
+ :ref:`CLI autodiscovery `
+
+ .. change:: Configurable exception logging and traceback truncation
+ :type: feature
+ :pr: 1296
+
+ Add three new configuration options to ``starlite.logging.config.BaseLoggingConfig``:
+
+ ``starlite.logging.config.LoggingConfig.log_exceptions``
+ Configure when exceptions are logged.
+
+ ``always``
+ Always log exceptions
+
+ ``debug``
+ Log exceptions in debug mode only
+
+ ``never``
+ Never log exception
+
+ ``starlite.logging.config.LoggingConfig.traceback_line_limit``
+ Configure how many lines of tracback are logged
+
+ ``starlite.logging.config.LoggingConfig.exception_logging_handler``
+ A callable that receives three parameters - the ``app.logger``, the connection scope and the traceback
+ list, and should handle logging
+
+ .. seealso::
+ ``starlite.logging.config.LoggingConfig``
+
+
+ .. change:: Allow overwriting default OpenAPI response descriptions
+ :type: bugfix
+ :issue: 1292
+ :pr: 1293
+
+ Fix https://github.com/litestar-org/litestar/issues/1292 by allowing to overwrite
+ the default OpenAPI response description instead of raising :exc:`ImproperlyConfiguredException`.
+
+
+ .. change:: Fix regression in path resolution that prevented 404's being raised for false paths
+ :type: bugfix
+ :pr: 1316
+ :breaking:
+
+ Invalid paths within controllers would under specific circumstances not raise a 404. This was a regression
+ compared to ``v1.51``
+
+ .. note::
+ This has been marked as breaking since one user has reported to rely on this "feature"
+
+
+ .. change:: Fix ``after_request`` hook not being called on responses returned from handlers
+ :type: bugfix
+ :pr: 1344
+ :issue: 1315
+
+ ``after_request`` hooks were not being called automatically when a ``starlite.response.Response``
+ instances was returned from a route handler directly.
+
+ .. seealso::
+ :ref:`after_request`
+
+
+ .. change:: Fix ``SQLAlchemyPlugin`` raises error when using SQLAlchemy UUID
+ :type: bugfix
+ :pr: 1355
+
+ An error would be raised when using the SQLAlchemy plugin with a
+ `sqlalchemy UUID `_. This
+ was fixed by adding it to the provider map.
+
+
+ .. change:: Fix ``JSON.parse`` error in ReDoc and Swagger OpenAPI handlers
+ :type: bugfix
+ :pr: 1363
+
+ The HTML generated by the ReDoc and Swagger OpenAPI handlers would cause
+ `JSON.parse `_
+ to throw an error. This was fixed by removing the call to ``JSON.parse``.
+
+
+ .. change:: Fix CLI prints application info twice
+ :type: bugfix
+ :pr: 1322
+
+ Fix an error where the CLI would print application info twice on startup
+
+
+ .. change:: Update ``SimpleEventEmitter`` to use worker pattern
+ :type: misc
+ :pr: 1346
+
+ ``starlite.events.emitter.SimpleEventEmitter`` was updated to using an async worker, pulling
+ emitted events from a queue and subsequently calling listeners. Previously listeners were called immediately,
+ making the operation effectively "blocking".
+
+
+ .. change:: Make ``BaseEventEmitterBackend.emit`` synchronous
+ :type: misc
+ :breaking:
+ :pr: 1376
+
+ ``starlite.events.emitter.BaseEventEmitterBackend``, and subsequently
+ ``starlite.events.emitter.SimpleEventEmitter`` and
+ ``starlite.app.Starlite.emit`` have been changed to synchronous function, allowing them to easily be
+ used within synchronous route handlers.
+
+
+ .. change:: Move 3rd party integration plugins to ``contrib``
+ :type: misc
+ :breaking:
+ :pr: 1279 1252
+
+ - Move ``plugins.piccolo_orm`` > ``contrib.piccolo_orm``
+ - Move ``plugins.tortoise_orm`` > ``contrib.tortoise_orm``
+
+
+ .. change:: Remove ``picologging`` dependency from the ``standard`` package extra
+ :type: misc
+ :breaking:
+ :pr: 1313
+
+ `picologging `_ has been removed form the ``standard`` package extra.
+ If you have been previously relying on this, you need to change ``pip install starlite[standard]`` to
+ ``pip install starlite[standard,picologging]``
+
+
+ .. change:: Replace ``Starlite()`` ``initial_state`` keyword argument with ``state``
+ :type: misc
+ :pr: 1350
+ :breaking:
+
+ The ``initial_state`` argument to ``starlite.app.Starlite`` has been replaced with a ``state`` keyword
+ argument, accepting an optional ``starlite.datastructures.state.State`` instance.
+
+ Existing code using this keyword argument will need to be changed from
+
+ .. code-block:: python
+
+ from starlite import Starlite
+
+ app = Starlite(..., initial_state={"some": "key"})
+
+ to
+
+ .. code-block:: python
+
+ from starlite import Starlite
+ from starlite.datastructures.state import State
+
+ app = Starlite(..., state=State({"some": "key"}))
+
+
+ .. change:: Remove support for 2 argument form of ``before_send``
+ :type: misc
+ :pr: 1354
+ :breaking:
+
+ ``before_send`` hook handlers initially accepted 2 arguments, but support for a 3 argument form was added
+ later on, accepting an additional ``scope`` parameter. Support for the 2 argument form has been dropped with
+ this release.
+
+ .. seealso::
+ :ref:`before_send`
+
+
+ .. change:: Standardize module exports
+ :type: misc
+ :pr: 1273
+ :breaking:
+
+ A large refactoring standardising the way submodules make their names available.
+
+ The following public modules have changed their location:
+
+ - ``config.openapi`` > ``openapi.config``
+ - ``config.logging`` > ``logging.config``
+ - ``config.template`` > ``template.config``
+ - ``config.static_files`` > ``static_files.config``
+
+ The following modules have been removed from the public namespace:
+
+ - ``asgi``
+ - ``kwargs``
+ - ``middleware.utils``
+ - ``cli.utils``
+ - ``contrib.htmx.utils``
+ - ``handlers.utils``
+ - ``openapi.constants``
+ - ``openapi.enums``
+ - ``openapi.datastructures``
+ - ``openapi.parameters``
+ - ``openapi.path_item``
+ - ``openapi.request_body``
+ - ``openapi.responses``
+ - ``openapi.schema``
+ - ``openapi.typescript_converter``
+ - ``openapi.utils``
+ - ``multipart``
+ - ``parsers``
+ - ``signature``
+
+
+.. changelog:: 2.0.0alpha1
+
+ .. change:: Validation of controller route handler methods
+ :type: feature
+ :pr: 1144
+
+ Starlite will now validate that no duplicate handlers (that is, they have the same
+ path and same method) exist.
+
+ .. change:: HTMX support
+ :type: feature
+ :pr: 1086
+
+ Basic support for HTMX requests and responses.
+
+ .. change:: Alternate constructor ``Starlite.from_config``
+ :type: feature
+ :pr: 1190
+
+ ``starlite.app.Starlite.from_config`` was added to the
+ ``starlite.app.Starlite`` class which allows to construct an instance
+ from an ``starlite.config.app.AppConfig`` instance.
+
+ .. change:: Web concurrency option for CLI ``run`` command
+ :pr: 1218
+ :type: feature
+
+ A ``--wc`` / --web-concurrency` option was added to the ``starlite run`` command,
+ enabling users to specify the amount of worker processes to use. A corresponding
+ environment variable ``WEB_CONCURRENCY`` was added as well
+
+ .. change:: Validation of ``state`` parameter in handler functions
+ :type: feature
+ :pr: 1264
+
+ Type annotations of the reserved ``state`` parameter in handler functions will
+ now be validated such that annotations using an unsupported type will raise a
+ ``starlite.exceptions.ImproperlyConfiguredException``.
+
+ .. change:: Generic application state
+ :type: feature
+ :pr: 1030
+
+ ``starlite.connection.base.ASGIConnection`` and its subclasses are now generic on ``State``
+ which allow to to fully type hint a request as ``Request[UserType, AuthType, StateType]``.
+
+ .. change:: Dependency injection of classes
+ :type: feature
+ :pr: 1143
+
+ Support using classes (not class instances, which were already supported) as dependency providers.
+ With this, now every callable is supported as a dependency provider.
+
+ .. change:: Event bus
+ :pr: 1105
+ :type: feature
+
+ A simple event bus system for Starlite, supporting synchronous and asynchronous listeners and emitters, providing a
+ similar interface to handlers. It currently features a simple in-memory, process-local backend
+
+ .. change:: Unified storage interfaces
+ :type: feature
+ :pr: 1184
+ :breaking:
+
+ Storage backends for server-side sessions ``starlite.cache.Cache``` have been unified and replaced
+ by the ``starlite.storages``, which implements generic asynchronous key/values stores backed
+ by memory, the file system or redis.
+
+ .. important::
+ This is a breaking change and you need to change your session / cache configuration accordingly
+
+
+ .. change:: Relaxed type annotations
+ :pr: 1140
+ :type: misc
+
+ Type annotations across the library have been relaxed to more generic forms, for example
+ ``Iterable[str]`` instead of ``List[str]`` or ``Mapping[str, str]`` instead of ``Dict[str, str]``.
+
+ .. change:: ``type_encoders`` support in ``AbstractSecurityConfig``
+ :type: misc
+ :pr: 1167
+
+ ``type_encoders`` support has been added to
+ ``starlite.security.base.AbstractSecurityConfig``, enabling support for customized
+ ``type_encoders`` for example in ``starlite.contrib.jwt.jwt_auth.JWTAuth``.
+
+
+ .. change:: Renamed handler module names
+ :type: misc
+ :breaking:
+ :pr: 1170
+
+ The modules containing route handlers have been renamed to prevent ambiguity between module and handler names.
+
+ - ``starlite.handlers.asgi`` > ``starlite.handlers.asgi_handlers``
+ - ``starlite.handlers.http`` > ``starlite.handlers.http_handlers``
+ - ``starlite.handlers.websocket`` > ``starlite.handlers.websocket_handlers``
+
+
+ .. change:: New plugin protocols
+ :type: misc
+ :pr: 1176
+ :breaking:
+
+ The plugin protocol has been split into three distinct protocols, covering different use cases:
+
+ ``starlite.plugins.InitPluginProtocol``
+ Hook into an application's initialization process
+
+ ``starlite.plugins.SerializationPluginProtocol``
+ Extend the serialization and deserialization capabilities of an application
+
+ ``starlite.plugins.OpenAPISchemaPluginProtocol``
+ Extend OpenAPI schema generation
+
+
+ .. change:: Unify response headers and cookies
+ :type: misc
+ :breaking:
+ :pr: 1209
+
+ :ref:`response headers ` and
+ :ref:`response cookies ` now have the
+ same interface, along with the ``headers`` and ``cookies`` keyword arguments to
+ ``starlite.response.Response``. They each allow to pass either a
+ `:class:`Mapping[str, str] `, e.g. a dictionary, or a :class:`Sequence ` of
+ ``starlite.datastructures.response_header.ResponseHeader`` or
+ ``starlite.datastructures.cookie.Cookie`` respectively.
+
+
+ .. change:: Replace Pydantic models with dataclasses
+ :type: misc
+ :breaking:
+ :pr: 1242
+
+ Several Pydantic models used for configuration have been replaced with dataclasses or plain classes. This change
+ should be mostly non-breaking, unless you relied on those configuration objects being Pydantic models. The changed
+ models are:
+
+ - ``starlite.config.allowed_hosts.AllowedHostsConfig``
+ - ``starlite.config.app.AppConfig``
+ - ``starlite.config.response_cache.ResponseCacheConfig``
+ - ``starlite.config.compression.CompressionConfig``
+ - ``starlite.config.cors.CORSConfig``
+ - ``starlite.config.csrf.CSRFConfig``
+ - ``starlite.logging.config.LoggingConfig``
+ - ``starlite.openapi.OpenAPIConfig``
+ - ``starlite.static_files.StaticFilesConfig``
+ - ``starlite.template.TemplateConfig``
+ - ``starlite.contrib.jwt.jwt_token.Token``
+ - ``starlite.contrib.jwt.jwt_auth.JWTAuth``
+ - ``starlite.contrib.jwt.jwt_auth.JWTCookieAuth``
+ - ``starlite.contrib.jwt.jwt_auth.OAuth2Login``
+ - ``starlite.contrib.jwt.jwt_auth.OAuth2PasswordBearerAuth``
+ - ``starlite.contrib.opentelemetry.OpenTelemetryConfig``
+ - ``starlite.middleware.logging.LoggingMiddlewareConfig``
+ - ``starlite.middleware.rate_limit.RateLimitConfig``
+ - ``starlite.middleware.session.base.BaseBackendConfig``
+ - ``starlite.middleware.session.client_side.CookieBackendConfig``
+ - ``starlite.middleware.session.server_side.ServerSideSessionConfig``
+ - ``starlite.response_containers.ResponseContainer``
+ - ``starlite.response_containers.File``
+ - ``starlite.response_containers.Redirect``
+ - ``starlite.response_containers.Stream``
+ - ``starlite.security.base.AbstractSecurityConfig``
+ - ``starlite.security.session_auth.SessionAuth``
+
+
+ .. change:: SQLAlchemy plugin moved to ``contrib``
+ :type: misc
+ :breaking:
+ :pr: 1252
+
+ The ``SQLAlchemyPlugin` has moved to ``starlite.contrib.sqlalchemy_1.plugin`` and will only be compatible
+ with the SQLAlchemy 1.4 release line. The newer SQLAlchemy 2.x releases will be supported by the
+ ``contrib.sqlalchemy`` module.
+
+
+ .. change:: Cleanup of the ``starlite`` namespace
+ :type: misc
+ :breaking:
+ :pr: 1135
+
+ The ``starlite`` namespace has been cleared up, removing many names from it, which now have to be imported from
+ their respective submodules individually. This was both done to improve developer experience as well as reduce
+ the time it takes to ``import starlite``.
+
+ .. change:: Fix resolving of relative paths in ``StaticFilesConfig``
+ :type: bugfix
+ :pr: 1256
+
+ Using a relative :class:`pathlib.Path` did not resolve correctly and result in a ``NotFoundException``
+
+ .. change:: Fix ``--reload`` flag to ``starlite run`` not working correctly
+ :type: bugfix
+ :pr: 1191
+
+ Passing the ``--reload`` flag to the ``starlite run`` command did not work correctly in all circumstances due to an
+ issue with uvicorn. This was resolved by invoking uvicorn in a subprocess.
+
+
+ .. change:: Fix optional types generate incorrect OpenAPI schemas
+ :type: bugfix
+ :pr: 1210
+
+ An optional query parameter was incorrectly represented as
+
+ .. code-block::
+
+ { "oneOf": [
+ { "type": null" },
+ { "oneOf": [] }
+ ]}
+
+
+ .. change:: Fix ``LoggingMiddleware`` is sending obfuscated session id to client
+ :type: bugfix
+ :pr: 1228
+
+ ``LoggingMiddleware`` would in some cases send obfuscated data to the client, due to a bug in the obfuscation
+ function which obfuscated values in the input dictionary in-place.
+
+
+ .. change:: Fix missing ``domain`` configuration value for JWT cookie auth
+ :type: bugfix
+ :pr: 1223
+
+ ``starlite.contrib.jwt.jwt_auth.JWTCookieAuth`` didn't set the ``domain`` configuration value on the response
+ cookie.
+
+
+ .. change:: Fix https://github.com/litestar-org/litestar/issues/1201: Can not serve static file in ``/`` path
+ :type: bugfix
+ :issue: 1201
+
+ A validation error made it impossible to serve static files from the root path ``/`` .
+
+ .. change:: Fix https://github.com/litestar-org/litestar/issues/1149: Middleware not excluding static path
+ :type: bugfix
+ :issue: 1149
+
+ A middleware's ``exclude`` parameter would sometimes not be honoured if the path was used to serve static files
+ using ``StaticFilesConfig``.
diff --git a/docs/release-notes/changelog.rst b/docs/release-notes/changelog.rst
index 5257fd1f37..a74da29703 100644
--- a/docs/release-notes/changelog.rst
+++ b/docs/release-notes/changelog.rst
@@ -1,5656 +1,14 @@
:orphan:
-2.x Changelog
+3.x Changelog
=============
+.. changelog:: 3.0.0
+ :date: 2024-08-30
-.. changelog:: 2.13.0
- :date: 2024-11-20
-
- .. change:: Add ``request_max_body_size`` layered parameter
- :type: feature
-
- Add a new ``request_max_body_size`` layered parameter, which limits the
- maximum size of a request body before returning a ``413 - Request Entity Too Large``.
-
- .. seealso::
- :ref:`usage/requests:limits`
-
-
- .. change:: Send CSRF request header in OpenAPI plugins
- :type: feature
- :pr: 3754
-
- Supported OpenAPI UI clients will extract the CSRF cookie value and attach it to
- the request headers if CSRF is enabled on the application.
-
- .. change:: deprecate `litestar.contrib.sqlalchemy`
- :type: feature
- :pr: 3755
-
- Deprecate the ``litestar.contrib.sqlalchemy`` module in favor of ``litestar.plugins.sqlalchemy``
-
-
- .. change:: implement `HTMX` plugin using `litestar-htmx`
- :type: feature
- :pr: 3837
-
- This plugin migrates the HTMX integration to ``litestar.plugins.htmx``.
-
- This logic has been moved to it's own repository named ``litestar-htmx``
-
- .. change:: Pydantic: honor ``hide_input_in_errors`` in throwing validation exceptions
- :type: feature
- :pr: 3843
-
- Pydantic's ``BaseModel`` supports configuration to hide data values when
- throwing exceptions, via setting ``hide_input_in_errors`` -- see
- https://docs.pydantic.dev/2.0/api/config/#pydantic.config.ConfigDict.hide_input_in_errors
- and https://docs.pydantic.dev/latest/usage/model_config/#hide-input-in-errors
-
- Litestar will now honour this setting
-
- .. change:: deprecate``litestar.contrib.pydantic``
- :type: feature
- :pr: 3852
- :issue: 3787
-
- ## Description
-
- Deprecate ``litestar.contrib.pydantic`` in favor of ``litestar.plugins.pydantic``
-
-
- .. change:: Fix sign bug in rate limit middelware
- :type: bugfix
- :pr: 3776
-
- Fix a bug in the rate limit middleware, that would cause the response header
- fields ``RateLimit-Remaining`` and ``RateLimit-Reset`` to have negative values.
-
-
- .. change:: OpenAPI: map JSONSchema spec naming convention to snake_case when names from ``schema_extra`` are not found
- :type: bugfix
- :pr: 3767
- :issue: 3766
-
- Address rejection of ``schema_extra`` values using JSONSchema spec-compliant
- key names by mapping between the relevant naming conventions.
-
- .. change:: Use correct path template for routes without path parameters
- :type: bugfix
- :pr: 3784
-
- Fix a but where, when using ``PrometheusConfig.group_path=True``, the metrics
- exporter response content would ignore all paths with no path parameters.
-
- .. change:: Fix a dangling anyio stream in ``TestClient``
- :type: bugfix
- :pr: 3836
- :issue: 3834
-
- Fix a dangling anyio stream in ``TestClient`` that would cause a resource warning
-
- Closes #3834.
-
- .. change:: Fix bug in handling of missing ``more_body`` key in ASGI response
- :type: bugfix
- :pr: 3845
-
- Some frameworks do not include the ``more_body`` key in the "http.response.body" ASGI event.
- According to the ASGI specification, this key should be set to ``False`` when
- there is no additional body content. Litestar expects ``more_body`` to be
- explicitly defined, but others might not.
-
- This leads to failures when an ASGI framework mounted on Litestar throws error
- if this key is missing.
-
-
- .. change:: Fix duplicate ``RateLimit-*`` headers with caching
- :type: bugfix
- :pr: 3855
- :issue: 3625
-
- Fix a bug where ``RateLimitMiddleware`` duplicate all ``RateLimit-*`` headers
- when handler cache is enabled.
-
-
-.. changelog:: 2.12.1
- :date: 2024-09-21
-
- .. change:: Fix base package requiring ``annotated_types`` dependency
- :type: bugfix
- :pr: 3750
- :issue: 3749
-
- Fix a bug introduced in #3721 that was released with ``2.12.0`` caused an
- :exc:`ImportError` when the ``annotated_types`` package was not installed.
-
-
-.. changelog:: 2.12.0
- :date: 2024-09-21
-
- .. change:: Fix overzealous warning for greedy middleware ``exclude`` pattern
- :type: bugfix
- :pr: 3712
-
- Fix a bug introduced in ``2.11.0`` (https://github.com/litestar-org/litestar/pull/3700),
- where the added warning for a greedy pattern use for the middleware ``exclude``
- parameter was itself greedy, and would warn for non-greedy patterns, e.g.
- ``^/$``.
-
- .. change:: Fix dangling coroutines in request extraction handling cleanup
- :type: bugfix
- :pr: 3735
- :issue: 3734
-
- Fix a bug where, when a required header parameter was defined for a request that
- also expects a request body, failing to provide the header resulted in a
- :exc:`RuntimeWarning`.
-
- .. code-block:: python
-
- @post()
- async def handler(data: str, secret: Annotated[str, Parameter(header="x-secret")]) -> None:
- return None
-
- If the ``x-secret`` header was not provided, warning like this would be seen:
-
- .. code-block::
-
- RuntimeWarning: coroutine 'json_extractor' was never awaited
-
-
- .. change:: OpenAPI: Correctly handle ``type`` keyword
- :type: bugfix
- :pr: 3715
- :issue: 3714
-
- Fix a bug where a type alias created with the ``type`` keyword would create an
- empty OpenAPI schema entry for that parameter
-
- .. change:: OpenAPI: Ensure valid schema keys
- :type: bugfix
- :pr: 3635
- :issue: 3630
-
- Ensure that generated schema component keys are always valid according to
- `§ 4.8.7.1 `_ of the
- OpenAPI specification.
-
-
- .. change:: OpenAPI: Correctly handle ``msgspec.Struct`` tagged unions
- :type: bugfix
- :pr: 3742
- :issue: 3659
-
- Fix a bug where the OpenAPI schema would not include the struct fields
- implicitly generated by msgspec for its
- `tagged union `_
- support.
-
- The tag field of the struct will now be added as a ``const`` of the appropriate
- type to the schema.
-
-
- .. change:: OpenAPI: Fix Pydantic 1 constrained string with default factory
- :type: bugfix
- :pr: 3721
- :issue: 3710
-
- Fix a bug where using a Pydantic model with a ``default_factory`` set for a
- constrained string field would raise a :exc:`SerializationException`.
-
- .. code-block:: python
-
- class Model(BaseModel):
- field: str = Field(default_factory=str, max_length=600)
-
-
- .. change:: OpenAPI/DTO: Fix missing Pydantic 2 computed fields
- :type: bugfix
- :pr: 3721
- :issue: 3656
-
- Fix a bug that would lead to Pydantic computed fields to be ignored during
- schema generation when the model was using a
- :class:`~litestar.contrib.pydantic.PydanticDTO`.
-
- .. code-block:: python
- :caption: Only the ``foo`` field would be included in the schema
-
- class MyModel(BaseModel):
- foo: int
-
- @computed_field
- def bar(self) -> int:
- return 123
-
- @get(path="/", return_dto=PydanticDTO[MyModel])
- async def test() -> MyModel:
- return MyModel.model_validate({"foo": 1})
-
- .. change:: OpenAPI: Fix Pydantic ``json_schema_extra`` overrides only being merged partially
- :type: bugfix
- :pr: 3721
- :issue: 3656
-
- Fix a bug where ``json_schema_extra`` were not reliably extracted from Pydantic
- models and included in the OpenAPI schema.
-
- .. code-block:: python
- :caption: Only the title set directly on the field would be used for the schema
-
- class Model(pydantic.BaseModel):
- with_title: str = pydantic.Field(title="new_title")
- with_extra_title: str = pydantic.Field(json_schema_extra={"title": "more_new_title"})
-
-
- @get("/example")
- async def example_route() -> Model:
- return Model(with_title="1", with_extra_title="2")
-
-
- .. change:: Support strings in ``media_type`` for ``ResponseSpec``
- :type: feature
- :pr: 3729
- :issue: 3728
-
- Accept strings for the ``media_type`` parameter of :class:`~litestar.openapi.datastructures.ResponseSpec`,
- making it behave the same way as :paramref:`~litestar.response.Response.media_type`.
-
-
- .. change:: OpenAPI: Allow customizing schema component keys
- :type: feature
- :pr: 3738
-
- Allow customizing the schema key used for a component in the OpenAPI schema.
- The supplied keys are enforced to be unique, and it is checked that they won't
- be reused across different types.
-
- The keys can be set with the newly introduced ``schema_component_key`` parameter,
- which is available on :class:`~litestar.params.KwargDefinition`,
- :func:`~litestar.params.Body` and :func:`~litestar.params.Parameter`.
-
- .. code-block:: python
- :caption: Two components will be generated: ``Data`` and ``not_data``
-
- @dataclass
- class Data:
- pass
-
- @post("/")
- def handler(
- data: Annotated[Data, Parameter(schema_component_key="not_data")],
- ) -> Data:
- return Data()
-
- @get("/")
- def handler_2() -> Annotated[Data, Parameter(schema_component_key="not_data")]:
- return Data()
-
- .. change:: Raise exception when body parameter is annotated with non-bytes type
- :type: feature
- :pr: 3740
-
- Add an informative error message to help avoid the common mistake of attempting
- to use the ``body`` parameter to receive validated / structured data by
- annotating it with a type such as ``list[str]``, instead of ``bytes``.
-
-
- .. change:: OpenAPI: Default to ``latest`` scalar version
- :type: feature
- :pr: 3747
-
- Change the default version of the scalar OpenAPI renderer to ``latest``
-
-
-.. changelog:: 2.11.0
- :date: 2024-08-27
-
- .. change:: Use PyJWT instead of python-jose
- :type: feature
- :pr: 3684
-
- The functionality in :mod:`litestar.security.jwt` is now backed by
- `PyJWT `_ instead of
- `python-jose `_, due to the unclear
- maintenance status of the latter.
-
- .. change:: DTO: Introduce ``forbid_unknown_fields`` config
- :type: feature
- :pr: 3690
-
- Add a new config option to :class:`~litestar.dto.config.DTOConfig`:
- :attr:`~litestar.dto.config.DTOConfig.forbid_unknown_fields`
- When set to ``True``, a validation error response will be returned if the source
- data contains fields not defined on the model.
-
- .. change:: DTO: Support ``extra="forbid"`` model config for ``PydanticDTO``
- :type: feature
- :pr: 3691
-
- For Pydantic models with `extra="forbid" `_
- in their configuration:
-
- .. tab-set::
-
- .. tab-item:: Pydantic 2
-
- .. code-block:: python
-
- class User(BaseModel):
- model_config = ConfigDict(extra='ignore')
- name: str
-
- .. tab-item:: Pydantic 1
-
- .. code-block:: python
-
- class User(BaseModel):
- class Config:
- extra = "ignore"
- name: str
-
- :attr:`~litestar.dto.config.DTOConfig.forbid_unknown_fields` will be set to ``True`` by default.
-
- .. note::
- It's still possible to override this configuration at the DTO level
-
-
- To facilitate this feature, :meth:`~litestar.dto.base_dto.AbstractDTO.get_config_for_model_type`
- has been added to :class:`~litestar.dto.base_dto.AbstractDTO`, allowing the
- customization of the base config defined on the DTO factory for a specific model
- type. It will be called on DTO factory initialization, and receives the concrete
- DTO model type along side the :class:`~litestar.dto.config.DTOConfig` defined
- on the base DTO, which it can alter and return a new version to be used within
- the DTO instance.
-
- .. change:: Custom JWT payload classes
- :type: feature
- :pr: 3692
-
- Support extending the default :class:`~litestar.security.jwt.Token` class used
- by the JWT backends decode the payload into.
-
- - Add new ``token_cls`` field on the JWT auth config classes
- - Add new ``token_cls`` parameter to JWT auth middlewares
- - Switch to using msgspec to convert the JWT payload into instances of the token
- class
-
- .. code-block:: python
-
- import dataclasses
- import secrets
- from typing import Any, Dict
-
- from litestar import Litestar, Request, get
- from litestar.connection import ASGIConnection
- from litestar.security.jwt import JWTAuth, Token
-
- @dataclasses.dataclass
- class CustomToken(Token):
- token_flag: bool = False
-
- @dataclasses.dataclass
- class User:
- id: str
-
- async def retrieve_user_handler(token: CustomToken, connection: ASGIConnection) -> User:
- return User(id=token.sub)
-
- TOKEN_SECRET = secrets.token_hex()
-
- jwt_auth = JWTAuth[User](
- token_secret=TOKEN_SECRET,
- retrieve_user_handler=retrieve_user_handler,
- token_cls=CustomToken,
- )
-
- @get("/")
- def handler(request: Request[User, CustomToken, Any]) -> Dict[str, Any]:
- return {"id": request.user.id, "token_flag": request.auth.token_flag}
-
-
- .. change:: Extended JWT configuration options
- :type: feature
- :pr: 3695
-
- **New JWT backend fields**
-
- - :attr:`~litestar.security.jwt.JWTAuth.accepted_audiences`
- - :attr:`~litestar.security.jwt.JWTAuth.accepted_issuers`
- - :attr:`~litestar.security.jwt.JWTAuth.require_claims`
- - :attr:`~litestar.security.jwt.JWTAuth.verify_expiry`
- - :attr:`~litestar.security.jwt.JWTAuth.verify_not_before`
- - :attr:`~litestar.security.jwt.JWTAuth.strict_audience`
-
- **New JWT middleware parameters**
-
- - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.token_audience`
- - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.token_issuer`
- - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.require_claims`
- - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.verify_expiry`
- - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.verify_not_before`
- - :paramref:`~litestar.security.jwt.JWTAuthenticationMiddleware.strict_audience`
-
- **New ``Token.decode`` parameters**
-
- - :paramref:`~litestar.security.jwt.Token.decode.audience`
- - :paramref:`~litestar.security.jwt.Token.decode.issuer`
- - :paramref:`~litestar.security.jwt.Token.decode.require_claims`
- - :paramref:`~litestar.security.jwt.Token.decode.verify_exp`
- - :paramref:`~litestar.security.jwt.Token.decode.verify_nbf`
- - :paramref:`~litestar.security.jwt.Token.decode.strict_audience`
-
- **Other changes**
-
- :meth`Token.decode_payload <~litestar.security.jwt.Token.decode_payload>` has
- been added to make customization of payload decoding / verification easier
- without having to re-implement the functionality of the base class method.
-
- .. seealso::
- :doc:`/usage/security/jwt`
-
- .. change:: Warn about greedy exclude patterns in middlewares
- :type: feature
- :pr: 3700
-
- Raise a warning when a middlewares ``exclude`` pattern greedily matches all
- paths.
-
- .. code-block:: python
-
- from litestar.middlewares
-
- class MyMiddleware(AbstractMiddleware):
- exclude = ["/", "/home"]
-
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
- await self.app(scope, receive, send)
-
- Middleware like this would silently be disabled for every route, since the
- exclude pattern ``/`` matches all paths. If a configuration like this is
- detected, a warning will now be raised at application startup.
-
- .. change:: RFC 9457 *Problem Details* plugin
- :type: feature
- :pr: 3323
- :issue: 3199
-
- Add a plugin to support `RFC 9457 `_
- *Problem Details* responses for error response.
-
- :class:`~litestar.plugins.problem_details.ProblemDetailsPlugin` enables to
- selectively or collectively turn responses with an error status code into
- *Problem Detail* responses.
-
- .. seealso::
- :doc:`/usage/plugins/problem_details`
-
- .. change:: Fix creation of ``FormMultiDict`` in ``Request.form`` to properly handle multi-keys
- :type: bugfix
- :pr: 3639
- :issue: 3627
-
- Fix https://github.com/litestar-org/litestar/issues/3627 by properly handling
- the creation of :class:`~litestar.datastructures.FormMultiDict` where multiple
- values are given for a single key, to make
- :meth:`~litestar.connection.Request.form` match the behaviour of receiving form
- data via the ``data`` kwarg inside a route handler.
-
- **Before**
-
- .. code-block:: python
-
- @post("/")
- async def handler(request: Request) -> Any:
- return (await request.form()).getall("foo")
-
- with create_test_client(handler) as client:
- print(client.post("/", data={"foo": ["1", "2"]}).json()) # [["1", "2"]]
-
- **After**
-
- .. code-block:: python
-
- @post("/")
- async def handler(request: Request) -> Any:
- return (await request.form()).getall("foo")
-
- with create_test_client(handler) as client:
- print(client.post("/", data={"foo": ["1", "2"]}).json()) # ["1", "2"]
-
- .. change:: DTO: Fix inconsistent use of strict decoding mode
- :type: bugfix
- :pr: 3685
-
- Fix inconsistent usage of msgspec's ``strict`` mode in the base DTO backend.
-
- ``strict=False`` was being used when transferring from builtins, while
- ``strict=True`` was used transferring from raw data, causing an unwanted
- discrepancy in behaviour.
-
- .. change:: Use path template for prometheus metrics
- :type: bugfix
- :pr: 3687
-
- Changed previous 1-by-1 replacement logic for
- ``PrometheusMiddleware.group_path=true`` with a more robust and slightly faster
- solution.
-
- .. change:: Ensure OpenTelemetry captures exceptions in the outermost application layers
- :type: bugfix
- :pr: 3689
- :issue: 3663
-
- A bug was fixed that resulted in exception occurring in the outermost
- application layer not being captured under the current request span, which led
- to incomplete traces.
-
- .. change:: Fix CSRFMiddleware sometimes setting cookies for excluded paths
- :type: bugfix
- :pr: 3698
- :issue: 3688
-
- Fix a bug that would cause :class:`~litestar.middleware.csrf.CSRFMiddleware` to
- set a cookie (which would not be used subsequently) on routes it had been
- excluded from via a path pattern.
-
- .. change:: Make override behaviour consistent between ``signature_namespace`` and ``signature_types``
- :type: bugfix
- :pr: 3696
- :issue: 3681
-
- Ensure that adding signature types to ``signature_namespace`` and
- ``signature_types`` behaves the same way when a name was already present in the
- namespace.
-
- Both will now issue a warning if a name is being overwritten with a different
- type. If a name is registered again for the same type, no warning will be given.
-
- .. note::
-
- You can disable this warning globally by setting
- ``LITESTAR_WARN_SIGNATURE_NAMESPACE_OVERRIDE=0`` in your environment
-
-.. changelog:: 2.10.0
- :date: 2024-07-26
-
- .. change:: Allow creating parent directories for a file store
- :type: feature
- :pr: 3526
-
- Allow ``mkdir`` True when creating a file store.
-
- .. change:: Add ``logging_module`` parameter to ``LoggingConfig``
- :type: feature
- :pr: 3578
- :issue: 3536
-
- Provide a way in the ``logging_module`` to switch easily from ``logging`` to ``picologging``.
-
- .. change:: Add handler name to exceptions in handler validation
- :type: feature
- :pr: 3575
-
- Add handler name to exceptions raise by ``_validate_handler_function``.
-
- .. change:: Add strict validation support for Pydantic plugin
- :type: feature
- :pr: 3608
- :issue: 3572
-
- Adds parameters in pydantic plugin to support strict validation and all the ``model_dump`` args
-
- .. change:: Fix signature model signatures clash
- :type: bugfix
- :pr: 3605
- :issue: 3593
-
- Ensures that the functions used by the signature model itself do not interfere with the signature model created.
-
- .. change:: Correctly handle Annotated ``NewType``
- :type: bugfix
- :pr: 3615
- :issue: 3614
-
- Resolves infinite loop in schema generation when a model has an Annotated ``NewType``.
-
- .. change:: Use `ASGIConnection` instead of ``Request`` for ``flash``
- :type: bugfix
- :pr: 3626
-
- Currently, the ``FlashPlugin`` expects the ``request`` parameter to be a type of ``Request``. However, there's no reason it can't use the parent class ``ASGIConnection``.
-
- Doing this, allows for flash to be called in guards that expect an ``ASGIConnection`` instead of ``Request``:
-
- .. code-block:: python
-
- def requires_active_user(connection: ASGIConnection, _: BaseRouteHandler) -> None:
- if connection.user.is_active:
- return
- msg = "Your user account is inactive."
- flash(connection, msg, category="error")
- raise PermissionDeniedException(msg)
-
- .. change:: Allow returning ``Response[None]`` from head route handlers
- :type: bugfix
- :pr: 3641
- :issue: 3640
-
- Fix a bug where the validation of the return annotation for the ``head`` route handler was too strict and would not allow returning a ``Response[None]``.
-
-
-.. changelog:: 2.9.1
- :date: 2024-06-21
-
- .. change:: Add OPTIONS to the default safe methods for CSRFConfig
- :type: bugfix
- :pr: 3538
-
- Add ``OPTIONS`` to the default safe methods for :class:`~litestar.config.csrf.CSRFConfig`
-
-
- .. change:: Prometheus: Capture templated route name for metrics
- :type: bugfix
- :pr: 3533
-
- Adding new extraction function for prometheus metrics to avoid high cardinality
- issue in prometheus, eg having metrics ``GET /v1/users/{id}`` is preferable over
- ``GET /v1/users/1``, ``GET /v1/users/2,GET /v1/users/3``
-
- More info about prometheus high cardinality
- https://grafana.com/blog/2022/02/15/what-are-cardinality-spikes-and-why-do-they-matter/
-
- .. change:: Respect ``base_url`` in ``.websocket_connect``
- :type: bugfix
- :pr: 3567
-
- Fix a bug that caused :meth:`~litestar.testing.TestClient.websocket_connect` /
- :meth:`~litestar.testing.AsyncTestClient.websocket_connect` to not respect the
- ``base_url`` set in the client's constructor, and instead would use the static
- ``ws://testerver`` URL as a base.
-
- Also removes most of the test client code as it was unneeded and in the way of
- this fix :)
-
- Explanation for the last part: All the extra code we had was just proxying
- method calls to the ``httpx.Client`` / ``httpx.AsyncClient``, while altering the
- base URL. Since we already set the base URL on the httpx Client's superclass
- instance, which in turn does this merging internally, this step isn't needed at
- all.
-
- .. change:: Fix deprecation warning for subclassing route handler decorators
- :type: bugfix
- :pr: 3569
- :issue: 3552
-
- Fix an issue where there was a deprecation warning emitted by all route handler
- decorators. This warning was introduced in ``2.9.0`` to warn about the upcoming
- deprecation, but should have only applied to user subclasses of the handler
- classes, and not the built-in ones (``get``, ``post``, etc.)
-
- .. change:: CLI: Don't call ``rich_click.patch`` if ``rich_click`` is installed
- :type: bugfix
- :pr: 3570
- :issue: 3534
-
- Don't call ``rich_click.patch`` if ``rich_click`` is installed. As this
- monkey patches click globally, it can introduce unwanted side effects. Instead,
- use conditional imports to refer to the correct library.
-
- External libraries will still be able to make use of ``rich_click`` implicitly
- when it's installed by inheriting from ``LitestarGroup`` /
- ``LitestarExtensionGroup``, which they will by default.
-
-
- .. change:: Correctly handle ``typing.NewType``
- :type: bugfix
- :pr: 3580
-
- When encountering a :class:`typing.NewType` during OpenAPI schema generation,
- we currently treat it as an opaque type. This PR changes the behaviour such
- that :class`typing.NewType`s are always unwrapped during schema generation.
-
- .. change:: Encode response content object returned from an exception handler.
- :type: bugfix
- :pr: 3585
-
- When an handler raises an exception and exception handler returns a Response
- with a model (e.g. pydantic) object, ensure that object can be encoded as when
- returning data from a regular handler.
-
-
-.. changelog:: 2.9.0
- :date: 2024-06-02
-
- .. change:: asgi lifespan msg after lifespan context exception
- :type: bugfix
- :pr: 3315
-
- An exception raised within an asgi lifespan context manager would result in a "lifespan.startup.failed" message
- being sent after we've already sent a "lifespan.startup.complete" message. This would cause uvicorn to raise a
- ``STATE_TRANSITION_ERROR`` assertion error due to their check for that condition , if asgi lifespan is
- forced (i.e., with ``$ uvicorn test_apps.test_app:app --lifespan on``).
-
- E.g.,
-
- .. code-block::
-
- During handling of the above exception, another exception occurred:
-
- Traceback (most recent call last):
- File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/lifespan/on.py", line 86, in main
- await app(scope, self.receive, self.send)
- File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 69, in __call__
- return await self.app(scope, receive, send)
- File "/home/peter/PycharmProjects/litestar/litestar/app.py", line 568, in __call__
- await self.asgi_router.lifespan(receive=receive, send=send) # type: ignore[arg-type]
- File "/home/peter/PycharmProjects/litestar/litestar/_asgi/asgi_router.py", line 180, in lifespan
- await send(failure_message)
- File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/lifespan/on.py", line 116, in send
- assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR
- AssertionError: Got invalid state transition on lifespan protocol.
-
- This PR modifies ``ASGIRouter.lifespan()`` so that it sends a shutdown failure message if we've already confirmed startup.
-
- .. change:: bug when pydantic==1.10 is installed
- :type: bugfix
- :pr: 3335
- :issue: 3334
-
- Fix a bug introduced in #3296 where it failed to take into account that the ``pydantic_v2`` variable could be
- ``Empty``.
-
-
- .. change:: OpenAPI router and controller on same app.
- :type: bugfix
- :pr: 3338
- :issue: 3337
-
- Fixes an :exc`ImproperlyConfiguredException` where an app that explicitly registers an ``OpenAPIController`` on
- the application, and implicitly uses the OpenAPI router via the `OpenAPIConfig` object. This was caused by the
- two different handlers being given the same name as defined in ``litestar.constants``.
-
- PR adds a distinct name for use by the handler that serves ``openapi.json`` on the controller.
-
-
- .. change:: pydantic v2 import tests for pydantic v1.10.15
- :type: bugfix
- :pr: 3347
- :issue: 3348
-
- Fixes bug with Pydantic V1 environment test where the test was run against v2. Adds assertion for version to the test.
-
- Fixes a bug exposed by above that relied on pydantic not having ``v1`` in the package namespace if ``v1`` is
- installed. This doesn't hold true after pydantic's ``1.10.15`` release.
-
-
- .. change:: schema for generic wrapped return types with DTO
- :type: bugfix
- :pr: 3371
- :issue: 2929
-
- Fix schema generated for DTOs where the supported type is wrapped in a generic outer type.
-
-
- Prior behavior of using the ``backend.annotation`` as the basis for generating the openapi schema for the
- represented type is not applicable for the case where the DTO supported type is wrapped in a generic outer
- object. In that case ``backend.annotation`` only represents the type of the attribute on the generic type that
- holds the DTO supported type annotation.
-
- This change detects the case where we unwrap an outer generic type, and rebuilds the generic annotation in a
- manner appropriate for schema generation, before generating the schema for the annotation. It does this by
- substituting the DTOs transfer model for the original model in the original annotations type arguments.
-
- .. change:: Ambiguous default warning for no signature default
- :type: bugfix
- :pr: 3378
- :issue: 3372
-
- We now only issue a single warning for the case where a default value is supplied via ``Parameter()`` and not
- via a regular signature default.
-
-
- .. change:: Path param consumed by dependency treated as unconsumed
- :type: bugfix
- :pr: 3380
- :issue: 3369
-
- Consider parameters defined in handler dependencies in order to determine if a path parameter has been consumed
- for openapi generation purposes.
-
- Fixes an issue where path parameters not consumed by the handler, but consumed by dependencies would cause an
- :exc`ImproperlyConfiguredException`.
-
- .. change:: "name" and "in" should not be included in openapi headers
- :type: bugfix
- :pr: 3417
- :issue: 3416
-
- Exclude the "name" and "in" fields from openapi schema generated for headers.
-
- Add ``BaseSchemaObject._iter_fields()`` method that allows schema types to
- define the fields that should be included in their openapi schema representation
- and override that method for ``OpenAPIHeader``.
-
- .. change:: top-level import of optional package
- :type: bugfix
- :pr: 3418
- :issue: 3415
-
- Fix import from ``contrib.minijinja`` without handling for case where dependency is not installed.
-
-
- .. change:: regular handler under mounted app
- :type: bugfix
- :pr: 3430
- :issue: 3429
-
- Fix an issue where a regular handler under a mounted asgi app would prevent a
- request from routing through the mounted application if the request path
- contained the path of the regular handler as a substring.
-
- .. change:: logging to file with structlog
- :type: bugfix
- :pr: 3425
-
- Fix and issue with converting ``StructLoggingConfig`` to dict during call to
- ``configure()`` when the config object has a custom logger factory that
- references a ``TextIO`` object, which cannot be pickled.
-
- .. change:: clear session cookie if new session exceeds ``CHUNK_SIZE``
- :type: bugfix
- :pr: 3446
- :issue: 3441
-
- Fix an issue where the connection session cookie is not cleared if the response
- session is stored across multiple cookies.
-
- .. change:: flash messages were not displayed on Redirect
- :type: bugfix
- :pr: 3420
- :issue: 3325
-
- Fix an issue where flashed messages were not shown after a redirect
-
- .. change:: Validation of optional sequence in multipart data with one value
- :type: bugfix
- :pr: 3408
- :issue: 3407
-
- A ``Sequence[UploadFile] | None`` would not pass validation when a single value
- was provided for a structured type, e.g. dataclass.
-
- .. change:: field not optional if default value
- :type: bugfix
- :pr: 3476
- :issue: 3471
-
- Fix issue where a pydantic v1 field annotation is wrapped with ``Optional`` if
- it is marked not required, but has a default value.
-
- .. change:: prevent starting multiple responses
- :type: bugfix
- :pr: 3479
-
- Prevent the app's exception handler middleware from starting a response after
- one has already started.
-
- When something in the middleware stack raises an exception after a
- "http.response.start" message has already been sent, we end up with long
- exception chains that obfuscate the original exception.
-
- This change implements tracking of when a response has started, and if so, we
- immediately raise the exception instead of sending it through the usual exception
- handling code path.
-
- .. change:: logging middleware with multi-body response
- :type: bugfix
- :pr: 3478
- :issue: 3477
-
- Prevent logging middleware from failing with a :exc:`KeyError` when a response
- sends multiple "http.response.body" messages.
-
- .. change:: handle dto type nested in mapping
- :type: bugfix
- :pr: 3486
- :issue: 3463
-
- Added handling for transferring data from a transfer model, to a DTO supported
- instance when the DTO supported type is nested in a mapping.
-
- I.e, handles this case:
-
- .. code-block:: python
-
- @dataclass
- class NestedDC:
- a: int
- b: str
-
- @dataclass
- class DC:
- nested_mapping: Dict[str, NestedDC]
-
- .. change:: examples omitted in schema produced by dto
- :type: bugfix
- :pr: 3510
- :issue: 3505
-
- Fixes issue where a ``BodyKwarg`` instance provided as metadata to a data type
- annotation was ignored for OpenAPI schema generation when the data type is
- managed by a DTO.
-
- .. change:: fix handling validation of subscribed generics
- :type: bugfix
- :pr: 3519
-
- Fix a bug that would lead to a :exc:`TypeError` when subscribed generics were
- used in a route handler signature and subject to validation.
-
- .. code-block:: python
-
- from typing import Generic, TypeVar
- from litestar import get
- from litestar.testing import create_test_client
-
- T = TypeVar("T")
-
- class Foo(Generic[T]):
- pass
-
- async def provide_foo() -> Foo[str]:
- return Foo()
-
- @get("/", dependencies={"foo": provide_foo})
- async def something(foo: Foo[str]) -> None:
- return None
-
- with create_test_client([something]) as client:
- client.get("/")
-
-
- .. change:: exclude static file from schema
- :type: bugfix
- :pr: 3509
- :issue: 3374
-
- Exclude static file routes created with ``create_static_files_router`` from the OpenAPI schema by default
-
- .. change:: use re.match instead of re.search for mounted app path (#3501)
- :type: bugfix
- :pr: 3511
- :issue: 3501
-
- When mounting an app, path resolution uses ``re.search`` instead or ``re.match``,
- thus mounted app matches any path which contains mount path.
-
- .. change:: do not log exceptions twice, deprecate ``traceback_line_limit`` and fix ``pretty_print_tty``
- :type: bugfix
- :pr: 3507
- :issue: 3228
-
- * The wording of the log message, when logging an exception, has been updated.
- * For structlog, the ``traceback`` field in the log message (which contained a
- truncated stacktrace) has been removed. The ``exception`` field is still around and contains the full stacktrace.
- * The option ``traceback_line_limit`` has been deprecated. The value is now ignored, the full stacktrace will be logged.
-
-
- .. change:: YAML schema dump
- :type: bugfix
- :pr: 3537
-
- Fix an issue in the OpenAPI YAML schema dump logic of ``OpenAPIController``
- where the endpoint for the OpenAPI YAML schema file returns an empty response
- if a request has been made to the OpenAPI JSON schema previously due to an
- incorrect variable check.
-
-
- .. change:: Add async ``websocket_connect`` to ``AsyncTestClient``
- :type: feature
- :pr: 3328
- :issue: 3133
-
- Add async ``websocket_connect`` to ``AsyncTestClient``
-
-
- .. change:: add ``SecretString`` and ``SecretBytes`` datastructures
- :type: feature
- :pr: 3322
- :issue: 1312, 3248
-
-
- Implement ``SecretString`` and ``SecretBytes`` data structures to hide sensitive
- data in tracebacks, etc.
-
- .. change:: Deprecate subclassing route handler decorators
- :type: feature
- :pr: 3439
-
- Deprecation for the 2.x release line of the semantic route handler classes
- removed in #3436.
-
-
-.. changelog:: 2.8.3
- :date: 2024-05-06
-
- .. change:: Fix improper limitation of a pathname to a restricted directory
- :type: bugfix
-
- Fix a path traversal vulnerability disclosed in https://github.com/litestar-org/litestar/security/advisories/GHSA-83pv-qr33-2vcf
-
- .. change:: Remove use of asserts for control flow.
- :type: bugfix
- :pr: 3359
- :issue: 3354
-
- #3347 introduced a new pattern to differentiate between Pydantic v1 and v2 installs, however it relies on using `assert` which is an issue as can optimised away.
-
- This PR changes the approach to manually throw an `ImportError` instead.
-
- .. change:: schema for generic wrapped return types with DTO
- :type: bugfix
- :pr: 3371
- :issue: 2929
-
- Fix schema generated for DTOs where the supported type is wrapped in a generic outer type.
-
- .. change:: Ambiguous default warning for no signature default
- :type: bugfix
- :pr: 3378
- :issue: 3372
-
- We now only issue a single warning for the case where a default value is supplied via `Parameter()` and not via a regular signature default.
-
- .. change:: Path param consumed by dependency treated as unconsumed
- :type: bugfix
- :pr: 3380
- :issue: 3369
-
- Consider parameters defined in handler dependencies in order to determine if a path parameter has been consumed for openapi generation purposes.
-
- Fixes an issue where path parameters not consumed by the handler, but consumed by dependencies would cause an `ImproperlyConfiguredException`.
-
- .. change:: Solve a caching issue in `CacheControlHeader`
- :type: bugfix
- :pr: 3383
-
- Fixes an issue causing return of invalid values from cache.
-
- .. change:: "name" and "in" should not be included in openapi headers
- :type: bugfix
- :pr: 3417
- :issue: 3416
-
- Exclude the "name" and "in" fields from openapi schema generated for headers.
-
- .. change:: top-level import of optional package
- :type: bugfix
- :pr: 3418
- :issue: 3415
-
- Fix import from `contrib.minijinja` without handling for case where dependency is not installed.
-
- .. change:: regular handler under mounted app
- :type: bugfix
- :pr: 3430
- :issue: 3429
-
- Fix an issue where a regular handler under a mounted asgi app would prevent a request from routing through the
- mounted application if the request path contained the path of the regular handler as a substring.
-
- .. change:: logging to file with structlog
- :type: bugfix
- :pr: 3425
-
- PR fixes issue with converting `StructLoggingConfig` to dict during call to `configure()` when the config object
- has a custom logger factory that references a `TextIO` object, which cannot be pickled.
-
- .. change:: clear session cookie if new session gt CHUNK_SIZE
- :type: bugfix
- :pr: 3446
- :issue: 3441
-
- Fix an issue where the connection session cookie is not cleared if the response session is stored across
- multiple cookies.
-
- .. change:: flash messages were not displayed on Redirect
- :type: bugfix
- :pr: 3420
- :issue: 3325
-
- Fixes issue where flash messages were not displayed on redirect.
-
- .. change:: Validation of optional sequence in multipart data with one value
- :type: bugfix
- :pr: 3408
- :issue: 3407
-
- A `Sequence[UploadFile] | None` would not pass validation when a single value was provided for a structured type, e.g. dataclass.
-
-.. changelog:: 2.8.2
- :date: 2024-04-09
-
- .. change:: pydantic v2 import tests for pydantic v1.10.15
- :type: bugfix
- :pr: 3347
- :issue: 3348
-
- Fixes bug with Pydantic v1 environment test causing the test to run against v2. Adds assertion for version to
- the test.
-
- Fixes a bug exposed by above that relied on Pydantic not having `v1` in the package namespace if `v1` is
- installed. This doesn't hold true after Pydantic's `1.10.15` release.
-
- Moves application environment tests from the release job into the normal CI run.
-
-.. changelog:: 2.8.1
- :date: 2024-04-08
-
- .. change:: ASGI lifespan msg after lifespan context exception
- :type: bugfix
- :pr: 3315
-
- An exception raised within an asgi lifespan context manager would result in a "lifespan.startup.failed" message
-
- This PR modifies `ASGIRouter.lifespan()` so that it sends a shutdown failure message if we've already confirmed
- startup.
-
- .. change:: Fix when pydantic==1.10 is installed
- :type: bugfix
- :pr: 3335
- :issue: 3334
-
- This PR fixes a bug introduced in #3296 where it failed to take into account that the `pydantic_v2` variable could be `Empty`.
-
- .. change:: OpenAPI router and controller on same app.
- :type: bugfix
- :pr: 3338
- :issue: 3337
-
- Fixes an `ImproperlyConfiguredException` where an app that explicitly registers an `OpenAPIController` on the application, and implicitly uses the OpenAPI router via the `OpenAPIConfig` object. This was caused by the two different handlers being given the same name as defined in `litestar.constants`.
-
- PR adds a distinct name for use by the handler that serves `openapi.json` on the controller.
-
-.. changelog:: 2.8.0
- :date: 2024-04-05
-
- .. change:: Unique schema names for nested models (#3134)
- :type: bugfix
- :pr: 3136
- :issue: 3134
-
- Fixes an issue where nested models beyond the ``max_nested_depth`` would not have
- unique schema names in the OpenAPI documentation. The fix appends the nested
- model's name to the ``unique_name`` to differentiate it from the parent model.
-
- .. change:: Add ``path`` parameter to Litestar application class
- :type: feature
- :pr: 3314
-
- Exposes :paramref:`~.app.Litestar.parameter` at :class:`~.app.Litestar` application class level
-
- .. change:: Remove duplicate ``rich-click`` config options
- :type: bugfix
- :pr: 3274
-
- Removes duplicate config options from click cli
-
- .. change:: Fix Pydantic ``json_schema_extra`` examples.
- :type: bugfix
- :pr: 3281
- :issue: 3277
-
- Fixes a regression introduced in ``2.7.0`` where an example for a field provided in Pydantic's
- ``Field.json_schema_extra`` would cause an error.
-
- .. change:: Set default on schema from :class:`~.typing.FieldDefinition`
- :type: bugfix
- :pr: 3280
- :issue: 3278
-
- Consider the following:
-
- .. code-block:: python
-
- def get_foo(foo_id: int = 10) -> None:
- ...
-
- In such cases, no :class:`~.params.KwargDefinition` is created since there is no metadata provided via
- ``Annotated``. The default is still parsed, and set on the generated ``FieldDefinition``,
- however the ``SchemaCreator`` currently only considers defaults that are set on ``KwargDefinition``.
-
- So in such cases, we should fallback to the default set on the ``FieldDefinition`` if there is a valid
- default value.
-
- .. change:: Custom types cause serialisation error in exception response with non-JSON media-type
- :type: bugfix
- :pr: 3284
- :issue: 3192
-
- Fixes a bug when using a non-JSON media type (e.g., ``text/plain``),
- :class:`~.exceptions.http_exceptions.ValidationException`'s would not get serialized properly because they
- would ignore custom ``type_encoders``.
-
- .. change:: Ensure default values are always represented in schema for dataclasses and :class:`msgspec.Struct`\ s
- :type: bugfix
- :pr: 3285
- :issue: 3201
-
- Fixes a bug that would prevent default values for dataclasses and ``msgspec.Struct`` s to be included in the
- OpenAPI schema.
-
- .. change:: Pydantic v2 error handling/serialization when for non-Pydantic exceptions
- :type: bugfix
- :pr: 3286
- :issue: 2365
-
- Fixes a bug that would cause a :exc:`TypeError` when non-Pydantic errors are raised during Pydantic's
- validation process while using DTOs.
-
- .. change:: Fix OpenAPI schema generation for paths with path parameters of different types on the same path
- :type: bugfix
- :pr: 3293
- :issue: 2700
-
- Fixes a bug that would cause no OpenAPI schema to be generated for paths with path
- parameters that only differ on the path parameter type, such as ``/{param:int}``
- and ``/{param:str}``. This was caused by an internal representation issue in
- Litestar's routing system.
-
- .. change:: Document unconsumed path parameters
- :type: bugfix
- :pr: 3295
- :issue: 3290
-
- Fixes a bug where path parameters not consumed by route handlers would not be included in the OpenAPI schema.
-
- This could/would not include the ``{param}`` in the schema, yet it is still required to be passed
- when calling the path.
-
- .. change:: Allow for console output to be silenced
- :type: feature
- :pr: 3180
-
- Introduces optional environment variables that allow customizing the "Application" name displayed
- in the console output and suppressing the initial ``from_env`` or the ``Rich`` info table at startup.
-
- Provides flexibility in tailoring the console output to better integrate Litestar into larger applications
- or CLIs.
-
- .. change:: Add flash plugin
- :type: feature
- :pr: 3145
- :issue: 1455
-
- Adds a flash plugin akin to Django or Flask that uses the request state
-
- .. change:: Use memoized :paramref:`~.handlers.HTTPRouteHandler.request_class` and :paramref:`~.handlers.HTTPRouteHandler.response_class` values
- :type: feature
- :pr: 3205
-
- Uses memoized ``request_class`` and ``response_class`` values
-
- .. change:: Enable codegen backend by default
- :type: feature
- :pr: 3215
-
- Enables the codegen backend for DTOs introduced in https://github.com/litestar-org/litestar/pull/2388 by default.
-
- .. change:: Added precedence of CLI parameters over envs
- :type: feature
- :pr: 3190
- :issue: 3188
-
- Adds precedence of CLI parameters over environment variables.
- Before this change, environment variables would take precedence over CLI parameters.
-
- Since CLI parameters are more explicit and are set by the user,
- they should take precedence over environment variables.
-
- .. change:: Only print when terminal is ``TTY`` enabled
- :type: feature
- :pr: 3219
-
- Sets ``LITESTAR_QUIET_CONSOLE`` and ``LITESTAR_APP_NAME`` in the autodiscovery function.
- Also prevents the tabular console output from printing when the terminal is not ``TTY``
-
- .. change:: Support ``schema_extra`` in :class:`~.openapi.spec.parameter.Parameter` and `Body`
- :type: feature
- :pr: 3204
-
- Introduces a way to modify the generated OpenAPI spec by adding a ``schema_extra`` parameter to the
- Parameter and Body classes. The ``schema_extra`` parameter accepts a ``dict[str, Any]`` where the keys correspond
- to the keyword parameter names in Schema, and the values are used to override items in the
- generated Schema object.
-
- Provides a convenient way to customize the OpenAPI documentation for inbound parameters.
-
- .. change:: Add :class:`typing.TypeVar` expansion
- :type: feature
- :pr: 3242
-
- Adds a method for TypeVar expansion on registration
- This allows the use of generic route handler and generic controller without relying on forward references.
-
- .. change:: Add ``LITESTAR_`` prefix before ``WEB_CONCURRENCY`` env option
- :type: feature
- :pr: 3227
-
- Adds ``LITESTAR_`` prefix before the ``WEB_CONCURRENCY`` environment option
-
- .. change:: Warn about ambiguous default values in parameter specifications
- :type: feature
- :pr: 3283
-
- As discussed in https://github.com/litestar-org/litestar/pull/3280#issuecomment-2026878325,
- we want to warn about, and eventually disallow specifying parameter defaults in two places.
-
- To achieve this, 2 warnings are added:
-
- - A deprecation warning if a default is specified when using
- ``Annotated``: ``param: Annotated[int, Parameter(..., default=1)]`` instead of
- ``param: Annotated[int, Parameter(...)] = 1``
- - An additional warning in the above case if two default values are specified which do not match in value:
- ``param: Annotated[int, Parameter(..., default=1)] = 2``
-
- In a future version, the first one should result in an exception at startup, preventing both of these scenarios.
-
- .. change:: Support declaring :class:`~.dto.field.DTOField` via ``Annotated``
- :type: feature
- :pr: 3289
- :issue: 2351
-
- Deprecates passing :class:`~.dto.field.DTOField` via ``[pydantic]`` extra.
-
- .. change:: Add "TRACE" to HttpMethod enum
- :type: feature
- :pr: 3294
-
- Adds the ``TRACE`` HTTP method to :class:`~.enums.HttpMethod` enum
-
- .. change:: Pydantic DTO non-instantiable types
- :type: feature
- :pr: 3296
-
- Simplifies the type that is applied to DTO transfer models for certain Pydantic field types.
- It addresses ``JsonValue``, ``EmailStr``, ``IPvAnyAddress``/``IPvAnyNetwork``/``IPvAnyInterface`` types by
- using appropriate :term:`type annotations ` on the transfer models to ensure compatibility with
- :doc:`msgspec:index` serialization and deserialization.
-
-.. changelog:: 2.7.1
- :date: 2024-03-22
-
- .. change:: replace TestClient.__enter__ return type with Self
- :type: bugfix
- :pr: 3194
-
- ``TestClient.__enter__`` and ``AsyncTestClient.__enter__`` return ``Self``.
- If you inherit ``TestClient``, its ``__enter__`` method should return derived class's instance
- unless override the method. ``Self`` is a more flexible return type.
-
- .. change:: use the full path for fetching openapi.json
- :type: bugfix
- :pr: 3196
- :issue: 3047
-
- This specifies the ``spec-url`` and ``apiDescriptionUrl`` of Rapidoc, and Stoplight Elements as absolute
- paths relative to the root of the site.
-
- This ensures that both of the send the request for the JSON of the OpenAPI schema to the right endpoint.
-
- .. change:: JSON schema ``examples`` were OpenAPI formatted
- :type: bugfix
- :pr: 3224
- :issue: 2849
-
- The generated ``examples`` in *JSON schema* objects were formatted as:
-
- .. code-block:: json
-
- "examples": {
- "some-id": {
- "description": "Lorem ipsum",
- "value": "the real beef"
- }
- }
-
- However, above is OpenAPI example format, and must not be used in JSON schema
- objects. Schema objects follow different formatting:
-
- .. code-block:: json
-
- "examples": [
- "the real beef"
- ]
-
- * Explained in `APIs You Won't Hate blog post `_.
- * `Schema objects spec `_
- * `OpenAPI example format spec `_.
-
- This is referenced at least from parameters, media types and components.
-
- The technical change here is to define ``Schema.examples`` as ``list[Any]`` instead
- of ``list[Example]``. Examples can and must still be defined as ``list[Example]``
- for OpenAPI objects (e.g. ``Parameter``, ``Body``) but for JSON schema ``examples``
- the code now internally generates/converts ``list[Any]`` format instead.
-
- Extra confusion here comes from the OpenAPI 3.0 vs OpenAPI 3.1 difference.
- OpenAPI 3.0 only allowed ``example`` (singular) field in schema objects.
- OpenAPI 3.1 supports the full JSON schema 2020-12 spec and so ``examples`` array
- in schema objects.
-
- Both ``example`` and ``examples`` seem to be supported, though the former is marked
- as deprecated in the latest specs.
-
- This can be tested over at https://editor-next.swagger.io by loading up the
- OpenAPI 3.1 Pet store example. Then add ``examples`` in ``components.schemas.Pet``
- using the both ways and see the Swagger UI only render the example once it's
- properly formatted (it ignores is otherwise).
-
- .. change:: queue_listener handler for Python >= 3.12
- :type: bugfix
- :pr: 3185
- :issue: 2954
-
- - Fix the ``queue_listener`` handler for Python 3.12
-
- Python 3.12 introduced a new way to configure ``QueueHandler`` and ``QueueListener`` via
- ``logging.config.dictConfig()``. As described in the
- `logging documentation `_.
-
- The listener still needs to be started & stopped, as previously.
- To do so, we've introduced ``LoggingQueueListener``.
-
- And as stated in the doc:
- * Any custom queue handler and listener classes will need to be defined with the same initialization signatures
- as `QueueHandler `_ and
- `QueueListener `_.
-
- .. change:: extend openapi meta collected from domain models
- :type: bugfix
- :pr: 3237
- :issue: 3232
-
- :class:`~litestar.typing.FieldDefinition` s pack any OpenAPI metadata onto a ``KwargDefinition`` instance when
- types are parsed from domain models.
-
- When we produce a DTO type, we transfer this meta from the `KwargDefinition` to a `msgspec.Meta` instance,
- however so far this has only included constraints, not attributes such as descriptions, examples and title.
-
- This change ensures that we transfer the openapi meta for the complete intersection of fields that exist on b
- oth `KwargDefinition` and `Meta`.
-
- .. change:: kwarg ambiguity exc msg for path params
- :type: bugfix
- :pr: 3261
-
- Fixes the way we construct the exception message when there is a kwarg ambiguity detected for path parameters.
-
-.. 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 `_ 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/+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 deepcopied 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
-
- .. change:: Pydantic V1 schema generation for PrivateAttr in GenericModel
- :type: bugfix
- :pr: 3161
- :issue: 3150
-
- Fixes a bug that caused a ``NameError`` when a Pydantic V1 ``GenericModel`` has a private attribute of which the type annotation cannot be resolved at the time of schema generation.
-
-
-.. changelog:: 2.6.2
- :date: 2024/03/02
-
- .. change:: DTO msgspec meta constraints not being included in transfer model
- :type: bugfix
- :pr: 3113
- :issue: 3026
-
- Fix an issue where msgspec constraints set in ``msgspec.Meta`` would not be
- honoured by the DTO.
-
- In the given example, the ``min_length=3`` constraint would be ignored by the
- model generated by ``MsgspecDTO``.
-
- .. code-block:: python
-
- from typing import Annotated
-
- import msgspec
- from litestar import post, Litestar
- from litestar.dto import MsgspecDTO
-
- class Request(msgspec.Struct):
- foo: Annotated[str, msgspec.Meta(min_length=3)]
-
- @post("/example/", dto=MsgspecDTO[Request])
- async def example(data: Request) -> Request:
- return data
-
- Constraints like these are now transferred.
-
- Two things to note are:
-
- - For DTOs with ``DTOConfig(partial=True)`` we cannot transfer the length
- constraints as they are only supported on fields that as subtypes of ``str``,
- ``bytes`` or a collection type, but ``partial=True`` sets all fields as
- ``T | UNSET``
- - For the ``PiccoloDTO``, fields which are not required will also drop the
- length constraints. A warning about this will be raised here.
-
- .. change:: Missing control header for static files
- :type: bugfix
- :pr: 3131
- :issue: 3129
-
- Fix an issue where a ``cache_control`` that is set on a router created by
- ``create_static_files_router`` wasn't passed to the generated handler
-
- .. change:: Fix OpenAPI schema generation for Pydantic v2 constrained ``Secret`` types
- :type: bugfix
- :pr: 3149
- :issue: 3148
-
- Fix schema generation for ``pydantic.SecretStr`` and ``pydantic.SecretBytes``
- which, when constrained, would not be recognised as such with Pydantic V2 since
- they're not subtypes of their respective bases anymore.
-
- .. change:: Fix OpenAPI schema generation for Pydantic private attributes
- :type: bugfix
- :pr: 3151
- :issue: 3150
-
- Fix a bug that caused a :exc:`NameError` when trying to resolve forward
- references in Pydantic private fields.
-
- Although private fields were respected excluded from the schema, it was still
- attempted to extract their type annotation. This was fixed by not relying on
- ``typing.get_type_hints`` to get the type information, but instead using
- Pydantic's own APIs, allowing us to only extract information about the types of
- relevant fields.
-
- .. change:: OpenAPI description not set for UUID based path parameters in OpenAPI
- :type: bugfix
- :pr: 3118
- :issue: 2967
-
- Resolved a bug where the description was not set for UUID-based path
- parameters in OpenAPI due to the reason mentioned in the issue.
-
- .. change:: Fix ``RedisStore`` client created with ``with_client`` unclosed
- :type: bugfix
- :pr: 3111
- :issue: 3083
-
- Fix a bug where, when a :class:`~litestar.stores.redis.RedisStore` was created
- with the :meth:`~litestar.stores.redis.RedisStore.with_client` method, that
- client wasn't closed explicitly
-
-
-.. changelog:: 2.6.1
- :date: 2024/02/14
-
- .. change:: SQLAlchemy: Use `IntegrityError` instead of deprecated `ConflictError`
- :type: bugfix
- :pr: 3094
-
- Updated the repository to return ``IntegrityError`` instead of the now
- deprecated ``ConflictError``
-
- .. change:: Remove usage of deprecated `static_files` property
- :type: bugfix
- :pr: 3087
-
- Remove the usage of the deprecated ``Litestar.static_files_config`` in
- ``Litestar.__init__``.
-
- .. change:: Sessions: Fix cookie naming for short cookies
- :type: bugfix
- :pr: 3095
- :issue: 3090
-
- Previously, cookie names always had a suffix of the form ``"-{i}"`` appended to
- them. With this change, the suffix is omitted if the cookie is short enough
- (< 4 KB) to not be split into multiple chunks.
-
- .. change:: Static files: Fix path resolution for windows
- :type: bugfix
- :pr: 3102
-
- Fix an issue with the path resolution on Windows introduced in
- https://github.com/litestar-org/litestar/pull/2960 that would lead to 404s
-
- .. change:: Fix logging middleware with structlog causes application to return a ``500`` when request body is malformed
- :type: bugfix
- :pr: 3109
- :issue: 3063
-
- Gracefully handle malformed request bodies during parsing when using structlog;
- Instead of erroring out and returning a ``500``, the raw body is now being used
- when an error occurs during parsing
-
- .. change:: OpenAPI: Generate correct response schema for ``ResponseSpec(None)``
- :type: bugfix
- :pr: 3098
- :issue: 3069
-
- Explicitly declaring ``responses={...: ResponseSpec(None)}`` used to generate
- OpenAPI a ``content`` property, when it should be omitted.
-
- .. change:: Prevent exception handlers from extracting details from non-Litestar exceptions
- :type: bugfix
- :pr: 3106
- :issue: 3082
-
- Fix a bug where exception classes that had a ``status_code`` attribute would be
- treated as Litestar exceptions and details from them would be extracted and
- added to the exception response.
-
-.. changelog:: 2.6.0
- :date: 2024/02/06
-
- .. change:: Enable disabling configuring ``root`` logger within ``LoggingConfig``
- :type: feature
- :pr: 2969
-
- The option :attr:`~litestar.logging.config.LoggingConfig.configure_root_logger` was
- added to :class:`~litestar.logging.config.LoggingConfig` attribute. It is enabled by
- default to not implement a breaking change.
-
- When set to ``False`` the ``root`` logger will not be modified for ``logging``
- or ``picologging`` loggers.
-
- .. change:: Simplified static file handling and enhancements
- :type: feature
- :pr: 2960
- :issue: 2629
-
- Static file serving has been implemented with regular route handlers instead of
- a specialised ASGI app. At the moment, this is complementary to the usage of
- :class:`~litestar.static_files.StaticFilesConfig` to maintain backwards
- compatibility.
-
- This achieves a few things:
-
- - Fixes https://github.com/litestar-org/litestar/issues/2629
- - Circumvents special casing needed in the routing logic for the static files app
- - Removes the need for a ``static_files_config`` attribute on the app
- - Removes the need for a special :meth:`~litestar.app.Litestar.url_for_static_asset`
- method on the app since `route_reverse` can be used instead
-
- Additionally:
-
- - Most router options can now be passed to the
- :func:`~litestar.static_files.create_static_files_router`, allowing further
- customisation
- - A new ``resolve_symlinks`` flag has been added, defaulting to ``True`` to keep
- backwards compatibility
-
- **Usage**
-
- Instead of
-
- .. code-block:: python
-
- app = Litestar(
- static_files_config=[StaticFilesConfig(path="/static", directories=["some_dir"])]
- )
-
-
- You can now simply use
-
- .. code-block:: python
-
- app = Litestar(
- route_handlers=[
- create_static_files_router(path="/static", directories=["some_dir"])
- ]
- )
-
- .. seealso::
- :doc:`/usage/static-files`
-
-
- .. change:: Exclude Piccolo ORM columns with ``secret=True`` from ``PydanticDTO`` output
- :type: feature
- :pr: 3030
-
- For Piccolo columns with ``secret=True`` set, corresponding ``PydanticDTO``
- attributes will be marked as ``WRITE_ONLY`` to prevent the column being included
- in ``return_dto``
-
-
- .. change:: Allow discovering registered plugins by their fully qualified name
- :type: feature
- :pr: 3027
-
- `PluginRegistryPluginRegistry`` now supports retrieving a plugin by its fully
- qualified name.
-
-
- .. change:: Support externally typed classes as dependency providers
- :type: feature
- :pr: 3066
- :issue: 2979
-
- - Implement a new :class:`~litestar.plugins.DIPlugin` class that allows the
- generation of signatures for arbitrary types where their signature cannot be
- extracted from the type's ``__init__`` method
- - Implement ``DIPlugin``\ s for Pydantic and Msgspec to allow using their
- respective modelled types as dependency providers. These plugins will be
- registered by default
-
- .. change:: Add structlog plugin
- :type: feature
- :pr: 2943
-
- A Structlog plugin to make it easier to configure structlog in a single place.
-
- The plugin:
-
- - Detects if a logger has ``setLevel`` before calling
- - Set even message name to be init-cap
- - Add ``set_level`` interface to config
- - Allows structlog printer to detect if console is TTY enabled. If so, a
- Struglog color formatter with Rich traceback printer is used
- - Auto-configures stdlib logger to use the structlog logger
-
- .. change:: Add reload-include and reload-exclude to CLI run command
- :type: feature
- :pr: 2973
- :issue: 2875
-
- The options ``reload-exclude`` and ``reload-include`` were added to the CLI
- ``run`` command to explicitly in-/exclude specific paths from the reloading
- watcher.
-
-
-.. changelog:: 2.5.5
- :date: 2024/02/04
-
- .. change:: Fix scope ``state`` key handling
- :type: bugfix
- :pr: 3070
-
- Fix a regression introduced in #2751 that would wrongfully assume the ``state``
- key is always present within the ASGI Scope. This is *only* the case when the
- Litestar root application is invoked first, since we enforce such a key there,
- but the presence of that key is not actually guaranteed by the ASGI spec and
- some servers, such as hypercorn, do not provide it.
-
-
-.. changelog:: 2.5.4
- :date: 2024/01/31
-
- .. change:: Handle ``KeyError`` when `root_path` is not present in ASGI scope
- :type: bugfix
- :pr: 3051
-
- Nginx Unit ASGI server does not set "root_path" in the ASGI scope, which is
- expected as part of the changes done in #3039. This PR fixes the assumption that
- the key is always present and instead tries to optionally retrieve it.
-
- .. code-block::
-
- KeyError on GET /
- 'root_path'
-
- .. change:: ServerSentEvent typing error
- :type: bugfix
- :pr: 3048
-
- fixes small typing error:
-
- .. code-block::
-
- error: Argument 1 to "ServerSentEvent" has incompatible type "AsyncIterable[ServerSentEventMessage]"; expected "str | bytes | Iterable[str | bytes] | Iterator[str | bytes] | AsyncIterable[str | bytes] | AsyncIterator[str | bytes]" [arg-type]
-
- inside ``test_sse`` there was a ``Any`` I changed to trigger the test then solved it.
-
-
-.. changelog:: 2.5.3
- :date: 2024/01/29
-
- .. change:: Handle diverging ASGI ``root_path`` behaviour
- :type: bugfix
- :pr: 3039
- :issue: 3041
-
- Uvicorn `0.26.0 `_
- introduced a breaking change in its handling of the ASGI ``root_path`` behaviour,
- which, while adhering to the spec, diverges from the interpretation of other
- ASGI servers of this aspect of the spec (e.g. hypercorn and daphne do not follow
- uvicorn's interpretation as of today). A fix was introduced that ensures
- consistent behaviour of applications in any case.
-
-.. changelog:: 2.5.2
- :date: 2024/01/27
-
- .. change:: Ensure ``MultiDict`` and ``ImmutableMultiDict`` copy methods return the instance's type
- :type: bugfix
- :pr: 3009
- :issue: 2549
-
- Ensure :class:`~litestar.datastructures.MultiDict` and
- :class:`~litestar.datastructures.ImmutableMultiDict` copy methods return a new
- instance of ``MultiDict`` and ``ImmutableMultiDict``. Previously, these would
- return a :class:`multidict.MultiDict` instance.
-
- .. change:: Ensure ``exceptiongroup`` is installed on Python 3.11
- :type: bugfix
- :pr: 3035
- :issue: 3029
-
- Add the `exceptiongroup `_ package
- as a required dependency on Python ``<3.11`` (previously ``<3.10``) as a
- backport of `Exception Groups `_
-
-
-.. changelog:: 2.5.1
- :date: 2024/01/18
-
- .. change:: Fix OpenAPI schema generation for Union of multiple ``msgspec.Struct``\ s and ``None``
- :type: bugfix
- :pr: 2982
- :issue: 2971
-
- The following code would raise a :exc:`TypeError`
-
- .. code-block:: python
-
- import msgspec
-
- from litestar import get
- from litestar.testing import create_test_client
-
-
- class StructA(msgspec.Struct):
- pass
-
-
- class StructB(msgspec.Struct):
- pass
-
-
- @get("/")
- async def handler() -> StructA | StructB | None:
- return StructA()
-
-
- .. change:: Fix misleading error message for missing dependencies provide by a package extra
- :type: bugfix
- :pr: 2921
-
- Ensure that :exc:`MissingDependencyException` includes the correct name of the
- package to install if the package name differs from the Litestar package extra.
- (e.g. ``pip install litestar[jinja]`` vs ``pip install jinja2``). Previously the
- exception assumed the same name for both the package and package-extra name.
-
-
- .. change:: Fix OpenAPI schema file upload schema types for swagger
- :type: bugfix
- :pr: 2745
- :issue: 2628
-
- - Always set ``format`` as ``binary``
- - Fix schema for swagger with multiple files, which requires the type of the
- request body schema to be ``object`` with ``properties`` instead of a schema
- of type ``array`` and ``items``.
-
-
-
-.. changelog:: 2.5.0
- :date: 2024/01/06
-
- .. change:: Fix serialization of custom types in exception responses
- :type: bugfix
- :issue: 2867
- :pr: 2941
-
- Fix a bug that would lead to a :exc:`SerializationException` when custom types
- were present in an exception response handled by the built-in exception
- handlers.
-
- .. code-block:: python
-
- class Foo:
- pass
-
-
- @get()
- def handler() -> None:
- raise ValidationException(extra={"foo": Foo("bar")})
-
-
- app = Litestar(route_handlers=[handler], type_encoders={Foo: lambda foo: "foo"})
-
- The cause was that, in examples like the one shown above, ``type_encoders``
- were not resolved properly from all layers by the exception handling middleware,
- causing the serializer to throw an exception for an unknown type.
-
- .. change:: Fix SSE reverting to default ``event_type`` after 1st message
- :type: bugfix
- :pr: 2888
- :issue: 2877
-
- The ``event_type`` set within an SSE returned from a handler would revert back
- to a default after the first message sent:
-
- .. code-block:: python
-
- @get("/stream")
- async def stream(self) -> ServerSentEvent:
- async def gen() -> AsyncGenerator[str, None]:
- c = 0
- while True:
- yield f"
{c}
\n"
- c += 1
-
- return ServerSentEvent(gen(), event_type="my_event")
-
- In this example, the event type would only be ``my_event`` for the first
- message, and fall back to a default afterwards. The implementation has been
- fixed and will now continue sending the set event type for all messages.
-
- .. change:: Correctly handle single file upload validation when multiple files are specified
- :type: bugfix
- :pr: 2950
- :issue: 2939
-
- Uploading a single file when the validation target allowed multiple would cause
- a :exc:`ValidationException`:
-
- .. code-block:: python
-
- class FileUpload(Struct):
- files: list[UploadFile]
-
-
- @post(path="/")
- async def upload_files_object(
- data: Annotated[FileUpload, Body(media_type=RequestEncodingType.MULTI_PART)]
- ) -> list[str]:
- pass
-
-
- This could would only allow for 2 or more files to be sent, and otherwise throw
- an exception.
-
- .. change:: Fix trailing messages after unsubscribe in channels
- :type: bugfix
- :pr: 2894
-
- Fix a bug that would allow some channels backend to receive messages from a
- channel it just unsubscribed from, for a short period of time, due to how the
- different brokers handle unsubscribes.
-
- .. code-block:: python
-
- await backend.subscribe(["foo", "bar"]) # subscribe to two channels
- await backend.publish(
- b"something", ["foo"]
- ) # publish a message to a channel we're subscribed to
-
- # start the stream after publishing. Depending on the backend
- # the previously published message might be in the stream
- event_generator = backend.stream_events()
-
- # unsubscribe from the channel we previously published to
- await backend.unsubscribe(["foo"])
-
- # this should block, as we expect messages from channels
- # we unsubscribed from to not appear in the stream anymore
- print(anext(event_generator))
-
- Backends affected by this were in-memory, Redis PubSub and asyncpg. The Redis
- stream and psycopg backends were not affected.
-
- .. change:: Postgres channels backends
- :type: feature
- :pr: 2803
-
- Two new channel backends were added to bring Postgres support:
-
- :class:`~litestar.channels.backends.asyncpg.AsyncPgChannelsBackend`, using the
- `asyncpg `_ driver and
- :class:`~litestar.channels.backends.psycopg.PsycoPgChannelsBackend` using the
- `psycopg3 `_ async driver.
-
- .. seealso::
- :doc:`/usage/channels`
-
-
- .. change:: Add ``--schema`` and ``--exclude`` option to ``litestar route`` CLI command
- :type: feature
- :pr: 2886
-
- Two new options were added to the ``litestar route`` CLI command:
-
- - ``--schema``, to include the routes serving OpenAPI schema and docs
- - ``--exclude`` to exclude routes matching a specified pattern
-
- .. seealso:: Read more in the CLI :doc:`/reference/cli` section.
-
- .. change:: Improve performance of threaded synchronous execution
- :type: misc
- :pr: 2937
-
- Performance of threaded synchronous code was improved by using the async
- library's native threading helpers instead of anyio. On asyncio,
- :meth:`asyncio.loop.run_in_executor` is now used and on trio
- :func:`trio.to_thread.run_sync`.
-
- Beneficiaries of these performance improvements are:
-
- - Synchronous route handlers making use of ``sync_to_thread=True``
- - Synchronous dependency providers making use of ``sync_to_thread=True``
- - Synchronous SSE generators
- - :class:`~litestar.stores.file.FileStore`
- - Large file uploads where the ``max_spool_size`` is exceeded and the spooled
- temporary file has been rolled to disk
- - :class:`~litestar.response.file.File` and
- :class:`~litestar.response.file.ASGIFileResponse`
-
-
-.. changelog:: 2.4.5
- :date: 2023/12/23
-
- .. change:: Fix validation of empty payload data with default values
- :type: bugfix
- :issue: 2902
- :pr: 2903
-
- Prior to this fix, a handler like:
-
- .. code-block:: python
-
- @post(path="/", sync_to_thread=False)
- def test(data: str = "abc") -> dict:
- return {"foo": data}
-
- ``$ curl localhost:8000 -X POST``
-
- would return a client error like:
-
- .. code-block:: bash
-
- {"status_code":400,"detail":"Validation failed for POST http://localhost:8000/","extra":[{"message":"Expected `str`, got `null`","key":"data","source":"body"}]}
-
- .. change:: Support for returning ``Response[None]`` with a ``204`` status code from a handler
- :type: bugfix
- :pr: 2915
- :issue: 2914
-
- Returning a ``Response[None]`` from a route handler for a response with a
- ``204`` now works as expected without resulting in an
- :exc:`ImproperlyConfiguredException`
-
- .. change:: Fix error message of ``get_logger_placeholder()``
- :type: bugfix
- :pr: 2919
-
- Using a method on
- :attr:`Request.logger ` when not
- setting a ``logging_config`` on the application would result in a non-descriptive
- :exc:`TypeError`. An :exc:`ImproperlyConfiguredException` with an explanation is
- now raised instead.
-
-
-.. changelog:: 2.4.4
- :date: 2023/12/13
-
- .. change:: Support non-valid identifier as serialization target name
- :type: bugfix
- :pr: 2850
- :issue: 2845
-
- Fix a bug where DTOs would raise a ``TypeError: __slots__ must be identifiers``
- during serialization, if a non-valid identifier (such as ``field-name``)was used
- for field renaming.
-
- .. change:: Fix regression signature validation for DTO validated types
- :type: bugfix
- :pr: 2854
- :issue: 2149
-
- Fix a regression introduced in ``2.0.0rc1`` that would cause data validated by
- the DTO to be validated again by the signature model.
-
- .. change:: Fix regression in OpenAPI schema key names
- :type: bugfix
- :pr: 2841
- :issue: 2804
-
- Fix a regression introduced in ``2.4.0`` regarding the naming of OpenAPI schema
- keys, in which a change was introduced to the way that keys for the OpenAPI
- components/schemas objects were calculated to address the possibility of name
- collisions.
-
- This behaviour was reverted for the case where a name has no collision, and now
- only introduces extended keys for the case where there are multiple objects with
- the same name, a case which would previously result in an exception.
-
- .. change:: Fix regression in OpenAPI handling of routes with multiple handlers
- :type: bugfix
- :pr: 2864
- :issue: 2863
-
- Fix a regression introduced in ``2.4.3`` causing two routes registered with the
- same path, but different methods to break OpenAPI schema generation due to both
- of them having the same value for operation ID.
-
- .. change:: Fix OpenAPI schema generation for recursive models
- :type: bugfix
- :pr: 2869
- :issue: 2429
-
- Fix an issue that would lead to a :exc:`RecursionError` when including nested
- models in the OpenAPI schema.
-
-
-.. changelog:: 2.4.3
- :date: 2023/12/07
-
- .. change:: Fix OpenAPI schema for ``Literal | None`` unions
- :type: bugfix
- :issue: 2812
- :pr: 2818
-
- Fix a bug where an incorrect OpenAPI schema was generated generated when any
- ``Literal | None``-union was present in an annotation.
-
- For example
-
- .. code-block:: python
-
- type: Literal["sink", "source"] | None
-
- would generate
-
- .. code-block:: json
-
- {
- "name": "type",
- "in": "query",
- "schema": {
- "type": "string",
- "enum": [ "sink", "source", null ]
- }
- }
-
- .. change:: Fix advanced-alchemy 0.6.0 compatibility issue with ``touch_updated_timestamp``
- :type: bugfix
- :pr: 2843
-
- Fix an incorrect import for ``touch_updated_timestamp`` of Advanced Alchemy,
- introduced in Advanced-Alchemy version 0.6.0.
-
-.. changelog:: 2.4.2
- :date: 2023/12/02
-
- .. change:: Fix OpenAPI handling of parameters with duplicated names
- :type: bugfix
- :issue: 2662
- :pr: 2788
-
- Fix a bug where schema generation would consider two parameters with the same
- name but declared in different places (eg., header, cookie) as an error.
-
- .. change:: Fix late failure where ``DTOData`` is used without a DTO
- :type: bugfix
- :issue: 2779
- :pr: 2789
-
- Fix an issue where a handler would be allowed to be registered with a
- ``DTOData`` annotation without having a DTO defined, which would result in a
- runtime exception. In cases like these, a configuration error is now raised
- during startup.
-
- .. change:: Correctly propagate camelCase names on OpenAPI schema
- :type: bugfix
- :pr: 2800
-
- Fix a bug where OpenAPI schema fields would be inappropriately propagated as
- camelCase where they should have been snake_case
-
- .. change:: Fix error handling in event handler stream
- :type: bugfix
- :pr: 2810, 2814
-
- Fix a class of errors that could result in the event listener stream being
- terminated when an exception occurred within an event listener. Errors in
- event listeners are now not propagated anymore but handled by the backend and
- logged instead.
-
- .. change:: Fix OpenAPI schema for Pydantic computed fields
- :type: bugfix
- :pr: 2797
- :issue: 2792
-
- Add support for including computed fields in schemas generated from Pydantic
- models.
-
-.. changelog:: 2.4.1
- :date: 2023/11/28
-
- .. change:: Fix circular import when importing from ``litestar.security.jwt``
- :type: bugfix
- :pr: 2784
- :issue: 2782
-
- An :exc:`ImportError` was raised when trying to import from ``litestar.security.jwt``. This was fixed
- by removing the imports from the deprecated ``litestar.contrib.jwt`` within ``litesetar.security.jwt``.
-
- .. change:: Raise config error when generator dependencies are cached
- :type: bugfix
- :pr: 2780
- :issue: 2771
-
- Previously, an :exc:`InternalServerError` was raised when attempting to use
- `use_cache=True` with generator dependencies. This will now raise a configuration
- error during application startup.
-
-.. changelog:: 2.4.0
- :date: 2023/11/27
-
- .. change:: Fix ``HTTPException`` handling during concurrent dependency resolving
- :type: bugfix
- :pr: 2596
- :issue: 2594
-
- An issue was fixed that would lead to :exc:`HTTPExceptions` not being re-raised
- properly when they occurred within the resolution of nested dependencies during
- the request lifecycle.
-
- .. change:: Fix OpenAPI examples format
- :type: bugfix
- :pr: 2660
- :issue: 2272
-
- Fix the OpenAPI examples format by removing the wrapping object.
-
- Before the change, for a given model
-
- .. code-block:: python
-
- @dataclass
- class Foo:
- foo: int
-
- The following example would be generated:
-
- .. code-block:: json
-
- {
- "description": "Example value",
- "value": {
- "foo": 7906
- }
- }
-
- After the fix, this is now:
-
- .. code-block:: json
-
- {
- "foo": 7906
- }
-
- .. change:: Fix CLI plugin commands not showing up in command list
- :type: bugfix
- :pr: 2441
-
- Fix a bug where commands registered by CLI plugins were available, but would not
- show up in the commands list
-
- .. change:: Fix missing ``write-only`` mark in ``dto_field()`` signature
- :type: bugfix
- :pr: 2684
-
- Fix the missing ``write-only`` string literal in the ``mark`` parameter of
- :func:`~litestar.dto.field.dto_field`
-
- .. change:: Fix OpenAPI schemas incorrectly flagged as duplicates
- :type: bugfix
- :pr: 2475
- :issue: 2471
-
- Fix an issue that would lead to OpenAPI schemas being incorrectly considered
- duplicates, resulting in an :exc:`ImproperlyConfiguredException` being raised.
-
- .. change:: Fix Pydantic URL type support in OpenAPI and serialization
- :type: bugfix
- :pr: 2701
- :issue: 2664
-
- Add missing support for Pydantic's URL types (``AnyUrl`` and its descendants)
- for both serialization and OpenAPI schema generation. These types were only
- partially supported previously; Serialization support was lacking for v1 and v2,
- and OpenAPI support was missing for v2.
-
- .. change:: Fix incorrect ``ValidationException`` message when multiple errors were encountered
- :type: bugfix
- :pr: 2716
- :issue: 2714
-
- Fix a bug where :exc:`ValidationException` could contain duplicated messages in
- ``extra`` field, when multiple errors were encountered during validation
-
- .. change:: Fix DTO renaming renames all fields of the same name in nested DTOs
- :type: bugfix
- :pr: 2764
- :issue: 2721
-
- Fix an issue with nested field renaming in DTOs that would lead to all fields
- with a given name to be renamed in a nested structure.
-
- In the below example, both ``Foo.id`` and ``Bar.id`` would have been renamed to
- ``foo_id``
-
- .. code-block:: python
-
- from dataclasses import dataclass
-
-
- @dataclass
- class Bar:
- id: str
-
-
- @dataclass
- class Foo:
- id: str
- bar: Bar
-
-
- FooDTO = DataclassDTO[Annotated[Foo, DTOConfig(rename_fields={"id": "foo_id"})]]
-
- .. change:: Fix handling of DTO objects nested in mappings
- :type: bugfix
- :pr: 2775
- :issue: 2737
-
- Fix a bug where DTOs nested in a :class:`~typing.Mapping` type would fail to
- serialize correctly.
-
- .. change:: Fix inconsistent sequence union parameter errors
- :type: bugfix
- :pr: 2776
- :issue: 2600
-
- Fix a bug where unions of collection types would result in different errors
- depending on whether the union included :obj:`None` or not.
-
- .. change:: Fix graceful handling of WebSocket disconnect in channels WebSockets handlers
- :type: bugfix
- :pr: 2691
-
- Fix the behaviour of WebSocket disconnect handling within the WebSocket handlers
- provided by :doc:`channels `, that would sometimes lead to
- a ``RuntimeError: Unexpected ASGI message 'websocket.close', after sending 'websocket.close'.``
- exception being raised upon the closing of a WebSocket connection.
-
-
- .. change:: Add ``server_lifespan`` hook
- :type: feature
- :pr: 2658
-
- A new ``server_lifespan`` hook is now available on :class:`~litestar.app.Litestar`.
- This hook works similar to the regular ``lifespan`` context manager, with the
- difference being is that it is only called once for the entire server lifespan,
- not for each application startup phase. Note that these only differ when running
- with an ASGI server that's using multiple worker processes.
-
- .. change:: Allow rendering templates directly from strings
- :type: feature
- :pr: 2689
- :issue: 2687
-
- A new ``template_string`` parameter was added to :class:`~litestar.template.Template`,
- allowing to render templates directly from strings.
-
- .. seealso::
- :ref:`usage/templating:Template Files vs. Strings`
-
- .. change:: Support nested DTO field renaming
- :type: feature
- :pr: 2764
- :issue: 2721
-
- Using similar semantics as for exclusion/inclusion, nested DTO fields can now
- also be renamed:
-
- .. code-block:: python
-
- from dataclasses import dataclass
-
-
- @dataclass
- class Bar:
- id: str
-
-
- @dataclass
- class Foo:
- id: str
- bars: list[Bar]
-
-
- FooDTO = DataclassDTO[Annotated[Foo, DTOConfig(rename_fields={"bars.0.id": "bar_id"})]]
-
-
-.. changelog:: 2.3.2
- :date: 2023/11/06
-
- .. change:: Fix recursion error when re-using the path of a route handler for static files
- :type: bugfix
- :pr: 2630
- :issue: 2629
-
- A regression was fixed that would cause a recursion error when the path of a
- static files host was reused for a route handler with a different HTTP method.
-
- .. code-block:: python
-
- from litestar import Litestar
- from litestar import post
- from litestar.static_files import StaticFilesConfig
-
-
- @post("/uploads")
- async def handler() -> None:
- pass
-
-
- app = Litestar(
- [handler],
- static_files_config=[
- StaticFilesConfig(directories=["uploads"], path="/uploads"),
- ],
- )
-
-
-.. changelog:: 2.3.1
- :date: 2023/11/04
-
- .. change:: CLI: Fix not providing SSL certfiles breaks uvicorn command when using reload or multiple workers
- :type: bugfix
- :pr: 2616
- :issue: 2613
-
- Fix an issue where not providing the ``--ssl-certfile`` and ``--ssl-keyfile``
- options to the ``litestar run`` command would cause a :exc:`FileNotFoundError`
- in uvicorn, when used together with the ``--reload``, ``--web-concurrency``
- options.
-
-
-.. changelog:: 2.3.0
- :date: 2023/11/02
-
- .. change:: Python 3.12 support
- :type: feature
- :pr: 2396
- :issue: 1862
-
- Python 3.12 is now fully supported and tested.
-
- .. change:: New layered parameter ``signature_types``
- :type: feature
- :pr: 2422
-
- Types in this collection are added to ``signature_namespace`` using the type's
- ``__name__`` attribute.
- This provides a nicer interface when adding names to the signature namespace
- w ithout modifying the type name, e.g.: ``signature_namespace={"Model": Model}``
- is equivalent to ``signature_types=[Model]``.
-
- The implementation makes it an error to supply a type in ``signature_types``
- that has a value for ``__name__`` already in the signature namespace.
-
- It will also throw an error if an item in ``signature_types`` has no
- ``__name__`` attribute.
-
- .. change:: Added RapiDoc for OpenAPI schema visualisation
- :type: feature
- :pr: 2522
-
- Add support for using `RapiDoc `_ for
- OpenAPI schema visualisation.
-
- .. change:: Support Pydantic 1 & 2 within the same application
- :type: feature
- :pr: 2487
-
- Added support for Pydantic 1 & 2 within the same application by integrating with
- Pydantic's backwards compatibility layer:
-
- .. code-block:: python
-
- from litestar import get
- from pydantic.v1 import BaseModel as BaseModelV1
- from pydantic import BaseModel
-
-
- class V1Foo(BaseModelV1):
- bar: str
-
-
- class V2Foo(BaseModel):
- bar: str
-
-
- @get("/1")
- def foo_v1(data: V1Foo) -> V1Foo:
- return data
-
-
- @get("/2")
- def foo_v2(data: V2Foo) -> V2Foo:
- return data
-
- .. change:: Add ``ResponseCacheConfig.cache_response_filter`` to allow filtering responses eligible for caching
- :type: feature
- :pr: 2537
- :issue: 2501
-
- ``ResponseCacheConfig.cache_response_filter`` is predicate called by the
- response cache middleware that discriminates whether a response should be
- cached, or not.
-
-
- .. change:: SSL support and self-signed certificates for CLI
- :type: feature
- :pr: 2554
- :issue: 2335
-
- Add support for SSL and generating self-signed certificates to the CLI.
-
- For this, three new arguments were added to the CLI's ``run`` command:
-
- - ``--ssl-certfile``
- - ``--ssl-keyfile``
- - ``--create-self-signed-cert``
-
- The ``--ssl-certfile`` and `--ssl-keyfile` flags are passed to uvicorn when
- using ``litestar run``. Uvicorn requires both to be passed (or neither) but
- additional validation was added to generate a more user friendly CLI errors.
-
- The other SSL-related flags (like password or CA) were not added (yet). See
- `uvicorn CLI docs `_
-
- **Generating of a self-signed certificate**
-
- One more CLI flag was added (``--create-devcert``) that uses the
- ``cryptography`` module to generate a self-signed development certificate. Both
- of the previous flags must be passed when using this flag. Then the following
- logic is used:
-
- - If both files already exists, they are used and nothing is generated
- - If neither file exists, the dev cert and key are generated
- - If only one file exists, it is ambiguous what to do so an exception is raised
-
- .. change:: Use custom request class when given during exception handling
- :type: bugfix
- :pr: 2444
- :issue: 2399
-
- When a custom ``request_class`` is provided, it will now be used while returning
- an error response
-
- .. change:: Fix missing OpenAPI schema for generic response type annotations
- :type: bugfix
- :pr: 2463
- :issue: 2383
-
- OpenAPI schemas are now correctly generated when a response type annotation
- contains a generic type such as
-
- .. code-block:: python
-
- from msgspec import Struct
- from litestar import Litestar, get, Response
- from typing import TypeVar, Generic, Optional
-
- T = TypeVar("T")
-
-
- class ResponseStruct(Struct, Generic[T]):
- code: int
- data: Optional[T]
-
-
- @get("/")
- def test_handler() -> Response[ResponseStruct[str]]:
- return Response(
- ResponseStruct(code=200, data="Hello World"),
- )
-
- .. change:: Fix rendering of OpenAPI examples
- :type: bugfix
- :pr: 2509
- :issue: 2494
-
- An issue was fixed where OpenAPI examples would be rendered as
-
- .. code-block:: json
-
- {
- "parameters": [
- {
- "schema": {
- "type": "string",
- "examples": [
- {
- "summary": "example summary",
- "value": "example value"
- }
- ]
- }
- }
- ]
- }
-
- instead of
-
- .. code-block:: json
-
- {
- "parameters": [
- {
- "schema": {
- "type": "string"
- },
- "examples": {
- "example1": {
- "summary": "example summary"
- "value": "example value"
- }
- }
- }
- ]
- }
-
- .. change:: Fix non UTF-8 handling when logging requests
- :type: bugfix
- :issue: 2529
- :pr: 2530
-
- When structlog is not installed, the request body would not get parsed and shown
- as a byte sequence. Instead, it was serialized into a string with the assumption
- that it is valid UTF-8. This was fixed by decoding the bytes with
- ``backslashreplace`` before displaying them.
-
- .. change:: Fix ``ExceptionHandler`` typing to properly support ``Exception`` subclasses
- :type: bugfix
- :issue: 2520
- :pr: 2533
-
- Fix the typing for ``ExceptionHandler`` to support subclasses of ``Exception``,
- such that code like this will type check properly:
-
- .. code-block:: python
-
- from litestar import Litestar, Request, Response
-
-
- class CustomException(Exception): ...
-
-
- def handle_exc(req: Request, exc: CustomException) -> Response: ...
-
- .. change:: Fix OpenAPI schema generation for variable length tuples
- :type: bugfix
- :issue: 2460
- :pr: 2552
-
- Fix a bug where an annotation such as ``tuple[str, ...]`` would cause a
- ``TypeError: '<' not supported between instances of 'NoneType' and 'OpenAPIType')``.
-
- .. change:: Fix channels performance issue when polling with no subscribers in ``arbitrary_channels_allowed`` mode
- :type: bugfix
- :pr: 2547
-
- Fix a bug that would cause high CPU loads while idling when using a
- ``ChannelsPlugin`` with the ``arbitrary_channels_allowed`` enabled and while no
- subscriptions for any channel were active.
-
- .. change:: Fix CLI schema export for non-serializable types when using ``create_examples=True``
- :type: bugfix
- :pr: 2581
- :issue: 2575
-
- When trying to export a schema via the
- ``litestar schema openapi --output schema.json`` making use of a non-JSON
- serializable type, would result in an encoding error because the standard
- library JSON serializer was used. This has been fixed by using Litestar's own
- JSON encoder, enabling the serialization of all types supplied by the schema.
-
- .. change:: Fix OpenAPI schema generation for ``Literal`` and ``Enum`` unions with ``None``
- :type: bugfix
- :pr: 2550
- :issue: 2546
-
- Existing behavior was to make the schema for every type that is a union with
- ``None`` a ``"one_of"`` schema, that includes ``OpenAPIType.NULL`` in the
- ``"one_of"`` types.
-
- When a ``Literal`` or ``Enum`` type is in a union with ``None``, this behavior
- is not desirable, as we want to have ``null`` available in the list of available
- options on the type's schema.
-
- This was fixed by modifying ``Literal`` and ``Enum`` schema generation so that i
- t can be identified that the types are in a union with ``None``, allowing
- ``null`` to be included in ``Schema.enum`` values.
-
- .. change:: Fix cache overrides when using same route with different handlers
- :type: bugfix
- :pr: 2592
- :issue: 2573, 2588
-
- A bug was fixed that would cause the cache for routes being overwritten by a
- route handler on that same route with a different HTTP method.
-
-
-
-.. changelog:: 2.2.0
- :date: 2023/10/12
-
- .. change:: Fix implicit conversion of objects to ``bool`` in debug response
- :type: bugfix
- :pr: 2384
- :issue: 2381
-
- The exception handler middleware would, when in debug mode, implicitly call an
- object's :meth:`__bool__ `, which would lead to errors if that
- object overloaded the operator, for example if the object in question was a
- SQLAlchemy element.
-
- .. change:: Correctly re-export filters and exceptions from ``advanced-alchemy``
- :type: bugfix
- :pr: 2360
- :issue: 2358
-
- Some re-exports of filter and exception types from ``advanced-alchemy`` were
- missing, causing various issues when ``advanced-alchemy`` was installed, but
- Litestar would still use its own version of these classes.
-
- .. change:: Re-add ``create_engine`` method to SQLAlchemy configs
- :type: bugfix
- :pr: 2382
-
- The ``create_engine`` method was removed in an ``advanced-alchemy`` releases.
- This was addresses by re-adding it to the versions provided by Litestar.
-
- .. change:: Fix ``before_request`` modifies route handler signature
- :type: bugfix
- :pr: 2391
- :issue: 2368
-
- The ``before_request`` would modify the return annotation of associated
- route handlers to conform with its own return type annotation, which would cause
- issues and unexpected behaviour when that annotation was not compatible with the
- original one.
-
- This was fixed by not having the ``before_request`` handler modify the
- route handler's signature. Users are now expected to ensure that values returned
- from a ``before_request`` handler conform to the return type annotation of the
- route handler.
-
- .. change:: Ensure compression is applied before caching when using compression middleware
- :type: bugfix
- :pr: 2393
- :issue: 1301
-
- A previous limitation was removed that would apply compression from the
- :class:`~litestar.middleware.compression.CompressionMiddleware` only *after* a
- response was restored from the cache, resulting in unnecessary repeated
- computation and increased size of the stored response.
-
- This was due to caching being handled on the response layer, where a response
- object would be pickled, restored upon a cache hit and then re-sent, including
- all middlewares.
-
- The new implementation now instead applies caching on the ASGI level; Individual
- messages sent to the ``send`` callable are cached, and later re-sent. This
- process ensures that the compression middleware has been applied before, and
- will be skipped when re-sending a cached response.
-
- In addition, this increases performance and reduces storage size even in cases
- where no compression is applied because the slow and inefficient pickle format
- can be avoided.
-
- .. change:: Fix implicit JSON parsing of URL encoded data
- :type: bugfix
- :pr: 2394
-
- A process was removed where Litestar would implicitly attempt to parse parts of
- URL encoded data as JSON. This was originally added to provide some performance
- boosts when that data was in fact meant to be JSON, but turned out to be too
- fragile.
-
- Regular data conversion / validation is unaffected by this.
-
- .. change:: CLI enabled by default
- :type: feature
- :pr: 2346
- :issue: 2318
-
- The CLI and all its dependencies are now included by default, to enable a better
- and more consistent developer experience out of the box.
-
- The previous ``litestar[cli]`` extra is still available for backwards
- compatibility, but as of ``2.2.0`` it is without effect.
-
- .. change:: Customization of Pydantic integration via ``PydanticPlugin``
- :type: feature
- :pr: 2404
- :issue: 2373
-
- A new :class:`~litestar.contrib.pydantic.PydanticPlugin` has been added, which
- can be used to configure Pydantic behaviour. Currently it supports setting a
- ``prefer_alias`` option, which will pass the ``by_alias=True`` flag to Pydantic
- when exporting models, as well as generate schemas accordingly.
-
- .. change:: Add ``/schema/openapi.yml`` to the available schema paths
- :type: feature
- :pr: 2411
-
- The YAML version of the OpenAPI schema is now available under
- ``/schema/openapi.yml`` in addition to ``/schema/openapi.yaml``.
-
- .. change:: Add experimental DTO codegen backend
- :type: feature
- :pr: 2388
-
- A new DTO backend was introduced which speeds up the transfer process by
- generating optimized Python code ahead of time. Testing shows that the new
- backend is between 2.5 and 5 times faster depending on the operation and data
- provided.
-
- The new backend can be enabled globally for all DTOs by passing the appropriate
- feature flag to the Litestar application:
-
- .. code-block:: python
-
- from litestar import Litestar
- from litestar.config.app import ExperimentalFeatures
-
- app = Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN])
-
- .. seealso::
- For more information see
- :ref:`usage/dto/0-basic-use:Improving performance with the codegen backend`
-
-
- .. change:: Improved error messages for missing required parameters
- :type: feature
- :pr: 2418
-
- Error messages for missing required parameters will now also contain the source
- of the expected parameter:
-
- Before:
-
- .. code-block:: json
-
- {
- "status_code": 400,
- "detail": "Missing required parameter foo for url http://testerver.local"
- }
-
-
- After:
-
- .. code-block:: json
-
- {
- "status_code": 400,
- "detail": "Missing required header parameter 'foo' for url http://testerver.local"
- }
-
-
-.. changelog:: 2.1.1
- :date: 2023/09/24
-
- .. change:: Fix ``DeprecationWarning`` raised by ``Response.to_asgi_response``
- :type: bugfix
- :pr: 2364
-
- :meth:`~litestar.response.Response.to_asgi_response` was passing a
- non-:obj:`None` default value (``[]``) to ``ASGIResponse`` for
- ``encoded_headers``, resulting in a :exc:`DeprecationWarning` being raised.
- This was fixed by leaving the default value as :obj:`None`.
-
-
-.. changelog:: 2.1.0
- :date: 2023/09/23
-
- `View the full changelog `_
-
- .. change:: Make ``302`` the default ``status_code`` for redirect responses
- :type: feature
- :pr: 2189
- :issue: 2138
-
- Make ``302`` the default ``status_code`` for redirect responses
-
- .. change:: Add :meth:`include_in_schema` option for all layers
- :type: feature
- :pr: 2295
- :issue: 2267
-
- Adds the :meth:`include_in_schema` option to all layers, allowing to include/exclude
- specific routes from the generated OpenAPI schema.
-
- .. change:: Deprecate parameter ``app`` of ``Response.to_asgi_response``
- :type: feature
- :pr: 2268
- :issue: 2217
-
- Adds deprecation warning for unused ``app`` parameter of ``to_asgi_response`` as
- it is unused and redundant due to ``request.app`` being available.
-
- .. change:: Authentication: Add parameters to set the JWT ``extras`` field
- :type: feature
- :pr: 2313
-
- Adds ``token_extras`` to both :func:`BaseJWTAuth.login` and :meth:`BaseJWTAuth.create_token` methods,
- to allow the definition of the ``extras`` JWT field.
-
- .. change:: Templating: Add possibility to customize Jinja environment
- :type: feature
- :pr: 2195
- :issue: 965
-
- Adds the ability to pass a custom Jinja2 ``Environment`` or Mako ``TemplateLookup`` by providing a
- dedicated class method.
-
- .. change:: Add support for `minjinja `_
- :type: feature
- :pr: 2250
-
- Adds support for MiniJinja, a minimal Jinja2 implementation.
-
- .. seealso:: :doc:`/usage/templating`
-
- .. change:: SQLAlchemy: Exclude implicit fields for SQLAlchemy DTO
- :type: feature
- :pr: 2170
-
- :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be
- configured using a separate config object. This can be set using both
- class inheritance and `Annotated `_:
-
- .. code-block:: python
- :caption: :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be configured using a separate config object using ``config`` object.
-
- class MyModelDTO(SQLAlchemyDTO[MyModel]):
- config = SQLAlchemyDTOConfig()
-
- or
-
- .. code-block:: python
- :caption: :class:`SQLAlchemyDTO (Advanced Alchemy) ` can now be configured using a separate config object using ``Annotated``.
-
- MyModelDTO = SQLAlchemyDTO[Annotated[MyModel, SQLAlchemyDTOConfig()]]
-
- The new configuration currently accepts a single attribute which is ``include_implicit_fields`` that has
- a default value of ``True``. If set to to ``False``, all implicitly mapped columns will be hidden
- from the ``DTO``. If set to ``hybrid-only``, then hybrid properties will be shown but not other
- implicit columns.
-
- Finally, implicit columns that are marked with ``Mark.READ_ONLY`` or ``Mark.WRITE_ONLY``
- will always be shown regardless of the value of ``include_implicit_fields``.
-
- .. change:: SQLAlchemy: Allow repository functions to be filtered by expressions
- :type: feature
- :pr: 2265
-
- Enhances the SQLALchemy repository so that you can more easily pass in complex ``where`` expressions into the repository functions.
-
- .. tip:: Without this, you have to override the ``statement`` parameter and it separates the where conditions from the filters and the ``kwargs``.
-
- Allows usage of this syntax:
-
- .. code-block:: python
-
- locations, total_count = await model_service.list_and_count(
- ST_DWithin(UniqueLocation.location, geog, 1000), account_id=str(account_id)
- )
-
- instead of the previous method of overriding the ``statement``:
-
- .. code-block:: python
-
- locations, total_count = await model_service.list_and_count(
- statement=select(Model).where(ST_DWithin(UniqueLocation.location, geog, 1000)),
- account_id=str(account_id),
- )
-
- .. change:: SQLAlchemy: Use :func:`lambda_stmt ` in the repository
- :type: feature
- :pr: 2179
-
- Converts the repository to use :func:`lambda_stmt `
- instead of the normal ``select``
-
- .. change:: SQLAlchemy: Swap to the `advanced_alchemy `_ implementations
- :type: feature
- :pr: 2312
-
- Swaps the internal SQLAlchemy repository to use the external
- `advanced_alchemy `_ library implementations
-
- .. change:: Remove usages of deprecated ``ExceptionHandlerMiddleware`` ``debug`` parameter
- :type: bugfix
- :pr: 2192
-
- Removes leftover usages of deprecated ``ExceptionHandlerMiddleware`` debug parameter.
-
- .. change:: DTOs: Raise :class:`ValidationException` when Pydantic validation fails
- :type: bugfix
- :pr: 2204
- :issue: 2190
-
- Ensures that when the Pydantic validation fails in the Pydantic DTO,
- a :class:`ValidationException` is raised with the extras set to the errors given by Pydantic.
-
- .. change:: Set the max width of the console to 80
- :type: bugfix
- :pr: 2244
-
- Sets the max width of the console to 80, to prevent the output from being
- wrapped.
-
- .. change:: Handling of optional path parameters
- :type: bugfix
- :pr: 2224
- :issue: 2222
-
- Resolves an issue where optional path parameters caused a 500 error to be raised.
-
- .. change:: Use os.replace instead of shutil.move for renaming files
- :type: bugfix
- :pr: 2223
-
- Change to using :func:`os.replace` instead of :func:`shutil.move` for renaming files, to
- ensure atomicity.
-
- .. change:: Exception detail attribute
- :type: bugfix
- :pr: 2231
-
- Set correctly the detail attribute on :class:`LitestarException` and :class:`HTTPException`
- regardless of whether it's passed positionally or by name.
-
- .. change:: Filters not available in ``exists()``
- :type: bugfix
- :pr: 2228
- :issue: 2221
-
- Fixes :meth:`exists` method for SQLAlchemy sync and async.
-
- .. change:: Add Pydantic types to SQLAlchemy registry only if Pydantic is installed
- :type: bugfix
- :pr: 2252
-
- Allows importing from ``litestar.contrib.sqlalchemy.base`` even if Pydantic is not installed.
-
- .. change:: Don't add content type for responses that don't have a body
- :type: bugfix
- :pr: 2263
- :issue: 2106
-
- Ensures that the ``content-type`` header is not added for responses that do not have a
- body such as responses with status code ``204 (No Content)``.
-
- .. change:: ``SQLAlchemyPlugin`` refactored
- :type: bugfix
- :pr: 2269
-
- Changes the way the ``SQLAlchemyPlugin`` to now append the other plugins instead of the
- inheritance that was previously used. This makes using the ``plugins.get`` function work as expected.
-
- .. change:: Ensure ``app-dir`` is appended to path during autodiscovery
- :type: bugfix
- :pr: 2277
- :issue: 2266
-
- Fixes a bug which caused the ``--app-dir`` option to the Litestar CLI to not be propagated during autodiscovery.
-
- .. change:: Set content length header by default
- :type: bugfix
- :pr: 2271
-
- Sets the ``content-length`` header by default even if the length of the body is ``0``.
-
- .. change:: Incorrect handling of mutable headers in :class:`ASGIResponse`
- :type: bugfix
- :pr: 2308
- :issue: 2196
-
- Update :class:`ASGIResponse`, :class:`Response` and friends to address a few issues related to headers:
-
- - If ``encoded_headers`` were passed in at any point, they were mutated within responses, leading to a growing list of headers with every response
- - While mutating ``encoded_headers``, the checks performed to assert a value was (not) already present, headers were not treated case-insensitive
- - Unnecessary work was performed while converting cookies / headers into an encoded headers list
-
- This was fixed by:
-
- - Removing the use of and deprecate ``encoded_headers``
- - Handling headers on :class:`ASGIResponse` with :class:`MutableScopeHeaders`, which allows for case-insensitive membership tests, ``.setdefault`` operations, etc.
-
- .. change:: Adds missing ORM registry export
- :type: bugfix
- :pr: 2316
-
- Adds an export that was overlooked for the base repo
-
- .. change:: Discrepancy in ``attrs``, ``msgspec`` and ``Pydantic`` for multi-part forms
- :type: bugfix
- :pr: 2280
- :issue: 2278
-
- Resolves issue in ``attrs``, ``msgspec`` and Pydantic for multi-part forms
-
- .. change:: Set proper default for ``exclude_http_methods`` in auth middleware
- :type: bugfix
- :pr: 2325
- :issue: 2205
-
- Sets ``OPTIONS`` as the default value for ``exclude_http_methods`` in the base authentication middleware class.
-
-.. changelog:: 2.0.0
- :date: 2023/08/19
-
- .. change:: Regression | Missing ``media_type`` information to error responses
- :type: bugfix
- :pr: 2131
- :issue: 2024
-
- Fixed a regression that caused error responses to be sent using a mismatched
- media type, e.g. an error response from a ``text/html`` endpoint would be sent
- as JSON.
-
- .. change:: Regression | ``Litestar.debug`` does not propagate to exception handling middleware
- :type: bugfix
- :pr: 2153
- :issue: 2147
-
- Fixed a regression where setting ``Litestar.debug`` would not propagate to the
- exception handler middleware, resulting in exception responses always being sent
- using the initial debug value.
-
- .. change:: Static files not being served if a route handler with the same base path was registered
- :type: bugfix
- :pr: 2154
-
- Fixed a bug that would result in a ``404 - Not Found`` when requesting a static
- file where the :attr:`~litestar.static_files.StaticFilesConfig.path` was also
- used by a route handler.
-
- .. change:: HTMX: Missing default values for ``receive`` and ``send`` parameters of ``HTMXRequest``
- :type: bugfix
- :pr: 2145
-
- Add missing default values for the ``receive`` and ``send`` parameters of
- :class:`~litestar.contrib.htmx.request.HTMXRequest`.
-
- .. change:: DTO: Excluded attributes accessed during transfer
- :type: bugfix
- :pr: 2127
- :issue: 2125
-
- Fix the behaviour of DTOs such that they will no longer access fields that have
- been included. This behaviour would previously cause issues when these
- attributes were either costly or impossible to access (e.g. lazy loaded
- relationships of a SQLAlchemy model).
-
- .. change:: DTO | Regression: ``DTOData.create_instance`` ignores renaming
- :type: bugfix
- :pr: 2144
-
- Fix a regression where calling
- :meth:`~litestar.dto.data_structures.DTOData.create_instance` would ignore the
- renaming settings of fields.
-
- .. change:: OpenAPI | Regression: Response schema for files and streams set ``application/octet-stream`` as ``contentEncoding`` instead of ``contentMediaType``
- :type: bugfix
- :pr: 2130
-
- Fix a regression that would set ``application/octet-stream`` as the ``contentEncoding``
- instead of ``contentMediaType`` in the response schema of
- :class:`~litestar.response.File` :class:`~litestar.response.Stream`.
-
- .. change:: OpenAPI | Regression: Response schema diverges from ``prefer_alias`` setting for Pydantic models
- :type: bugfix
- :pr: 2150
-
- Fix a regression that made the response schema use ``prefer_alias=True``,
- diverging from how Pydantic models are exported by default.
-
- .. change:: OpenAPI | Regression: Examples not being generated deterministically
- :type: bugfix
- :pr: 2161
-
- Fix a regression that made generated examples non-deterministic, caused by a
- misconfiguration of the random seeding.
-
- .. change:: SQLAlchemy repository: Handling of dialects not supporting JSON
- :type: bugfix
- :pr: 2139
- :issue: 2137
-
- Fix a bug where SQLAlchemy would raise a :exc:`TypeError` when using a dialect
- that does not support JSON with the SQLAlchemy repositories.
-
- .. change:: JWT | Regression: ``OPTIONS`` and ``HEAD`` being authenticated by default
- :type: bugfix
- :pr: 2160
-
- Fix a regression that would make
- ``litestar.contrib.jwt.JWTAuthenticationMiddleware`` authenticate
- ``OPTIONS`` and ``HEAD`` requests by default.
-
- .. change:: SessionAuth | Regression: ``OPTIONS`` and ``HEAD`` being authenticated by default
- :type: bugfix
- :pr: 2182
-
- Fix a regression that would make
- :class:`~litestar.security.session_auth.middleware.SessionAuthMiddleware` authenticate
- ``OPTIONS`` and ``HEAD`` requests by default.
-
-.. changelog:: 2.0.0rc1
- :date: 2023/08/05
-
- .. change:: Support for server-sent-events
- :type: feature
- :pr: 2035
- :issue: 1185
-
- Support for `Server-sent events ` has been
- added with the :class:`ServerSentEvent <.response.ServerSentEvent>`:
-
- .. code-block:: python
-
- async def my_generator() -> AsyncGenerator[bytes, None]:
- count = 0
- while count < 10:
- await sleep(0.01)
- count += 1
- yield str(count)
-
-
- @get(path="/count")
- def sse_handler() -> ServerSentEvent:
- return ServerSentEvent(my_generator())
-
- .. seealso::
- :ref:`Server Sent Events `
-
-
- .. change:: SQLAlchemy repository: allow specifying ``id_attribute`` per method
- :type: feature
- :pr: 2052
-
- The following methods now accept an ``id_attribute`` argument, allowing to
- specify an alternative value to the models primary key:
-
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete_many``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.update``
-
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete_many``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.get``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.update``
-
- .. change:: SQLAlchemy repository: New ``upsert_many`` method
- :type: feature
- :pr: 2056
-
- A new method ``upsert_many`` has been added to the SQLAlchemy repositories,
- providing equivalent functionality to the ``upsert`` method for multiple
- model instances.
-
- .. seealso::
- ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.upsert_many``
- ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.upsert_many``
-
- .. change:: SQLAlchemy repository: New filters: ``OnBeforeAfter``, ``NotInCollectionFilter`` and ``NotInSearchFilter``
- :type: feature
- :pr: 2057
-
- The following filters have been added to the SQLAlchemy repositories:
-
- ``litestar.contrib.repository.filters.OnBeforeAfter``
-
- Allowing to filter :class:`datetime.datetime` columns
-
- ``litestar.contrib.repository.filters.NotInCollectionFilter``
-
- Allowing to filter using a ``WHERE ... NOT IN (...)`` clause
-
- ``litestar.contrib.repository.filters.NotInSearchFilter``
-
- Allowing to filter using a `WHERE field_name NOT LIKE '%' || :value || '%'`` clause
-
- .. change:: SQLAlchemy repository: Configurable chunk sizing for ``delete_many``
- :type: feature
- :pr: 2061
-
- The repository now accepts a ``chunk_size`` parameter, determining the maximum
- amount of parameters in an ``IN`` statement before it gets chunked.
-
- This is currently only used in the ``delete_many`` method.
-
-
- .. change:: SQLAlchemy repository: Support InstrumentedAttribute for attribute columns
- :type: feature
- :pr: 2054
-
- Support :class:`~sqlalchemy.orm.InstrumentedAttribute` for in the repository's
- ``id_attribute``, and the following methods:
-
-
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.delete_many``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.update``
-
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.delete_many``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.get``
- - ``~litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository.update``
-
- .. change:: OpenAPI: Support callable ``operation_id`` on route handlers
- :type: feature
- :pr: 2078
-
- Route handlers may be passed a callable to ``operation_id`` to create the
- OpenAPI operation ID.
-
- .. change:: Run event listeners concurrently
- :type: feature
- :pr: 2096
-
- :doc:`/usage/events` now run concurrently inside a task group.
-
- .. change:: Support extending the CLI with plugins
- :type: feature
- :pr: 2066
-
- A new plugin protocol :class:`~litestar.plugins.CLIPluginProtocol` has been
- added that can be used to extend the Litestar CLI.
-
- .. seealso::
- :ref:`usage/cli:Using a plugin`
-
- .. change:: DTO: Support renamed fields in ``DTOData`` and ``create_instance``
- :type: bugfix
- :pr: 2065
-
- A bug was fixed that would cause field renaming to be skipped within
- :class:`~litestar.dto.data_structures.DTOData` and
- :meth:`~litestar.dto.data_structures.DTOData.create_instance`.
-
- .. change:: SQLAlchemy repository: Fix ``health_check`` for oracle
- :type: bugfix
- :pr: 2060
-
- The emitted statement for oracle has been changed to ``SELECT 1 FROM DUAL``.
-
- .. change:: Fix serialization of empty strings in multipart form
- :type: bugfix
- :pr: 2044
-
- A bug was fixed that would cause a validation error to be raised for empty
- strings during multipart form decoding.
-
- .. change:: Use debug mode by default in test clients
- :type: misc
- :pr: 2113
-
- The test clients will now default to ``debug=True`` instead of ``debug=None``.
-
- .. change:: Removal of deprecated ``partial`` module
- :type: misc
- :pr: 2113
- :breaking:
-
- The deprecated ``litestar.partial`` has been removed. It can be replaced with
- DTOs, making use of the :class:`~litestar.dto.config.DTOConfig` option
- ``partial=True``.
-
- .. change:: Removal of deprecated ``dto/factory`` module
- :type: misc
- :pr: 2114
- :breaking:
-
- The deprecated module ``litestar.dto.factory`` has been removed.
-
- .. change:: Removal of deprecated ``contrib/msgspec`` module
- :type: misc
- :pr: 2114
- :breaking:
-
- The deprecated module ``litestar.contrib.msgspec`` has been removed.
-
-
-.. changelog:: 2.0.0beta4
- :date: 2023/07/21
-
- .. change:: Fix extra package dependencies
- :type: bugfix
- :pr: 2029
-
- A workaround for a
- `bug in poetry `_ that
- caused development / extra dependencies to be installed alongside the package
- has been added.
-
-.. changelog:: 2.0.0beta3
- :date: 2023/07/20
-
- .. change:: :class:`SQLAlchemyDTO (Advanced Alchemy) `: column/relationship type inference
- :type: feature
- :pr: 1879
- :issue: 1853
-
- If type annotations aren't available for a given column/relationship, they may
- be inferred from the mapped object.
-
- For columns, the :attr:`~sqlalchemy.engine.interfaces.ReflectedColumn.type`\ 's
- :attr:`~sqlalchemy.types.TypeEngine.python_type` will be used as the type of the
- column, and the :attr:`~sqlalchemy.engine.interfaces.ReflectedColumn.nullable`
- property to determine if the field should have a :obj:`None` union.
-
- For relationships, where the ``RelationshipProperty.direction`` is
- :attr:`~sqlalchemy.orm.RelationshipDirection.ONETOMANY` or
- :attr:`~sqlalchemy.orm.RelationshipDirection.MANYTOMANY`,
- ``RelationshipProperty.collection_class`` and
- ``RelationshipProperty.mapper.class_`` are used to construct an annotation for
- the collection.
-
- For one-to-one relationships, ``RelationshipProperty.mapper.class_`` is used to
- get the type annotation, and will be made a union with :obj:`None` if all of the
- foreign key columns are nullable.
-
- .. change:: DTO: Piccolo ORM
- :type: feature
- :pr: 1896
-
- Add support for piccolo ORM with the
- :class:`~litestar.contrib.piccolo.PiccoloDTO`.
-
- .. change:: OpenAPI: Allow setting ``OpenAPIController.path`` from ```OpenAPIConfig``
- :type: feature
- :pr: 1886
-
- :attr:`~litestar.openapi.OpenAPIConfig.path` has been added, which can be used
- to set the ``path`` for :class:`~litestar.openapi.OpenAPIController` directly,
- without needing to create a custom instance of it.
-
- If ``path`` is set in both :class:`~litestar.openapi.OpenAPIConfig` and
- :class:`~litestar.openapi.OpenAPIController`, the path set on the controller
- will take precedence.
-
- .. change:: SQLAlchemy repository: ``auto_commit``, ``auto_expunge`` and ``auto_refresh`` options
- :type: feature
- :pr: 1900
-
- .. currentmodule:: litestar.contrib.sqlalchemy.repository
-
- Three new parameters have been added to the repository and various methods:
-
- ``auto_commit``
- When this :obj:`True`, the session will
- :meth:`~sqlalchemy.orm.Session.commit` instead of
- :meth:`~sqlalchemy.orm.Session.flush` before returning.
-
- Available in:
-
- - ``~SQLAlchemyAsyncRepository.add``
- - ``~SQLAlchemyAsyncRepository.add_many``
- - ``~SQLAlchemyAsyncRepository.delete``
- - ``~SQLAlchemyAsyncRepository.delete_many``
- - ``~SQLAlchemyAsyncRepository.get_or_create``
- - ``~SQLAlchemyAsyncRepository.update``
- - ``~SQLAlchemyAsyncRepository.update_many``
- - ``~SQLAlchemyAsyncRepository.upsert``
-
- (and their sync equivalents)
-
- ``auto_refresh``
- When :obj:`True`, the session will execute
- :meth:`~sqlalchemy.orm.Session.refresh` objects before returning.
-
- Available in:
-
- - ``~SQLAlchemyAsyncRepository.add``
- - ``~SQLAlchemyAsyncRepository.get_or_create``
- - ``~SQLAlchemyAsyncRepository.update``
- - ``~SQLAlchemyAsyncRepository.upsert``
-
- (and their sync equivalents)
-
-
- ``auto_expunge``
- When this is :obj:`True`, the session will execute
- :meth:`~sqlalchemy.orm.Session.expunge` all objects before returning.
-
- Available in:
-
- - ``~SQLAlchemyAsyncRepository.add``
- - ``~SQLAlchemyAsyncRepository.add_many``
- - ``~SQLAlchemyAsyncRepository.delete``
- - ``~SQLAlchemyAsyncRepository.delete_many``
- - ``~SQLAlchemyAsyncRepository.get``
- - ``~SQLAlchemyAsyncRepository.get_one``
- - ``~SQLAlchemyAsyncRepository.get_one_or_none``
- - ``~SQLAlchemyAsyncRepository.get_or_create``
- - ``~SQLAlchemyAsyncRepository.update``
- - ``~SQLAlchemyAsyncRepository.update_many``
- - ``~SQLAlchemyAsyncRepository.list``
- - ``~SQLAlchemyAsyncRepository.upsert``
-
- (and their sync equivalents)
-
- .. change:: Include path name in ``ImproperlyConfiguredException`` message for missing param types
- :type: feature
- :pr: 1935
-
- The message of a :exc:`ImproperlyConfiguredException` raised when a path
- parameter is missing a type now contains the name of the path.
-
- .. change:: DTO: New ``include`` parameter added to ``DTOConfig``
- :type: feature
- :pr: 1950
-
- :attr:`~litestar.dto.config.DTOConfig.include` has been added to
- :class:`~litestar.dto.config.DTOConfig`, providing a counterpart to
- :attr:`~litestar.dto.config.DTOConfig.exclude`.
-
- If ``include`` is provided, only those fields specified within it will be
- included.
-
- .. change:: ``AbstractDTOFactory`` moved to ``dto.factory.base``
- :type: misc
- :breaking:
- :pr: 1950
-
- :class:`~litestar.dto.base_factory.AbstractDTOFactory` has moved from
- ``litestar.dto.factory.abc`` to ``litestar.dto.factory.base``.
-
- .. change:: SQLAlchemy repository: Rename ``_sentinel`` column to ``sa_orm_sentinel``
- :type: misc
- :breaking:
- :pr: 1933
-
-
- The ``_sentinel`` column of
- ``~litestar.contrib.sqlalchemy.base.UUIDPrimaryKey`` has been renamed to
- ``sa_orm_sentinel``, to support Spanner, which does not support tables starting
- with ``_``.
-
- .. change:: SQLAlchemy repository: Fix audit columns defaulting to app startup time
- :type: bugfix
- :pr: 1894
-
- A bug was fixed where
- ``~litestar.contrib.sqlalchemy.base.AuditColumns.created_at`` and
- ``~litestar.contrib.sqlalchemy.base.AuditColumns.updated_at`` would default
- to the :class:`~datetime.datetime` at initialization time, instead of the time
- of the update.
-
- .. change:: :class:`SQLAlchemyDTO (Advanced Alchemy) `: Fix handling of ``Sequence`` with defaults
- :type: bugfix
- :pr: 1883
- :issue: 1851
-
- Fixes handling of columns defined with
- `Sequence `_
- default values.
-
- The SQLAlchemy default value for a :class:`~sqlalchemy.schema.Column` will be
- ignored when it is a :class:`~sqlalchemy.schema.Sequence` object. This is
- because the SQLAlchemy sequence types represent server generated values, and
- there is no way for us to generate a reasonable default value for that field
- from it without making a database query, which is not possible deserialization.
-
- .. change:: Allow JSON as redirect response
- :type: bugfix
- :pr: 1908
-
- Enables using redirect responses with a JSON media type.
-
- .. change:: DTO / OpenAPI: Fix detection of required fields for Pydantic and msgspec DTOs
- :type: bugfix
- :pr: 1946
-
- A bug was fixed that would lead to fields of a Pydantic model or msgspec Structs
- being marked as "not required" in the generated OpenAPI schema when used with
- DTOs.
-
- .. change:: Replace ``Header``, ``CacheControlHeader`` and ``ETag`` Pydantic models with dataclasses
- :type: misc
- :pr: 1917
- :breaking:
-
- As part of the removal of Pydantic as a hard dependency, the header models
- :class:`~litestar.datastructures.Header`,
- :class:`~litestar.datastructures.CacheControlHeader` and
- :class:`~litestar.datastructures.ETag` have been replaced with dataclasses.
-
-
- .. note::
- Although marked breaking, this change should not affect usage unless you
- relied on these being Pydantic models in some way.
-
- .. change:: Pydantic as an optional dependency
- :breaking:
- :pr: 1963
- :type: misc
-
- As of this release, Pydantic is no longer a required dependency of Litestar.
- It is still supported in the same capacity as before, but Litestar itself does
- not depend on it anymore in its internals.
-
- .. change:: Pydantic 2 support
- :type: feature
- :pr: 1956
-
- Pydantic 2 is now supported alongside Pydantic 1.
-
- .. change:: Deprecation of ``partial`` module
- :type: misc
- :pr: 2002
-
- The ``litestar.partial`` and ``litestar.partial.Partial`` have been
- deprecated and will be removed in a future release. Users are advised to upgrade
- to DTOs, making use of the :class:`~litestar.dto.config.DTOConfig` option
- ``partial=True``.
-
-
-.. changelog:: 2.0.0beta2
- :date: 2023/06/24
-
- .. change:: Support ``annotated-types``
- :type: feature
- :pr: 1847
-
- Extended support for the
- `annotated-types `_ library is now
- available.
-
- .. change:: Increased verbosity of validation error response keys
- :type: feature
- :pr: 1774
- :breaking:
-
- The keys in validation error responses now include the full path to the field
- where the originated.
-
- An optional ``source`` key has been added, signifying whether the value is from
- the body, a cookie, a header, or a query param.
-
- .. code-block:: json
- :caption: before
-
- {
- "status_code": 400,
- "detail": "Validation failed for POST http://localhost:8000/some-route",
- "extra": [
- {"key": "int_param", "message": "value is not a valid integer"},
- {"key": "int_header", "message": "value is not a valid integer"},
- {"key": "int_cookie", "message": "value is not a valid integer"},
- {"key": "my_value", "message": "value is not a valid integer"}
- ]
- }
-
- .. code-block:: json
- :caption: after
-
- {
- "status_code": 400,
- "detail": "Validation failed for POST http://localhost:8000/some-route",
- "extra": [
- {"key": "child.my_value", "message": "value is not a valid integer", "source": "body"},
- {"key": "int_param", "message": "value is not a valid integer", "source": "query"},
- {"key": "int_header", "message": "value is not a valid integer", "source": "header"},
- {"key": "int_cookie", "message": "value is not a valid integer", "source": "cookie"},
- ]
- }
-
- .. change:: ``TestClient`` default timeout
- :type: feature
- :pr: 1840
- :breaking:
-
- A ``timeout`` parameter was added to
-
- - :class:`~litestar.testing.TestClient`
- - :class:`~litestar.testing.AsyncTestClient`
- - :class:`~litestar.testing.create_test_client`
- - :class:`~litestar.testing.create_async_test_client`
-
- The value is passed down to the underlying HTTPX client and serves as a default
- timeout for all requests.
-
- .. change:: SQLAlchemy DTO: Explicit error messages when type annotations for a column are missing
- :type: feature
- :pr: 1852
-
- Replace the nondescript :exc:`KeyError` raised when a SQLAlchemy DTO is
- constructed from a model that is missing a type annotation for an included
- column with an :exc:`ImproperlyConfiguredException`, including an explicit error
- message, pointing at the potential cause.
-
- .. change:: Remove exception details from Internal Server Error responses
- :type: bugfix
- :pr: 1857
- :issue: 1856
-
- Error responses with a ``500`` status code will now always use
- `"Internal Server Error"` as default detail.
-
- .. change:: Pydantic v1 regex validation
- :type: bugfix
- :pr: 1865
- :issue: 1860
-
- A regression has been fixed in the Pydantic signature model logic, which was
- caused by the renaming of ``regex`` to ``pattern``, which would lead to the
- :attr:`~litestar.params.KwargDefinition.pattern` not being validated.
-
-
-.. changelog:: 2.0.0beta1
- :date: 2023/06/16
-
- .. change:: Expose ``ParsedType`` as public API
- :type: feature
- :pr: 1677 1567
-
- Expose the previously private :class:`litestar.typing.ParsedType`. This is
- mainly indented for usage with
- :meth:`litestar.plugins.SerializationPluginProtocol.supports_type`
-
- .. change:: Improved debugging capabilities
- :type: feature
- :pr: 1742
-
- - A new ``pdb_on_exception`` parameter was added to
- :class:`~litestar.app.Litestar`. When set to ``True``, Litestar will drop into
- a the Python debugger when an exception occurs. It defaults to ``None``
- - When ``pdb_on_exception`` is ``None``, setting the environment variable
- ``LITESTAR_PDB=1`` can be used to enable this behaviour
- - When using the CLI, passing the ``--pdb`` flag to the ``run`` command will
- temporarily set the environment variable ``LITESTAR_PDB=1``
-
- .. change:: OpenAPI: Add `operation_class` argument to HTTP route handlers
- :type: feature
- :pr: 1732
-
- The ``operation_class`` argument was added to
- :class:`~litestar.handlers.HTTPRouteHandler` and the corresponding decorators,
- allowing to override the :class:`~litestar.openapi.spec.Operation` class, to
- enable further customization of the generated OpenAPI schema.
-
- .. change:: OpenAPI: Support nested ``Literal`` annotations
- :type: feature
- :pr: 1829
-
- Support nested :class:`typing.Literal` annotations by flattening them into
- a single ``Literal``.
-
- .. change:: CLI: Add ``--reload-dir`` option to ``run`` command
- :type: feature
- :pr: 1689
-
- A new ``--reload-dir`` option was added to the ``litestar run`` command. When
- used, ``--reload`` is implied, and the server will watch for changes in the
- given directory.
-
- .. change:: Allow extra attributes on JWTs via ``extras`` attribute
- :type: feature
- :pr: 1695
-
- Add the ``litestar.contrib.jwt.Token.extras`` attribute, containing extra
- attributes found on the JWT.
-
- .. change:: Add default modes for ``Websocket.iter_json`` and ``WebSocket.iter_data``
- :type: feature
- :pr: 1733
-
- Add a default ``mode`` for :meth:`~litestar.connection.WebSocket.iter_json` and
- :meth:`~litestar.connection.WebSocket.iter_data`, with a value of ``text``.
-
- .. change:: SQLAlchemy repository: Synchronous repositories
- :type: feature
- :pr: 1683
-
- Add a new synchronous repository base class:
- ``litestar.contrib.sqlalchemy.repository.SQLAlchemySyncRepository``,
- which offer the same functionality as its asynchronous counterpart while
- operating on a synchronous :class:`sqlalchemy.orm.Session`.
-
- .. change:: SQLAlchemy repository: Oracle Database support
- :type: feature
- :pr: 1694
-
- Add support for Oracle Database via
- `oracledb `_.
-
- .. change:: SQLAlchemy repository: DuckDB support
- :type: feature
- :pr: 1744
-
- Add support for `DuckDB `_.
-
- .. change:: SQLAlchemy repository: Google Spanner support
- :type: feature
- :pr: 1744
-
- Add support for `Google Spanner `_.
-
- .. change:: SQLAlchemy repository: JSON check constraint for Oracle Database
- :type: feature
- :pr: 1780
-
- When using the :class:`litestar.contrib.sqlalchemy.types.JsonB` type with an
- Oracle Database engine, a JSON check constraint will be created for that
- column.
-
- .. change:: SQLAlchemy repository: Remove ``created`` and ``updated`` columns
- :type: feature
- :pr: 1816
- :breaking:
-
- The ``created`` and ``updated`` columns have been superseded by
- ``created_at`` and ``updated_at`` respectively, to prevent name clashes.
-
-
- .. change:: SQLAlchemy repository: Add timezone aware type
- :type: feature
- :pr: 1816
- :breaking:
-
- A new timezone aware type ``litestar.contrib.sqlalchemy.types.DateTimeUTC``
- has been added, which enforces UTC timestamps stored in the database.
-
- .. change:: SQLAlchemy repository: Exclude unloaded columns in ``to_dict``
- :type: feature
- :pr: 1802
-
- When exporting models using the
- ``~litestar.contrib.sqlalchemy.base.CommonTableAttributes.to_dict`` method,
- unloaded columns will now always be excluded. This prevents implicit I/O via
- lazy loading, and errors when using an asynchronous session.
-
- .. change:: DTOs: Nested keyword arguments in ``.create_instance()``
- :type: feature
- :pr: 1741
- :issue: 1727
-
- The
- :meth:`DTOData.create_instance `
- method now supports providing values for arbitrarily nested data via kwargs
- using a double-underscore syntax, for example
- ``data.create_instance(foo__bar="baz")``.
-
- .. seealso::
- :ref:`usage/dto/1-abstract-dto:Providing values for nested data`
-
- .. change:: DTOs: Hybrid properties and association proxies in
- :class:`SQLAlchemyDTO (Advanced Alchemy) `
- :type: feature
- :pr: 1754 1776
-
- The :class:`SQLAlchemyDTO (Advanced Alchemy) `
- now supports `hybrid attribute `_
- and `associationproxy `_.
-
- The generated field will be marked read-only.
-
- .. change:: DTOs: Transfer to generic collection types
- :type: feature
- :pr: 1764
- :issue: 1763
-
- DTOs can now be wrapped in generic collection types such as
- :class:`typing.Sequence`. These will be substituted with a concrete and
- instantiable type at run time, e.g. in the case of ``Sequence`` a :class:`list`.
-
- .. change:: DTOs: Data transfer for non-generic builtin collection annotations
- :type: feature
- :pr: 1799
-
- Non-parametrized generics in annotations (e.g. ``a: dict``) will now be inferred
- as being parametrized with ``Any``. ``a: dict`` is then equivalent to
- ``a: dict[Any, Any]``.
-
- .. change:: DTOs: Exclude leading underscore fields by default
- :type: feature
- :pr: 1777
- :issue: 1768
- :breaking:
-
- Leading underscore fields will not be excluded by default. This behaviour can be
- configured with the newly introduced
- :attr:`~litestar.dto.factory.DTOConfig.underscore_fields_private` configuration
- value, which defaults to ``True``.
-
- .. change:: DTOs: Msgspec and Pydantic DTO factory implementation
- :type: feature
- :pr: 1712
- :issue: 1531, 1532
-
- DTO factories for `msgspec `_ and
- `Pydantic `_ have been added:
-
- - :class:`~litestar.contrib.msgspec.MsgspecDTO`
- - :class:`~litestar.contrib.pydantic.PydanticDTO`
-
- .. change:: DTOs: Arbitrary generic wrappers
- :pr: 1801
- :issue: 1631, 1798
-
- When a handler returns a type that is not supported by the DTO, but:
-
- - the return type is generic
- - it has a generic type argument that is supported by the dto
- - the type argument maps to an attribute on the return type
-
- the DTO operations will be performed on the data retrieved from that attribute
- of the instance returned from the handler, and return the instance.
-
- The constraints are:
-
- - the type returned from the handler must be a type that litestar can
- natively encode
- - the annotation of the attribute that holds the data must be a type that DTOs
- can otherwise manage
-
- .. code-block:: python
-
- from dataclasses import dataclass
- from typing import Generic, List, TypeVar
-
- from typing_extensions import Annotated
-
- from litestar import Litestar, get
- from litestar.dto import DTOConfig
- from litestar.dto.factory.dataclass_factory import DataclassDTO
-
-
- @dataclass
- class User:
- name: str
- age: int
-
-
- T = TypeVar("T")
- V = TypeVar("V")
-
-
- @dataclass
- class Wrapped(Generic[T, V]):
- data: List[T]
- other: V
-
-
- @get(dto=DataclassDTO[Annotated[User, DTOConfig(exclude={"age"})]])
- def handler() -> Wrapped[User, int]:
- return Wrapped(
- data=[User(name="John", age=42), User(name="Jane", age=43)],
- other=2,
- )
-
-
- app = Litestar(route_handlers=[handler])
-
- # GET "/": {"data": [{"name": "John"}, {"name": "Jane"}], "other": 2}
-
- .. change:: Store and reuse state `deep_copy` directive when copying state
- :type: bugfix
- :issue: 1674
- :pr: 1678
-
- App state can be created using ``deep_copy=False``, however state would still be
- deep copied for dependency injection.
-
- This was fixed memoizing the value of ``deep_copy`` when state is created, and
- reusing it on subsequent copies.
-
- .. change:: ``ParsedType.is_subclass_of(X)`` ``True`` for union if all union types are subtypes of ``X``
- :type: bugfix
- :pr: 1690
- :issue: 1652
-
- When :class:`~litestar.typing.ParsedType` was introduced,
- :meth:`~litestar.typing.ParsedType.is_subclass_of` any union was deliberately
- left to return ``False`` with the intention of waiting for some use-cases to
- arrive.
-
- This behaviour was changed to address an issue where a handler may be typed to
- return a union of multiple response types; If all response types are
- :class:`~litestar.response.Response` subtypes then the correct response handler
- will now be applied.
-
- .. change:: Inconsistent template autoescape behavior
- :type: bugfix
- :pr: 1718
- :issue: 1699
-
- The mako template engine now defaults to autoescaping expressions, making it
- consistent with config of Jinja template engine.
-
- .. change:: Missing ``ChannelsPlugin`` in signature namespace population
- :type: bugfix
- :pr: 1719
- :issue: 1691
-
- The :class:`~litestar.channels.plugin.ChannelsPlugin` has been added to the
- signature namespace, fixing an issue where using
- ``from __future__ import annotations`` or stringized annotations would lead to
- a :exc:`NameError`, if the plugin was not added to the signatured namespace
- manually.
-
- .. change:: Gzip middleware not sending small streaming responses
- :type: bugfix
- :pr: 1723
- :issue: 1681
-
- A bug was fixed that would cause smaller streaming responses to not be sent at
- all when the :class:`~litestar.middleware.compression.CompressionMiddleware` was
- used with ``gzip``.
-
- .. change:: Premature transfer to nested models with `DTOData`
- :type: bugfix
- :pr: 1731
- :issue: 1726
-
- An issue was fixed where data that should be transferred to builtin types on
- instantiation of :class:`~litestar.dto.factory.DTOData` was being instantiated
- into a model type for nested models.
-
- .. change:: Incorrect ``sync_to_thread`` usage warnings for generator dependencies
- :type: bugfix
- :pr: 1716 1740
- :issue: 1711
-
- A bug was fixed that caused incorrect warnings about missing ``sync_to_thread``
- usage were issues when asynchronous generators were being used as dependencies.
-
- .. change:: Dependency injection custom dependencies in ``WebSocketListener``
- :type: bugfix
- :pr: 1807
- :issue: 1762
-
- An issue was resolved that would cause failures when dependency injection was
- being used with custom dependencies (that is, injection of things other than
- ``state``, ``query``, path parameters, etc.) within a
- :class:`~litestar.handlers.WebsocketListener`.
-
- .. change:: OpenAPI schema for ``Dict[K, V]`` ignores generic
- :type: bugfix
- :pr: 1828
- :issue: 1795
-
- An issue with the OpenAPI schema generation was fixed that would lead to generic
- arguments to :class:`dict` being ignored.
-
- An type like ``dict[str, int]`` now correctly renders as
- ``{"type": "object", "additionalProperties": { "type": "integer" }}``.
-
- .. change:: ``WebSocketTestSession`` not timing out without when connection is not accepted
- :type: bugfix
- :pr: 1696
-
- A bug was fixed that caused :class:`~litestar.testing.WebSocketTestSession` to
- block indefinitely when if :meth:`~litestar.connection.WebSocket.accept` was
- never called, ignoring the ``timeout`` parameter.
-
- .. change:: SQLAlchemy repository: Fix alembic migrations generated for models using ``GUID``
- :type: bugfix
- :pr: 1676
-
- Migrations generated for models with a
- ``~litestar.contrib.sqlalchemy.types.GUID`` type would erroneously add a
- ``length=16`` on the input. Since this parameter is not defined in the type's
- the ``__init__`` method. This was fixed by adding the appropriate parameter to
- the type's signature.
-
- .. change:: Remove ``state`` parameter from ``AfterExceptionHookHandler`` and ``BeforeMessageSendHookHandler``
- :type: misc
- :pr: 1739
- :breaking:
-
- Remove the ``state`` parameter from ``AfterExceptionHookHandler`` and
- ``BeforeMessageSendHookHandler``.
-
- ``AfterExceptionHookHandler``\ s will have to be updated from
-
- .. code-block:: python
-
- async def after_exception_handler(
- exc: Exception, scope: Scope, state: State
- ) -> None: ...
-
- to
-
- .. code-block:: python
-
- async def after_exception_handler(exc: Exception, scope: Scope) -> None: ...
-
- The state can still be accessed like so:
-
- .. code-block:: python
-
- async def after_exception_handler(exc: Exception, scope: Scope) -> None:
- state = scope["app"].state
-
-
- ``BeforeMessageSendHookHandler``\ s will have to be updated from
-
- .. code-block:: python
-
- async def before_send_hook_handler(
- message: Message, state: State, scope: Scope
- ) -> None: ...
-
-
- to
-
- .. code-block:: python
-
- async def before_send_hook_handler(message: Message, scope: Scope) -> None: ...
-
- where state can be accessed in the same manner:
-
- .. code-block:: python
-
- async def before_send_hook_handler(message: Message, scope: Scope) -> None:
- state = scope["app"].state
-
- .. change:: Removal of ``dto.exceptions`` module
- :pr: 1773
- :breaking:
-
- The module ``dto.exceptions`` has been removed, since it was not used anymore
- internally by the DTO implementations, and superseded by standard exceptions.
-
-
- .. change:: ``BaseRouteHandler`` no longer generic
- :pr: 1819
- :breaking:
-
- :class:`~litestar.handlers.BaseRouteHandler` was originally made generic to
- support proper typing of the ``ownership_layers`` property, but the same effect
- can now be achieved using :class:`typing.Self`.
-
- .. change:: Deprecation of ``Litestar`` parameter ``preferred_validation_backend``
- :pr: 1810
- :breaking:
-
- The following changes have been made regarding the
- ``preferred_validation_backend``:
-
- - The ``preferred_validation_backend`` parameter of
- :class:`~litestar.app.Litestar` has been renamed to
- ``_preferred_validation_backend`` and deprecated. It will be removed
- completely in a future version.
- - The ``Litestar.preferred_validation_backend`` attribute has been made private
- - The ``preferred_validation_backend`` attribute has been removed from
- :class:`~litestar.config.app.AppConfig`
-
- In addition, the logic for selecting a signature validation backend has been
- simplified as follows: If the preferred backend is set to ``attrs``, or the
- signature contains attrs types, ``attrs`` is selected. In all other cases,
- Pydantic will be used.
-
- .. change:: ``Response.get_serializer`` moved to ``serialization.get_serializer``
- :pr: 1820
- :breaking:
-
-
- The ``Response.get_serializer()`` method has been removed in favor of the
- :func:`~litestar.serialization.get_serializer` function.
-
- In the previous :class:`~litestar.response.Response` implementation,
- ``get_serializer()`` was called on the response inside the response's
- ``__init__``, and the merging of class-level ``type_encoders`` with the
- ``Response``\ 's ``type_encoders`` occurred inside its ``get_serializer``
- method.
-
- In the current version of ``Response``, the response body is not encoded until
- after the response object has been returned from the handler, and it is
- converted into a low-level :class:`~litestar.response.base.ASGIResponse` object.
- Due to this, there is still opportunity for the handler layer resolved
- ``type_encoders`` object to be merged with the ``Response`` defined
- ``type_encoders``, making the merge inside the ``Response`` no longer necessary.
-
- In addition, the separate ``get_serializer`` function greatly simplifies the
- interaction between middlewares and serializers, allowing to retrieve one
- independently from a ``Response``.
-
- .. change:: Remove response containers and introduce ``ASGIResponse``
- :pr: 1790
- :breaking:
-
- Response Containers were wrapper classes used to indicate the type of response
- returned by a handler, for example ``File``, ``Redirect``, ``Template`` and
- ``Stream`` types. These types abstracted the interface of responses from the
- underlying response itself.
-
- Response containers have been removed and their functionality largely merged with
- that of :class:`~litestar.response.Response`. The predefined response containers
- still exist functionally, as subclasses of
- :class:`Response <.response.Response>` and are now located within the
- :mod:`litestar.response` module.
- In addition to the functionality of Response containers, they now also feature
- all of the response's functionality, such as methods to add headers and cookies.
-
- The :class:`~litestar.response.Response` class now only serves as a wrapper and
- context object, and does not handle the data sending part, which has been
- delegated to a newly introduced
- :class:`ASGIResponse <.response.base.ASGIResponse>`. This type (and its
- subclasses) represent the response as an immutable object and are used
- internally by Litestar to perform the I/O operations of the response. These can
- be created and returned from handlers like any other ASGI application, however
- they are low-level, and lack the utility of the higher-level response types.
-
-
-
-.. changelog:: 2.0.0alpha7
- :date: 2023/05/14
-
- .. change:: Warn about sync callables in route handlers and dependencies without an explicit ``sync_to_thread`` value
- :type: feature
- :pr: 1648 1655
-
- A warning will now be raised when a synchronous callable is being used in an
- :class:`~.handlers.HTTPRouteHandler` or :class:`~.di.Provide`, without setting
- ``sync_to_thread``. This is to ensure that synchronous callables are handled
- properly, and to prevent accidentally using callables which might block the main
- thread.
-
- This warning can be turned off globally by setting the environment variable
- ``LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0``.
-
- .. seealso::
- :doc:`/topics/sync-vs-async`
-
-
- .. change:: Warn about ``sync_to_thread`` with async callables
- :type: feature
- :pr: 1664
-
- A warning will be raised when ``sync_to_thread`` is being used in
- :class:`~.handlers.HTTPRouteHandler` or :class:`~.di.Provide` with an
- asynchronous callable, as this will have no effect.
-
- This warning can be turned off globally by setting the environment variable
- ``LITESTAR_WARN_SYNC_TO_THREAD_WITH_ASYNC=0``.
-
-
- .. change:: WebSockets: Dependencies in listener hooks
- :type: feature
- :pr: 1647
-
- Dependencies can now be used in the
- :class:`~litestar.handlers.websocket_listener` hooks
- ``on_accept``, ``on_disconnect`` and the ``connection_lifespan`` context
- manager. The ``socket`` parameter is therefore also not mandatory anymore in
- those callables.
-
- .. change:: Declaring dependencies without ``Provide``
- :type: feature
- :pr: 1647
-
- Dependencies can now be declared without using :class:`~litestar.di.Provide`.
- The callables can be passed directly to the ``dependencies`` dictionary.
-
-
- .. change:: Add ``DTOData`` to receive unstructured but validated DTO data
- :type: feature
- :pr: 1650
-
- :class:`~litestar.dto.factory.DTOData` is a datastructure for interacting with
- DTO validated data in its unstructured form.
-
- This utility is to support the case where the amount of data that is available
- from the client request is not complete enough to instantiate an instance of the
- model that would otherwise be injected.
-
-
- .. change:: Partial DTOs
- :type: feature
- :pr: 1651
-
- Add a ``partial`` flag to :class:`~litestar.dto.factory.DTOConfig`, making all
- DTO fields options. Subsequently, any unset values will be filtered when
- extracting data from transfer models.
-
- This allows for example to use a to handle PATCH requests more easily.
-
-
- .. change:: SQLAlchemy repository: ``psycopg`` asyncio support
- :type: feature
- :pr: 1657
-
- Async `psycopg `_ is now officially supported and
- tested for the SQLAlchemy repository.
-
- .. change:: SQLAlchemy repository: ``BigIntPrimaryKey`` mixin
- :type: feature
- :pr: 1657
-
- ``~litestar.contrib.sqlalchemy.base.BigIntPrimaryKey`` mixin, providing a
- ``BigInt`` primary key column, with a fallback to ``Integer`` for sqlite.
-
- .. change:: SQLAlchemy repository: Store GUIDs as binary on databases that don't have a native GUID type
- :type: feature
- :pr: 1657
-
- On databases without native support for GUIDs,
- ``~litestar.contrib.sqlalchemy.types.GUID`` will now fall back to
- ``BINARY(16)``.
-
- .. change:: Application lifespan context managers
- :type: feature
- :pr: 1635
-
- A new ``lifespan`` argument has been added to :class:`~litestar.app.Litestar`,
- accepting an asynchronous context manager, wrapping the lifespan of the
- application. It will be entered with the startup phase and exited on shutdown,
- providing functionality equal to the ``on_startup`` and ``on_shutdown`` hooks.
-
- .. change:: Unify application lifespan hooks: Remove ``before_`` and ``after_``
- :breaking:
- :type: feature
- :pr: 1663
-
- The following application lifespan hooks have been removed:
-
- - ``before_startup``
- - ``after_startup``
- - ``before_shutdown``
- - ``after_shutdown``
-
- The remaining hooks ``on_startup`` and ``on_shutdown`` will now receive as their
- optional first argument the :class:`~litestar.app.Litestar` application instead
- of the application's state.
-
- .. change:: Trio-compatible event emitter
- :type: feature
- :pr: 1666
-
- The default :class:`~litestar.events.emitter.SimpleEventEmitter` is now
- compatible with `trio `_.
-
-
- .. change:: OpenAPI: Support ``msgspec.Meta``
- :type: feature
- :pr: 1669
-
- :class:`msgspec.Meta` is now fully supported for OpenAPI schema generation.
-
- .. change:: OpenAPI: Support Pydantic ``FieldInfo``
- :type: feature
- :pr: 1670
- :issue: 1541
-
- Pydantic's ``FieldInfo`` (``regex``, ``gt``, ``title``, etc.) now have full
- support for OpenAPI schema generation.
-
- .. change:: OpenAPI: Fix name collision in DTO models
- :type: bugfix
- :pr: 1649
- :issue: 1643
-
- A bug was fixed that would lead to name collisions in the OpenAPI schema when
- using DTOs with the same class name. DTOs now include a short 8 byte random
- string in their generated name to prevent this.
-
- .. change:: Fix validated attrs model being injected as a dictionary
- :type: bugfix
- :pr: 1668
- :issue: 1643
-
- A bug was fixed that would lead to an attrs model used to validate a route
- handler's ``data`` not being injected itself but as a dictionary representation.
-
-
- .. change:: Validate unknown media types
- :breaking:
- :type: bugfix
- :pr: 1671
- :issue: 1446
-
- An unknown media type in places where Litestar can't infer the type from the
- return annotation, an :exc:`ImproperlyConfiguredException` will now be raised.
-
-
-.. changelog:: 2.0.0alpha6
- :date: 2023/05/09
-
- .. change:: Relax typing of ``**kwargs`` in ``ASGIConnection.url_for``
- :type: bugfix
- :pr: 1610
-
- Change the typing of the ``**kwargs`` in
- :meth:`ASGIConnection.url_for ` from
- ``dict[str, Any]`` to ``Any``
-
-
- .. change:: Fix: Using ``websocket_listener`` in controller causes ``TypeError``
- :type: bugfix
- :pr: 1627
- :issue: 1615
-
- A bug was fixed that would cause a type error when using a
- :class:`websocket_listener `
- in a ``Controller``
-
- .. change:: Add ``connection_accept_handler`` to ``websocket_listener``
- :type: feature
- :pr: 1572
- :issue: 1571
-
- Add a new ``connection_accept_handler`` parameter to
- :class:`websocket_listener `,
- which can be used to customize how a connection is accepted, for example to
- add headers or subprotocols
-
- .. change:: Testing: Add ``block`` and ``timeout`` parameters to ``WebSocketTestSession`` receive methods
- :type: feature
- :pr: 1593
-
- Two parameters, ``block`` and ``timeout`` have been added to the following methods:
-
- - :meth:`receive `
- - :meth:`receive_text `
- - :meth:`receive_bytes `
- - :meth:`receive_json `
-
- .. change:: CLI: Add ``--app-dir`` option to root command
- :type: feature
- :pr: 1506
-
- The ``--app-dir`` option was added to the root CLI command, allowing to set the
- run applications from a path that's not the current working directory.
-
-
- .. change:: WebSockets: Data iterators
- :type: feature
- :pr: 1626
-
- Two new methods were added to the :class:`WebSocket `
- connection, which allow to continuously receive data and iterate over it:
-
- - :meth:`iter_data `
- - :meth:`iter_json `
-
-
- .. change:: WebSockets: MessagePack support
- :type: feature
- :pr: 1626
-
- Add support for `MessagePack `_ to the
- :class:`WebSocket ` connection.
-
- Three new methods have been added for handling MessagePack:
-
- - :meth:`send_msgpack `
- - :meth:`receive_msgpack `
- - :meth:`iter_msgpack `
-
- In addition, two MessagePack related methods were added to
- :class:`WebSocketTestSession `:
-
- - :meth:`send_msgpack `
- - :meth:`receive_msgpack `
-
- .. change:: SQLAlchemy repository: Add support for sentinel column
- :type: feature
- :pr: 1603
-
- This change adds support for ``sentinel column`` feature added in ``sqlalchemy``
- 2.0.10. Without it, there are certain cases where ``add_many`` raises an
- exception.
-
- The ``_sentinel`` value added to the declarative base should be excluded from
- normal select operations automatically and is excluded in the ``to_dict``
- methods.
-
- .. change:: DTO: Alias generator for field names
- :type: feature
- :pr: 1590
-
- A new argument ``rename_strategy`` has been added to the :class:`DTOConfig `,
- allowing to remap field names with strategies such as "camelize".
-
- .. change:: DTO: Nested field exclusion
- :type: feature
- :pr: 1596
- :issue: 1197
-
- This feature adds support for excluding nested model fields using dot-notation,
- e.g., ``"a.b"`` excludes field ``b`` from nested model field ``a``
-
- .. change:: WebSockets: Managing a socket's lifespan using a context manager in websocket listeners
- :type: feature
- :pr: 1625
-
- Changes the way a socket's lifespan - accepting the connection and calling the
- appropriate event hooks - to use a context manager.
-
- The ``connection_lifespan`` argument was added to the
- :class:`WebSocketListener `, which accepts
- an asynchronous context manager, which can be used to handle the lifespan of
- the socket.
-
- .. change:: New module: Channels
- :type: feature
- :pr: 1587
-
- A new module :doc:`channels ` has been added: A general purpose
- event streaming library, which can for example be used to broadcast messages
- via WebSockets.
-
- .. change:: DTO: Undocumented ``dto.factory.backends`` has been made private
- :breaking:
- :type: misc
- :pr: 1589
-
- The undocumented ``dto.factory.backends`` module has been made private
-
-
-
-.. changelog:: 2.0.0alpha5
-
- .. change:: Pass template context to HTMX template response
- :type: feature
- :pr: 1488
-
- Pass the template context to the :class:`Template ` returned by
- :class:`htmx.Response `.
-
-
- .. change:: OpenAPI support for attrs and msgspec classes
- :type: feature
- :pr: 1487
-
- Support OpenAPI schema generation for `attrs `_ classes and
- `msgspec `_ ``Struct``\ s.
-
- .. change:: SQLAlchemy repository: Add ``ModelProtocol``
- :type: feature
- :pr: 1503
-
- Add a new class ``contrib.sqlalchemy.base.ModelProtocol``, serving as a generic model base type, allowing to
- specify custom base classes while preserving typing information
-
- .. change:: SQLAlchemy repository: Support MySQL/MariaDB
- :type: feature
- :pr: 1345
-
- Add support for MySQL/MariaDB to the SQLAlchemy repository, using the
- `asyncmy `_ driver.
-
- .. change:: SQLAlchemy repository: Support MySQL/MariaDB
- :type: feature
- :pr: 1345
-
- Add support for MySQL/MariaDB to the SQLAlchemy repository, using the
- `asyncmy `_ driver.
-
- .. change:: SQLAlchemy repository: Add matching logic to ``get_or_create``
- :type: feature
- :pr: 1345
-
- Add a ``match_fields`` argument to
- ``litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository.get_or_create``.
- This lets you lookup a model using a subset of the kwargs you've provided. If the remaining kwargs are different
- from the retrieved model's stored values, an update is performed.
-
- .. change:: Repository: Extend filter types
- :type: feature
- :pr: 1345
-
- Add new filters ``litestar.contrib.repository.filters.OrderBy`` and
- ``litestar.contrib.repository.filters.SearchFilter``, providing ``ORDER BY ...`` and
- ``LIKE ...`` / ``ILIKE ...`` clauses respectively
-
- .. change:: SQLAlchemy repository: Rename ``SQLAlchemyRepository`` > ``SQLAlchemyAsyncRepository``
- :breaking:
- :type: misc
- :pr: 1345
-
- ``SQLAlchemyRepository`` has been renamed to
- ``litestar.contrib.sqlalchemy.repository.SQLAlchemyAsyncRepository``.
-
-
- .. change:: DTO: Add ``AbstractDTOFactory`` and backends
- :type: feature
- :pr: 1461
-
- An all-new DTO implementation was added, using ``AbstractDTOFactory`` as a base class, providing Pydantic and
- msgspec backends to facilitate (de)serialization and validation.
-
- .. change:: DTO: Remove ``from_connection`` / extend ``from_data``
- :breaking:
- :type: misc
- :pr: 1500
-
- The method ``DTOInterface.from_connection`` has been removed and replaced by ``DTOInterface.from_bytes``, which
- receives both the raw bytes from the connection, and the connection instance. Since ``from_bytes`` now does not
- handle connections anymore, it can also be a synchronous method, improving symmetry with
- ``DTOInterface.from_bytes``.
-
- The signature of ``from_data`` has been changed to also accept the connection, matching ``from_bytes``'
- signature.
-
- As a result of these changes,
- :meth:`DTOInterface.from_bytes ` no longer needs to
- receive the connection instance, so the ``request`` parameter has been dropped.
-
- .. change:: WebSockets: Support DTOs in listeners
- :type: feature
- :pr: 1518
-
- Support for DTOs has been added to :class:`WebSocketListener ` and
- :class:`WebSocketListener `. A ``dto`` and ``return_dto`` parameter has
- been added, providing the same functionality as their route handler counterparts.
-
- .. change:: DTO based serialization plugin
- :breaking:
- :type: feature
- :pr: 1501
-
- :class:`SerializationPluginProtocol ` has been re-implemented,
- leveraging the new :class:`DTOInterface `.
-
- If a handler defines a plugin supported type as either the ``data`` kwarg type annotation, or as the return
- annotation for a handler function, and no DTO has otherwise been resolved to handle the type, the protocol
- creates a DTO implementation to represent that type which is then used to de-serialize into, and serialize from
- instances of that supported type.
-
- .. important::
- The `Piccolo ORM `_ and `Tortoise ORM `_ plugins have
- been removed by this change, but will be re-implemented using the new patterns in a future release leading
- up to the 2.0 release.
-
- .. change:: SQLAlchemy 1 contrib module removed
- :breaking:
- :type: misc
- :pr: 1501
-
- As a result of the changes introduced in `#1501 `_,
- SQLAlchemy 1 support has been dropped.
-
- .. note::
- If you rely on SQLAlchemy 1, you can stick to Starlite *1.51* for now. In the future, a SQLAlchemy 1 plugin
- may be released as a standalone package.
-
- .. change:: Fix inconsistent parsing of unix timestamp between Pydantic and cattrs
- :type: bugfix
- :pr: 1492
- :issue: 1491
-
- Timestamps parsed as :class:`date ` with Pydantic return a UTC date, while cattrs implementation
- return a date with the local timezone.
-
- This was corrected by forcing dates to UTC when being parsed by attrs.
-
- .. change:: Fix: Retrieve type hints from class with no ``__init__`` method causes error
- :type: bugfix
- :pr: 1505
- :issue: 1504
-
- An error would occur when using a callable without an :meth:`object.__init__` method was used in a placed that
- would cause it to be inspected (such as a route handler's signature).
-
- This was caused by trying to access the ``__module__`` attribute of :meth:`object.__init__`, which would fail
- with
-
- .. code-block::
-
- 'wrapper_descriptor' object has no attribute '__module__'
-
- .. change:: Fix error raised for partially installed attrs dependencies
- :type: bugfix
- :pr: 1543
-
- An error was fixed that would cause a :exc:`MissingDependencyException` to be raised when dependencies for
- `attrs `_ were partially installed. This was fixed by being more specific about the
- missing dependencies in the error messages.
-
- .. change:: Change ``MissingDependencyException`` to be a subclass of ``ImportError``
- :type: misc
- :pr: 1557
-
- :exc:`MissingDependencyException` is now a subclass of :exc:`ImportError`, to make handling cases where both
- of them might be raised easier.
-
- .. change:: Remove bool coercion in URL parsing
- :breaking:
- :type: bugfix
- :pr: 1550
- :issue: 1547
-
- When defining a query parameter as ``param: str``, and passing it a string value of ``"true"``, the value
- received by the route handler was the string ``"True"``, having been title cased. The same was true for the value
- of ``"false"``.
-
- This has been fixed by removing the coercing of boolean-like values during URL parsing and leaving it up to
- the parsing utilities of the receiving side (i.e. the handler's signature model) to handle these values
- according to the associated type annotations.
-
- .. change:: Update ``standard`` and ``full`` package extras
- :type: misc
- :pr: 1494
-
- - Add SQLAlchemy, uvicorn, attrs and structlog to the ``full`` extra
- - Add uvicorn to the ``standard`` extra
- - Add ``uvicorn[standard]`` as an optional dependency to be used in the extras
-
- .. change:: Remove support for declaring DTOs as handler types
- :breaking:
- :type: misc
- :pr: 1534
-
- Prior to this, a DTO type could be declared implicitly using type annotations. With the addition of the ``dto``
- and ``return_dto`` parameters, this feature has become superfluous and, in the spirit of offering only one clear
- way of doing things, has been removed.
-
- .. change:: Fix missing ``content-encoding`` headers on gzip/brotli compressed files
- :type: bugfix
- :pr: 1577
- :issue: 1576
-
- Fixed a bug that would cause static files served via ``StaticFilesConfig`` that have been compressed with gripz
- or brotli to miss the appropriate ``content-encoding`` header.
-
- .. change:: DTO: Simplify ``DTOConfig``
- :type: misc
- :breaking:
- :pr: 1580
-
- - The ``include`` parameter has been removed, to provide a more accessible interface and avoid overly complex
- interplay with ``exclude`` and its support for dotted attributes
- - ``field_mapping`` has been renamed to ``rename_fields`` and support to remap field types has been dropped
- - experimental ``field_definitions`` has been removed. It may be replaced with a "ComputedField" in a future
- release that will allow multiple field definitions to be added to the model, and a callable that transforms
- them into a value for a model field. See
-
-
-.. changelog:: 2.0.0alpha4
-
- .. change:: ``attrs`` and ``msgspec`` support in :class:`Partial `
- :type: feature
- :pr: 1462
-
- :class:`Partial ` now supports constructing partial models for attrs and msgspec
-
- .. change:: :class:`Annotated ` support for route handler and dependency annotations
- :type: feature
- :pr: 1462
-
- :class:`Annotated ` can now be used in route handler and dependencies to specify additional
- information about the fields.
-
- .. code-block:: python
-
- @get("/")
- def index(param: int = Parameter(gt=5)) -> dict[str, int]: ...
-
- .. code-block:: python
-
- @get("/")
- def index(param: Annotated[int, Parameter(gt=5)]) -> dict[str, int]: ...
-
- .. change:: Support ``text/html`` Media-Type in ``Redirect`` response container
- :type: bugfix
- :issue: 1451
- :pr: 1474
-
- The media type in :class:`Redirect ` won't be forced to ``text/plain`` anymore and
- now supports setting arbitrary media types.
-
-
- .. change:: Fix global namespace for type resolution
- :type: bugfix
- :pr: 1477
- :issue: 1472
-
- Fix a bug where certain annotations would cause a :exc:`NameError`
-
-
- .. change:: Add uvicorn to ``cli`` extra
- :type: bugfix
- :issue: 1478
- :pr: 1480
-
- Add the ``uvicorn`` package to the ``cli`` extra, as it is required unconditionally
-
-
- .. change:: Update logging levels when setting ``Litestar.debug`` dynamically
- :type: bugfix
- :issue: 1476
- :pr: 1482
-
- When passing ``debug=True`` to :class:`Litestar `, the ``litestar`` logger would be set
- up in debug mode, but changing the ``debug`` attribute after the class had been instantiated did not update the
- logger accordingly.
-
- This lead to a regression where the ``--debug`` flag to the CLI's ``run`` command would no longer have the
- desired affect, as loggers would still be on the ``INFO`` level.
-
-
-.. changelog:: 2.0.0alpha3
-
- .. change:: SQLAlchemy 2.0 Plugin
- :type: feature
- :pr: 1395
-
- A :class:`SQLAlchemyInitPlugin ` was added,
- providing support for managed synchronous and asynchronous sessions.
-
- .. seealso::
- :doc:`/usage/databases/sqlalchemy/index`
-
- .. change:: Attrs signature modelling
- :type: feature
- :pr: 1382
-
- Added support to model route handler signatures with attrs instead of Pydantic
-
- .. change:: Support setting status codes in ``Redirect`` container
- :type: feature
- :pr: 1412
- :issue: 1371
-
- Add support for manually setting status codes in the
- :class:`RedirectResponse ` response container.
- This was previously only possible by setting the ``status_code`` parameter on
- the corresponding route handler, making dynamic redirect status codes and
- conditional redirects using this container hard to implement.
-
- .. change:: Sentinel value to support caching responses indefinitely
- :type: feature
- :pr: 1414
- :issue: 1365
-
- Add the :class:`CACHE_FOREVER ` sentinel value, that, when passed
- to a route handlers ``cache argument``, will cause it to be cached forever, skipping the default expiration.
-
- Additionally, add support for setting
- :attr:`ResponseCacheConfig.default_expiration ` to ``None``,
- allowing to cache values indefinitely by default when setting ``cache=True`` on a route handler.
-
- .. change:: `Accept`-header parsing and content negotiation
- :type: feature
- :pr: 1317
-
- Add an :attr:`accept ` property to
- :class:`Request `, returning the newly added
- :class:`Accept ` header wrapper, representing the requests ``Accept``
- HTTP header, offering basic content negotiation.
-
- .. seealso::
- :ref:`usage/responses:Content Negotiation`
-
- .. change:: Enhanced WebSockets support
- :type: feature
- :pr: 1402
-
- Add a new set of features for handling WebSockets, including automatic connection handling, (de)serialization
- of incoming and outgoing data analogous to route handlers and OOP based event dispatching.
-
- .. seealso::
- :doc:`/usage/websockets`
-
- .. change:: SQLAlchemy 1 plugin mutates app state destructively
- :type: bugfix
- :pr: 1391
- :issue: 1368
-
- When using the SQLAlchemy 1 plugin, repeatedly running through the application lifecycle (as done when testing
- an application not provided by a factory function), would result in a :exc:`KeyError` on the second pass.
-
- This was caused be the plugin's ``on_shutdown`` handler deleting the ``engine_app_state_key`` from the
- application's state on application shutdown, but only adding it on application init.
-
- This was fixed by adding performing the necessary setup actions on application startup rather than init.
-
- .. change:: Fix SQLAlchemy 1 Plugin - ``'Request' object has no attribute 'dict'``
- :type: bugfix
- :pr: 1389
- :issue: 1388
-
- An annotation such as
-
- .. code-block:: python
-
- async def provide_user(request: Request[User, Token, Any]) -> User: ...
-
- would result in the error ``'Request' object has no attribute 'dict'``.
-
- This was fixed by changing how ``get_plugin_for_value`` interacts with :func:`typing.get_args`
-
- .. change:: Support OpenAPI schema generation with stringized return annotation
- :type: bugfix
- :pr: 1410
- :issue: 1409
-
- The following code would result in non-specific and incorrect information being generated for the OpenAPI schema:
-
- .. code-block:: python
-
- from __future__ import annotations
-
- from starlite import Starlite, get
-
-
- @get("/")
- def hello_world() -> dict[str, str]:
- return {"hello": "world"}
-
- This could be alleviated by removing ``from __future__ import annotations``. Stringized annotations in any form
- are now fully supported.
-
- .. change:: Fix OpenAPI schema generation crashes for models with ``Annotated`` type attribute
- :type: bugfix
- :issue: 1372
- :pr: 1400
-
- When using a model that includes a type annotation with :class:`typing.Annotated` in a route handler, the
- interactive documentation would raise an error when accessed. This has been fixed and :class:`typing.Annotated`
- is now fully supported.
-
- .. change:: Support empty ``data`` in ``RequestFactory``
- :type: bugfix
- :issue: 1419
- :pr: 1420
-
- Add support for passing an empty ``data`` parameter to a
- :class:`RequestFactory `, which would previously lead to an error.
-
- .. change:: ``create_test_client`` and ``crate_async_test_client`` signatures and docstrings to to match ``Litestar``
- :type: misc
- :pr: 1417
-
- Add missing parameters to :func:`create_test_client ` and
- :func:`create_test_client `. The following parameters were added:
-
- - ``cache_control``
- - ``debug``
- - ``etag``
- - ``opt``
- - ``response_cache_config``
- - ``response_cookies``
- - ``response_headers``
- - ``security``
- - ``stores``
- - ``tags``
- - ``type_encoders``
-
-
-
-.. changelog:: 2.0.0alpha2
-
- .. change:: Repository contrib & SQLAlchemy repository
- :type: feature
- :pr: 1254
-
- Add a a ``repository`` module to ``contrib``, providing abstract base classes
- to implement the repository pattern. Also added was the ``contrib.repository.sqlalchemy``
- module, implementing a SQLAlchemy repository, offering hand-tuned abstractions
- over commonly used tasks, such as handling of object sessions, inserting,
- updating and upserting individual models or collections.
-
- .. change:: Data stores & registry
- :type: feature
- :pr: 1330
- :breaking:
-
- The ``starlite.storage`` module added in the previous version has been
- renamed ``starlite.stores`` to reduce ambiguity, and a new feature, the
- ``starlite.stores.registry.StoreRegistry`` has been introduced;
- It serves as a central place to manage stores and reduces the amount of
- configuration needed for various integrations.
-
- - Add ``stores`` kwarg to ``Starlite`` and ``AppConfig`` to allow seeding of the ``StoreRegistry``
- - Add ``Starlite.stores`` attribute, containing a ``StoreRegistry``
- - Change ``RateLimitMiddleware`` to use ``app.stores``
- - Change request caching to use ``app.stores``
- - Change server side sessions to use ``app.stores``
- - Move ``starlite.config.cache.CacheConfig`` to ``starlite.config.response_cache.ResponseCacheConfig``
- - Rename ``Starlite.cache_config`` > ``Starlite.response_cache_config``
- - Rename ``AppConfig.cache_config`` > ``response_cache_config``
- - Remove ``starlite/cache`` module
- - Remove ``ASGIConnection.cache`` property
- - Remove ``Starlite.cache`` attribute
-
- .. attention::
- ``starlite.middleware.rate_limit.RateLimitMiddleware``,
- ``starlite.config.response_cache.ResponseCacheConfig``,
- and ``starlite.middleware.session.server_side.ServerSideSessionConfig``
- instead of accepting a ``storage`` argument that could be passed a ``Storage`` instance now have to be
- configured via the ``store`` attribute, accepting a string key for the store to be used from the registry.
- The ``store`` attribute has a unique default set, guaranteeing a unique
- ``starlite.stores.memory.MemoryStore`` instance is acquired for every one of them from the
- registry by default
-
- .. seealso::
-
- :doc:`/usage/stores`
-
-
- .. change:: Add ``starlite.__version__``
- :type: feature
- :pr: 1277
-
- Add a ``__version__`` constant to the ``starlite`` namespace, containing a
- :class:`NamedTuple `, holding information about the currently
- installed version of Starlite
-
-
- .. change:: Add ``starlite version`` command to CLI
- :type: feature
- :pr: 1322
-
- Add a new ``version`` command to the CLI which displays the currently installed
- version of Starlite
-
-
- .. change:: Enhance CLI autodiscovery logic
- :type: feature
- :breaking:
- :pr: 1322
-
- Update the CLI :ref:`usage/cli:autodiscovery` to only consider canonical modules app and application, but every
- ``starlite.app.Starlite`` instance or application factory able to return a ``Starlite`` instance within
- those or one of their submodules, giving priority to the canonical names app and application for application
- objects and submodules containing them.
-
- .. seealso::
- :ref:`CLI autodiscovery `
-
- .. change:: Configurable exception logging and traceback truncation
- :type: feature
- :pr: 1296
-
- Add three new configuration options to ``starlite.logging.config.BaseLoggingConfig``:
-
- ``starlite.logging.config.LoggingConfig.log_exceptions``
- Configure when exceptions are logged.
-
- ``always``
- Always log exceptions
-
- ``debug``
- Log exceptions in debug mode only
-
- ``never``
- Never log exception
-
- ``starlite.logging.config.LoggingConfig.traceback_line_limit``
- Configure how many lines of tracback are logged
-
- ``starlite.logging.config.LoggingConfig.exception_logging_handler``
- A callable that receives three parameters - the ``app.logger``, the connection scope and the traceback
- list, and should handle logging
-
- .. seealso::
- ``starlite.logging.config.LoggingConfig``
-
-
- .. change:: Allow overwriting default OpenAPI response descriptions
- :type: bugfix
- :issue: 1292
- :pr: 1293
-
- Fix https://github.com/litestar-org/litestar/issues/1292 by allowing to overwrite
- the default OpenAPI response description instead of raising :exc:`ImproperlyConfiguredException`.
-
-
- .. change:: Fix regression in path resolution that prevented 404's being raised for false paths
- :type: bugfix
- :pr: 1316
- :breaking:
-
- Invalid paths within controllers would under specific circumstances not raise a 404. This was a regression
- compared to ``v1.51``
-
- .. note::
- This has been marked as breaking since one user has reported to rely on this "feature"
-
-
- .. change:: Fix ``after_request`` hook not being called on responses returned from handlers
- :type: bugfix
- :pr: 1344
- :issue: 1315
-
- ``after_request`` hooks were not being called automatically when a ``starlite.response.Response``
- instances was returned from a route handler directly.
-
- .. seealso::
- :ref:`after_request`
-
-
- .. change:: Fix ``SQLAlchemyPlugin`` raises error when using SQLAlchemy UUID
- :type: bugfix
- :pr: 1355
-
- An error would be raised when using the SQLAlchemy plugin with a
- `sqlalchemy UUID `_. This
- was fixed by adding it to the provider map.
-
-
- .. change:: Fix ``JSON.parse`` error in ReDoc and Swagger OpenAPI handlers
- :type: bugfix
- :pr: 1363
-
- The HTML generated by the ReDoc and Swagger OpenAPI handlers would cause
- `JSON.parse `_
- to throw an error. This was fixed by removing the call to ``JSON.parse``.
-
-
- .. change:: Fix CLI prints application info twice
- :type: bugfix
- :pr: 1322
-
- Fix an error where the CLI would print application info twice on startup
-
-
- .. change:: Update ``SimpleEventEmitter`` to use worker pattern
- :type: misc
- :pr: 1346
-
- ``starlite.events.emitter.SimpleEventEmitter`` was updated to using an async worker, pulling
- emitted events from a queue and subsequently calling listeners. Previously listeners were called immediately,
- making the operation effectively "blocking".
-
-
- .. change:: Make ``BaseEventEmitterBackend.emit`` synchronous
- :type: misc
- :breaking:
- :pr: 1376
-
- ``starlite.events.emitter.BaseEventEmitterBackend``, and subsequently
- ``starlite.events.emitter.SimpleEventEmitter`` and
- ``starlite.app.Starlite.emit`` have been changed to synchronous function, allowing them to easily be
- used within synchronous route handlers.
-
-
- .. change:: Move 3rd party integration plugins to ``contrib``
- :type: misc
- :breaking:
- :pr: 1279 1252
-
- - Move ``plugins.piccolo_orm`` > ``contrib.piccolo_orm``
- - Move ``plugins.tortoise_orm`` > ``contrib.tortoise_orm``
-
-
- .. change:: Remove ``picologging`` dependency from the ``standard`` package extra
- :type: misc
- :breaking:
- :pr: 1313
-
- `picologging `_ has been removed form the ``standard`` package extra.
- If you have been previously relying on this, you need to change ``pip install starlite[standard]`` to
- ``pip install starlite[standard,picologging]``
-
-
- .. change:: Replace ``Starlite()`` ``initial_state`` keyword argument with ``state``
- :type: misc
- :pr: 1350
- :breaking:
-
- The ``initial_state`` argument to ``starlite.app.Starlite`` has been replaced with a ``state`` keyword
- argument, accepting an optional ``starlite.datastructures.state.State`` instance.
-
- Existing code using this keyword argument will need to be changed from
-
- .. code-block:: python
-
- from starlite import Starlite
-
- app = Starlite(..., initial_state={"some": "key"})
-
- to
-
- .. code-block:: python
-
- from starlite import Starlite
- from starlite.datastructures.state import State
-
- app = Starlite(..., state=State({"some": "key"}))
-
-
- .. change:: Remove support for 2 argument form of ``before_send``
- :type: misc
- :pr: 1354
- :breaking:
-
- ``before_send`` hook handlers initially accepted 2 arguments, but support for a 3 argument form was added
- later on, accepting an additional ``scope`` parameter. Support for the 2 argument form has been dropped with
- this release.
-
- .. seealso::
- :ref:`before_send`
-
-
- .. change:: Standardize module exports
- :type: misc
- :pr: 1273
- :breaking:
-
- A large refactoring standardising the way submodules make their names available.
-
- The following public modules have changed their location:
-
- - ``config.openapi`` > ``openapi.config``
- - ``config.logging`` > ``logging.config``
- - ``config.template`` > ``template.config``
- - ``config.static_files`` > ``static_files.config``
-
- The following modules have been removed from the public namespace:
-
- - ``asgi``
- - ``kwargs``
- - ``middleware.utils``
- - ``cli.utils``
- - ``contrib.htmx.utils``
- - ``handlers.utils``
- - ``openapi.constants``
- - ``openapi.enums``
- - ``openapi.datastructures``
- - ``openapi.parameters``
- - ``openapi.path_item``
- - ``openapi.request_body``
- - ``openapi.responses``
- - ``openapi.schema``
- - ``openapi.typescript_converter``
- - ``openapi.utils``
- - ``multipart``
- - ``parsers``
- - ``signature``
-
-
-.. changelog:: 2.0.0alpha1
-
- .. change:: Validation of controller route handler methods
- :type: feature
- :pr: 1144
-
- Starlite will now validate that no duplicate handlers (that is, they have the same
- path and same method) exist.
-
- .. change:: HTMX support
- :type: feature
- :pr: 1086
-
- Basic support for HTMX requests and responses.
-
- .. change:: Alternate constructor ``Starlite.from_config``
- :type: feature
- :pr: 1190
-
- ``starlite.app.Starlite.from_config`` was added to the
- ``starlite.app.Starlite`` class which allows to construct an instance
- from an ``starlite.config.app.AppConfig`` instance.
-
- .. change:: Web concurrency option for CLI ``run`` command
- :pr: 1218
- :type: feature
-
- A ``--wc`` / --web-concurrency` option was added to the ``starlite run`` command,
- enabling users to specify the amount of worker processes to use. A corresponding
- environment variable ``WEB_CONCURRENCY`` was added as well
-
- .. change:: Validation of ``state`` parameter in handler functions
- :type: feature
- :pr: 1264
-
- Type annotations of the reserved ``state`` parameter in handler functions will
- now be validated such that annotations using an unsupported type will raise a
- ``starlite.exceptions.ImproperlyConfiguredException``.
-
- .. change:: Generic application state
+ .. change:: New stuff
:type: feature
- :pr: 1030
-
- ``starlite.connection.base.ASGIConnection`` and its subclasses are now generic on ``State``
- which allow to to fully type hint a request as ``Request[UserType, AuthType, StateType]``.
-
- .. change:: Dependency injection of classes
- :type: feature
- :pr: 1143
-
- Support using classes (not class instances, which were already supported) as dependency providers.
- With this, now every callable is supported as a dependency provider.
-
- .. change:: Event bus
- :pr: 1105
- :type: feature
-
- A simple event bus system for Starlite, supporting synchronous and asynchronous listeners and emitters, providing a
- similar interface to handlers. It currently features a simple in-memory, process-local backend
-
- .. change:: Unified storage interfaces
- :type: feature
- :pr: 1184
- :breaking:
-
- Storage backends for server-side sessions ``starlite.cache.Cache``` have been unified and replaced
- by the ``starlite.storages``, which implements generic asynchronous key/values stores backed
- by memory, the file system or redis.
-
- .. important::
- This is a breaking change and you need to change your session / cache configuration accordingly
-
-
- .. change:: Relaxed type annotations
- :pr: 1140
- :type: misc
-
- Type annotations across the library have been relaxed to more generic forms, for example
- ``Iterable[str]`` instead of ``List[str]`` or ``Mapping[str, str]`` instead of ``Dict[str, str]``.
-
- .. change:: ``type_encoders`` support in ``AbstractSecurityConfig``
- :type: misc
- :pr: 1167
-
- ``type_encoders`` support has been added to
- ``starlite.security.base.AbstractSecurityConfig``, enabling support for customized
- ``type_encoders`` for example in ``starlite.contrib.jwt.jwt_auth.JWTAuth``.
-
-
- .. change:: Renamed handler module names
- :type: misc
- :breaking:
- :pr: 1170
-
- The modules containing route handlers have been renamed to prevent ambiguity between module and handler names.
-
- - ``starlite.handlers.asgi`` > ``starlite.handlers.asgi_handlers``
- - ``starlite.handlers.http`` > ``starlite.handlers.http_handlers``
- - ``starlite.handlers.websocket`` > ``starlite.handlers.websocket_handlers``
-
-
- .. change:: New plugin protocols
- :type: misc
- :pr: 1176
- :breaking:
-
- The plugin protocol has been split into three distinct protocols, covering different use cases:
-
- ``starlite.plugins.InitPluginProtocol``
- Hook into an application's initialization process
-
- ``starlite.plugins.SerializationPluginProtocol``
- Extend the serialization and deserialization capabilities of an application
-
- ``starlite.plugins.OpenAPISchemaPluginProtocol``
- Extend OpenAPI schema generation
-
-
- .. change:: Unify response headers and cookies
- :type: misc
- :breaking:
- :pr: 1209
-
- :ref:`response headers ` and
- :ref:`response cookies ` now have the
- same interface, along with the ``headers`` and ``cookies`` keyword arguments to
- ``starlite.response.Response``. They each allow to pass either a
- `:class:`Mapping[str, str] `, e.g. a dictionary, or a :class:`Sequence ` of
- ``starlite.datastructures.response_header.ResponseHeader`` or
- ``starlite.datastructures.cookie.Cookie`` respectively.
-
-
- .. change:: Replace Pydantic models with dataclasses
- :type: misc
- :breaking:
- :pr: 1242
-
- Several Pydantic models used for configuration have been replaced with dataclasses or plain classes. This change
- should be mostly non-breaking, unless you relied on those configuration objects being Pydantic models. The changed
- models are:
-
- - ``starlite.config.allowed_hosts.AllowedHostsConfig``
- - ``starlite.config.app.AppConfig``
- - ``starlite.config.response_cache.ResponseCacheConfig``
- - ``starlite.config.compression.CompressionConfig``
- - ``starlite.config.cors.CORSConfig``
- - ``starlite.config.csrf.CSRFConfig``
- - ``starlite.logging.config.LoggingConfig``
- - ``starlite.openapi.OpenAPIConfig``
- - ``starlite.static_files.StaticFilesConfig``
- - ``starlite.template.TemplateConfig``
- - ``starlite.contrib.jwt.jwt_token.Token``
- - ``starlite.contrib.jwt.jwt_auth.JWTAuth``
- - ``starlite.contrib.jwt.jwt_auth.JWTCookieAuth``
- - ``starlite.contrib.jwt.jwt_auth.OAuth2Login``
- - ``starlite.contrib.jwt.jwt_auth.OAuth2PasswordBearerAuth``
- - ``starlite.contrib.opentelemetry.OpenTelemetryConfig``
- - ``starlite.middleware.logging.LoggingMiddlewareConfig``
- - ``starlite.middleware.rate_limit.RateLimitConfig``
- - ``starlite.middleware.session.base.BaseBackendConfig``
- - ``starlite.middleware.session.client_side.CookieBackendConfig``
- - ``starlite.middleware.session.server_side.ServerSideSessionConfig``
- - ``starlite.response_containers.ResponseContainer``
- - ``starlite.response_containers.File``
- - ``starlite.response_containers.Redirect``
- - ``starlite.response_containers.Stream``
- - ``starlite.security.base.AbstractSecurityConfig``
- - ``starlite.security.session_auth.SessionAuth``
-
-
- .. change:: SQLAlchemy plugin moved to ``contrib``
- :type: misc
+ :pr: 1234
:breaking:
- :pr: 1252
-
- The ``SQLAlchemyPlugin` has moved to ``starlite.contrib.sqlalchemy_1.plugin`` and will only be compatible
- with the SQLAlchemy 1.4 release line. The newer SQLAlchemy 2.x releases will be supported by the
- ``contrib.sqlalchemy`` module.
-
-
- .. change:: Cleanup of the ``starlite`` namespace
- :type: misc
- :breaking:
- :pr: 1135
-
- The ``starlite`` namespace has been cleared up, removing many names from it, which now have to be imported from
- their respective submodules individually. This was both done to improve developer experience as well as reduce
- the time it takes to ``import starlite``.
-
- .. change:: Fix resolving of relative paths in ``StaticFilesConfig``
- :type: bugfix
- :pr: 1256
-
- Using a relative :class:`pathlib.Path` did not resolve correctly and result in a ``NotFoundException``
-
- .. change:: Fix ``--reload`` flag to ``starlite run`` not working correctly
- :type: bugfix
- :pr: 1191
-
- Passing the ``--reload`` flag to the ``starlite run`` command did not work correctly in all circumstances due to an
- issue with uvicorn. This was resolved by invoking uvicorn in a subprocess.
-
-
- .. change:: Fix optional types generate incorrect OpenAPI schemas
- :type: bugfix
- :pr: 1210
-
- An optional query parameter was incorrectly represented as
-
- .. code-block::
-
- { "oneOf": [
- { "type": null" },
- { "oneOf": [] }
- ]}
-
-
- .. change:: Fix ``LoggingMiddleware`` is sending obfuscated session id to client
- :type: bugfix
- :pr: 1228
-
- ``LoggingMiddleware`` would in some cases send obfuscated data to the client, due to a bug in the obfuscation
- function which obfuscated values in the input dictionary in-place.
-
-
- .. change:: Fix missing ``domain`` configuration value for JWT cookie auth
- :type: bugfix
- :pr: 1223
-
- ``starlite.contrib.jwt.jwt_auth.JWTCookieAuth`` didn't set the ``domain`` configuration value on the response
- cookie.
-
-
- .. change:: Fix https://github.com/litestar-org/litestar/issues/1201: Can not serve static file in ``/`` path
- :type: bugfix
- :issue: 1201
-
- A validation error made it impossible to serve static files from the root path ``/`` .
-
- .. change:: Fix https://github.com/litestar-org/litestar/issues/1149: Middleware not excluding static path
- :type: bugfix
- :issue: 1149
- A middleware's ``exclude`` parameter would sometimes not be honoured if the path was used to serve static files
- using ``StaticFilesConfig``
+ This is a changelog entry
diff --git a/docs/release-notes/index.rst b/docs/release-notes/index.rst
index 7a573e5acb..dc9a704183 100644
--- a/docs/release-notes/index.rst
+++ b/docs/release-notes/index.rst
@@ -6,6 +6,8 @@ Release notes
.. toctree::
:titlesonly:
+ whats-new-3
whats-new-2
+ changelog
2.x Changelog
1.x Changelog
diff --git a/docs/release-notes/whats-new-2.rst b/docs/release-notes/whats-new-2.rst
index 6e6415e74e..a66f19423f 100644
--- a/docs/release-notes/whats-new-2.rst
+++ b/docs/release-notes/whats-new-2.rst
@@ -93,7 +93,7 @@ Imports
+----------------------------------------------------+------------------------------------------------------------------------+
| ``starlite.OpenAPIConfig`` | :class:`.openapi.OpenAPIConfig` |
+----------------------------------------------------+------------------------------------------------------------------------+
-| ``starlite.StaticFilesConfig`` | :class:`.static_files.config.StaticFilesConfig` |
+| ``starlite.StaticFilesConfig`` | ``.static_files.config.StaticFilesConfig`` |
+----------------------------------------------------+------------------------------------------------------------------------+
| ``starlite.TemplateConfig`` | :class:`.template.TemplateConfig` |
+----------------------------------------------------+------------------------------------------------------------------------+
diff --git a/docs/release-notes/whats-new-3.rst b/docs/release-notes/whats-new-3.rst
new file mode 100644
index 0000000000..a076ebc8f6
--- /dev/null
+++ b/docs/release-notes/whats-new-3.rst
@@ -0,0 +1,206 @@
+.. py:currentmodule:: litestar
+
+
+What's changed in 3.0?
+======================
+
+This document is an overview of the changes between version **2.11.x** and **3.0**.
+For a detailed list of all changes, including changes between versions leading up to the
+3.0 release, consult the :doc:`/release-notes/changelog`.
+
+.. note:: The **2.11** release line is unaffected by this change
+
+Imports
+-------
+
++----------------------------------------------------+------------------------------------------------------------------------+
+| ``2.11`` | ``3.x`` |
++====================================================+========================================================================+
+| **SECTION** |
++----------------------------------------------------+------------------------------------------------------------------------+
+| Put your shit here from v2 | Put your shit here from v3 |
++----------------------------------------------------+------------------------------------------------------------------------+
+
+
+Removal of ``StaticFileConfig``
+-------------------------------
+
+The ``StaticFilesConfig`` has been removed, alongside these related parameters and
+functions:
+
+- ``Litestar.static_files_config``
+- ``Litestar.url_for_static_asset``
+- ``Request.url_for_static_asset``
+
+:func:`create_static_files_router` is a drop-in replacement for ``StaticFilesConfig``,
+and can simply be added to the ``route_handlers`` like any other regular handler.
+
+Usage of ``url_for_static_assets`` should be replaced with a ``url_for("static", ...)``
+call.
+
+
+Implicit Optional Default Parameters
+------------------------------------
+
+In v2, if a handler was typed with an optional parameter it would be implicitly given a default value of ``None``. For
+example, if the following handler is called with no query parameter, the value ``None`` would be passed in to the
+handler for the ``param`` parameter:
+
+.. code-block:: python
+
+ @get("/")
+ def my_handler(param: int | None) -> ...:
+ ...
+
+This legacy behavior originates from our history of using Pydantic v1 models to represent handler signatures. In v3, we
+no longer make this implicit conversion. If you want to have a default value of ``None`` for an optional parameter, you
+must explicitly set it:
+
+.. code-block:: python
+
+ @get("/")
+ def my_handler(param: int | None = None) -> ...:
+ ...
+
+
+OpenAPI Controller Replaced by Plugins
+--------------------------------------
+
+In version 3.0, the OpenAPI controller pattern, deprecated in v2.8, has been removed in
+favor of a more flexible plugin system.
+
+Elimination of ``OpenAPIController`` Subclassing
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Previously, users configured elements such as the root path and styling by subclassing OpenAPIController and setting it
+on the ``OpenAPIConfig.openapi_controller`` attribute. As of version 3.0, this pattern has been removed. Instead, users
+are required to transition to using UI plugins for configuration.
+
+Migration Steps:
+
+1. Remove any implementations subclassing ``OpenAPIController``.
+2. Use the :attr:`OpenAPIConfig.render_plugins` attribute to configure the OpenAPI UI made available to your users.
+ If no plugin is supplied, we automatically add the :class:`ScalarRenderPlugin` for the default configuration.
+3. Use the :attr:`OpenAPIConfig.openapi_router` attribute for additional configuration.
+
+See the :doc:`/usage/openapi/ui_plugins` documentation for more information on how to configure OpenAPI plugins.
+
+Changes to Endpoint Configuration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``OpenAPIConfig.enabled_endpoints`` attribute is no longer available in version 3.0.0. This attribute previously
+enabled a set of endpoints that would serve different OpenAPI UIs. In the new version, only the ``openapi.json``
+endpoint is enabled by default, alongside the ``Scalar`` UI plugin as the default.
+
+To adapt to this change, you should explicitly configure any additional endpoints you need by properly setting up the
+necessary plugins within the :attr:`OpenAPIConfig.render_plugins` parameter.
+
+Modification to ``root_schema_site`` Handling
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``root_schema_site`` attribute, which enabled serving a particular UI at the OpenAPI root path, has been removed in
+version 3.0. The new approach automatically assigns the first :class:`OpenAPIRenderPlugin` listed in the
+:attr:`OpenAPIConfig.render_plugins` list to serve at the ``/schema`` endpoint, unless a plugin has been defined with
+the root path (``/``), in which case that plugin will be used.
+
+For those previously using the ``root_schema_site`` attribute, the migration involves ensuring that the UI intended to
+be served at the ``/schema`` endpoint is the first plugin listed in the :attr:`OpenAPIConfig.render_plugins`.
+
+
+Deprecated ``app`` parameter for ``Response.to_asgi_response`` has been removed
+-------------------------------------------------------------------------------
+
+The parameter ``app`` for :meth:`~response.Response.to_asgi_response` has been removed.
+If you need access to the app instance inside a custom ``to_asgi_response`` method,
+replace the usages of ``app`` with ``request.app``.
+
+
+Deprecated scope state utilities removed
+----------------------------------------
+
+Litestar has previously made available utilities for storing and retrieving data in the ASGI scope state. These
+utilities have been removed in version 3.0.0. If you need to store data in the ASGI scope state, you should use do so
+using a namespace that is unique to your application and unlikely to conflict with other applications.
+
+The following utilities have been removed:
+
+- ``get_litestar_scope_state``
+- ``set_litestar_scope_state``
+- ``delete_litestar_scope_state``
+
+
+Deprecated utility function ``is_sync_or_async_generator`` removed
+------------------------------------------------------------------
+
+The utility function ``is_sync_or_async_generator`` has been removed as it is no longer used internally.
+
+If you were relying on this utility, you can define it yourself as follows:
+
+.. code-block:: python
+
+ from inspect import isasyncgenfunction, isgeneratorfunction
+
+ def is_sync_or_async_generator(obj: Any) -> bool:
+ return isgeneratorfunction(obj) or isasyncgenfunction(obj)
+
+
+Removal of semantic HTTP route handler classes
+-----------------------------------------------
+
+The semantic ``HTTPRouteHandler`` classes have been removed in favour of functional
+decorators. ``route``, ``get``, ``post``, ``patch``, ``put``, ``head`` and ``delete``
+are now all decorator functions returning :class:`~.handlers.HTTPRouteHandler`
+instances.
+
+As a result, customizing the decorators directly is not possible anymore. Instead, to
+use a route handler decorator with a custom route handler class, the ``handler_class``
+parameter to the decorator function can be used:
+
+Before:
+
+.. code-block:: python
+
+ class my_get_handler(get):
+ ... # custom handler
+
+ @my_get_handler()
+ async def handler() -> Any:
+ ...
+
+After:
+
+.. code-block:: python
+
+ class MyHTTPRouteHandler(HTTPRouteHandler):
+ ... # custom handler
+
+
+ @get(handler_class=MyHTTPRouteHandler)
+ async def handler() -> Any:
+ ...
+
+
+Deprecated ``app`` parameter for ``Response.to_asgi_response`` has been removed
+-------------------------------------------------------------------------------
+
+The parameter ``app`` for :meth:`~response.Response.to_asgi_response` has been removed.
+If you need access to the app instance inside a custom ``to_asgi_response`` method,
+replace the usages of ``app`` with ``request.app``.
+
+
+Removal of deprecated ``litestar.middleware.exceptions`` module and ``ExceptionHandlerMiddleware``
+--------------------------------------------------------------------------------------------------
+
+The deprecated ``litestar.middleware.exceptions`` module and the
+``ExceptionHandlerMiddleware`` have been removed. Since ``ExceptionHandlerMiddleware``
+has been applied automatically behind the scenes if necessary, no action is required.
+
+
+Update MessagePack media type to ``application/vnd.msgpack``
+------------------------------------------------------------
+
+Change the media type of :attr:`~enums.MediaType.MESSAGEPACK` and
+:attr:`~enums.RequestEncodingType.MESSAGEPACK` from ``application/x-msgpack`` to the
+newly introduced official ``application/vnd.msgpack``.
+
+https://www.iana.org/assignments/media-types/application/vnd.msgpack
diff --git a/docs/usage/openapi/ui_plugins.rst b/docs/usage/openapi/ui_plugins.rst
index c8c06d3956..df10675c57 100644
--- a/docs/usage/openapi/ui_plugins.rst
+++ b/docs/usage/openapi/ui_plugins.rst
@@ -139,57 +139,6 @@ In the following example, we configure the OpenAPI root path to be ``/docs``:
This will result in any of the OpenAPI endpoints being served at ``/docs`` instead of ``/schema``, e.g.,
``/docs/openapi.json``.
-Backward Compatibility
-----------------------
-
-OpenAPI UI plugins are a new feature introduced in ``v2.8.0``.
-
-Providing a subclass of OpenAPIController
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. deprecated:: v2.8.0
-
-The previous method of configuring elements such as the root path and styling was to subclass
-:class:`OpenAPIController`, and set it on the :attr:`OpenAPIConfig.openapi_controller` attribute. This approach is now
-deprecated and slated for removal in ``v3.0.0``, but if you are using it, there should be no change in behavior.
-
-To maintain backward compatibility with the previous approach, if neither the :attr:`OpenAPIConfig.openapi_controller`
-or :attr:`OpenAPIConfig.render_plugins` attributes are set, we will automatically add the plugins to respect the also
-deprecated :attr:`OpenAPIConfig.enabled_endpoints` attribute. By default, this will result in the following endpoints
-being enabled:
-
-- ``/schema/openapi.json``
-- ``/schema/redoc``
-- ``/schema/rapidoc``
-- ``/schema/elements``
-- ``/schema/swagger``
-- ``/schema/openapi.yml``
-- ``/schema/openapi.yaml``
-
-In ``v3.0.0``, the :attr:`OpenAPIConfig.enabled_endpoints` attribute will be removed, and only a single UI plugin will be
-enabled by default, in addition to the ``openapi.json`` endpoint which will always be enabled. ``Scalar`` will also
-become the default UI plugin in ``v3.0.0``.
-
-To adopt the future behavior, explicitly set the :attr:`OpenAPIConfig.render_plugins` field to an instance of
-:class:`ScalarRenderPlugin`:
-
-.. literalinclude:: /examples/openapi/plugins/scalar_simple.py
- :language: python
- :lines: 13-21
-
-Backward compatibility with ``root_schema_site``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Litestar has always supported a ``root_schema_site`` attribute on the :class:`OpenAPIConfig` class. This attribute
-allows you to elect to serve a UI at the OpenAPI root path, e.g., by default ``redoc`` would be served at both
-``/schema`` and ``/schema/redoc``.
-
-In ``v3.0.0``, the ``root_schema_site`` attribute will be removed, and the first :class:`OpenAPIRenderPlugin` in the
-:attr:`OpenAPIConfig.render_plugins` list will be assigned to the ``/schema`` endpoint.
-
-As of ``v2.8.0``, if you explicitly use the new :attr:`OpenAPIConfig.render_plugins` attribute, you will be
-automatically opted in to the new behavior, and the ``root_schema_site`` attribute will be ignored.
-
Building your own OpenAPI UI Plugin
-----------------------------------
@@ -277,9 +226,9 @@ This can be used for a variety of purposes, including adding additional routes t
OAuth2 in Swagger UI
--------------------
-When using Swagger, OAuth2 settings can be configured via
-:attr:`swagger_ui_init_oauth `, which can be set to
-a dictionary containing the parameters described in the Swagger UI documentation for OAuth2
+When using Swagger, OAuth2 settings can be configured via the :paramref:`~.openapi.plugins.SwaggerRenderPlugin.init_oauth` param of
+:meth:`SwaggerRenderPlugin `, which can be set to a dictionary
+containing the parameters described in the Swagger UI documentation for OAuth2
`here `_.
With that, you can preset your clientId or enable PKCE support.
diff --git a/docs/usage/routing/handlers.rst b/docs/usage/routing/handlers.rst
index d6207ed954..fb615f757a 100644
--- a/docs/usage/routing/handlers.rst
+++ b/docs/usage/routing/handlers.rst
@@ -7,7 +7,7 @@ handler :term:`decorators ` exported from Litestar.
For example:
.. code-block:: python
- :caption: Defining a route handler by decorating a function with the :class:`@get() <.handlers.get>` :term:`decorator`
+ :caption: Defining a route handler by decorating a function with the :func:`@get() <.handlers.get>` :term:`decorator`
from litestar import get
@@ -146,12 +146,11 @@ There are several reasons for why this limitation is enforced:
HTTP route handlers
-------------------
-The most commonly used route handlers are those that handle HTTP requests and responses.
-These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` class, which is aliased as the
-:term:`decorator` called :func:`~.handlers.route`:
+The :class:`~.handlers.HTTPRouteHandler` is used to handle HTTP requests, and can be
+created with the :func:`~.handlers.route` :term:`decorator`:
.. code-block:: python
- :caption: Defining a route handler by decorating a function with the :class:`@route() <.handlers.route>`
+ :caption: Defining a route handler by decorating a function with the :func:`@route() <.handlers.route>`
:term:`decorator`
from litestar import HttpMethod, route
@@ -160,20 +159,24 @@ These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` c
@route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST])
async def my_endpoint() -> None: ...
-As mentioned above, :func:`@route() <.handlers.route>` is merely an alias for ``HTTPRouteHandler``,
-thus the below code is equivalent to the one above:
+The same can be achieved without a decorator, by using ``HTTPRouteHandler`` directly:
.. code-block:: python
- :caption: Defining a route handler by decorating a function with the
- :class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>` class
+ :caption: Defining a route handler creating an instance of
+ :class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>`
from litestar import HttpMethod
from litestar.handlers.http_handlers import HTTPRouteHandler
- @HTTPRouteHandler(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST])
async def my_endpoint() -> None: ...
+ handler = HTTPRouteHandler(
+ path="/some-path",
+ http_method=[HttpMethod.GET, HttpMethod.POST],
+ fn=my_endpoint
+ )
+
Semantic handler :term:`decorators `
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -189,8 +192,8 @@ which correlates with their name:
* :func:`@post() <.handlers.post>`
* :func:`@put() <.handlers.put>`
-These are used exactly like :func:`@route() <.handlers.route>` with the sole exception that you cannot configure the
-:paramref:`~.handlers.HTTPRouteHandler.http_method` :term:`kwarg `:
+These are used exactly like :func:`@route() <.handlers.route>` with the sole exception that you don't need to configure
+the :paramref:`~.handlers.HTTPRouteHandler.http_method` :term:`kwarg `:
.. dropdown:: Click to see the predefined route handlers
@@ -240,11 +243,6 @@ These are used exactly like :func:`@route() <.handlers.route>` with the sole exc
@delete(path="/resources/{pk:int}")
async def delete_resource(pk: int) -> None: ...
-Although these :term:`decorators ` are merely subclasses of :class:`~.handlers.HTTPRouteHandler` that pre-set
-the :paramref:`~.handlers.HTTPRouteHandler.http_method`, using :func:`@get() <.handlers.get>`,
-:func:`@patch() <.handlers.patch>`, :func:`@put() <.handlers.put>`, :func:`@delete() <.handlers.delete>`, or
-:func:`@post() <.handlers.post>` instead of :func:`@route() <.handlers.route>` makes the code clearer and simpler.
-
Furthermore, in the OpenAPI specification each unique combination of HTTP verb (e.g. ``GET``, ``POST``, etc.) and path
is regarded as a distinct `operation `_\ , and each
operation should be distinguished by a unique :paramref:`~.handlers.HTTPRouteHandler.operation_id` and optimally
@@ -277,8 +275,8 @@ A WebSocket connection can be handled with a :func:`@websocket() <.handlers.Webs
await socket.send_json({...})
await socket.close()
-The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` is an alias of the
-:class:`~.handlers.WebsocketRouteHandler` class. Thus, the below code is equivalent to the one above:
+The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` can be used to create an instance of
+:class:`~.handlers.WebsocketRouteHandler`. Therefore, the below code is equivalent to the one above:
.. code-block:: python
:caption: Using the :class:`~.handlers.WebsocketRouteHandler` class directly
@@ -286,13 +284,16 @@ The :func:`@websocket() <.handlers.WebsocketRouteHandler>` :term:`decorator` is
from litestar import WebSocket
from litestar.handlers.websocket_handlers import WebsocketRouteHandler
-
- @WebsocketRouteHandler(path="/socket")
async def my_websocket_handler(socket: WebSocket) -> None:
await socket.accept()
await socket.send_json({...})
await socket.close()
+ my_websocket_handler = WebsocketRouteHandler(
+ path="/socket",
+ fn=my_websocket_handler,
+ )
+
In difference to HTTP routes handlers, websocket handlers have the following requirements:
#. They **must** declare a ``socket`` :term:`kwarg `.
@@ -332,8 +333,8 @@ If you need to write your own ASGI application, you can do so using the :func:`@
)
await response(scope=scope, receive=receive, send=send)
-Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator` is an alias of the
-:class:`~.handlers.ASGIRouteHandler` class. Thus, the code below is equivalent to the one above:
+:func:`@asgi() <.handlers.asgi>` :term:`decorator` can be used to create an instance of
+:class:`~.handlers.ASGIRouteHandler`. Therefore, the code below is equivalent to the one above:
.. code-block:: python
:caption: Using the :class:`~.handlers.ASGIRouteHandler` class directly
@@ -343,8 +344,6 @@ Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator
from litestar.status_codes import HTTP_400_BAD_REQUEST
from litestar.types import Scope, Receive, Send
-
- @ASGIRouteHandler(path="/my-asgi-app")
async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
if scope["method"] == "GET":
@@ -356,7 +355,10 @@ Like other route handlers, the :func:`@asgi() <.handlers.asgi>` :term:`decorator
)
await response(scope=scope, receive=receive, send=send)
-Limitations of ASGI route handlers
+ my_asgi_app = ASGIRouteHandler(path="/my-asgi-app", fn=my_asgi_app)
+
+
+ASGI route handler considerations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In difference to the other route handlers, the :func:`@asgi() <.handlers.asgi>` route handler accepts only three
diff --git a/docs/usage/templating.rst b/docs/usage/templating.rst
index c43a62235b..dcfa96eef0 100644
--- a/docs/usage/templating.rst
+++ b/docs/usage/templating.rst
@@ -380,9 +380,10 @@ Built-in callables
if you wish to insert the ``csrf_token`` into non-HTML based templates, or insert it into HTML templates not using a hidden input field but
by some other means, for example inside a special ```` tag.
-``url_for_static_asset``
- URLs for static files can be created using the ``url_for_static_asset`` function. It's signature and behaviour are identical to
- :meth:`app.url_for_static_asset `.
+``url_for`` for static files
+ For static file serving as provided by :func:`~litestar.static_files.create_static_files_router`,
+ ``url_for`` can be used with the ``static`` handler name: ``url_for("static", file_name="style.css")``
+
Registering template callables
diff --git a/docs/usage/websockets.rst b/docs/usage/websockets.rst
index dc9dc7eeb5..ab005a6ca7 100644
--- a/docs/usage/websockets.rst
+++ b/docs/usage/websockets.rst
@@ -3,12 +3,12 @@ WebSockets
There are three ways to handle WebSockets in Litestar:
-1. The low-level :class:`~litestar.handlers.websocket` route handler, providing basic
+1. The low-level :func:`~litestar.handlers.websocket` route handler, providing basic
abstractions over the ASGI WebSocket interface
-2. :class:`~litestar.handlers.websocket_listener` and :class:`~litestar.handlers.WebsocketListener`\ :
+2. :func:`~litestar.handlers.websocket_listener` and :class:`~litestar.handlers.WebsocketListener`\ :
Reactive, event-driven WebSockets with full serialization and DTO support and support
for a synchronous interface
-3. :class:`~litestar.handlers.websocket_stream` and :func:`~litestar.handlers.send_websocket_stream`\ :
+3. :func:`~litestar.handlers.websocket_stream` and :func:`~litestar.handlers.send_websocket_stream`\ :
Proactive, stream oriented WebSockets with full serialization and DTO support
@@ -49,7 +49,7 @@ type of data which should be received, and it will be converted accordingly.
.. note::
Contrary to WebSocket route handlers, functions decorated with
- :class:`websocket_listener <.handlers.websocket_listener>` don't have to be
+ :func:`websocket_listener <.handlers.websocket_listener>` don't have to be
asynchronous.
diff --git a/litestar/_asgi/routing_trie/mapping.py b/litestar/_asgi/routing_trie/mapping.py
index c96db46cff..319a821041 100644
--- a/litestar/_asgi/routing_trie/mapping.py
+++ b/litestar/_asgi/routing_trie/mapping.py
@@ -49,7 +49,6 @@ def add_mount_route(
current_node = current_node.children[component] # type: ignore[index]
current_node.is_mount = True
- current_node.is_static = route.route_handler.is_static
if route.path != "/":
mount_routes[route.path] = root_node.children[route.path] = current_node
@@ -145,8 +144,7 @@ def configure_node(
node.path_parameters = {}
if isinstance(route, HTTPRoute):
- for method, handler_mapping in route.route_handler_map.items():
- handler, _ = handler_mapping
+ for method, handler in route.route_handler_map.items():
node.asgi_handlers[method] = ASGIHandlerTuple(
asgi_app=build_route_middleware_stack(app=app, route=route, route_handler=handler),
handler=handler,
diff --git a/litestar/_asgi/routing_trie/traversal.py b/litestar/_asgi/routing_trie/traversal.py
index 77d1b4b3d3..a4988c2cc3 100644
--- a/litestar/_asgi/routing_trie/traversal.py
+++ b/litestar/_asgi/routing_trie/traversal.py
@@ -150,8 +150,7 @@ def parse_path_to_route(
if not any(remaining_path.startswith(f"{sub_route}/") for sub_route in children):
asgi_app, handler = parse_node_handlers(node=mount_node, method=method)
remaining_path = remaining_path or "/"
- if not mount_node.is_static:
- remaining_path = remaining_path if remaining_path.endswith("/") else f"{remaining_path}/"
+ remaining_path = remaining_path if remaining_path.endswith("/") else f"{remaining_path}/"
return asgi_app, handler, remaining_path, {}, root_node.path_template
node, path_parameters, path = traverse_route_map(
diff --git a/litestar/_asgi/routing_trie/types.py b/litestar/_asgi/routing_trie/types.py
index a07e3d3567..ae367ec40e 100644
--- a/litestar/_asgi/routing_trie/types.py
+++ b/litestar/_asgi/routing_trie/types.py
@@ -57,8 +57,6 @@ class RouteTrieNode:
"""Designate the node as having an `asgi` type handler."""
is_mount: bool
"""Designate the node as being a mount route."""
- is_static: bool
- """Designate the node as being a static mount route."""
path_parameters: dict[Method | Literal["websocket"] | Literal["asgi"], tuple[PathParameterDefinition, ...]]
"""A list of tuples containing path parameter definitions.
@@ -82,7 +80,6 @@ def create_node() -> RouteTrieNode:
is_path_param_node=False,
is_asgi=False,
is_mount=False,
- is_static=False,
is_path_type=False,
path_parameters={},
path_template="",
diff --git a/litestar/_asgi/utils.py b/litestar/_asgi/utils.py
index 88dd7690e6..d9b6deb42a 100644
--- a/litestar/_asgi/utils.py
+++ b/litestar/_asgi/utils.py
@@ -21,7 +21,7 @@ def wrap_in_exception_handler(app: ASGIApp) -> ASGIApp:
"""
from litestar.middleware._internal.exceptions import ExceptionHandlerMiddleware
- return ExceptionHandlerMiddleware(app=app, debug=None)
+ return ExceptionHandlerMiddleware(app=app)
def get_route_handlers(route: BaseRoute) -> list[RouteHandlerType]:
diff --git a/litestar/_kwargs/parameter_definition.py b/litestar/_kwargs/parameter_definition.py
index 02b09fcebd..0be8dcf973 100644
--- a/litestar/_kwargs/parameter_definition.py
+++ b/litestar/_kwargs/parameter_definition.py
@@ -37,7 +37,6 @@ def create_parameter_definition(
Returns:
A ParameterDefinition tuple.
"""
- default = field_definition.default if field_definition.has_default else None
kwarg_definition = (
field_definition.kwarg_definition if isinstance(field_definition.kwarg_definition, ParameterKwarg) else None
)
@@ -59,11 +58,8 @@ def create_parameter_definition(
param_type=param_type,
field_name=field_name,
field_alias=field_alias,
- default=default,
- is_required=field_definition.is_required
- and default is None
- and not field_definition.is_optional
- and not field_definition.is_any,
+ default=field_definition.default,
+ is_required=field_definition.is_required and not field_definition.is_optional and not field_definition.is_any,
is_sequence=field_definition.is_non_string_sequence,
)
diff --git a/litestar/_openapi/path_item.py b/litestar/_openapi/path_item.py
index 0081eead24..987c89335e 100644
--- a/litestar/_openapi/path_item.py
+++ b/litestar/_openapi/path_item.py
@@ -35,9 +35,7 @@ def create_path_item(self) -> PathItem:
Returns:
A PathItem instance.
"""
- for http_method, handler_tuple in self.route.route_handler_map.items():
- route_handler, _ = handler_tuple
-
+ for http_method, route_handler in self.route.route_handler_map.items():
if not route_handler.resolve_include_in_schema():
continue
diff --git a/litestar/_openapi/plugin.py b/litestar/_openapi/plugin.py
index 78349b7e3f..2727dc91c7 100644
--- a/litestar/_openapi/plugin.py
+++ b/litestar/_openapi/plugin.py
@@ -185,10 +185,7 @@ def _handler(request: Request) -> bytes:
def on_app_init(self, app_config: AppConfig) -> AppConfig:
if app_config.openapi_config:
self._openapi_config = app_config.openapi_config
- if (controller := app_config.openapi_config.openapi_controller) is not None:
- app_config.route_handlers.append(controller)
- else:
- app_config.route_handlers.append(self.create_openapi_router())
+ app_config.route_handlers.append(self.create_openapi_router())
return app_config
@property
@@ -201,7 +198,7 @@ def receive_route(self, route: BaseRoute) -> None:
if not isinstance(route, HTTPRoute):
return
- if any(route_handler.resolve_include_in_schema() for route_handler, _ in route.route_handler_map.values()):
+ if any(route_handler.resolve_include_in_schema() for route_handler in route.route_handler_map.values()):
# Force recompute the schema if a new route is added
self._openapi = None
self.included_routes[route.path] = route
diff --git a/litestar/app.py b/litestar/app.py
index 399c01dc03..e66a1c579b 100644
--- a/litestar/app.py
+++ b/litestar/app.py
@@ -44,8 +44,6 @@
)
from litestar.plugins.base import CLIPlugin
from litestar.router import Router
-from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute
-from litestar.static_files.base import StaticFiles
from litestar.stores.registry import StoreRegistry
from litestar.types import Empty, TypeDecodersSequence
from litestar.types.internal_types import PathParameterDefinition, TemplateConfigType
@@ -68,13 +66,12 @@
from litestar.openapi.spec import SecurityRequirement
from litestar.openapi.spec.open_api import OpenAPI
from litestar.response import Response
- from litestar.static_files.config import StaticFilesConfig
+ from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute
from litestar.stores.base import Store
from litestar.types import (
AfterExceptionHookHandler,
AfterRequestHookHandler,
AfterResponseHookHandler,
- AnyCallable,
ASGIApp,
BeforeMessageSendHookHandler,
BeforeRequestHookHandler,
@@ -124,7 +121,7 @@ class HandlerIndex(TypedDict):
identifier: str
"""Unique identifier of the handler.
- Either equal to :attr`__name__ ` attribute or ``__str__`` value of the handler.
+ Either equal to ``__name__`` attribute or ``__str__`` value of the handler.
"""
@@ -140,7 +137,6 @@ class Litestar(Router):
"_lifespan_managers",
"_openapi_schema",
"_server_lifespan_managers",
- "_static_files_config",
"after_exception",
"allowed_hosts",
"asgi_handler",
@@ -212,7 +208,6 @@ def __init__(
signature_namespace: Mapping[str, Any] | None = None,
signature_types: Sequence[Any] | None = None,
state: State | None = None,
- static_files_config: Sequence[StaticFilesConfig] | None = None,
stores: StoreRegistry | dict[str, Store] | None = None,
tags: Sequence[str] | None = None,
template_config: TemplateConfigType | None = None,
@@ -306,7 +301,6 @@ def __init__(
signature_types: A sequence of types for use in forward reference resolution during signature modeling.
These types will be added to the signature namespace using their ``__name__`` attribute.
state: An optional :class:`State <.datastructures.State>` for application state.
- static_files_config: A sequence of :class:`StaticFilesConfig <.static_files.StaticFilesConfig>`
stores: Central registry of :class:`Store <.stores.base.Store>` that will be available throughout the
application. If this is a dictionary to it will be passed to a
:class:`StoreRegistry <.stores.registry.StoreRegistry>`. If it is a
@@ -375,7 +369,6 @@ def __init__(
signature_namespace=dict(signature_namespace or {}),
signature_types=list(signature_types or []),
state=state or State(),
- static_files_config=list(static_files_config or []),
stores=stores,
tags=list(tags or []),
template_config=template_config,
@@ -435,7 +428,6 @@ def __init__(
self.request_class: type[Request] = config.request_class or Request
self.response_cache_config = config.response_cache_config
self.state = config.state
- self._static_files_config = config.static_files_config
self.template_engine = config.template_config.engine_instance if config.template_config else None
self.websocket_class: type[WebSocket] = config.websocket_class or WebSocket
self.debug = config.debug
@@ -494,9 +486,6 @@ def __init__(
self.get_logger = self.logging_config.configure()
self.logger = self.get_logger("litestar")
- for static_config in self._static_files_config:
- self.register(static_config.to_static_files_app())
-
self.asgi_handler = self._create_asgi_handler()
@staticmethod
@@ -516,11 +505,6 @@ def _patch_opentelemetry_middleware(config: AppConfig) -> AppConfig:
pass
return config
- @property
- @deprecated(version="2.6.0", kind="property", info="Use create_static_files router instead")
- def static_files_config(self) -> list[StaticFilesConfig]:
- return self._static_files_config
-
@property
@deprecated(version="2.0", alternative="Litestar.plugins.cli", kind="property")
def cli_plugins(self) -> list[CLIPluginProtocol]:
@@ -689,14 +673,7 @@ def register(self, value: ControllerRouterHandler) -> None: # type: ignore[over
route_handlers = get_route_handlers(route)
for route_handler in route_handlers:
- route_handler.on_registration(self)
-
- if isinstance(route, HTTPRoute):
- route.create_handler_map()
-
- elif isinstance(route, WebSocketRoute):
- handler = route.route_handler
- route.handler_parameter_model = handler.create_kwargs_model(path_parameters=route.path_parameters)
+ route_handler.on_registration(self, route=route)
for plugin in self.plugins.receive_route:
plugin.receive_route(route)
@@ -804,49 +781,6 @@ def get_membership_details(group_id: int, user_id: int) -> None:
return join_paths(output)
- @deprecated(
- "2.6.0", info="Use create_static_files router instead of StaticFilesConfig, which works with route_reverse"
- )
- def url_for_static_asset(self, name: str, file_path: str) -> str:
- """Receives a static files handler name, an asset file path and returns resolved url path to the asset.
-
- Examples:
- .. code-block:: python
-
- from litestar import Litestar
- from litestar.static_files.config import StaticFilesConfig
-
- app = Litestar(
- static_files_config=[
- StaticFilesConfig(directories=["css"], path="/static/css", name="css")
- ]
- )
-
- path = app.url_for_static_asset("css", "main.css")
-
- # /static/css/main.css
-
- Args:
- name: A static handler unique name.
- file_path: a string containing path to an asset.
-
- Raises:
- NoRouteMatchFoundException: If static files handler with ``name`` does not exist.
-
- Returns:
- A url path to the asset.
- """
-
- handler_index = self.get_handler_index_by_name(name)
- if handler_index is None:
- raise NoRouteMatchFoundException(f"Static handler {name} can not be found")
-
- handler_fn = cast("AnyCallable", handler_index["handler"].fn)
- if not isinstance(handler_fn, StaticFiles):
- raise NoRouteMatchFoundException(f"Handler with name {name} is not a static files handler")
-
- return join_paths([handler_index["paths"][0], file_path])
-
@property
def route_handler_method_view(self) -> dict[str, list[str]]:
"""Map route handlers to paths.
diff --git a/litestar/channels/plugin.py b/litestar/channels/plugin.py
index 59884454d4..5bdac7f9c5 100644
--- a/litestar/channels/plugin.py
+++ b/litestar/channels/plugin.py
@@ -116,11 +116,11 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig:
if self._create_route_handlers:
if self._arbitrary_channels_allowed:
path = self._handler_root_path + "{channel_name:str}"
- route_handlers = [WebsocketRouteHandler(path)(self._ws_handler_func)]
+ route_handlers = [WebsocketRouteHandler(path, fn=self._ws_handler_func)]
else:
route_handlers = [
- WebsocketRouteHandler(self._handler_root_path + channel_name)(
- self._create_ws_handler_func(channel_name)
+ WebsocketRouteHandler(
+ self._handler_root_path + channel_name, fn=self._create_ws_handler_func(channel_name)
)
for channel_name in self._channels
]
diff --git a/litestar/cli/_utils.py b/litestar/cli/_utils.py
index c6542c1b32..0107866b04 100644
--- a/litestar/cli/_utils.py
+++ b/litestar/cli/_utils.py
@@ -375,11 +375,7 @@ def show_app_info(app: Litestar) -> None: # pragma: no cover
openapi_enabled = _format_is_enabled(app.openapi_config)
if app.openapi_config:
- path = (
- app.openapi_config.openapi_controller.path
- if app.openapi_config.openapi_controller
- else app.openapi_config.path or "/schema"
- )
+ path = app.openapi_config.get_path()
openapi_enabled += f" path=[yellow]{path}"
table.add_row("OpenAPI", openapi_enabled)
@@ -388,15 +384,6 @@ def show_app_info(app: Litestar) -> None: # pragma: no cover
if app.template_engine:
table.add_row("Template engine", type(app.template_engine).__name__)
- if app.static_files_config:
- static_files_configs = app.static_files_config
- static_files_info = [
- f"path=[yellow]{static_files.path}[/] dirs=[yellow]{', '.join(map(str, static_files.directories))}[/] "
- f"html_mode={_format_is_enabled(static_files.html_mode)}"
- for static_files in static_files_configs
- ]
- table.add_row("Static files", "\n".join(static_files_info))
-
middlewares = []
for middleware in app.middleware:
updated_middleware = middleware.middleware if isinstance(middleware, DefineMiddleware) else middleware
@@ -549,11 +536,7 @@ def remove_routes_with_patterns(
def remove_default_schema_routes(
routes: list[HTTPRoute | ASGIRoute | WebSocketRoute], openapi_config: OpenAPIConfig
) -> list[HTTPRoute | ASGIRoute | WebSocketRoute]:
- schema_path = (
- (openapi_config.path or "/schema")
- if openapi_config.openapi_controller is None
- else openapi_config.openapi_controller.path
- )
+ schema_path = openapi_config.path if openapi_config.openapi_router is None else openapi_config.openapi_router.path
return remove_routes_with_patterns(routes, (schema_path,))
diff --git a/litestar/config/app.py b/litestar/config/app.py
index 170598a422..f542fa5661 100644
--- a/litestar/config/app.py
+++ b/litestar/config/app.py
@@ -27,7 +27,6 @@
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.spec import SecurityRequirement
from litestar.plugins import PluginProtocol
- from litestar.static_files.config import StaticFilesConfig
from litestar.stores.base import Store
from litestar.stores.registry import StoreRegistry
from litestar.types import (
@@ -196,8 +195,6 @@ class AppConfig:
"""
state: State = field(default_factory=State)
"""A :class:`State` <.datastructures.State>` instance holding application state."""
- static_files_config: list[StaticFilesConfig] = field(default_factory=list)
- """An instance or list of :class:`StaticFilesConfig <.static_files.StaticFilesConfig>`."""
stores: StoreRegistry | dict[str, Store] | None = None
"""Central registry of :class:`Store <.stores.base.Store>` to be made available and be used throughout the
application. Can be either a dictionary mapping strings to :class:`Store <.stores.base.Store>` instances, or an
diff --git a/litestar/connection/base.py b/litestar/connection/base.py
index 6c80e96522..28830229c9 100644
--- a/litestar/connection/base.py
+++ b/litestar/connection/base.py
@@ -325,21 +325,3 @@ def url_for(self, name: str, **path_parameters: Any) -> str:
url_path = litestar_instance.route_reverse(name, **path_parameters)
return make_absolute_url(url_path, self.base_url)
-
- def url_for_static_asset(self, name: str, file_path: str) -> str:
- """Receives a static files handler name, an asset file path and returns resolved absolute url to the asset.
-
- Args:
- name: A static handler unique name.
- file_path: a string containing path to an asset.
-
- Raises:
- NoRouteMatchFoundException: If static files handler with ``name`` does not exist.
-
- Returns:
- A string representing absolute url to the asset.
- """
- litestar_instance = self.scope["app"]
- url_path = litestar_instance.url_for_static_asset(name, file_path)
-
- return make_absolute_url(url_path, self.base_url)
diff --git a/litestar/constants.py b/litestar/constants.py
index 2ba12f3694..e4a331654e 100644
--- a/litestar/constants.py
+++ b/litestar/constants.py
@@ -2,14 +2,13 @@
from dataclasses import MISSING
from inspect import Signature
-from typing import Any, Final
+from typing import Final
from uuid import uuid4
from msgspec import UnsetType
from litestar.enums import MediaType
from litestar.types import Empty
-from litestar.utils.deprecation import warn_deprecation
DEFAULT_ALLOWED_CORS_HEADERS: Final = {"Accept", "Accept-Language", "Content-Language", "Content-Type"}
DEFAULT_CHUNK_SIZE: Final = 1024 * 128 # 128KB
@@ -26,35 +25,3 @@
UNDEFINED_SENTINELS: Final = {Signature.empty, Empty, Ellipsis, MISSING, UnsetType}
WEBSOCKET_CLOSE: Final = "websocket.close"
WEBSOCKET_DISCONNECT: Final = "websocket.disconnect"
-
-# deprecated constants
-_SCOPE_STATE_CSRF_TOKEN_KEY = "csrf_token" # noqa: S105 # possible hardcoded password
-_SCOPE_STATE_DEPENDENCY_CACHE: Final = "dependency_cache"
-_SCOPE_STATE_NAMESPACE: Final = "__litestar__"
-_SCOPE_STATE_RESPONSE_COMPRESSED: Final = "response_compressed"
-_SCOPE_STATE_DO_CACHE: Final = "do_cache"
-_SCOPE_STATE_IS_CACHED: Final = "is_cached"
-
-_deprecated_names = {
- "SCOPE_STATE_CSRF_TOKEN_KEY": _SCOPE_STATE_CSRF_TOKEN_KEY,
- "SCOPE_STATE_DEPENDENCY_CACHE": _SCOPE_STATE_DEPENDENCY_CACHE,
- "SCOPE_STATE_NAMESPACE": _SCOPE_STATE_NAMESPACE,
- "SCOPE_STATE_RESPONSE_COMPRESSED": _SCOPE_STATE_RESPONSE_COMPRESSED,
- "SCOPE_STATE_DO_CACHE": _SCOPE_STATE_DO_CACHE,
- "SCOPE_STATE_IS_CACHED": _SCOPE_STATE_IS_CACHED,
-}
-
-
-def __getattr__(name: str) -> Any:
- if name in _deprecated_names:
- warn_deprecation(
- deprecated_name=f"litestar.constants.{name}",
- version="2.4",
- kind="import",
- removal_in="3.0",
- info=f"'{name}' from 'litestar.constants' is deprecated and will be removed in 3.0. "
- "Direct access to Litestar scope state is not recommended.",
- )
-
- return globals()["_deprecated_names"][name]
- raise AttributeError(f"module {__name__} has no attribute {name}") # pragma: no cover
diff --git a/litestar/contrib/jinja.py b/litestar/contrib/jinja.py
index 30d0b9fdf1..c0ceacb3aa 100644
--- a/litestar/contrib/jinja.py
+++ b/litestar/contrib/jinja.py
@@ -10,7 +10,6 @@
TemplateEngineProtocol,
csrf_token,
url_for,
- url_for_static_asset,
)
try:
@@ -52,7 +51,6 @@ def __init__(
self.engine = Environment(loader=loader, autoescape=True)
elif engine_instance:
self.engine = engine_instance
- self.register_template_callable(key="url_for_static_asset", template_callable=url_for_static_asset)
self.register_template_callable(key="csrf_token", template_callable=csrf_token)
self.register_template_callable(key="url_for", template_callable=url_for)
diff --git a/litestar/contrib/mako.py b/litestar/contrib/mako.py
index 76b954d97d..44cc8633c2 100644
--- a/litestar/contrib/mako.py
+++ b/litestar/contrib/mako.py
@@ -12,7 +12,6 @@
TemplateProtocol,
csrf_token,
url_for,
- url_for_static_asset,
)
try:
@@ -82,7 +81,6 @@ def __init__(self, directory: Path | list[Path] | None = None, engine_instance:
self.engine = engine_instance
self._template_callables: list[tuple[str, TemplateCallableType]] = []
- self.register_template_callable(key="url_for_static_asset", template_callable=url_for_static_asset)
self.register_template_callable(key="csrf_token", template_callable=csrf_token)
self.register_template_callable(key="url_for", template_callable=url_for)
diff --git a/litestar/contrib/minijinja.py b/litestar/contrib/minijinja.py
index 6b32b15034..9d556ef75b 100644
--- a/litestar/contrib/minijinja.py
+++ b/litestar/contrib/minijinja.py
@@ -13,7 +13,6 @@
TemplateProtocol,
csrf_token,
url_for,
- url_for_static_asset,
)
from litestar.utils.deprecation import warn_deprecation
@@ -141,7 +140,6 @@ def _loader(name: str) -> str:
self.register_template_callable("url_for", _transform_state(url_for))
self.register_template_callable("csrf_token", _transform_state(csrf_token))
- self.register_template_callable("url_for_static_asset", _transform_state(url_for_static_asset))
def get_template(self, template_name: str) -> MiniJinjaTemplate:
"""Retrieve a template by matching its name (dotted path) with files in the directory or directories provided.
diff --git a/litestar/controller.py b/litestar/controller.py
index a49a297c5e..331e1037ad 100644
--- a/litestar/controller.py
+++ b/litestar/controller.py
@@ -218,7 +218,7 @@ def as_router(self) -> Router:
router = Router(
path=self.path,
- route_handlers=self.get_route_handlers(),
+ route_handlers=self.get_route_handlers(), # type: ignore[arg-type]
after_request=self.after_request,
after_response=self.after_response,
before_request=self.before_request,
@@ -268,7 +268,7 @@ def get_route_handlers(self) -> list[BaseRouteHandler]:
route_handler = deepcopy(self_handler)
# at the point we get a reference to the handler function, it's unbound, so
# we replace it with a regular bound method here
- route_handler._fn = types.MethodType(route_handler._fn, self)
+ route_handler.fn = types.MethodType(route_handler.fn, self)
route_handler.owner = self
route_handlers.append(route_handler)
diff --git a/litestar/enums.py b/litestar/enums.py
index a8d9c2daf9..3f0a83fc8f 100644
--- a/litestar/enums.py
+++ b/litestar/enums.py
@@ -28,7 +28,7 @@ class MediaType(str, Enum):
"""An Enum for ``Content-Type`` header values."""
JSON = "application/json"
- MESSAGEPACK = "application/x-msgpack"
+ MESSAGEPACK = "application/vnd.msgpack"
HTML = "text/html"
TEXT = "text/plain"
CSS = "text/css"
@@ -46,7 +46,7 @@ class RequestEncodingType(str, Enum):
"""An Enum for request ``Content-Type`` header values designating encoding formats."""
JSON = "application/json"
- MESSAGEPACK = "application/x-msgpack"
+ MESSAGEPACK = "application/vnd.msgpack"
MULTI_PART = "multipart/form-data"
URL_ENCODED = "application/x-www-form-urlencoded"
diff --git a/litestar/handlers/asgi_handlers.py b/litestar/handlers/asgi_handlers.py
index 91f35172d3..03f0507308 100644
--- a/litestar/handlers/asgi_handlers.py
+++ b/litestar/handlers/asgi_handlers.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Mapping, Sequence
+from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence
from litestar.exceptions import ImproperlyConfiguredException
from litestar.handlers.base import BaseRouteHandler
@@ -11,59 +11,56 @@
if TYPE_CHECKING:
+ from litestar.connection import ASGIConnection
from litestar.types import (
+ AsyncAnyCallable,
ExceptionHandlersMap,
Guard,
- MaybePartial, # noqa: F401
)
class ASGIRouteHandler(BaseRouteHandler):
- """ASGI Route Handler decorator.
-
- Use this decorator to decorate ASGI applications.
- """
-
- __slots__ = ("is_mount", "is_static")
+ __slots__ = ("is_mount",)
def __init__(
self,
path: str | Sequence[str] | None = None,
*,
+ fn: AsyncAnyCallable,
exception_handlers: ExceptionHandlersMap | None = None,
guards: Sequence[Guard] | None = None,
name: str | None = None,
opt: Mapping[str, Any] | None = None,
is_mount: bool = False,
- is_static: bool = False,
signature_namespace: Mapping[str, Any] | None = None,
**kwargs: Any,
) -> None:
- """Initialize ``ASGIRouteHandler``.
+ """Route handler for ASGI routes.
Args:
+ path: A path fragment for the route handler function or a list of path fragments. If not given defaults to
+ ``/``.
+ fn: The handler function.
+
+ .. versionadded:: 3.0
exception_handlers: A mapping of status codes and/or exception types to handler functions.
guards: A sequence of :class:`Guard <.types.Guard>` callables.
name: A string identifying the route handler.
opt: A string key mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
wherever you have access to :class:`Request <.connection.Request>` or
:class:`ASGI Scope <.types.Scope>`.
- path: A path fragment for the route handler function or a list of path fragments. If not given defaults to
- ``/``
is_mount: A boolean dictating whether the handler's paths should be regarded as mount paths. Mount path
accept any arbitrary paths that begin with the defined prefixed path. For example, a mount with the path
``/some-path/`` will accept requests for ``/some-path/`` and any sub path under this, e.g.
``/some-path/sub-path/`` etc.
- is_static: A boolean dictating whether the handler's paths should be regarded as static paths. Static paths
- are used to deliver static files.
signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
type_encoders: A mapping of types to callables that transform them into types supported for serialization.
**kwargs: Any additional kwarg - will be set in the opt dictionary.
"""
- self.is_mount = is_mount or is_static
- self.is_static = is_static
+ self.is_mount = is_mount
super().__init__(
path,
+ fn=fn,
exception_handlers=exception_handlers,
guards=guards,
name=name,
@@ -86,5 +83,68 @@ def _validate_handler_function(self) -> None:
if not is_async_callable(self.fn):
raise ImproperlyConfiguredException("Functions decorated with 'asgi' must be async functions")
+ async def handle(self, connection: ASGIConnection[ASGIRouteHandler, Any, Any, Any]) -> None:
+ """ASGI app that authorizes the connection and then awaits the handler function.
+
+ .. versionadded: 3.0
+
+ Args:
+ connection: The ASGI connection
+
+ Returns:
+ None
+ """
+
+ if self.resolve_guards():
+ await self.authorize_connection(connection=connection)
+
+ await self.fn(scope=connection.scope, receive=connection.receive, send=connection.send)
+
+
+def asgi(
+ path: str | Sequence[str] | None = None,
+ *,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ name: str | None = None,
+ opt: Mapping[str, Any] | None = None,
+ is_mount: bool = False,
+ signature_namespace: Mapping[str, Any] | None = None,
+ handler_class: type[ASGIRouteHandler] = ASGIRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AsyncAnyCallable], ASGIRouteHandler]:
+ """Create an :class:`ASGIRouteHandler`.
+
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults
+ to ``/``
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or
+ :class:`ASGI Scope <.types.Scope>`.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature
+ modelling.
+ is_mount: A boolean dictating whether the handler's paths should be regarded as mount paths. Mount path
+ accept any arbitrary paths that begin with the defined prefixed path. For example, a mount with the path
+ ``/some-path/`` will accept requests for ``/some-path/`` and any sub path under this, e.g.
+ ``/some-path/sub-path/`` etc.
+ handler_class: Route handler class instantiated by the decorator
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
+ """
+
+ def decorator(fn: AsyncAnyCallable) -> ASGIRouteHandler:
+ return handler_class(
+ fn=fn,
+ path=path,
+ exception_handlers=exception_handlers,
+ guards=guards,
+ name=name,
+ opt=opt,
+ is_mount=is_mount,
+ signature_namespace=signature_namespace,
+ **kwargs,
+ )
-asgi = ASGIRouteHandler
+ return decorator
diff --git a/litestar/handlers/base.py b/litestar/handlers/base.py
index 6f07d393a5..039bf602a6 100644
--- a/litestar/handlers/base.py
+++ b/litestar/handlers/base.py
@@ -2,7 +2,7 @@
from copy import copy
from functools import partial
-from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence, cast
+from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, Sequence, cast
from litestar._signature import SignatureModel
from litestar.di import Provide
@@ -34,9 +34,9 @@
from litestar.dto import AbstractDTO
from litestar.params import ParameterKwarg
from litestar.router import Router
+ from litestar.routes import BaseRoute
from litestar.types import AnyCallable, AsyncAnyCallable, ExceptionHandler
from litestar.types.empty import EmptyType
- from litestar.types.internal_types import PathParameterDefinition
__all__ = ("BaseRouteHandler",)
@@ -48,7 +48,6 @@ class BaseRouteHandler:
"""
__slots__ = (
- "_fn",
"_parsed_data_field",
"_parsed_fn_signature",
"_parsed_return_field",
@@ -64,6 +63,7 @@ class BaseRouteHandler:
"dependencies",
"dto",
"exception_handlers",
+ "fn",
"guards",
"middleware",
"name",
@@ -80,6 +80,7 @@ def __init__(
self,
path: str | Sequence[str] | None = None,
*,
+ fn: AsyncAnyCallable,
dependencies: Dependencies | None = None,
dto: type[AbstractDTO] | None | EmptyType = Empty,
exception_handlers: ExceptionHandlersMap | None = None,
@@ -99,6 +100,9 @@ def __init__(
Args:
path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults
to ``/``
+ fn: The handler function
+
+ .. versionadded:: 3.0
dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
validation of request data.
@@ -150,11 +154,10 @@ def __init__(
self.paths = (
{normalize_path(p) for p in path} if path and isinstance(path, list) else {normalize_path(path or "/")} # type: ignore[arg-type]
)
+ self.fn = self._prepare_fn(fn)
- def __call__(self, fn: AsyncAnyCallable) -> Self:
- """Replace a function with itself."""
- self._fn = fn
- return self
+ def _prepare_fn(self, fn: AsyncAnyCallable) -> AsyncAnyCallable:
+ return fn
@property
def handler_id(self) -> str:
@@ -199,20 +202,6 @@ def signature_model(self) -> type[SignatureModel]:
)
return self._signature_model
- @property
- def fn(self) -> AsyncAnyCallable:
- """Get the handler function.
-
- Raises:
- ImproperlyConfiguredException: if handler fn is not set.
-
- Returns:
- Handler function
- """
- if not hasattr(self, "_fn"):
- raise ImproperlyConfiguredException("No callable has been registered for this handler")
- return self._fn
-
@property
def parsed_fn_signature(self) -> ParsedSignature:
"""Return the parsed signature of the handler function.
@@ -433,14 +422,14 @@ def resolve_signature_namespace(self) -> dict[str, Any]:
When merging keys from multiple layers, if the same key is defined by multiple layers, the value from the
layer closest to the response handler will take precedence.
"""
- if self._resolved_layered_parameters is Empty:
+ if self._resolved_signature_namespace is Empty:
ns: dict[str, Any] = {}
for layer in self.ownership_layers:
merge_signature_namespaces(
signature_namespace=ns, additional_signature_namespace=layer.signature_namespace
)
self._resolved_signature_namespace = ns
- return cast("dict[str, Any]", self._resolved_signature_namespace)
+ return self._resolved_signature_namespace
def resolve_data_dto(self) -> type[AbstractDTO] | None:
"""Resolve the data_dto by starting from the route handler and moving up.
@@ -526,11 +515,12 @@ def _validate_dependency_is_unique(dependencies: dict[str, Provide], key: str, p
f"If you wish to override a provider, it must have the same key."
)
- def on_registration(self, app: Litestar) -> None:
+ def on_registration(self, app: Litestar, route: BaseRoute) -> None:
"""Called once per handler when the app object is instantiated.
Args:
app: The :class:`Litestar<.app.Litestar>` app object.
+ route: The route this handler is being registered on
Returns:
None
@@ -567,9 +557,9 @@ def __str__(self) -> str:
target = type(target)
return f"{target.__module__}.{target.__qualname__}"
- def create_kwargs_model(
+ def _create_kwargs_model(
self,
- path_parameters: dict[str, PathParameterDefinition],
+ path_parameters: Iterable[str],
) -> KwargsModel:
"""Create a `KwargsModel` for a given route handler."""
from litestar._kwargs import KwargsModel
@@ -578,6 +568,6 @@ def create_kwargs_model(
signature_model=self.signature_model,
parsed_signature=self.parsed_fn_signature,
dependencies=self.resolve_dependencies(),
- path_parameters=set(path_parameters.keys()),
+ path_parameters=set(path_parameters),
layered_parameters=self.resolve_layered_parameters(),
)
diff --git a/litestar/handlers/http_handlers/__init__.py b/litestar/handlers/http_handlers/__init__.py
index 844f046895..3009fda95b 100644
--- a/litestar/handlers/http_handlers/__init__.py
+++ b/litestar/handlers/http_handlers/__init__.py
@@ -1,7 +1,7 @@
from __future__ import annotations
-from .base import HTTPRouteHandler, route
-from .decorators import delete, get, head, patch, post, put
+from .base import HTTPRouteHandler
+from .decorators import delete, get, head, patch, post, put, route
__all__ = (
"HTTPRouteHandler",
diff --git a/litestar/handlers/http_handlers/_options.py b/litestar/handlers/http_handlers/_options.py
new file mode 100644
index 0000000000..fb6a409114
--- /dev/null
+++ b/litestar/handlers/http_handlers/_options.py
@@ -0,0 +1,37 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Iterable
+
+from litestar.enums import HttpMethod, MediaType
+from litestar.handlers import HTTPRouteHandler
+from litestar.response import Response
+from litestar.status_codes import HTTP_204_NO_CONTENT
+
+if TYPE_CHECKING:
+ from litestar.types import Method
+
+
+def create_options_handler(path: str, allow_methods: Iterable[Method]) -> HTTPRouteHandler:
+ """Args:
+ path: The route path
+
+ Returns:
+ An HTTP route handler for OPTIONS requests.
+ """
+
+ def options_handler() -> Response:
+ """Handler function for OPTIONS requests.
+
+ Returns:
+ Response
+ """
+ return Response(
+ content=None,
+ status_code=HTTP_204_NO_CONTENT,
+ headers={"Allow": ", ".join(sorted(allow_methods))}, # pyright: ignore
+ media_type=MediaType.TEXT,
+ )
+
+ return HTTPRouteHandler(
+ path=path, http_method=[HttpMethod.OPTIONS], include_in_schema=False, sync_to_thread=False, fn=options_handler
+ )
diff --git a/litestar/handlers/http_handlers/_utils.py b/litestar/handlers/http_handlers/_utils.py
index d8104b1491..d24b238f3b 100644
--- a/litestar/handlers/http_handlers/_utils.py
+++ b/litestar/handlers/http_handlers/_utils.py
@@ -4,6 +4,7 @@
from inspect import isawaitable
from typing import TYPE_CHECKING, Any, Sequence, cast
+from litestar.datastructures import UploadFile
from litestar.enums import HttpMethod
from litestar.exceptions import ValidationException
from litestar.response import Response
@@ -11,7 +12,6 @@
from litestar.types.builtin_types import NoneType
if TYPE_CHECKING:
- from litestar.app import Litestar
from litestar.background_tasks import BackgroundTask, BackgroundTasks
from litestar.connection import Request
from litestar.datastructures import Cookie, ResponseHeader
@@ -59,7 +59,6 @@ def create_data_handler(
async def handler(
data: Any,
request: Request[Any, Any, Any],
- app: Litestar,
**kwargs: Any,
) -> ASGIApp:
if isawaitable(data):
@@ -76,7 +75,7 @@ async def handler(
if after_request:
response = await after_request(response) # type: ignore[arg-type,misc]
- return response.to_asgi_response(app=None, request=request, headers=normalize_headers(headers), cookies=cookies) # pyright: ignore
+ return response.to_asgi_response(request=request, headers=normalize_headers(headers), cookies=cookies) # pyright: ignore
return handler
@@ -144,13 +143,11 @@ def create_response_handler(
async def handler(
data: Response,
- app: Litestar,
request: Request,
**kwargs: Any, # kwargs is for return dto
) -> ASGIApp:
response = await after_request(data) if after_request else data # type:ignore[arg-type,misc]
return response.to_asgi_response( # type: ignore[no-any-return]
- app=None,
background=background,
cookies=cookie_list,
headers=normalized_headers,
@@ -217,3 +214,9 @@ def is_empty_response_annotation(return_annotation: FieldDefinition) -> bool:
HTTP_METHOD_NAMES = {m.value for m in HttpMethod}
+
+
+async def cleanup_temporary_files(form_data: dict[str, Any]) -> None:
+ for v in form_data.values():
+ if isinstance(v, UploadFile) and not v.file.closed:
+ await v.close()
diff --git a/litestar/handlers/http_handlers/base.py b/litestar/handlers/http_handlers/base.py
index 461c4c9bf1..b9c11461e5 100644
--- a/litestar/handlers/http_handlers/base.py
+++ b/litestar/handlers/http_handlers/base.py
@@ -1,16 +1,21 @@
from __future__ import annotations
from enum import Enum
-from typing import TYPE_CHECKING, AnyStr, Mapping, Sequence, TypedDict, cast
+from typing import TYPE_CHECKING, AnyStr, Iterable, Mapping, Sequence, TypedDict, cast
+
+from msgspec.msgpack import decode as _decode_msgpack_plain
from litestar._layers.utils import narrow_response_cookies, narrow_response_headers
from litestar.connection import Request
+from litestar.datastructures import CacheControlHeader, ETag, FormMultiDict
from litestar.datastructures.cookie import Cookie
from litestar.datastructures.response_header import ResponseHeader
from litestar.enums import HttpMethod, MediaType
from litestar.exceptions import (
+ ClientException,
HTTPException,
ImproperlyConfiguredException,
+ SerializationException,
)
from litestar.handlers.base import BaseRouteHandler
from litestar.handlers.http_handlers._utils import (
@@ -22,7 +27,8 @@
normalize_http_method,
)
from litestar.openapi.spec import Operation
-from litestar.response import Response
+from litestar.response import File, Response
+from litestar.response.file import ASGIFileResponse
from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_304_NOT_MODIFIED
from litestar.types import (
AfterRequestHookHandler,
@@ -38,28 +44,35 @@
Guard,
Method,
Middleware,
+ Receive,
ResponseCookies,
ResponseHeaders,
+ Scope,
+ Send,
TypeEncodersMap,
)
+from litestar.types.builtin_types import NoneType
from litestar.utils import ensure_async_callable
-from litestar.utils.predicates import is_async_callable
+from litestar.utils.predicates import is_async_callable, is_class_and_subclass
+from litestar.utils.scope.state import ScopeState
from litestar.utils.warnings import warn_implicit_sync_to_thread, warn_sync_to_thread_with_async_callable
if TYPE_CHECKING:
from typing import Any, Awaitable, Callable
+ from litestar._kwargs import KwargsModel
+ from litestar._kwargs.cleanup import DependencyCleanupGroup
from litestar.app import Litestar
from litestar.background_tasks import BackgroundTask, BackgroundTasks
from litestar.config.response_cache import CACHE_FOREVER
- from litestar.datastructures import CacheControlHeader, ETag
from litestar.dto import AbstractDTO
from litestar.openapi.datastructures import ResponseSpec
from litestar.openapi.spec import SecurityRequirement
+ from litestar.routes import BaseRoute
from litestar.types.callable_types import AsyncAnyCallable, OperationIDCreator
from litestar.types.composite_types import TypeDecodersSequence
-__all__ = ("HTTPRouteHandler", "route")
+__all__ = ("HTTPRouteHandler",)
class ResponseHandlerMap(TypedDict):
@@ -68,12 +81,8 @@ class ResponseHandlerMap(TypedDict):
class HTTPRouteHandler(BaseRouteHandler):
- """HTTP Route Decorator.
-
- Use this decorator to decorate an HTTP handler with multiple methods.
- """
-
__slots__ = (
+ "_kwargs_models",
"_resolved_after_response",
"_resolved_before_request",
"_resolved_include_in_schema",
@@ -117,12 +126,11 @@ class HTTPRouteHandler(BaseRouteHandler):
"template_name",
)
- has_sync_callable: bool
-
def __init__(
self,
path: str | Sequence[str] | None = None,
*,
+ fn: AnyCallable,
after_request: AfterRequestHookHandler | None = None,
after_response: AfterResponseHookHandler | None = None,
background: BackgroundTask | BackgroundTasks | None = None,
@@ -167,11 +175,12 @@ def __init__(
type_encoders: TypeEncodersMap | None = None,
**kwargs: Any,
) -> None:
- """Initialize ``HTTPRouteHandler``.
+ """Route handler for HTTP routes.
Args:
path: A path fragment for the route handler function or a sequence of path fragments.
If not given defaults to ``/``
+ fn: The handler function
after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
to any route handler. If this function returns a value, the request will not reach the route handler,
and instead this value will be used.
@@ -248,7 +257,18 @@ def __init__(
self.http_methods = normalize_http_method(http_methods=http_method)
self.status_code = status_code or get_default_status_code(http_methods=self.http_methods)
+ if has_sync_callable := not is_async_callable(fn):
+ if sync_to_thread is None:
+ warn_implicit_sync_to_thread(fn, stacklevel=3)
+ elif sync_to_thread is not None:
+ warn_sync_to_thread_with_async_callable(fn, stacklevel=3)
+
+ if has_sync_callable and sync_to_thread:
+ fn = ensure_async_callable(fn)
+ has_sync_callable = False
+
super().__init__(
+ fn=fn,
path=path,
dependencies=dependencies,
dto=dto,
@@ -279,7 +299,7 @@ def __init__(
self.response_headers: Sequence[ResponseHeader] | None = narrow_response_headers(response_headers)
self.request_max_body_size = request_max_body_size
- self.sync_to_thread = sync_to_thread
+ self.has_sync_callable = has_sync_callable
# OpenAPI related attributes
self.content_encoding = content_encoding
self.content_media_type = content_media_type
@@ -303,19 +323,9 @@ def __init__(
self._resolved_request_class: type[Request] | EmptyType = Empty
self._resolved_security: list[SecurityRequirement] | EmptyType = Empty
self._resolved_tags: list[str] | EmptyType = Empty
+ self._kwargs_models: dict[tuple[str, ...], KwargsModel] = {}
self._resolved_request_max_body_size: int | EmptyType | None = Empty
- def __call__(self, fn: AnyCallable) -> HTTPRouteHandler:
- """Replace a function with itself."""
- if not is_async_callable(fn):
- if self.sync_to_thread is None:
- warn_implicit_sync_to_thread(fn, stacklevel=3)
- elif self.sync_to_thread is not None:
- warn_sync_to_thread_with_async_callable(fn, stacklevel=3)
-
- super().__call__(fn)
- return self
-
def resolve_request_class(self) -> type[Request]:
"""Return the closest custom Request class in the owner graph or the default Request class.
@@ -499,7 +509,7 @@ def resolve_request_max_body_size(self) -> int | None:
)
return max_body_size
- def get_response_handler(self, is_response_type_data: bool = False) -> Callable[[Any], Awaitable[ASGIApp]]:
+ def get_response_handler(self, is_response_type_data: bool = False) -> Callable[..., Awaitable[ASGIApp]]:
"""Resolve the response_handler function for the route handler.
This method is memoized so the computation occurs only once.
@@ -565,11 +575,10 @@ def get_response_handler(self, is_response_type_data: bool = False) -> Callable[
else self._response_handler_mapping["default_handler"],
)
- async def to_response(self, app: Litestar, data: Any, request: Request) -> ASGIApp:
+ async def to_response(self, data: Any, request: Request) -> ASGIApp:
"""Return a :class:`Response <.response.Response>` from the handler by resolving and calling it.
Args:
- app: The :class:`Litestar ` app instance
data: Either an instance of a :class:`Response <.response.Response>`,
a Response instance or an arbitrary value.
request: A :class:`Request <.connection.Request>` instance
@@ -581,17 +590,20 @@ async def to_response(self, app: Litestar, data: Any, request: Request) -> ASGIA
data = return_dto_type(request).data_to_encodable_type(data)
response_handler = self.get_response_handler(is_response_type_data=isinstance(data, Response))
- return await response_handler(app=app, data=data, request=request) # type: ignore[call-arg]
+ return await response_handler(data=data, request=request)
- def on_registration(self, app: Litestar) -> None:
- super().on_registration(app)
+ def on_registration(self, app: Litestar, route: BaseRoute) -> None:
+ super().on_registration(app, route=route)
self.resolve_after_response()
self.resolve_include_in_schema()
- self.has_sync_callable = not is_async_callable(self.fn)
- if self.has_sync_callable and self.sync_to_thread:
- self._fn = ensure_async_callable(self.fn)
- self.has_sync_callable = False
+ self._get_kwargs_model_for_route(route.path_parameters)
+
+ def _get_kwargs_model_for_route(self, path_parameters: Iterable[str]) -> KwargsModel:
+ key = tuple(path_parameters)
+ if (model := self._kwargs_models.get(key)) is None:
+ model = self._kwargs_models[key] = self._create_kwargs_model(path_parameters)
+ return model
def _validate_handler_function(self) -> None:
"""Validate the route handler function once it is set by inspecting its return annotations."""
@@ -625,6 +637,19 @@ def _validate_handler_function(self) -> None:
if "data" in self.parsed_fn_signature.parameters and "GET" in self.http_methods:
raise ImproperlyConfiguredException("'data' kwarg is unsupported for 'GET' request handlers")
+ if self.http_methods == {HttpMethod.HEAD} and not self.parsed_fn_signature.return_type.is_subclass_of(
+ (NoneType, File, ASGIFileResponse)
+ ):
+ field_definition = self.parsed_fn_signature.return_type
+ if not (
+ is_empty_response_annotation(field_definition)
+ or is_class_and_subclass(field_definition.annotation, File)
+ or is_class_and_subclass(field_definition.annotation, ASGIFileResponse)
+ ):
+ raise ImproperlyConfiguredException(
+ f"{self}: Handlers for 'HEAD' requests must not return a value. Either return 'None' or a response type without a body."
+ )
+
if (body_param := self.parsed_fn_signature.parameters.get("body")) and not body_param.is_subclass_of(bytes):
raise ImproperlyConfiguredException(
f"Invalid type annotation for 'body' parameter in route handler {self}. 'body' will always receive the "
@@ -632,5 +657,137 @@ def _validate_handler_function(self) -> None:
"processed request data, use the 'data' parameter."
)
+ async def handle(self, connection: Request[Any, Any, Any]) -> None:
+ """ASGI app that creates a :class:`~.connection.Request` from the passed in args, determines which handler function to call and then
+ handles the call.
+
+ .. versionadded: 3.0
+
+ Args:
+ connection: The request
+
+ Returns:
+ None
+ """
+
+ if self.resolve_guards():
+ await self.authorize_connection(connection=connection)
+
+ try:
+ response = await self._get_response_for_request(request=connection)
+
+ await response(connection.scope, connection.receive, connection.send)
+
+ if after_response_handler := self.resolve_after_response():
+ await after_response_handler(connection)
+ finally:
+ if (form_data := ScopeState.from_scope(connection.scope).form) is not Empty:
+ await FormMultiDict.from_form_data(form_data).close()
+
+ async def _get_response_for_request(
+ self,
+ request: Request[Any, Any, Any],
+ ) -> ASGIApp:
+ """Return a response for the request.
+
+ If caching is enabled and a response exist in the cache, the cached response will be returned.
+ If caching is enabled and a response does not exist in the cache, the newly created
+ response will be cached.
+
+ Args:
+ request: The Request instance
+
+ Returns:
+ An instance of Response or a compatible ASGIApp or a subclass of it
+ """
+ if self.cache and (response := await self._get_cached_response(request=request)):
+ return response
+
+ return await self._call_handler_function(request=request)
+
+ async def _call_handler_function(self, request: Request) -> ASGIApp:
+ """Call the before request handlers, retrieve any data required for the route handler, and call the route
+ handler's ``to_response`` method.
+
+ This is wrapped in a try except block - and if an exception is raised,
+ it tries to pass it to an appropriate exception handler - if defined.
+ """
+ response_data: Any = None
+ cleanup_group: DependencyCleanupGroup | None = None
+
+ if before_request_handler := self.resolve_before_request():
+ response_data = await before_request_handler(request)
+
+ if not response_data:
+ response_data, cleanup_group = await self._get_response_data(request=request)
+
+ response: ASGIApp = await self.to_response(data=response_data, request=request)
+
+ if cleanup_group:
+ await cleanup_group.cleanup()
+
+ return response
+
+ async def _get_response_data(self, request: Request) -> tuple[Any, DependencyCleanupGroup | None]:
+ """Determine what kwargs are required for the given route handler's ``fn`` and calls it."""
+ parsed_kwargs: dict[str, Any] = {}
+ cleanup_group: DependencyCleanupGroup | None = None
+ parameter_model = self._get_kwargs_model_for_route(request.scope["path_params"].keys())
+
+ if parameter_model.has_kwargs and self.signature_model:
+ try:
+ kwargs = await parameter_model.to_kwargs(connection=request)
+ except SerializationException as e:
+ raise ClientException(str(e)) from e
+
+ if "data" in kwargs:
+ data = kwargs["data"]
+
+ if data is Empty:
+ del kwargs["data"]
+
+ if parameter_model.dependency_batches:
+ cleanup_group = await parameter_model.resolve_dependencies(request, kwargs)
+
+ parsed_kwargs = self.signature_model.parse_values_from_connection_kwargs(
+ connection=request,
+ kwargs=kwargs,
+ )
+
+ if cleanup_group:
+ async with cleanup_group:
+ data = self.fn(**parsed_kwargs) if self.has_sync_callable else await self.fn(**parsed_kwargs)
+ elif self.has_sync_callable:
+ data = self.fn(**parsed_kwargs)
+ else:
+ data = await self.fn(**parsed_kwargs)
+
+ return data, cleanup_group
+
+ async def _get_cached_response(self, request: Request) -> ASGIApp | None:
+ """Retrieve and un-pickle the cached response, if existing.
+
+ Args:
+ request: The :class:`Request ` instance
+
+ Returns:
+ A cached response instance, if existing.
+ """
+
+ cache_config = request.app.response_cache_config
+ cache_key = (self.cache_key_builder or cache_config.key_builder)(request)
+ store = cache_config.get_store_from_app(request.app)
+
+ if not (cached_response_data := await store.get(key=cache_key)):
+ return None
+
+ # we use the regular msgspec.msgpack.decode here since we don't need any of
+ # the added decoders
+ messages = _decode_msgpack_plain(cached_response_data)
+
+ async def cached_response(scope: Scope, receive: Receive, send: Send) -> None:
+ ScopeState.from_scope(scope).is_cached = True
+ for message in messages:
+ await send(message)
-route = HTTPRouteHandler
+ return cached_response
diff --git a/litestar/handlers/http_handlers/decorators.py b/litestar/handlers/http_handlers/decorators.py
index 69df2c95a9..748531e801 100644
--- a/litestar/handlers/http_handlers/decorators.py
+++ b/litestar/handlers/http_handlers/decorators.py
@@ -1,17 +1,30 @@
from __future__ import annotations
-import warnings
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence
from litestar.enums import HttpMethod, MediaType
-from litestar.exceptions import HTTPException, ImproperlyConfiguredException
-from litestar.openapi.spec import Operation
-from litestar.response.file import ASGIFileResponse, File
-from litestar.types import Empty, TypeDecodersSequence
-from litestar.utils import is_class_and_subclass
-
-from ._utils import is_empty_response_annotation
-from .base import HTTPRouteHandler
+from litestar.exceptions import HTTPException
+from litestar.handlers.http_handlers.base import HTTPRouteHandler
+from litestar.openapi.spec import Operation, SecurityRequirement
+from litestar.types import (
+ AfterRequestHookHandler,
+ AfterResponseHookHandler,
+ AnyCallable,
+ BeforeRequestHookHandler,
+ CacheKeyBuilder,
+ Dependencies,
+ Empty,
+ EmptyType,
+ ExceptionHandlersMap,
+ Guard,
+ Method,
+ Middleware,
+ OperationIDCreator,
+ ResponseCookies,
+ ResponseHeaders,
+ TypeDecodersSequence,
+ TypeEncodersMap,
+)
if TYPE_CHECKING:
from typing import Any, Mapping, Sequence
@@ -21,6 +34,7 @@
from litestar.connection import Request
from litestar.datastructures import CacheControlHeader, ETag
from litestar.dto import AbstractDTO
+ from litestar.exceptions import HTTPException
from litestar.openapi.datastructures import ResponseSpec
from litestar.openapi.spec import SecurityRequirement
from litestar.response import Response
@@ -38,150 +52,137 @@
ResponseHeaders,
TypeEncodersMap,
)
- from litestar.types.callable_types import OperationIDCreator
-
+ from litestar.types.callable_types import AnyCallable, OperationIDCreator
__all__ = ("delete", "get", "head", "patch", "post", "put")
-MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP = "semantic route handlers cannot define http_method"
-
-
-def _subclass_warning() -> None:
- warnings.warn(
- "Semantic HTTP route handler classes are deprecated and will be replaced by "
- "functional decorators in Litestar 3.0.",
- category=DeprecationWarning,
- stacklevel=2,
- )
+def route(
+ path: str | None | Sequence[str] = None,
+ *,
+ http_method: HttpMethod | Method | Sequence[HttpMethod | Method],
+ after_request: AfterRequestHookHandler | None = None,
+ after_response: AfterResponseHookHandler | None = None,
+ background: BackgroundTask | BackgroundTasks | None = None,
+ before_request: BeforeRequestHookHandler | None = None,
+ cache: bool | int | type[CACHE_FOREVER] = False,
+ cache_control: CacheControlHeader | None = None,
+ cache_key_builder: CacheKeyBuilder | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ etag: ETag | None = None,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ media_type: MediaType | str | None = None,
+ middleware: Sequence[Middleware] | None = None,
+ name: str | None = None,
+ opt: Mapping[str, Any] | None = None,
+ request_class: type[Request] | None = None,
+ response_class: type[Response] | None = None,
+ response_cookies: ResponseCookies | None = None,
+ response_headers: ResponseHeaders | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ status_code: int | None = None,
+ sync_to_thread: bool | None = None,
+ # OpenAPI related attributes
+ content_encoding: str | None = None,
+ content_media_type: str | None = None,
+ deprecated: bool = False,
+ description: str | None = None,
+ include_in_schema: bool | EmptyType = Empty,
+ operation_class: type[Operation] = Operation,
+ operation_id: str | OperationIDCreator | None = None,
+ raises: Sequence[type[HTTPException]] | None = None,
+ response_description: str | None = None,
+ responses: Mapping[int, ResponseSpec] | None = None,
+ security: Sequence[SecurityRequirement] | None = None,
+ summary: str | None = None,
+ tags: Sequence[str] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ handler_class: type[HTTPRouteHandler] = HTTPRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], HTTPRouteHandler]:
+ """Create an :class:`HTTPRouteHandler`.
-class delete(HTTPRouteHandler):
- """DELETE Route Decorator.
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments.
+ If not given defaults to ``/``
+ after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
+ to any route handler. If this function returns a value, the request will not reach the route handler,
+ and instead this value will be used.
+ after_response: A sync or async function called after the response has been awaited. It receives the
+ :class:`Request <.connection.Request>` object and should not return any values.
+ background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
+ :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
+ Defaults to ``None``.
+ before_request: A sync or async function called immediately before calling the route handler. Receives
+ the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
+ bypassing the route handler.
+ cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
+ of seconds (e.g. ``120``) to cache the response.
+ cache_control: A ``cache-control`` header of type
+ :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
+ cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
+ of the cache key if caching is configured on the application level.
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
+ validation of request data.
+ etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ http_method: An :class:`http method string <.types.Method>`, a member of the enum
+ :class:`HttpMethod ` or a list of these that correlates to the methods the
+ route handler function should handle.
+ media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
+ valid IANA Media-Type.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
+ request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
+ default request.
+ response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
+ default response.
+ response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
+ response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
+ instances.
+ responses: A mapping of additional status codes and a description of their expected content.
+ This information will be included in the OpenAPI schema
+ return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
+ outbound response data.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
+ status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
+ ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
+ sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
+ main event loop. This has an effect only for sync handler functions. See using sync handler functions.
+ content_encoding: A string describing the encoding of the content, e.g. ``base64``.
+ content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
+ deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
+ description: Text used for the route's schema description section.
+ include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
+ operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
+ operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
+ raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
+ This list should describe all exceptions raised within the route handler's function/method. The Litestar
+ ValidationException will be added automatically for the schema if any validation is involved.
+ response_description: Text used for the route's response schema description section.
+ security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
+ summary: Text used for the route's schema summary section.
+ tags: A sequence of string tags that will be appended to the OpenAPI schema.
+ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
+ hook for deserialization.
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
+ handler_class: Route handler class instantiated by the decorator
- Use this decorator to decorate an HTTP handler for DELETE requests.
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
"""
- def __init__(
- self,
- path: str | None | Sequence[str] = None,
- *,
- after_request: AfterRequestHookHandler | None = None,
- after_response: AfterResponseHookHandler | None = None,
- background: BackgroundTask | BackgroundTasks | None = None,
- before_request: BeforeRequestHookHandler | None = None,
- cache: bool | int | type[CACHE_FOREVER] = False,
- cache_control: CacheControlHeader | None = None,
- cache_key_builder: CacheKeyBuilder | None = None,
- dependencies: Dependencies | None = None,
- dto: type[AbstractDTO] | None | EmptyType = Empty,
- etag: ETag | None = None,
- exception_handlers: ExceptionHandlersMap | None = None,
- guards: Sequence[Guard] | None = None,
- media_type: MediaType | str | None = None,
- middleware: Sequence[Middleware] | None = None,
- name: str | None = None,
- opt: Mapping[str, Any] | None = None,
- request_class: type[Request] | None = None,
- response_class: type[Response] | None = None,
- response_cookies: ResponseCookies | None = None,
- response_headers: ResponseHeaders | None = None,
- return_dto: type[AbstractDTO] | None | EmptyType = Empty,
- signature_namespace: Mapping[str, Any] | None = None,
- status_code: int | None = None,
- sync_to_thread: bool | None = None,
- # OpenAPI related attributes
- content_encoding: str | None = None,
- content_media_type: str | None = None,
- deprecated: bool = False,
- description: str | None = None,
- include_in_schema: bool | EmptyType = Empty,
- operation_class: type[Operation] = Operation,
- operation_id: str | OperationIDCreator | None = None,
- raises: Sequence[type[HTTPException]] | None = None,
- response_description: str | None = None,
- responses: Mapping[int, ResponseSpec] | None = None,
- security: Sequence[SecurityRequirement] | None = None,
- summary: str | None = None,
- tags: Sequence[str] | None = None,
- type_decoders: TypeDecodersSequence | None = None,
- type_encoders: TypeEncodersMap | None = None,
- **kwargs: Any,
- ) -> None:
- """Initialize ``delete``
-
- Args:
- path: A path fragment for the route handler function or a sequence of path fragments.
- If not given defaults to ``/``
- after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
- to any route handler. If this function returns a value, the request will not reach the route handler,
- and instead this value will be used.
- after_response: A sync or async function called after the response has been awaited. It receives the
- :class:`Request <.connection.Request>` object and should not return any values.
- background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
- :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
- Defaults to ``None``.
- before_request: A sync or async function called immediately before calling the route handler. Receives
- the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
- bypassing the route handler.
- cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
- of seconds (e.g. ``120``) to cache the response.
- cache_control: A ``cache-control`` header of type
- :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
- cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
- of the cache key if caching is configured on the application level.
- dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
- validation of request data.
- dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
- etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
- exception_handlers: A mapping of status codes and/or exception types to handler functions.
- guards: A sequence of :class:`Guard <.types.Guard>` callables.
- http_method: An :class:`http method string <.types.Method>`, a member of the enum
- :class:`HttpMethod ` or a list of these that correlates to the methods the
- route handler function should handle.
- media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
- valid IANA Media-Type.
- middleware: A sequence of :class:`Middleware <.types.Middleware>`.
- name: A string identifying the route handler.
- opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
- wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
- request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
- default request.
- response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
- default response.
- response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
- response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
- instances.
- responses: A mapping of additional status codes and a description of their expected content.
- This information will be included in the OpenAPI schema
- return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
- outbound response data.
- signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
- status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT``
- and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
- sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
- main event loop. This has an effect only for sync handler functions. See using sync handler functions.
- content_encoding: A string describing the encoding of the content, e.g. ``base64``.
- content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
- deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
- description: Text used for the route's schema description section.
- include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
- operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
- operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
- raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
- This list should describe all exceptions raised within the route handler's function/method. The Litestar
- ValidationException will be added automatically for the schema if any validation is involved.
- response_description: Text used for the route's response schema description section.
- security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
- summary: Text used for the route's schema summary section.
- tags: A sequence of string tags that will be appended to the OpenAPI schema.
- type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
- hook for deserialization.
- type_encoders: A mapping of types to callables that transform them into types supported for serialization.
- **kwargs: Any additional kwarg - will be set in the opt dictionary.
- """
- if "http_method" in kwargs:
- raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP)
- super().__init__(
+ def decorator(fn: AnyCallable) -> HTTPRouteHandler:
+ return handler_class(
+ fn=fn,
+ http_method=http_method,
after_request=after_request,
after_response=after_response,
background=background,
@@ -198,7 +199,6 @@ def __init__(
etag=etag,
exception_handlers=exception_handlers,
guards=guards,
- http_method=HttpMethod.DELETE,
include_in_schema=include_in_schema,
media_type=media_type,
middleware=middleware,
@@ -226,138 +226,130 @@ def __init__(
**kwargs,
)
- def __init_subclass__(cls, **kwargs: Any) -> None:
- _subclass_warning()
+ return decorator
-class get(HTTPRouteHandler):
- """GET Route Decorator.
+def get(
+ path: str | None | Sequence[str] = None,
+ *,
+ after_request: AfterRequestHookHandler | None = None,
+ after_response: AfterResponseHookHandler | None = None,
+ background: BackgroundTask | BackgroundTasks | None = None,
+ before_request: BeforeRequestHookHandler | None = None,
+ cache: bool | int | type[CACHE_FOREVER] = False,
+ cache_control: CacheControlHeader | None = None,
+ cache_key_builder: CacheKeyBuilder | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ etag: ETag | None = None,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ media_type: MediaType | str | None = None,
+ middleware: Sequence[Middleware] | None = None,
+ name: str | None = None,
+ opt: Mapping[str, Any] | None = None,
+ request_class: type[Request] | None = None,
+ response_class: type[Response] | None = None,
+ response_cookies: ResponseCookies | None = None,
+ response_headers: ResponseHeaders | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ status_code: int | None = None,
+ sync_to_thread: bool | None = None,
+ # OpenAPI related attributes
+ content_encoding: str | None = None,
+ content_media_type: str | None = None,
+ deprecated: bool = False,
+ description: str | None = None,
+ include_in_schema: bool | EmptyType = Empty,
+ operation_class: type[Operation] = Operation,
+ operation_id: str | OperationIDCreator | None = None,
+ raises: Sequence[type[HTTPException]] | None = None,
+ response_description: str | None = None,
+ responses: Mapping[int, ResponseSpec] | None = None,
+ security: Sequence[SecurityRequirement] | None = None,
+ summary: str | None = None,
+ tags: Sequence[str] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ handler_class: type[HTTPRouteHandler] = HTTPRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], HTTPRouteHandler]:
+ """Create an :class:`HTTPRouteHandler` with a ``GET`` method.
- Use this decorator to decorate an HTTP handler for GET requests.
- """
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments.
+ If not given defaults to ``/``
+ after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
+ to any route handler. If this function returns a value, the request will not reach the route handler,
+ and instead this value will be used.
+ after_response: A sync or async function called after the response has been awaited. It receives the
+ :class:`Request <.connection.Request>` object and should not return any values.
+ background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
+ :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
+ Defaults to ``None``.
+ before_request: A sync or async function called immediately before calling the route handler. Receives
+ the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
+ bypassing the route handler.
+ cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
+ of seconds (e.g. ``120``) to cache the response.
+ cache_control: A ``cache-control`` header of type
+ :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
+ cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
+ of the cache key if caching is configured on the application level.
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
+ validation of request data.
+ etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
+ valid IANA Media-Type.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
+ request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
+ default request.
+ response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
+ default response.
+ response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
+ response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
+ instances.
+ responses: A mapping of additional status codes and a description of their expected content.
+ This information will be included in the OpenAPI schema
+ return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
+ outbound response data.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
+ status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
+ ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
+ sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
+ main event loop. This has an effect only for sync handler functions. See using sync handler functions.
+ content_encoding: A string describing the encoding of the content, e.g. ``base64``.
+ content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
+ deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
+ description: Text used for the route's schema description section.
+ include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
+ operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
+ operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
+ raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
+ This list should describe all exceptions raised within the route handler's function/method. The Litestar
+ ValidationException will be added automatically for the schema if any validation is involved.
+ response_description: Text used for the route's response schema description section.
+ security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
+ summary: Text used for the route's schema summary section.
+ tags: A sequence of string tags that will be appended to the OpenAPI schema.
+ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
+ hook for deserialization.
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
+ handler_class: Route handler class instantiated by the decorator
- def __init__(
- self,
- path: str | None | Sequence[str] = None,
- *,
- after_request: AfterRequestHookHandler | None = None,
- after_response: AfterResponseHookHandler | None = None,
- background: BackgroundTask | BackgroundTasks | None = None,
- before_request: BeforeRequestHookHandler | None = None,
- cache: bool | int | type[CACHE_FOREVER] = False,
- cache_control: CacheControlHeader | None = None,
- cache_key_builder: CacheKeyBuilder | None = None,
- dependencies: Dependencies | None = None,
- dto: type[AbstractDTO] | None | EmptyType = Empty,
- etag: ETag | None = None,
- exception_handlers: ExceptionHandlersMap | None = None,
- guards: Sequence[Guard] | None = None,
- media_type: MediaType | str | None = None,
- middleware: Sequence[Middleware] | None = None,
- name: str | None = None,
- opt: Mapping[str, Any] | None = None,
- request_class: type[Request] | None = None,
- response_class: type[Response] | None = None,
- response_cookies: ResponseCookies | None = None,
- response_headers: ResponseHeaders | None = None,
- return_dto: type[AbstractDTO] | None | EmptyType = Empty,
- signature_namespace: Mapping[str, Any] | None = None,
- status_code: int | None = None,
- sync_to_thread: bool | None = None,
- # OpenAPI related attributes
- content_encoding: str | None = None,
- content_media_type: str | None = None,
- deprecated: bool = False,
- description: str | None = None,
- include_in_schema: bool | EmptyType = Empty,
- operation_class: type[Operation] = Operation,
- operation_id: str | OperationIDCreator | None = None,
- raises: Sequence[type[HTTPException]] | None = None,
- response_description: str | None = None,
- responses: Mapping[int, ResponseSpec] | None = None,
- security: Sequence[SecurityRequirement] | None = None,
- summary: str | None = None,
- tags: Sequence[str] | None = None,
- type_decoders: TypeDecodersSequence | None = None,
- type_encoders: TypeEncodersMap | None = None,
- **kwargs: Any,
- ) -> None:
- """Initialize ``get``.
-
- Args:
- path: A path fragment for the route handler function or a sequence of path fragments.
- If not given defaults to ``/``
- after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
- to any route handler. If this function returns a value, the request will not reach the route handler,
- and instead this value will be used.
- after_response: A sync or async function called after the response has been awaited. It receives the
- :class:`Request <.connection.Request>` object and should not return any values.
- background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
- :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
- Defaults to ``None``.
- before_request: A sync or async function called immediately before calling the route handler. Receives
- the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
- bypassing the route handler.
- cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
- of seconds (e.g. ``120``) to cache the response.
- cache_control: A ``cache-control`` header of type
- :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
- cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
- of the cache key if caching is configured on the application level.
- dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
- dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
- validation of request data.
- etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
- exception_handlers: A mapping of status codes and/or exception types to handler functions.
- guards: A sequence of :class:`Guard <.types.Guard>` callables.
- http_method: An :class:`http method string <.types.Method>`, a member of the enum
- :class:`HttpMethod ` or a list of these that correlates to the methods the
- route handler function should handle.
- media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
- valid IANA Media-Type.
- middleware: A sequence of :class:`Middleware <.types.Middleware>`.
- name: A string identifying the route handler.
- opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
- wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
- request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
- default request.
- response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
- default response.
- response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
- response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
- instances.
- responses: A mapping of additional status codes and a description of their expected content.
- This information will be included in the OpenAPI schema
- return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
- outbound response data.
- signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
- status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
- ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
- sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
- main event loop. This has an effect only for sync handler functions. See using sync handler functions.
- content_encoding: A string describing the encoding of the content, e.g. ``base64``.
- content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
- deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
- description: Text used for the route's schema description section.
- include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
- operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
- operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
- raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
- This list should describe all exceptions raised within the route handler's function/method. The Litestar
- ValidationException will be added automatically for the schema if any validation is involved.
- response_description: Text used for the route's response schema description section.
- security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
- summary: Text used for the route's schema summary section.
- tags: A sequence of string tags that will be appended to the OpenAPI schema.
- type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
- hook for deserialization.
- type_encoders: A mapping of types to callables that transform them into types supported for serialization.
- **kwargs: Any additional kwarg - will be set in the opt dictionary.
- """
- if "http_method" in kwargs:
- raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP)
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
+ """
- super().__init__(
+ def decorator(fn: AnyCallable) -> HTTPRouteHandler:
+ return handler_class(
+ fn=fn,
after_request=after_request,
after_response=after_response,
background=background,
@@ -402,142 +394,134 @@ def __init__(
**kwargs,
)
- def __init_subclass__(cls, **kwargs: Any) -> None:
- _subclass_warning()
+ return decorator
-class head(HTTPRouteHandler):
- """HEAD Route Decorator.
+def head(
+ path: str | None | Sequence[str] = None,
+ *,
+ after_request: AfterRequestHookHandler | None = None,
+ after_response: AfterResponseHookHandler | None = None,
+ background: BackgroundTask | BackgroundTasks | None = None,
+ before_request: BeforeRequestHookHandler | None = None,
+ cache: bool | int | type[CACHE_FOREVER] = False,
+ cache_control: CacheControlHeader | None = None,
+ cache_key_builder: CacheKeyBuilder | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ etag: ETag | None = None,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ media_type: MediaType | str | None = None,
+ middleware: Sequence[Middleware] | None = None,
+ name: str | None = None,
+ opt: Mapping[str, Any] | None = None,
+ request_class: type[Request] | None = None,
+ response_class: type[Response] | None = None,
+ response_cookies: ResponseCookies | None = None,
+ response_headers: ResponseHeaders | None = None,
+ signature_namespace: Mapping[str, Any] | None = None,
+ status_code: int | None = None,
+ sync_to_thread: bool | None = None,
+ # OpenAPI related attributes
+ content_encoding: str | None = None,
+ content_media_type: str | None = None,
+ deprecated: bool = False,
+ description: str | None = None,
+ include_in_schema: bool | EmptyType = Empty,
+ operation_class: type[Operation] = Operation,
+ operation_id: str | OperationIDCreator | None = None,
+ raises: Sequence[type[HTTPException]] | None = None,
+ response_description: str | None = None,
+ responses: Mapping[int, ResponseSpec] | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ security: Sequence[SecurityRequirement] | None = None,
+ summary: str | None = None,
+ tags: Sequence[str] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ handler_class: type[HTTPRouteHandler] = HTTPRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], HTTPRouteHandler]:
+ """Create an :class:`HTTPRouteHandler` with a ``HEAD`` method.
- Use this decorator to decorate an HTTP handler for HEAD requests.
- """
-
- def __init__(
- self,
- path: str | None | Sequence[str] = None,
- *,
- after_request: AfterRequestHookHandler | None = None,
- after_response: AfterResponseHookHandler | None = None,
- background: BackgroundTask | BackgroundTasks | None = None,
- before_request: BeforeRequestHookHandler | None = None,
- cache: bool | int | type[CACHE_FOREVER] = False,
- cache_control: CacheControlHeader | None = None,
- cache_key_builder: CacheKeyBuilder | None = None,
- dependencies: Dependencies | None = None,
- dto: type[AbstractDTO] | None | EmptyType = Empty,
- etag: ETag | None = None,
- exception_handlers: ExceptionHandlersMap | None = None,
- guards: Sequence[Guard] | None = None,
- media_type: MediaType | str | None = None,
- middleware: Sequence[Middleware] | None = None,
- name: str | None = None,
- opt: Mapping[str, Any] | None = None,
- request_class: type[Request] | None = None,
- response_class: type[Response] | None = None,
- response_cookies: ResponseCookies | None = None,
- response_headers: ResponseHeaders | None = None,
- signature_namespace: Mapping[str, Any] | None = None,
- status_code: int | None = None,
- sync_to_thread: bool | None = None,
- # OpenAPI related attributes
- content_encoding: str | None = None,
- content_media_type: str | None = None,
- deprecated: bool = False,
- description: str | None = None,
- include_in_schema: bool | EmptyType = Empty,
- operation_class: type[Operation] = Operation,
- operation_id: str | OperationIDCreator | None = None,
- raises: Sequence[type[HTTPException]] | None = None,
- response_description: str | None = None,
- responses: Mapping[int, ResponseSpec] | None = None,
- return_dto: type[AbstractDTO] | None | EmptyType = Empty,
- security: Sequence[SecurityRequirement] | None = None,
- summary: str | None = None,
- tags: Sequence[str] | None = None,
- type_decoders: TypeDecodersSequence | None = None,
- type_encoders: TypeEncodersMap | None = None,
- **kwargs: Any,
- ) -> None:
- """Initialize ``head``.
+ Notes:
+ - A response to a head request cannot include a body.
+ See: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD).
- Notes:
- - A response to a head request cannot include a body.
- See: [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD).
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments.
+ If not given defaults to ``/``
+ after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
+ to any route handler. If this function returns a value, the request will not reach the route handler,
+ and instead this value will be used.
+ after_response: A sync or async function called after the response has been awaited. It receives the
+ :class:`Request <.connection.Request>` object and should not return any values.
+ background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
+ :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
+ Defaults to ``None``.
+ before_request: A sync or async function called immediately before calling the route handler. Receives
+ the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
+ bypassing the route handler.
+ cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
+ of seconds (e.g. ``120``) to cache the response.
+ cache_control: A ``cache-control`` header of type
+ :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
+ cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
+ of the cache key if caching is configured on the application level.
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
+ validation of request data.
+ etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
+ valid IANA Media-Type.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
+ request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
+ default request.
+ response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
+ default response.
+ response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
+ response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
+ instances.
+ responses: A mapping of additional status codes and a description of their expected content.
+ This information will be included in the OpenAPI schema
+ return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
+ outbound response data.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
+ status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
+ ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
+ sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
+ main event loop. This has an effect only for sync handler functions. See using sync handler functions.
+ content_encoding: A string describing the encoding of the content, e.g. ``base64``.
+ content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
+ deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
+ description: Text used for the route's schema description section.
+ include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
+ operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
+ operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
+ raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
+ This list should describe all exceptions raised within the route handler's function/method. The Litestar
+ ValidationException will be added automatically for the schema if any validation is involved.
+ response_description: Text used for the route's response schema description section.
+ security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
+ summary: Text used for the route's schema summary section.
+ tags: A sequence of string tags that will be appended to the OpenAPI schema.
+ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
+ hook for deserialization.
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
+ handler_class: Route handler class instantiated by the decorator
- Args:
- path: A path fragment for the route handler function or a sequence of path fragments.
- If not given defaults to ``/``
- after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
- to any route handler. If this function returns a value, the request will not reach the route handler,
- and instead this value will be used.
- after_response: A sync or async function called after the response has been awaited. It receives the
- :class:`Request <.connection.Request>` object and should not return any values.
- background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
- :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
- Defaults to ``None``.
- before_request: A sync or async function called immediately before calling the route handler. Receives
- the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
- bypassing the route handler.
- cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
- of seconds (e.g. ``120``) to cache the response.
- cache_control: A ``cache-control`` header of type
- :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
- cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
- of the cache key if caching is configured on the application level.
- dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
- dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
- validation of request data.
- etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
- exception_handlers: A mapping of status codes and/or exception types to handler functions.
- guards: A sequence of :class:`Guard <.types.Guard>` callables.
- http_method: An :class:`http method string <.types.Method>`, a member of the enum
- :class:`HttpMethod ` or a list of these that correlates to the methods the
- route handler function should handle.
- media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
- valid IANA Media-Type.
- middleware: A sequence of :class:`Middleware <.types.Middleware>`.
- name: A string identifying the route handler.
- opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
- wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
- request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
- default request.
- response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
- default response.
- response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
- response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
- instances.
- responses: A mapping of additional status codes and a description of their expected content.
- This information will be included in the OpenAPI schema
- return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
- outbound response data.
- signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
- status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
- ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
- sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
- main event loop. This has an effect only for sync handler functions. See using sync handler functions.
- content_encoding: A string describing the encoding of the content, e.g. ``base64``.
- content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
- deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
- description: Text used for the route's schema description section.
- include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
- operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
- operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
- raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
- This list should describe all exceptions raised within the route handler's function/method. The Litestar
- ValidationException will be added automatically for the schema if any validation is involved.
- response_description: Text used for the route's response schema description section.
- security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
- summary: Text used for the route's schema summary section.
- tags: A sequence of string tags that will be appended to the OpenAPI schema.
- type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
- hook for deserialization.
- type_encoders: A mapping of types to callables that transform them into types supported for serialization.
- **kwargs: Any additional kwarg - will be set in the opt dictionary.
- """
- if "http_method" in kwargs:
- raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP)
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
+ """
- super().__init__(
+ def decorator(fn: AnyCallable) -> HTTPRouteHandler:
+ return handler_class(
+ fn=fn,
after_request=after_request,
after_response=after_response,
background=background,
@@ -582,155 +566,133 @@ def __init__(
**kwargs,
)
- def __init_subclass__(cls, **kwargs: Any) -> None:
- _subclass_warning()
+ return decorator
- def _validate_handler_function(self) -> None:
- """Validate the route handler function once it is set by inspecting its return annotations."""
- super()._validate_handler_function()
- # we allow here File and File because these have special setting for head responses
- field_definition = self.parsed_fn_signature.return_type
- if not (
- is_empty_response_annotation(field_definition)
- or is_class_and_subclass(field_definition.annotation, File)
- or is_class_and_subclass(field_definition.annotation, ASGIFileResponse)
- ):
- raise ImproperlyConfiguredException(
- f"{self}: Handlers for 'HEAD' requests must not return a value. Either return 'None' or a response type without a body."
- )
+def patch(
+ path: str | None | Sequence[str] = None,
+ *,
+ after_request: AfterRequestHookHandler | None = None,
+ after_response: AfterResponseHookHandler | None = None,
+ background: BackgroundTask | BackgroundTasks | None = None,
+ before_request: BeforeRequestHookHandler | None = None,
+ cache: bool | int | type[CACHE_FOREVER] = False,
+ cache_control: CacheControlHeader | None = None,
+ cache_key_builder: CacheKeyBuilder | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ etag: ETag | None = None,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ media_type: MediaType | str | None = None,
+ middleware: Sequence[Middleware] | None = None,
+ name: str | None = None,
+ opt: Mapping[str, Any] | None = None,
+ request_class: type[Request] | None = None,
+ request_max_body_size: int | None | EmptyType = Empty,
+ response_class: type[Response] | None = None,
+ response_cookies: ResponseCookies | None = None,
+ response_headers: ResponseHeaders | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ status_code: int | None = None,
+ sync_to_thread: bool | None = None,
+ # OpenAPI related attributes
+ content_encoding: str | None = None,
+ content_media_type: str | None = None,
+ deprecated: bool = False,
+ description: str | None = None,
+ include_in_schema: bool | EmptyType = Empty,
+ operation_class: type[Operation] = Operation,
+ operation_id: str | OperationIDCreator | None = None,
+ raises: Sequence[type[HTTPException]] | None = None,
+ response_description: str | None = None,
+ responses: Mapping[int, ResponseSpec] | None = None,
+ security: Sequence[SecurityRequirement] | None = None,
+ summary: str | None = None,
+ tags: Sequence[str] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ handler_class: type[HTTPRouteHandler] = HTTPRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], HTTPRouteHandler]:
+ """Create an :class:`HTTPRouteHandler` with a ``PATCH`` method.
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments.
+ If not given defaults to ``/``
+ after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
+ to any route handler. If this function returns a value, the request will not reach the route handler,
+ and instead this value will be used.
+ after_response: A sync or async function called after the response has been awaited. It receives the
+ :class:`Request <.connection.Request>` object and should not return any values.
+ background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
+ :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
+ Defaults to ``None``.
+ before_request: A sync or async function called immediately before calling the route handler. Receives
+ the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
+ bypassing the route handler.
+ cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
+ of seconds (e.g. ``120``) to cache the response.
+ cache_control: A ``cache-control`` header of type
+ :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
+ cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
+ of the cache key if caching is configured on the application level.
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
+ validation of request data.
+ etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
+ valid IANA Media-Type.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
+ request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
+ default request.
+ request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded,
+ a '413 - Request Entity Too Large' error response is returned.
+ response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
+ default response.
+ response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
+ response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
+ instances.
+ responses: A mapping of additional status codes and a description of their expected content.
+ This information will be included in the OpenAPI schema
+ return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
+ outbound response data.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
+ status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
+ ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
+ sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
+ main event loop. This has an effect only for sync handler functions. See using sync handler functions.
+ content_encoding: A string describing the encoding of the content, e.g. ``base64``.
+ content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
+ deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
+ description: Text used for the route's schema description section.
+ include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
+ operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
+ operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
+ raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
+ This list should describe all exceptions raised within the route handler's function/method. The Litestar
+ ValidationException will be added automatically for the schema if any validation is involved.
+ response_description: Text used for the route's response schema description section.
+ security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
+ summary: Text used for the route's schema summary section.
+ tags: A sequence of string tags that will be appended to the OpenAPI schema.
+ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
+ hook for deserialization.
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
+ handler_class: Route handler class instantiated by the decorator
-class patch(HTTPRouteHandler):
- """PATCH Route Decorator.
-
- Use this decorator to decorate an HTTP handler for PATCH requests.
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
"""
- def __init__(
- self,
- path: str | None | Sequence[str] = None,
- *,
- after_request: AfterRequestHookHandler | None = None,
- after_response: AfterResponseHookHandler | None = None,
- background: BackgroundTask | BackgroundTasks | None = None,
- before_request: BeforeRequestHookHandler | None = None,
- cache: bool | int | type[CACHE_FOREVER] = False,
- cache_control: CacheControlHeader | None = None,
- cache_key_builder: CacheKeyBuilder | None = None,
- dependencies: Dependencies | None = None,
- dto: type[AbstractDTO] | None | EmptyType = Empty,
- etag: ETag | None = None,
- exception_handlers: ExceptionHandlersMap | None = None,
- guards: Sequence[Guard] | None = None,
- media_type: MediaType | str | None = None,
- middleware: Sequence[Middleware] | None = None,
- name: str | None = None,
- opt: Mapping[str, Any] | None = None,
- request_class: type[Request] | None = None,
- request_max_body_size: int | None | EmptyType = Empty,
- response_class: type[Response] | None = None,
- response_cookies: ResponseCookies | None = None,
- response_headers: ResponseHeaders | None = None,
- return_dto: type[AbstractDTO] | None | EmptyType = Empty,
- signature_namespace: Mapping[str, Any] | None = None,
- status_code: int | None = None,
- sync_to_thread: bool | None = None,
- # OpenAPI related attributes
- content_encoding: str | None = None,
- content_media_type: str | None = None,
- deprecated: bool = False,
- description: str | None = None,
- include_in_schema: bool | EmptyType = Empty,
- operation_class: type[Operation] = Operation,
- operation_id: str | OperationIDCreator | None = None,
- raises: Sequence[type[HTTPException]] | None = None,
- response_description: str | None = None,
- responses: Mapping[int, ResponseSpec] | None = None,
- security: Sequence[SecurityRequirement] | None = None,
- summary: str | None = None,
- tags: Sequence[str] | None = None,
- type_decoders: TypeDecodersSequence | None = None,
- type_encoders: TypeEncodersMap | None = None,
- **kwargs: Any,
- ) -> None:
- """Initialize ``patch``.
-
- Args:
- path: A path fragment for the route handler function or a sequence of path fragments.
- If not given defaults to ``/``
- after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
- to any route handler. If this function returns a value, the request will not reach the route handler,
- and instead this value will be used.
- after_response: A sync or async function called after the response has been awaited. It receives the
- :class:`Request <.connection.Request>` object and should not return any values.
- background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
- :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
- Defaults to ``None``.
- before_request: A sync or async function called immediately before calling the route handler. Receives
- the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
- bypassing the route handler.
- cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
- of seconds (e.g. ``120``) to cache the response.
- cache_control: A ``cache-control`` header of type
- :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
- cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
- of the cache key if caching is configured on the application level.
- dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
- dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
- validation of request data.
- etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
- exception_handlers: A mapping of status codes and/or exception types to handler functions.
- guards: A sequence of :class:`Guard <.types.Guard>` callables.
- http_method: An :class:`http method string <.types.Method>`, a member of the enum
- :class:`HttpMethod ` or a list of these that correlates to the methods the
- route handler function should handle.
- media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
- valid IANA Media-Type.
- middleware: A sequence of :class:`Middleware <.types.Middleware>`.
- name: A string identifying the route handler.
- opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
- wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
- request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
- default request.
- request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded,
- a '413 - Request Entity Too Large' error response is returned.
- response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
- default response.
- response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
- response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
- instances.
- responses: A mapping of additional status codes and a description of their expected content.
- This information will be included in the OpenAPI schema
- return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
- outbound response data.
- signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
- status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
- ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
- sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
- main event loop. This has an effect only for sync handler functions. See using sync handler functions.
- content_encoding: A string describing the encoding of the content, e.g. ``base64``.
- content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
- deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
- description: Text used for the route's schema description section.
- include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
- operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
- operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
- raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
- This list should describe all exceptions raised within the route handler's function/method. The Litestar
- ValidationException will be added automatically for the schema if any validation is involved.
- response_description: Text used for the route's response schema description section.
- security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
- summary: Text used for the route's schema summary section.
- tags: A sequence of string tags that will be appended to the OpenAPI schema.
- type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
- hook for deserialization.
- type_encoders: A mapping of types to callables that transform them into types supported for serialization.
- **kwargs: Any additional kwarg - will be set in the opt dictionary.
- """
- if "http_method" in kwargs:
- raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP)
- super().__init__(
+ def decorator(fn: AnyCallable) -> HTTPRouteHandler:
+ return handler_class(
+ fn=fn,
after_request=after_request,
after_response=after_response,
background=background,
@@ -776,140 +738,133 @@ def __init__(
**kwargs,
)
- def __init_subclass__(cls, **kwargs: Any) -> None:
- _subclass_warning()
+ return decorator
-class post(HTTPRouteHandler):
- """POST Route Decorator.
+def post(
+ path: str | None | Sequence[str] = None,
+ *,
+ after_request: AfterRequestHookHandler | None = None,
+ after_response: AfterResponseHookHandler | None = None,
+ background: BackgroundTask | BackgroundTasks | None = None,
+ before_request: BeforeRequestHookHandler | None = None,
+ cache: bool | int | type[CACHE_FOREVER] = False,
+ cache_control: CacheControlHeader | None = None,
+ cache_key_builder: CacheKeyBuilder | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ etag: ETag | None = None,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ media_type: MediaType | str | None = None,
+ middleware: Sequence[Middleware] | None = None,
+ name: str | None = None,
+ opt: Mapping[str, Any] | None = None,
+ request_class: type[Request] | None = None,
+ request_max_body_size: int | None | EmptyType = Empty,
+ response_class: type[Response] | None = None,
+ response_cookies: ResponseCookies | None = None,
+ response_headers: ResponseHeaders | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ status_code: int | None = None,
+ sync_to_thread: bool | None = None,
+ # OpenAPI related attributes
+ content_encoding: str | None = None,
+ content_media_type: str | None = None,
+ deprecated: bool = False,
+ description: str | None = None,
+ include_in_schema: bool | EmptyType = Empty,
+ operation_class: type[Operation] = Operation,
+ operation_id: str | OperationIDCreator | None = None,
+ raises: Sequence[type[HTTPException]] | None = None,
+ response_description: str | None = None,
+ responses: Mapping[int, ResponseSpec] | None = None,
+ security: Sequence[SecurityRequirement] | None = None,
+ summary: str | None = None,
+ tags: Sequence[str] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ handler_class: type[HTTPRouteHandler] = HTTPRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], HTTPRouteHandler]:
+ """Create an :class:`HTTPRouteHandler` with a ``POST`` method.
- Use this decorator to decorate an HTTP handler for POST requests.
- """
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments.
+ If not given defaults to ``/``
+ after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
+ to any route handler. If this function returns a value, the request will not reach the route handler,
+ and instead this value will be used.
+ after_response: A sync or async function called after the response has been awaited. It receives the
+ :class:`Request <.connection.Request>` object and should not return any values.
+ background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
+ :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
+ Defaults to ``None``.
+ before_request: A sync or async function called immediately before calling the route handler. Receives
+ the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
+ bypassing the route handler.
+ cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
+ of seconds (e.g. ``120``) to cache the response.
+ cache_control: A ``cache-control`` header of type
+ :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
+ cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
+ of the cache key if caching is configured on the application level.
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
+ validation of request data.
+ etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
+ valid IANA Media-Type.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
+ request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
+ default request.
+ request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded,
+ a '413 - Request Entity Too Large' error response is returned.
+ response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
+ default response.
+ response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
+ response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
+ instances.
+ responses: A mapping of additional status codes and a description of their expected content.
+ This information will be included in the OpenAPI schema
+ return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
+ outbound response data.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
+ status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
+ ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
+ sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
+ main event loop. This has an effect only for sync handler functions. See using sync handler functions.
+ content_encoding: A string describing the encoding of the content, e.g. ``base64``.
+ content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
+ deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
+ description: Text used for the route's schema description section.
+ include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
+ operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
+ operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
+ raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
+ This list should describe all exceptions raised within the route handler's function/method. The Litestar
+ ValidationException will be added automatically for the schema if any validation is involved.
+ response_description: Text used for the route's response schema description section.
+ security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
+ summary: Text used for the route's schema summary section.
+ tags: A sequence of string tags that will be appended to the OpenAPI schema.
+ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
+ hook for deserialization.
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
+ handler_class: Route handler class instantiated by the decorator
- def __init__(
- self,
- path: str | None | Sequence[str] = None,
- *,
- after_request: AfterRequestHookHandler | None = None,
- after_response: AfterResponseHookHandler | None = None,
- background: BackgroundTask | BackgroundTasks | None = None,
- before_request: BeforeRequestHookHandler | None = None,
- cache: bool | int | type[CACHE_FOREVER] = False,
- cache_control: CacheControlHeader | None = None,
- cache_key_builder: CacheKeyBuilder | None = None,
- dependencies: Dependencies | None = None,
- dto: type[AbstractDTO] | None | EmptyType = Empty,
- etag: ETag | None = None,
- exception_handlers: ExceptionHandlersMap | None = None,
- guards: Sequence[Guard] | None = None,
- media_type: MediaType | str | None = None,
- middleware: Sequence[Middleware] | None = None,
- name: str | None = None,
- opt: Mapping[str, Any] | None = None,
- request_class: type[Request] | None = None,
- request_max_body_size: int | None | EmptyType = Empty,
- response_class: type[Response] | None = None,
- response_cookies: ResponseCookies | None = None,
- response_headers: ResponseHeaders | None = None,
- return_dto: type[AbstractDTO] | None | EmptyType = Empty,
- signature_namespace: Mapping[str, Any] | None = None,
- status_code: int | None = None,
- sync_to_thread: bool | None = None,
- # OpenAPI related attributes
- content_encoding: str | None = None,
- content_media_type: str | None = None,
- deprecated: bool = False,
- description: str | None = None,
- include_in_schema: bool | EmptyType = Empty,
- operation_class: type[Operation] = Operation,
- operation_id: str | OperationIDCreator | None = None,
- raises: Sequence[type[HTTPException]] | None = None,
- response_description: str | None = None,
- responses: Mapping[int, ResponseSpec] | None = None,
- security: Sequence[SecurityRequirement] | None = None,
- summary: str | None = None,
- tags: Sequence[str] | None = None,
- type_decoders: TypeDecodersSequence | None = None,
- type_encoders: TypeEncodersMap | None = None,
- **kwargs: Any,
- ) -> None:
- """Initialize ``post``
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
+ """
- Args:
- path: A path fragment for the route handler function or a sequence of path fragments.
- If not given defaults to ``/``
- after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
- to any route handler. If this function returns a value, the request will not reach the route handler,
- and instead this value will be used.
- after_response: A sync or async function called after the response has been awaited. It receives the
- :class:`Request <.connection.Request>` object and should not return any values.
- background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
- :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
- Defaults to ``None``.
- before_request: A sync or async function called immediately before calling the route handler. Receives
- the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
- bypassing the route handler.
- cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
- of seconds (e.g. ``120``) to cache the response.
- cache_control: A ``cache-control`` header of type
- :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
- cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
- of the cache key if caching is configured on the application level.
- dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
- dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
- validation of request data.
- etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
- exception_handlers: A mapping of status codes and/or exception types to handler functions.
- guards: A sequence of :class:`Guard <.types.Guard>` callables.
- http_method: An :class:`http method string <.types.Method>`, a member of the enum
- :class:`HttpMethod ` or a list of these that correlates to the methods the
- route handler function should handle.
- media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
- valid IANA Media-Type.
- middleware: A sequence of :class:`Middleware <.types.Middleware>`.
- name: A string identifying the route handler.
- opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
- wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
- request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
- default request.
- request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded,
- a '413 - Request Entity Too Large' error response is returned.
- response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
- default response.
- response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
- response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
- instances.
- responses: A mapping of additional status codes and a description of their expected content.
- This information will be included in the OpenAPI schema
- return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
- outbound response data.
- signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
- status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
- ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
- sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
- main event loop. This has an effect only for sync handler functions. See using sync handler functions.
- content_encoding: A string describing the encoding of the content, e.g. ``base64``.
- content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
- deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
- description: Text used for the route's schema description section.
- include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
- operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
- operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
- raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
- This list should describe all exceptions raised within the route handler's function/method. The Litestar
- ValidationException will be added automatically for the schema if any validation is involved.
- response_description: Text used for the route's response schema description section.
- security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
- summary: Text used for the route's schema summary section.
- tags: A sequence of string tags that will be appended to the OpenAPI schema.
- type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
- hook for deserialization.
- type_encoders: A mapping of types to callables that transform them into types supported for serialization.
- **kwargs: Any additional kwarg - will be set in the opt dictionary.
- """
- if "http_method" in kwargs:
- raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP)
- super().__init__(
+ def decorator(fn: AnyCallable) -> HTTPRouteHandler:
+ return handler_class(
+ fn=fn,
after_request=after_request,
after_response=after_response,
background=background,
@@ -955,140 +910,133 @@ def __init__(
**kwargs,
)
- def __init_subclass__(cls, **kwargs: Any) -> None:
- _subclass_warning()
+ return decorator
-class put(HTTPRouteHandler):
- """PUT Route Decorator.
+def put(
+ path: str | None | Sequence[str] = None,
+ *,
+ after_request: AfterRequestHookHandler | None = None,
+ after_response: AfterResponseHookHandler | None = None,
+ background: BackgroundTask | BackgroundTasks | None = None,
+ before_request: BeforeRequestHookHandler | None = None,
+ cache: bool | int | type[CACHE_FOREVER] = False,
+ cache_control: CacheControlHeader | None = None,
+ cache_key_builder: CacheKeyBuilder | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ etag: ETag | None = None,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ media_type: MediaType | str | None = None,
+ middleware: Sequence[Middleware] | None = None,
+ name: str | None = None,
+ opt: Mapping[str, Any] | None = None,
+ request_class: type[Request] | None = None,
+ request_max_body_size: int | None | EmptyType = Empty,
+ response_class: type[Response] | None = None,
+ response_cookies: ResponseCookies | None = None,
+ response_headers: ResponseHeaders | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ status_code: int | None = None,
+ sync_to_thread: bool | None = None,
+ # OpenAPI related attributes
+ content_encoding: str | None = None,
+ content_media_type: str | None = None,
+ deprecated: bool = False,
+ description: str | None = None,
+ include_in_schema: bool | EmptyType = Empty,
+ operation_class: type[Operation] = Operation,
+ operation_id: str | OperationIDCreator | None = None,
+ raises: Sequence[type[HTTPException]] | None = None,
+ response_description: str | None = None,
+ responses: Mapping[int, ResponseSpec] | None = None,
+ security: Sequence[SecurityRequirement] | None = None,
+ summary: str | None = None,
+ tags: Sequence[str] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ handler_class: type[HTTPRouteHandler] = HTTPRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], HTTPRouteHandler]:
+ """Create an :class:`HTTPRouteHandler` with a ``PUT`` method.
- Use this decorator to decorate an HTTP handler for PUT requests.
- """
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments.
+ If not given defaults to ``/``
+ after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
+ to any route handler. If this function returns a value, the request will not reach the route handler,
+ and instead this value will be used.
+ after_response: A sync or async function called after the response has been awaited. It receives the
+ :class:`Request <.connection.Request>` object and should not return any values.
+ background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
+ :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
+ Defaults to ``None``.
+ before_request: A sync or async function called immediately before calling the route handler. Receives
+ the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
+ bypassing the route handler.
+ cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
+ of seconds (e.g. ``120``) to cache the response.
+ cache_control: A ``cache-control`` header of type
+ :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
+ cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
+ of the cache key if caching is configured on the application level.
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
+ validation of request data.
+ etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
+ valid IANA Media-Type.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
+ request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
+ default request.
+ request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded,
+ a '413 - Request Entity Too Large' error response is returned.
+ response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
+ default response.
+ response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
+ response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
+ instances.
+ responses: A mapping of additional status codes and a description of their expected content.
+ This information will be included in the OpenAPI schema
+ return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
+ outbound response data.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
+ status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
+ ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
+ sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
+ main event loop. This has an effect only for sync handler functions. See using sync handler functions.
+ content_encoding: A string describing the encoding of the content, e.g. ``base64``.
+ content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
+ deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
+ description: Text used for the route's schema description section.
+ include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
+ operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
+ operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
+ raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
+ This list should describe all exceptions raised within the route handler's function/method. The Litestar
+ ValidationException will be added automatically for the schema if any validation is involved.
+ response_description: Text used for the route's response schema description section.
+ security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
+ summary: Text used for the route's schema summary section.
+ tags: A sequence of string tags that will be appended to the OpenAPI schema.
+ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
+ hook for deserialization.
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
+ handler_class: Route handler class instantiated by the decorator
- def __init__(
- self,
- path: str | None | Sequence[str] = None,
- *,
- after_request: AfterRequestHookHandler | None = None,
- after_response: AfterResponseHookHandler | None = None,
- background: BackgroundTask | BackgroundTasks | None = None,
- before_request: BeforeRequestHookHandler | None = None,
- cache: bool | int | type[CACHE_FOREVER] = False,
- cache_control: CacheControlHeader | None = None,
- cache_key_builder: CacheKeyBuilder | None = None,
- dependencies: Dependencies | None = None,
- dto: type[AbstractDTO] | None | EmptyType = Empty,
- etag: ETag | None = None,
- exception_handlers: ExceptionHandlersMap | None = None,
- guards: Sequence[Guard] | None = None,
- media_type: MediaType | str | None = None,
- middleware: Sequence[Middleware] | None = None,
- name: str | None = None,
- opt: Mapping[str, Any] | None = None,
- request_class: type[Request] | None = None,
- request_max_body_size: int | None | EmptyType = Empty,
- response_class: type[Response] | None = None,
- response_cookies: ResponseCookies | None = None,
- response_headers: ResponseHeaders | None = None,
- return_dto: type[AbstractDTO] | None | EmptyType = Empty,
- signature_namespace: Mapping[str, Any] | None = None,
- status_code: int | None = None,
- sync_to_thread: bool | None = None,
- # OpenAPI related attributes
- content_encoding: str | None = None,
- content_media_type: str | None = None,
- deprecated: bool = False,
- description: str | None = None,
- include_in_schema: bool | EmptyType = Empty,
- operation_class: type[Operation] = Operation,
- operation_id: str | OperationIDCreator | None = None,
- raises: Sequence[type[HTTPException]] | None = None,
- response_description: str | None = None,
- responses: Mapping[int, ResponseSpec] | None = None,
- security: Sequence[SecurityRequirement] | None = None,
- summary: str | None = None,
- tags: Sequence[str] | None = None,
- type_decoders: TypeDecodersSequence | None = None,
- type_encoders: TypeEncodersMap | None = None,
- **kwargs: Any,
- ) -> None:
- """Initialize ``put``
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
+ """
- Args:
- path: A path fragment for the route handler function or a sequence of path fragments.
- If not given defaults to ``/``
- after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
- to any route handler. If this function returns a value, the request will not reach the route handler,
- and instead this value will be used.
- after_response: A sync or async function called after the response has been awaited. It receives the
- :class:`Request <.connection.Request>` object and should not return any values.
- background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
- :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
- Defaults to ``None``.
- before_request: A sync or async function called immediately before calling the route handler. Receives
- the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
- bypassing the route handler.
- cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
- of seconds (e.g. ``120``) to cache the response.
- cache_control: A ``cache-control`` header of type
- :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
- cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
- of the cache key if caching is configured on the application level.
- dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
- dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
- validation of request data.
- etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
- exception_handlers: A mapping of status codes and/or exception types to handler functions.
- guards: A sequence of :class:`Guard <.types.Guard>` callables.
- http_method: An :class:`http method string <.types.Method>`, a member of the enum
- :class:`HttpMethod ` or a list of these that correlates to the methods the
- route handler function should handle.
- media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
- valid IANA Media-Type.
- middleware: A sequence of :class:`Middleware <.types.Middleware>`.
- name: A string identifying the route handler.
- opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
- wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
- request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
- default request.
- request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded,
- a '413 - Request Entity Too Large' error response is returned.
- response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
- default response.
- response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
- response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
- instances.
- responses: A mapping of additional status codes and a description of their expected content.
- This information will be included in the OpenAPI schema
- return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
- outbound response data.
- signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
- status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT`` and
- ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
- sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
- main event loop. This has an effect only for sync handler functions. See using sync handler functions.
- content_encoding: A string describing the encoding of the content, e.g. ``base64``.
- content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
- deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
- description: Text used for the route's schema description section.
- include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
- operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
- operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
- raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
- This list should describe all exceptions raised within the route handler's function/method. The Litestar
- ValidationException will be added automatically for the schema if any validation is involved.
- response_description: Text used for the route's response schema description section.
- security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
- summary: Text used for the route's schema summary section.
- tags: A sequence of string tags that will be appended to the OpenAPI schema.
- type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
- hook for deserialization.
- type_encoders: A mapping of types to callables that transform them into types supported for serialization.
- **kwargs: Any additional kwarg - will be set in the opt dictionary.
- """
- if "http_method" in kwargs:
- raise ImproperlyConfiguredException(MSG_SEMANTIC_ROUTE_HANDLER_WITH_HTTP)
- super().__init__(
+ def decorator(fn: AnyCallable) -> HTTPRouteHandler:
+ return handler_class(
+ fn=fn,
after_request=after_request,
after_response=after_response,
background=background,
@@ -1134,5 +1082,171 @@ def __init__(
**kwargs,
)
- def __init_subclass__(cls, **kwargs: Any) -> None:
- _subclass_warning()
+ return decorator
+
+
+def delete(
+ path: str | None | Sequence[str] = None,
+ *,
+ after_request: AfterRequestHookHandler | None = None,
+ after_response: AfterResponseHookHandler | None = None,
+ background: BackgroundTask | BackgroundTasks | None = None,
+ before_request: BeforeRequestHookHandler | None = None,
+ cache: bool | int | type[CACHE_FOREVER] = False,
+ cache_control: CacheControlHeader | None = None,
+ cache_key_builder: CacheKeyBuilder | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ etag: ETag | None = None,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ media_type: MediaType | str | None = None,
+ middleware: Sequence[Middleware] | None = None,
+ name: str | None = None,
+ opt: Mapping[str, Any] | None = None,
+ request_class: type[Request] | None = None,
+ response_class: type[Response] | None = None,
+ response_cookies: ResponseCookies | None = None,
+ response_headers: ResponseHeaders | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ status_code: int | None = None,
+ sync_to_thread: bool | None = None,
+ # OpenAPI related attributes
+ content_encoding: str | None = None,
+ content_media_type: str | None = None,
+ deprecated: bool = False,
+ description: str | None = None,
+ include_in_schema: bool | EmptyType = Empty,
+ operation_class: type[Operation] = Operation,
+ operation_id: str | OperationIDCreator | None = None,
+ raises: Sequence[type[HTTPException]] | None = None,
+ response_description: str | None = None,
+ responses: Mapping[int, ResponseSpec] | None = None,
+ security: Sequence[SecurityRequirement] | None = None,
+ summary: str | None = None,
+ tags: Sequence[str] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ handler_class: type[HTTPRouteHandler] = HTTPRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], HTTPRouteHandler]:
+ """Create an :class:`HTTPRouteHandler` with a ``DELETE`` method.
+
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments.
+ If not given defaults to ``/``
+ after_request: A sync or async function executed before a :class:`Request <.connection.Request>` is passed
+ to any route handler. If this function returns a value, the request will not reach the route handler,
+ and instead this value will be used.
+ after_response: A sync or async function called after the response has been awaited. It receives the
+ :class:`Request <.connection.Request>` object and should not return any values.
+ background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
+ :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
+ Defaults to ``None``.
+ before_request: A sync or async function called immediately before calling the route handler. Receives
+ the :class:`.connection.Request` instance and any non-``None`` return value is used for the response,
+ bypassing the route handler.
+ cache: Enables response caching if configured on the application level. Valid values are ``True`` or a number
+ of seconds (e.g. ``120``) to cache the response.
+ cache_control: A ``cache-control`` header of type
+ :class:`CacheControlHeader <.datastructures.CacheControlHeader>` that will be added to the response.
+ cache_key_builder: A :class:`cache-key builder function <.types.CacheKeyBuilder>`. Allows for customization
+ of the cache key if caching is configured on the application level.
+ dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
+ validation of request data.
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ etag: An ``etag`` header of type :class:`ETag <.datastructures.ETag>` that will be added to the response.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ media_type: A member of the :class:`MediaType <.enums.MediaType>` enum or a string with a
+ valid IANA Media-Type.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or :class:`ASGI Scope <.types.Scope>`.
+ request_class: A custom subclass of :class:`Request <.connection.Request>` to be used as route handler's
+ default request.
+ response_class: A custom subclass of :class:`Response <.response.Response>` to be used as route handler's
+ default response.
+ response_cookies: A sequence of :class:`Cookie <.datastructures.Cookie>` instances.
+ response_headers: A string keyed mapping of :class:`ResponseHeader <.datastructures.ResponseHeader>`
+ instances.
+ responses: A mapping of additional status codes and a description of their expected content.
+ This information will be included in the OpenAPI schema
+ return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
+ outbound response data.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
+ status_code: An http status code for the response. Defaults to ``200`` for mixed method or ``GET``, ``PUT``
+ and ``PATCH``, ``201`` for ``POST`` and ``204`` for ``DELETE``.
+ sync_to_thread: A boolean dictating whether the handler function will be executed in a worker thread or the
+ main event loop. This has an effect only for sync handler functions. See using sync handler functions.
+ content_encoding: A string describing the encoding of the content, e.g. ``base64``.
+ content_media_type: A string designating the media-type of the content, e.g. ``image/png``.
+ deprecated: A boolean dictating whether this route should be marked as deprecated in the OpenAPI schema.
+ description: Text used for the route's schema description section.
+ include_in_schema: A boolean flag dictating whether the route handler should be documented in the OpenAPI schema.
+ operation_class: :class:`Operation <.openapi.spec.operation.Operation>` to be used with the route's OpenAPI schema.
+ operation_id: Either a string or a callable returning a string. An identifier used for the route's schema operationId.
+ raises: A list of exception classes extending from litestar.HttpException that is used for the OpenAPI documentation.
+ This list should describe all exceptions raised within the route handler's function/method. The Litestar
+ ValidationException will be added automatically for the schema if any validation is involved.
+ response_description: Text used for the route's response schema description section.
+ security: A sequence of dictionaries that contain information about which security scheme can be used on the endpoint.
+ summary: Text used for the route's schema summary section.
+ tags: A sequence of string tags that will be appended to the OpenAPI schema.
+ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
+ hook for deserialization.
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
+ handler_class: Route handler class instantiated by the decorator
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
+ """
+
+ def decorator(fn: AnyCallable) -> HTTPRouteHandler:
+ return handler_class(
+ fn=fn,
+ after_request=after_request,
+ after_response=after_response,
+ background=background,
+ before_request=before_request,
+ cache=cache,
+ cache_control=cache_control,
+ cache_key_builder=cache_key_builder,
+ content_encoding=content_encoding,
+ content_media_type=content_media_type,
+ dependencies=dependencies,
+ deprecated=deprecated,
+ description=description,
+ dto=dto,
+ etag=etag,
+ exception_handlers=exception_handlers,
+ guards=guards,
+ http_method=HttpMethod.DELETE,
+ include_in_schema=include_in_schema,
+ media_type=media_type,
+ middleware=middleware,
+ name=name,
+ operation_class=operation_class,
+ operation_id=operation_id,
+ opt=opt,
+ path=path,
+ raises=raises,
+ request_class=request_class,
+ response_class=response_class,
+ response_cookies=response_cookies,
+ response_description=response_description,
+ response_headers=response_headers,
+ responses=responses,
+ return_dto=return_dto,
+ security=security,
+ signature_namespace=signature_namespace,
+ status_code=status_code,
+ summary=summary,
+ sync_to_thread=sync_to_thread,
+ tags=tags,
+ type_decoders=type_decoders,
+ type_encoders=type_encoders,
+ **kwargs,
+ )
+
+ return decorator
diff --git a/litestar/handlers/websocket_handlers/listener.py b/litestar/handlers/websocket_handlers/listener.py
index cfe35eb7ed..15ac6b3540 100644
--- a/litestar/handlers/websocket_handlers/listener.py
+++ b/litestar/handlers/websocket_handlers/listener.py
@@ -42,8 +42,6 @@
if TYPE_CHECKING:
from typing import Coroutine
- from typing_extensions import Self
-
from litestar import Router
from litestar.dto import AbstractDTO
from litestar.types.asgi_types import WebSocketMode
@@ -74,6 +72,7 @@ def __init__(
self,
path: str | list[str] | None = None,
*,
+ fn: AnyCallable,
connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None,
dependencies: Dependencies | None = None,
dto: type[AbstractDTO] | None | EmptyType = Empty,
@@ -97,6 +96,7 @@ def __init__(
self,
path: str | list[str] | None = None,
*,
+ fn: AnyCallable,
connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept,
dependencies: Dependencies | None = None,
dto: type[AbstractDTO] | None | EmptyType = Empty,
@@ -121,6 +121,7 @@ def __init__(
self,
path: str | list[str] | None = None,
*,
+ fn: AnyCallable,
connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept,
connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None,
dependencies: Dependencies | None = None,
@@ -146,6 +147,7 @@ def __init__(
Args:
path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults
to ``/``
+ fn: The handler function
connection_accept_handler: A callable that accepts a :class:`WebSocket <.connection.WebSocket>` instance
and returns a coroutine that when awaited, will accept the connection. Defaults to ``WebSocket.accept``.
connection_lifespan: An asynchronous context manager, handling the lifespan of the connection. By default,
@@ -210,6 +212,7 @@ def __init__(
listener_dependencies["on_disconnect_dependencies"] = create_stub_dependency(self.on_disconnect)
super().__init__(
+ fn=fn,
path=path,
dependencies=listener_dependencies,
exception_handlers=exception_handlers,
@@ -226,7 +229,7 @@ def __init__(
**kwargs,
)
- def __call__(self, fn: AnyCallable) -> Self:
+ def _prepare_fn(self, fn: AnyCallable) -> ListenerHandler:
parsed_signature = ParsedSignature.from_fn(fn, self.resolve_signature_namespace())
if "data" not in parsed_signature.parameters:
@@ -248,10 +251,8 @@ def __call__(self, fn: AnyCallable) -> Self:
},
)
- return super().__call__(
- ListenerHandler(
- listener=self, fn=fn, parsed_signature=parsed_signature, namespace=self.resolve_signature_namespace()
- )
+ return ListenerHandler(
+ listener=self, fn=fn, parsed_signature=parsed_signature, namespace=self.resolve_signature_namespace()
)
def _validate_handler_function(self) -> None:
@@ -319,9 +320,6 @@ def resolve_send_handler(self) -> Callable[[WebSocket, Any], Coroutine[None, Non
return self._send_handler
-websocket_listener = WebsocketListenerRouteHandler
-
-
class WebsocketListener(ABC):
path: str | list[str] | None = None
"""A path fragment for the route handler function or a sequence of path fragments. If not given defaults to ``/``"""
@@ -397,7 +395,8 @@ def to_handler(self) -> WebsocketListenerRouteHandler:
type_decoders=self.type_decoders,
type_encoders=self.type_encoders,
websocket_class=self.websocket_class,
- )(self.on_receive)
+ fn=self.on_receive,
+ )
handler.owner = self._owner
return handler
@@ -423,3 +422,140 @@ def on_receive(self, *args: Any, **kwargs: Any) -> Any:
according to handler configuration.
"""
raise NotImplementedError
+
+
+@overload
+def websocket_listener(
+ path: str | list[str] | None = None,
+ *,
+ connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None,
+ guards: list[Guard] | None = None,
+ middleware: list[Middleware] | None = None,
+ receive_mode: WebSocketMode = "text",
+ send_mode: WebSocketMode = "text",
+ name: str | None = None,
+ opt: dict[str, Any] | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ websocket_class: type[WebSocket] | None = None,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], WebsocketListenerRouteHandler]: ...
+
+
+@overload
+def websocket_listener(
+ path: str | list[str] | None = None,
+ *,
+ connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None,
+ guards: list[Guard] | None = None,
+ middleware: list[Middleware] | None = None,
+ receive_mode: WebSocketMode = "text",
+ send_mode: WebSocketMode = "text",
+ name: str | None = None,
+ on_accept: AnyCallable | None = None,
+ on_disconnect: AnyCallable | None = None,
+ opt: dict[str, Any] | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ websocket_class: type[WebSocket] | None = None,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], WebsocketListenerRouteHandler]: ...
+
+
+def websocket_listener(
+ path: str | list[str] | None = None,
+ *,
+ connection_accept_handler: Callable[[WebSocket], Coroutine[Any, Any, None]] = WebSocket.accept,
+ connection_lifespan: Callable[..., AbstractAsyncContextManager[Any]] | None = None,
+ dependencies: Dependencies | None = None,
+ dto: type[AbstractDTO] | None | EmptyType = Empty,
+ exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None,
+ guards: list[Guard] | None = None,
+ middleware: list[Middleware] | None = None,
+ receive_mode: WebSocketMode = "text",
+ send_mode: WebSocketMode = "text",
+ name: str | None = None,
+ on_accept: AnyCallable | None = None,
+ on_disconnect: AnyCallable | None = None,
+ opt: dict[str, Any] | None = None,
+ return_dto: type[AbstractDTO] | None | EmptyType = Empty,
+ signature_namespace: Mapping[str, Any] | None = None,
+ type_decoders: TypeDecodersSequence | None = None,
+ type_encoders: TypeEncodersMap | None = None,
+ websocket_class: type[WebSocket] | None = None,
+ **kwargs: Any,
+) -> Callable[[AnyCallable], WebsocketListenerRouteHandler]:
+ """Create a :class:`WebsocketListenerRouteHandler`.
+
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults
+ to ``/``
+ connection_accept_handler: A callable that accepts a :class:`WebSocket <.connection.WebSocket>` instance
+ and returns a coroutine that when awaited, will accept the connection. Defaults to ``WebSocket.accept``.
+ connection_lifespan: An asynchronous context manager, handling the lifespan of the connection. By default,
+ it calls the ``connection_accept_handler``, ``on_connect`` and ``on_disconnect``. Can request any
+ dependencies, for example the :class:`WebSocket <.connection.WebSocket>` connection
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for (de)serializing and
+ validation of request data.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ receive_mode: Websocket mode to receive data in, either `text` or `binary`.
+ send_mode: Websocket mode to receive data in, either `text` or `binary`.
+ name: A string identifying the route handler.
+ on_accept: Callback invoked after a connection has been accepted. Can request any dependencies, for example
+ the :class:`WebSocket <.connection.WebSocket>` connection
+ on_disconnect: Callback invoked after a connection has been closed. Can request any dependencies, for
+ example the :class:`WebSocket <.connection.WebSocket>` connection
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or
+ :class:`ASGI Scope <.types.Scope>`.
+ return_dto: :class:`AbstractDTO <.dto.base_dto.AbstractDTO>` to use for serializing
+ outbound response data.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature
+ modelling.
+ type_decoders: A sequence of tuples, each composed of a predicate testing for type identity and a msgspec
+ hook for deserialization.
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
+ websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's
+ default websocket class.
+ """
+
+ def decorator(fn: AnyCallable) -> WebsocketListenerRouteHandler:
+ return WebsocketListenerRouteHandler(
+ fn=fn,
+ path=path,
+ connection_accept_handler=connection_accept_handler,
+ connection_lifespan=connection_lifespan,
+ dependencies=dependencies,
+ dto=dto,
+ exception_handlers=exception_handlers,
+ guard=guards,
+ middleware=middleware,
+ receive_mode=receive_mode,
+ send_mode=send_mode,
+ name=name,
+ on_accept=on_accept,
+ on_disconnect=on_disconnect,
+ opt=opt,
+ return_dto=return_dto,
+ signature_namespace=signature_namespace,
+ type_decoders=type_decoders,
+ type_encoders=type_encoders,
+ websocket_class=websocket_class,
+ **kwargs,
+ )
+
+ return decorator
diff --git a/litestar/handlers/websocket_handlers/route_handler.py b/litestar/handlers/websocket_handlers/route_handler.py
index 4356d618fc..5c108db429 100644
--- a/litestar/handlers/websocket_handlers/route_handler.py
+++ b/litestar/handlers/websocket_handlers/route_handler.py
@@ -1,29 +1,30 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Mapping
+from typing import TYPE_CHECKING, Any, Callable, Mapping
from litestar.connection import WebSocket
from litestar.exceptions import ImproperlyConfiguredException
from litestar.handlers import BaseRouteHandler
+from litestar.types import AsyncAnyCallable, Empty
from litestar.types.builtin_types import NoneType
from litestar.utils.predicates import is_async_callable
if TYPE_CHECKING:
- from litestar.types import Dependencies, ExceptionHandler, Guard, Middleware
+ from litestar._kwargs import KwargsModel
+ from litestar._kwargs.cleanup import DependencyCleanupGroup
+ from litestar.app import Litestar
+ from litestar.routes import BaseRoute
+ from litestar.types import Dependencies, EmptyType, ExceptionHandler, Guard, Middleware
class WebsocketRouteHandler(BaseRouteHandler):
- """Websocket route handler decorator.
-
- Use this decorator to decorate websocket handler functions.
- """
-
- __slots__ = ("websocket_class",)
+ __slots__ = ("_kwargs_model", "websocket_class")
def __init__(
self,
path: str | list[str] | None = None,
*,
+ fn: AsyncAnyCallable,
dependencies: Dependencies | None = None,
exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None,
guards: list[Guard] | None = None,
@@ -34,11 +35,14 @@ def __init__(
websocket_class: type[WebSocket] | None = None,
**kwargs: Any,
) -> None:
- """Initialize ``WebsocketRouteHandler``
+ """Route handler for WebSocket routes.
Args:
path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults
to ``/``
+ fn: The handler function
+
+ .. versionadded:: 3.0
dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
exception_handlers: A mapping of status codes and/or exception types to handler functions.
guards: A sequence of :class:`Guard <.types.Guard>` callables.
@@ -54,8 +58,10 @@ def __init__(
**kwargs: Any additional kwarg - will be set in the opt dictionary.
"""
self.websocket_class = websocket_class
+ self._kwargs_model: KwargsModel | EmptyType = Empty
super().__init__(
+ fn=fn,
path=path,
dependencies=dependencies,
exception_handlers=exception_handlers,
@@ -99,5 +105,95 @@ def _validate_handler_function(self) -> None:
if not is_async_callable(self.fn):
raise ImproperlyConfiguredException(f"{self}: WebSocket handler functions must be asynchronous")
+ def on_registration(self, app: Litestar, route: BaseRoute) -> None:
+ super().on_registration(app=app, route=route)
+ self._kwargs_model = self._create_kwargs_model(path_parameters=route.path_parameters)
+
+ async def handle(self, connection: WebSocket[Any, Any, Any]) -> None:
+ """ASGI app that creates a WebSocket from the passed in args, and then awaits the handler function.
+
+ Args:
+ connection: WebSocket connection
+
+ Returns:
+ None
+ """
+
+ handler_parameter_model = self._kwargs_model
+ if handler_parameter_model is Empty:
+ raise ImproperlyConfiguredException("handler parameter model not defined")
+
+ if self.resolve_guards():
+ await self.authorize_connection(connection=connection)
+
+ parsed_kwargs: dict[str, Any] = {}
+ cleanup_group: DependencyCleanupGroup | None = None
+
+ if handler_parameter_model.has_kwargs and self.signature_model:
+ parsed_kwargs = await handler_parameter_model.to_kwargs(connection=connection)
+
+ if handler_parameter_model.dependency_batches:
+ cleanup_group = await handler_parameter_model.resolve_dependencies(connection, parsed_kwargs)
+
+ parsed_kwargs = self.signature_model.parse_values_from_connection_kwargs(
+ connection=connection, kwargs=parsed_kwargs
+ )
+
+ if cleanup_group:
+ async with cleanup_group:
+ await self.fn(**parsed_kwargs)
+ await cleanup_group.cleanup()
+ else:
+ await self.fn(**parsed_kwargs)
+
+
+def websocket(
+ path: str | list[str] | None = None,
+ *,
+ dependencies: Dependencies | None = None,
+ exception_handlers: dict[int | type[Exception], ExceptionHandler] | None = None,
+ guards: list[Guard] | None = None,
+ middleware: list[Middleware] | None = None,
+ name: str | None = None,
+ opt: dict[str, Any] | None = None,
+ signature_namespace: Mapping[str, Any] | None = None,
+ websocket_class: type[WebSocket] | None = None,
+ handler_class: type[WebsocketRouteHandler] = WebsocketRouteHandler,
+ **kwargs: Any,
+) -> Callable[[AsyncAnyCallable], WebsocketRouteHandler]:
+ """Create a :class:`WebsocketRouteHandler`.
+
+ Args:
+ path: A path fragment for the route handler function or a sequence of path fragments. If not given defaults
+ to ``/``
+ dependencies: A string keyed mapping of dependency :class:`Provider <.di.Provide>` instances.
+ exception_handlers: A mapping of status codes and/or exception types to handler functions.
+ guards: A sequence of :class:`Guard <.types.Guard>` callables.
+ middleware: A sequence of :class:`Middleware <.types.Middleware>`.
+ name: A string identifying the route handler.
+ opt: A string keyed mapping of arbitrary values that can be accessed in :class:`Guards <.types.Guard>` or
+ wherever you have access to :class:`Request <.connection.Request>` or
+ :class:`ASGI Scope <.types.Scope>`.
+ signature_namespace: A mapping of names to types for use in forward reference resolution during signature modelling.
+ websocket_class: A custom subclass of :class:`WebSocket <.connection.WebSocket>` to be used as route handler's
+ default websocket class.
+ handler_class: Route handler class instantiated by the decorator
+ **kwargs: Any additional kwarg - will be set in the opt dictionary.
+ """
+
+ def decorator(fn: AsyncAnyCallable) -> WebsocketRouteHandler:
+ return handler_class(
+ path=path,
+ fn=fn,
+ dependencies=dependencies,
+ exception_handlers=exception_handlers,
+ guards=guards,
+ middleware=middleware,
+ name=name,
+ opt=opt,
+ signature_namespace=signature_namespace,
+ websocket_class=websocket_class,
+ **kwargs,
+ )
-websocket = WebsocketRouteHandler
+ return decorator
diff --git a/litestar/handlers/websocket_handlers/stream.py b/litestar/handlers/websocket_handlers/stream.py
index b52a3eb8d2..0b65b438d6 100644
--- a/litestar/handlers/websocket_handlers/stream.py
+++ b/litestar/handlers/websocket_handlers/stream.py
@@ -7,7 +7,6 @@
import anyio
from msgspec.json import Encoder as JsonEncoder
-from typing_extensions import Self
from litestar.exceptions import ImproperlyConfiguredException, LitestarWarning, WebSocketDisconnect
from litestar.handlers.websocket_handlers.route_handler import WebsocketRouteHandler
@@ -19,6 +18,7 @@
if TYPE_CHECKING:
from litestar import Litestar, WebSocket
from litestar.dto import AbstractDTO
+ from litestar.routes import BaseRoute
from litestar.types import Dependencies, EmptyType, ExceptionHandler, Guard, Middleware, TypeEncodersMap
from litestar.types.asgi_types import WebSocketMode
@@ -183,6 +183,7 @@ async def send_time() -> AsyncGenerator[str, None]:
def decorator(fn: Callable[..., AsyncGenerator[Any, Any]]) -> WebsocketRouteHandler:
return WebSocketStreamHandler(
+ fn=fn, # type: ignore[arg-type]
path=path,
dependencies=dependencies,
exception_handlers=exception_handlers,
@@ -194,14 +195,13 @@ def decorator(fn: Callable[..., AsyncGenerator[Any, Any]]) -> WebsocketRouteHand
websocket_class=websocket_class,
return_dto=return_dto,
type_encoders=type_encoders,
- **kwargs,
- )(
- _WebSocketStreamOptions(
+ stream_options=_WebSocketStreamOptions(
generator_fn=fn,
send_mode=mode,
listen_for_disconnect=listen_for_disconnect,
warn_on_data_discard=warn_on_data_discard,
- )
+ ),
+ **kwargs,
)
return decorator
@@ -211,12 +211,9 @@ class WebSocketStreamHandler(WebsocketRouteHandler):
__slots__ = ("_ws_stream_options",)
_ws_stream_options: _WebSocketStreamOptions
- def __call__(self, fn: _WebSocketStreamOptions) -> Self: # type: ignore[override]
- self._ws_stream_options = fn
- self._fn = self._ws_stream_options.generator_fn # type: ignore[assignment]
- return self
+ def on_registration(self, app: Litestar, route: BaseRoute) -> None:
+ self._ws_stream_options = self.opt["stream_options"]
- def on_registration(self, app: Litestar) -> None:
parsed_handler_signature = parsed_stream_fn_signature = ParsedSignature.from_fn(
self.fn, self.resolve_signature_namespace()
)
@@ -293,9 +290,9 @@ async def handler_fn(*args: Any, socket: WebSocket, **kw: Any) -> None:
send_handler=send_handler,
)
- self._fn = handler_fn
+ self.fn = handler_fn
- super().on_registration(app)
+ super().on_registration(app, route)
class _WebSocketStreamOptions:
diff --git a/litestar/logging/config.py b/litestar/logging/config.py
index 19615a229f..8c5d3801a1 100644
--- a/litestar/logging/config.py
+++ b/litestar/logging/config.py
@@ -242,7 +242,7 @@ class LoggingConfig(BaseLoggingConfig):
"""
configure_root_logger: bool = field(default=True)
"""Should the root logger be configured, defaults to True for ease of configuration."""
- log_exceptions: Literal["always", "debug", "never"] = field(default="debug")
+ log_exceptions: Literal["always", "debug", "never"] = field(default="always")
"""Should exceptions be logged, defaults to log exceptions when 'app.debug == True'"""
traceback_line_limit: int = field(default=-1)
"""Max number of lines to print for exception traceback.
@@ -471,7 +471,7 @@ class StructLoggingConfig(BaseLoggingConfig):
"""Logger factory to use."""
cache_logger_on_first_use: bool = field(default=True)
"""Whether to cache the logger configuration and reuse."""
- log_exceptions: Literal["always", "debug", "never"] = field(default="debug")
+ log_exceptions: Literal["always", "debug", "never"] = field(default="always")
"""Should exceptions be logged, defaults to log exceptions when 'app.debug == True'"""
traceback_line_limit: int = field(default=-1)
"""Max number of lines to print for exception traceback.
diff --git a/litestar/middleware/_internal/cors.py b/litestar/middleware/_internal/cors.py
index 623eb9b2eb..b85257b628 100644
--- a/litestar/middleware/_internal/cors.py
+++ b/litestar/middleware/_internal/cors.py
@@ -46,7 +46,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == ScopeType.HTTP and scope["method"] == HttpMethod.OPTIONS and origin:
request = scope["app"].request_class(scope=scope, receive=receive, send=send)
asgi_response = self._create_preflight_response(origin=origin, request_headers=headers).to_asgi_response(
- app=None, request=request
+ request=request
)
await asgi_response(scope, receive, send)
elif origin:
diff --git a/litestar/middleware/_internal/exceptions/middleware.py b/litestar/middleware/_internal/exceptions/middleware.py
index 3801d995cf..c8154ff01f 100644
--- a/litestar/middleware/_internal/exceptions/middleware.py
+++ b/litestar/middleware/_internal/exceptions/middleware.py
@@ -13,7 +13,6 @@
create_debug_response,
)
from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
-from litestar.utils.deprecation import warn_deprecation
from litestar.utils.empty import value_or_raise
from litestar.utils.scope.state import ScopeState
@@ -92,46 +91,14 @@ class ExceptionHandlerMiddleware:
This used in multiple layers of Litestar.
"""
- def __init__(
- self, app: ASGIApp, debug: bool | None, exception_handlers: ExceptionHandlersMap | None = None
- ) -> None:
+ def __init__(self, app: ASGIApp) -> None:
"""Initialize ``ExceptionHandlerMiddleware``.
Args:
app: The ``next`` ASGI app to call.
- debug: Whether ``debug`` mode is enabled. Deprecated. Debug mode will be inferred from the request scope
- exception_handlers: A dictionary mapping status codes and/or exception types to handler functions.
-
- .. deprecated:: 2.0.0
- The ``debug`` parameter is deprecated. It will be inferred from the request scope
- .. deprecated:: 2.9.0
- The ``exception_handlers`` parameter is deprecated. It will be inferred from the application or the
- route handler.
"""
self.app = app
- self.exception_handlers = exception_handlers
- self.debug = debug
-
- if debug is not None:
- warn_deprecation(
- "2.0.0",
- deprecated_name="debug",
- kind="parameter",
- info="Debug mode will be inferred from the request scope",
- removal_in="3.0.0",
- )
-
- if exception_handlers is not None:
- warn_deprecation(
- "2.9.0",
- deprecated_name="exception_handlers",
- kind="parameter",
- info="It will be inferred from the application or the route handler",
- removal_in="3.0.0",
- )
-
- self._get_debug = self._get_debug_scope if debug is None else lambda *a: debug
@staticmethod
def _get_debug_scope(scope: Scope) -> bool:
@@ -195,17 +162,13 @@ async def handle_request_exception(
None.
"""
- exception_handlers = (
- value_or_raise(ScopeState.from_scope(scope).exception_handlers)
- if self.exception_handlers is None
- else self.exception_handlers
- )
+ exception_handlers = value_or_raise(ScopeState.from_scope(scope).exception_handlers)
exception_handler = get_exception_handler(exception_handlers, exc) or self.default_http_exception_handler
request: Request[Any, Any, Any] = litestar_app.request_class(scope=scope, receive=receive, send=send)
response = exception_handler(request, exc)
route_handler: BaseRouteHandler | None = scope.get("route_handler")
type_encoders = route_handler.resolve_type_encoders() if route_handler else litestar_app.type_encoders
- await response.to_asgi_response(app=None, request=request, type_encoders=type_encoders)(
+ await response.to_asgi_response(request=request, type_encoders=type_encoders)(
scope=scope, receive=receive, send=send
)
diff --git a/litestar/middleware/exceptions/__init__.py b/litestar/middleware/exceptions/__init__.py
deleted file mode 100644
index 6bbe58e7ad..0000000000
--- a/litestar/middleware/exceptions/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-from litestar.middleware._internal.exceptions import middleware
-from litestar.utils.deprecation import warn_deprecation
-
-
-def __getattr__(name: str) -> Any:
- if name == "ExceptionHandlerMiddleware":
- warn_deprecation(
- version="2.9",
- deprecated_name=name,
- kind="class",
- removal_in="3.0",
- info="ExceptionHandlerMiddleware has been removed from the public API.",
- )
- return middleware.ExceptionHandlerMiddleware
- raise AttributeError(f"module {__name__} has no attribute {name}")
diff --git a/litestar/middleware/exceptions/_debug_response.py b/litestar/middleware/exceptions/_debug_response.py
deleted file mode 100644
index fa075dccfe..0000000000
--- a/litestar/middleware/exceptions/_debug_response.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-from litestar.exceptions import responses
-from litestar.utils.deprecation import warn_deprecation
-
-
-def __getattr__(name: str) -> Any:
- if name == "create_debug_response":
- warn_deprecation(
- version="2.9",
- deprecated_name=name,
- kind="function",
- removal_in="3.0",
- alternative="litestar.exceptions.responses.create_debug_response",
- )
- return responses.create_debug_response
-
- raise AttributeError(f"module {__name__} has no attribute {name}")
diff --git a/litestar/middleware/exceptions/middleware.py b/litestar/middleware/exceptions/middleware.py
deleted file mode 100644
index 3d653ebac1..0000000000
--- a/litestar/middleware/exceptions/middleware.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-from litestar.exceptions import responses
-from litestar.middleware._internal.exceptions import middleware
-from litestar.utils.deprecation import warn_deprecation
-
-
-def __getattr__(name: str) -> Any:
- if name == "ExceptionHandlerMiddleware":
- warn_deprecation(
- version="2.9",
- deprecated_name=name,
- kind="class",
- removal_in="3.0",
- info="ExceptionHandlerMiddleware has been removed from the public API.",
- )
- return middleware.ExceptionHandlerMiddleware
-
- if name == "create_exception_response":
- warn_deprecation(
- version="2.9",
- deprecated_name=name,
- kind="function",
- removal_in="3.0",
- alternative="litestar.exceptions.responses.create_exception_response",
- )
- return responses.create_exception_response
-
- if name == "ExceptionResponseContent":
- warn_deprecation(
- version="2.9",
- deprecated_name=name,
- kind="class",
- removal_in="3.0",
- alternative="litestar.exceptions.responses.ExceptionResponseContent",
- )
- return responses.ExceptionResponseContent
-
- raise AttributeError(f"module {__name__} has no attribute {name}")
diff --git a/litestar/middleware/rate_limit.py b/litestar/middleware/rate_limit.py
index 11a6653924..4e0d2afd75 100644
--- a/litestar/middleware/rate_limit.py
+++ b/litestar/middleware/rate_limit.py
@@ -129,9 +129,6 @@ def cache_key_from_request(self, request: Request[Any, Any, Any]) -> str:
if getattr(route_handler, "is_mount", False):
identifier += "::mount"
- if getattr(route_handler, "is_static", False):
- identifier += "::static"
-
return f"{type(self).__name__}::{identifier}"
async def retrieve_cached_history(self, key: str, store: Store) -> CacheObject:
diff --git a/litestar/openapi/__init__.py b/litestar/openapi/__init__.py
index 50019b7863..f512858b12 100644
--- a/litestar/openapi/__init__.py
+++ b/litestar/openapi/__init__.py
@@ -1,5 +1,4 @@
from .config import OpenAPIConfig
-from .controller import OpenAPIController
from .datastructures import ResponseSpec
-__all__ = ("OpenAPIConfig", "OpenAPIController", "ResponseSpec")
+__all__ = ("OpenAPIConfig", "ResponseSpec")
diff --git a/litestar/openapi/config.py b/litestar/openapi/config.py
index ebb763eb47..d500df298a 100644
--- a/litestar/openapi/config.py
+++ b/litestar/openapi/config.py
@@ -2,17 +2,10 @@
from copy import deepcopy
from dataclasses import dataclass, field, fields
-from typing import TYPE_CHECKING, Final, Literal, Sequence
+from typing import TYPE_CHECKING, Sequence
from litestar._openapi.utils import default_operation_id_creator
-from litestar.openapi.plugins import (
- JsonRenderPlugin,
- RapidocRenderPlugin,
- RedocRenderPlugin,
- StoplightRenderPlugin,
- SwaggerRenderPlugin,
- YamlRenderPlugin,
-)
+from litestar.openapi.plugins import ScalarRenderPlugin
from litestar.openapi.spec import (
Components,
Contact,
@@ -26,30 +19,15 @@
Server,
Tag,
)
-from litestar.utils.deprecation import warn_deprecation
from litestar.utils.path import normalize_path
if TYPE_CHECKING:
- from litestar.openapi.controller import OpenAPIController
from litestar.openapi.plugins import OpenAPIRenderPlugin
from litestar.router import Router
from litestar.types.callable_types import OperationIDCreator
__all__ = ("OpenAPIConfig",)
-_enabled_plugin_map = {
- "elements": StoplightRenderPlugin,
- "openapi.json": JsonRenderPlugin,
- "openapi.yaml": YamlRenderPlugin,
- "openapi.yml": YamlRenderPlugin,
- "rapidoc": RapidocRenderPlugin,
- "redoc": RedocRenderPlugin,
- "swagger": SwaggerRenderPlugin,
- "oauth2-redirect.html": None,
-}
-
-_DEFAULT_SCHEMA_SITE: Final = "redoc"
-
@dataclass
class OpenAPIConfig:
@@ -110,130 +88,46 @@ class OpenAPIConfig:
"""
operation_id_creator: OperationIDCreator = default_operation_id_creator
"""A callable that generates unique operation ids"""
- path: str | None = field(default=None)
+ path: str = "/schema"
"""Base path for the OpenAPI documentation endpoints.
If no path is provided the default is ``/schema``.
Ignored if :attr:`openapi_router` is provided.
"""
- render_plugins: Sequence[OpenAPIRenderPlugin] = field(default=())
- """Plugins for rendering OpenAPI documentation UIs."""
+ render_plugins: Sequence[OpenAPIRenderPlugin] = field(default=(ScalarRenderPlugin(),))
+ """Plugins for rendering OpenAPI documentation UIs.
+
+ .. versionchanged:: 3.0.0
+
+ Default behavior changed to serve only :class:`ScalarRenderPlugin`.
+ """
openapi_router: Router | None = None
"""An optional router for serving OpenAPI documentation and schema files.
If provided, ``path`` is ignored.
- This parameter is also ignored if the deprecated :attr:`openapi_router <.openapi.OpenAPIConfig.openapi_controller>`
- kwarg is provided.
-
:attr:`openapi_router` is not required, but it can be passed to customize the configuration of the router used to
serve the documentation endpoints. For example, you can add middleware or guards to the router.
Handlers to serve the OpenAPI schema and documentation sites are added to this router according to
:attr:`render_plugins`, so routes shouldn't be added that conflict with these.
"""
- openapi_controller: type[OpenAPIController] | None = None
- """Controller for generating OpenAPI routes.
-
- Must be subclass of :class:`OpenAPIController `.
-
- .. deprecated:: v2.8.0
- """
- root_schema_site: Literal["redoc", "swagger", "elements", "rapidoc"] | None = None
- """The static schema generator to use for the "root" path of ``/schema/``.
-
- .. deprecated:: v2.8.0
- """
- enabled_endpoints: set[str] | None = None
- """A set of the enabled documentation sites and schema download endpoints.
-
- .. deprecated:: v2.8.0
- """
def __post_init__(self) -> None:
- self._issue_deprecations()
-
- self.root_schema_site = self.root_schema_site or _DEFAULT_SCHEMA_SITE
-
- self.enabled_endpoints = (
- set(_enabled_plugin_map.keys()) if self.enabled_endpoints is None else self.enabled_endpoints
- )
-
- if self.path:
- self.path = normalize_path(self.path)
-
- if self.path and self.openapi_controller is not None:
- self.openapi_controller = type("OpenAPIController", (self.openapi_controller,), {"path": self.path})
+ self.path = normalize_path(self.path)
self.default_plugin: OpenAPIRenderPlugin | None = None
- if self.openapi_controller is None:
- if not self.render_plugins:
- self._plugin_backward_compatibility()
- else:
- # user is implicitly opted into the future plugin-based OpenAPI implementation
- # behavior by explicitly providing a list of render plugins
- for plugin in self.render_plugins:
- if plugin.has_path("/"):
- self.default_plugin = plugin
- break
- else:
- self.default_plugin = self.render_plugins[0]
-
- def _issue_deprecations(self) -> None:
- """Handle deprecated config options."""
- deprecated_in = "v2.8.0"
- removed_in = "v3.0.0"
- if self.openapi_controller is not None:
- warn_deprecation(
- deprecated_in,
- "openapi_controller",
- "attribute",
- removal_in=removed_in,
- alternative="render_plugins",
- )
-
- if self.root_schema_site is not None:
- warn_deprecation(
- deprecated_in,
- "root_schema_site",
- "attribute",
- removal_in=removed_in,
- alternative="render_plugins",
- info="Any 'render_plugin' with path '/' or first 'render_plugin' in list will be served at the OpenAPI root.",
- )
-
- if self.enabled_endpoints is not None:
- warn_deprecation(
- deprecated_in,
- "enabled_endpoints",
- "attribute",
- removal_in=removed_in,
- alternative="render_plugins",
- info="Configure a 'render_plugin' to enable an endpoint.",
- )
-
- def _plugin_backward_compatibility(self) -> None:
- """Backward compatibility for the plugin-based OpenAPI implementation.
-
- This preserves backward compatibility with the Controller-based OpenAPI implementation.
-
- We add a plugin for each enabled endpoint and set the default plugin to the plugin
- that has a path ending in the value of ``root_schema_site``.
- """
-
- def is_default_plugin(plugin_: OpenAPIRenderPlugin) -> bool:
- """Return True if the plugin is the default plugin."""
- root_schema_site = self.root_schema_site or _DEFAULT_SCHEMA_SITE
- return any(path.endswith(root_schema_site) for path in plugin_.paths)
-
- self.render_plugins = rps = []
- for key in self.enabled_endpoints or ():
- if plugin_type := _enabled_plugin_map[key]:
- plugin = plugin_type()
- rps.append(plugin)
- if is_default_plugin(plugin):
- self.default_plugin = plugin
+ for plugin in self.render_plugins:
+ if plugin.has_path("/"):
+ self.default_plugin = plugin
+ break
+ else:
+ if self.render_plugins:
+ self.default_plugin = self.render_plugins[0]
+
+ def get_path(self) -> str:
+ return self.openapi_router.path if self.openapi_router else self.path
def to_openapi_schema(self) -> OpenAPI:
"""Return an ``OpenAPI`` instance from the values stored in ``self``.
diff --git a/litestar/openapi/controller.py b/litestar/openapi/controller.py
index ca5c7e56ed..e69de29bb2 100644
--- a/litestar/openapi/controller.py
+++ b/litestar/openapi/controller.py
@@ -1,610 +0,0 @@
-from __future__ import annotations
-
-from functools import cached_property
-from typing import TYPE_CHECKING, Any, Callable, Final, Literal
-from uuid import uuid4
-
-from litestar.constants import OPENAPI_NOT_INITIALIZED
-from litestar.controller import Controller
-from litestar.enums import MediaType, OpenAPIMediaType
-from litestar.exceptions import ImproperlyConfiguredException
-from litestar.handlers import get
-from litestar.openapi.config import _DEFAULT_SCHEMA_SITE
-from litestar.response.base import ASGIResponse
-from litestar.serialization import encode_json
-from litestar.serialization.msgspec_hooks import decode_json
-from litestar.status_codes import HTTP_404_NOT_FOUND
-
-if TYPE_CHECKING:
- from litestar.connection.request import Request
- from litestar.openapi.spec.open_api import OpenAPI
-
-__all__ = ("OpenAPIController",)
-
-# NOTE: We are explicitly using a different name to the one defined in litestar.constants so that an openapi
-# controller can be added to a router on the same application as the openapi router.
-# See: https://github.com/litestar-org/litestar/issues/3337
-OPENAPI_JSON_HANDLER_NAME: Final = f"{uuid4().hex}_litestar_openapi_json"
-
-
-class OpenAPIController(Controller):
- """Controller for OpenAPI endpoints."""
-
- path: str = "/schema"
- """Base path for the OpenAPI documentation endpoints."""
- style: str = "body { margin: 0; padding: 0 }"
- """Base styling of the html body."""
- redoc_version: str = "next"
- """Redoc version to download from the CDN."""
- swagger_ui_version: str = "5.18.2"
- """SwaggerUI version to download from the CDN."""
- stoplight_elements_version: str = "7.7.18"
- """StopLight Elements version to download from the CDN."""
- rapidoc_version: str = "9.3.4"
- """RapiDoc version to download from the CDN."""
- favicon_url: str = ""
- """URL to download a favicon from."""
- redoc_google_fonts: bool = True
- """Download google fonts via CDN.
-
- Should be set to ``False`` when not using a CDN.
- """
- redoc_js_url: str = f"https://cdn.jsdelivr.net/npm/redoc@{redoc_version}/bundles/redoc.standalone.js"
- """Download url for the Redoc JS bundle."""
- swagger_css_url: str = f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{swagger_ui_version}/swagger-ui.css"
- """Download url for the Swagger UI CSS bundle."""
- swagger_ui_bundle_js_url: str = (
- f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{swagger_ui_version}/swagger-ui-bundle.js"
- )
- """Download url for the Swagger UI JS bundle."""
- swagger_ui_standalone_preset_js_url: str = (
- f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{swagger_ui_version}/swagger-ui-standalone-preset.js"
- )
- """Download url for the Swagger Standalone Preset JS bundle."""
- swagger_ui_init_oauth: dict[Any, Any] | bytes = {}
- """
- JSON to initialize Swagger UI OAuth2 by calling the `initOAuth` method.
-
- Refer to the following URL for details:
- `Swagger-UI `_.
- """
- stoplight_elements_css_url: str = (
- f"https://unpkg.com/@stoplight/elements@{stoplight_elements_version}/styles.min.css"
- )
- """Download url for the Stoplight Elements CSS bundle."""
- stoplight_elements_js_url: str = (
- f"https://unpkg.com/@stoplight/elements@{stoplight_elements_version}/web-components.min.js"
- )
- """Download url for the Stoplight Elements JS bundle."""
- rapidoc_js_url: str = f"https://unpkg.com/rapidoc@{rapidoc_version}/dist/rapidoc-min.js"
- """Download url for the RapiDoc JS bundle."""
-
- # internal
- _dumped_json_schema: str = ""
- _dumped_yaml_schema: bytes = b""
- # until swagger-ui supports v3.1.* of OpenAPI officially, we need to modify the schema for it and keep it
- # separate from the redoc version of the schema, which is unmodified.
- dto = None
- return_dto = None
-
- @staticmethod
- def get_schema_from_request(request: Request[Any, Any, Any]) -> OpenAPI:
- """Return the OpenAPI pydantic model from the request instance.
-
- Args:
- request: A :class:`Litestar <.connection.Request>` instance.
-
- Returns:
- An :class:`OpenAPI ` instance.
- """
- return request.app.openapi_schema
-
- def should_serve_endpoint(self, request: Request[Any, Any, Any]) -> bool:
- """Verify that the requested path is within the enabled endpoints in the openapi_config.
-
- Args:
- request: To be tested if endpoint enabled.
-
- Returns:
- A boolean.
-
- Raises:
- ImproperlyConfiguredException: If the application ``openapi_config`` attribute is ``None``.
- """
- if not request.app.openapi_config: # pragma: no cover
- raise ImproperlyConfiguredException("Litestar has not been instantiated with an OpenAPIConfig")
-
- asgi_root_path = set(filter(None, request.scope.get("root_path", "").split("/")))
- full_request_path = set(filter(None, request.url.path.split("/")))
- request_path = full_request_path.difference(asgi_root_path)
- root_path = set(filter(None, self.path.split("/")))
-
- config = request.app.openapi_config
- enabled_endpoints = config.enabled_endpoints or set()
- root_schema_site = config.root_schema_site or _DEFAULT_SCHEMA_SITE
-
- if request_path == root_path and root_schema_site in enabled_endpoints:
- return True
-
- return bool(request_path & enabled_endpoints)
-
- @property
- def favicon(self) -> str:
- """Return favicon ```` tag, if applicable.
-
- Returns:
- A ```` tag if ``self.favicon_url`` is not empty, otherwise returns a placeholder meta tag.
- """
- return f"" if self.favicon_url else ""
-
- @cached_property
- def render_methods_map(
- self,
- ) -> dict[Literal["redoc", "swagger", "elements", "rapidoc"], Callable[[Request], bytes]]:
- """Map render method names to render methods.
-
- Returns:
- A mapping of string keys to render methods.
- """
- return {
- "redoc": self.render_redoc,
- "swagger": self.render_swagger_ui,
- "elements": self.render_stoplight_elements,
- "rapidoc": self.render_rapidoc,
- }
-
- @get(
- path=["/openapi.yaml", "openapi.yml"],
- media_type=OpenAPIMediaType.OPENAPI_YAML,
- include_in_schema=False,
- sync_to_thread=False,
- )
- def retrieve_schema_yaml(self, request: Request[Any, Any, Any]) -> ASGIResponse:
- """Return the OpenAPI schema as YAML with an ``application/vnd.oai.openapi`` Content-Type header.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A Response instance with the YAML object rendered into a string.
- """
- from yaml import dump as dump_yaml
-
- if self.should_serve_endpoint(request):
- if not self._dumped_yaml_schema:
- schema_json = decode_json(self._get_schema_as_json(request))
- schema_yaml = dump_yaml(schema_json, default_flow_style=False)
- self._dumped_yaml_schema = schema_yaml.encode("utf-8")
- return ASGIResponse(body=self._dumped_yaml_schema, media_type=OpenAPIMediaType.OPENAPI_YAML)
- return ASGIResponse(body=b"", status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
-
- @get(
- path="/openapi.json",
- media_type=OpenAPIMediaType.OPENAPI_JSON,
- include_in_schema=False,
- sync_to_thread=False,
- name=OPENAPI_JSON_HANDLER_NAME,
- )
- def retrieve_schema_json(self, request: Request[Any, Any, Any]) -> ASGIResponse:
- """Return the OpenAPI schema as JSON with an ``application/vnd.oai.openapi+json`` Content-Type header.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A Response instance with the JSON object rendered into a string.
- """
- if self.should_serve_endpoint(request):
- return ASGIResponse(
- body=self._get_schema_as_json(request),
- media_type=OpenAPIMediaType.OPENAPI_JSON,
- )
- return ASGIResponse(body=b"", status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
-
- @get(path="/", include_in_schema=False, sync_to_thread=False)
- def root(self, request: Request[Any, Any, Any]) -> ASGIResponse:
- """Render a static documentation site.
-
- The site to be rendered is based on the ``root_schema_site`` value set in the application's
- :class:`OpenAPIConfig <.openapi.OpenAPIConfig>`. Defaults to ``redoc``.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A response with the rendered site defined in root_schema_site.
-
- Raises:
- ImproperlyConfiguredException: If the application ``openapi_config`` attribute is ``None``.
- """
- config = request.app.openapi_config
- if not config: # pragma: no cover
- raise ImproperlyConfiguredException(OPENAPI_NOT_INITIALIZED)
-
- render_method = self.render_methods_map[config.root_schema_site or _DEFAULT_SCHEMA_SITE]
-
- if self.should_serve_endpoint(request):
- return ASGIResponse(body=render_method(request), media_type=MediaType.HTML)
- return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
-
- @get(path="/swagger", include_in_schema=False, sync_to_thread=False)
- def swagger_ui(self, request: Request[Any, Any, Any]) -> ASGIResponse:
- """Route handler responsible for rendering Swagger-UI.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A response with a rendered swagger documentation site
- """
- if self.should_serve_endpoint(request):
- return ASGIResponse(body=self.render_swagger_ui(request), media_type=MediaType.HTML)
- return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
-
- @get(path="/elements", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False)
- def stoplight_elements(self, request: Request[Any, Any, Any]) -> ASGIResponse:
- """Route handler responsible for rendering StopLight Elements.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A response with a rendered stoplight elements documentation site
- """
- if self.should_serve_endpoint(request):
- return ASGIResponse(body=self.render_stoplight_elements(request), media_type=MediaType.HTML)
- return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
-
- @get(path="/redoc", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False)
- def redoc(self, request: Request[Any, Any, Any]) -> ASGIResponse: # pragma: no cover
- """Route handler responsible for rendering Redoc.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A response with a rendered redoc documentation site
- """
- if self.should_serve_endpoint(request):
- return ASGIResponse(body=self.render_redoc(request), media_type=MediaType.HTML)
- return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
-
- @get(path="/rapidoc", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False)
- def rapidoc(self, request: Request[Any, Any, Any]) -> ASGIResponse:
- if self.should_serve_endpoint(request):
- return ASGIResponse(body=self.render_rapidoc(request), media_type=MediaType.HTML)
- return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
-
- @get(path="/oauth2-redirect.html", media_type=MediaType.HTML, include_in_schema=False, sync_to_thread=False)
- def swagger_ui_oauth2_redirect(self, request: Request[Any, Any, Any]) -> ASGIResponse: # pragma: no cover
- """Route handler responsible for rendering oauth2-redirect.html page for Swagger-UI.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A response with a rendered oauth2-redirect.html page for Swagger-UI.
- """
- if self.should_serve_endpoint(request):
- return ASGIResponse(body=self.render_swagger_ui_oauth2_redirect(request), media_type=MediaType.HTML)
- return ASGIResponse(body=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
-
- def render_swagger_ui_oauth2_redirect(self, request: Request[Any, Any, Any]) -> bytes:
- """Render an HTML oauth2-redirect.html page for Swagger-UI.
-
- Notes:
- - override this method to customize the template.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A rendered html string.
- """
- return rb"""
-
-
- Swagger UI: OAuth2 Redirect
-
-
-
-
- """
-
- def render_swagger_ui(self, request: Request[Any, Any, Any]) -> bytes:
- """Render an HTML page for Swagger-UI.
-
- Notes:
- - override this method to customize the template.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A rendered html string.
- """
- schema = self.get_schema_from_request(request)
-
- head = f"""
-
- {schema.info.title}
- {self.favicon}
-
-
-
-
-
-
-
- """
-
- body = f"""
-
-
-
-
- """
-
- return f"""
-
-
- {head}
- {body}
-
- """.encode()
-
- def render_stoplight_elements(self, request: Request[Any, Any, Any]) -> bytes:
- """Render an HTML page for StopLight Elements.
-
- Notes:
- - override this method to customize the template.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A rendered html string.
- """
- schema = self.get_schema_from_request(request)
- head = f"""
-
- {schema.info.title}
- {self.favicon}
-
-
-
-
-
-
- """
-
- body = f"""
-
-
-
- """
-
- return f"""
-
-
- {head}
- {body}
-
- """.encode()
-
- def render_rapidoc(self, request: Request[Any, Any, Any]) -> bytes: # pragma: no cover
- schema = self.get_schema_from_request(request)
-
- head = f"""
-
- {schema.info.title}
- {self.favicon}
-
-
-
-
-
- """
-
- body = f"""
-
-
-
- """
-
- return f"""
-
-
- {head}
- {body}
-
- """.encode()
-
- def render_redoc(self, request: Request[Any, Any, Any]) -> bytes: # pragma: no cover
- """Render an HTML page for Redoc.
-
- Notes:
- - override this method to customize the template.
-
- Args:
- request:
- A :class:`Request <.connection.Request>` instance.
-
- Returns:
- A rendered html string.
- """
- schema = self.get_schema_from_request(request)
-
- head = f"""
-
- {schema.info.title}
- {self.favicon}
-
-
- """
-
- if self.redoc_google_fonts:
- head += """
-
- """
-
- head += f"""
-
-
-
- """
-
- body = f"""
-
-
-
-
- """
-
- return f"""
-
-
- {head}
- {body}
-
- """.encode()
-
- def render_404_page(self) -> bytes:
- """Render an HTML 404 page.
-
- Returns:
- A rendered html string.
- """
-
- return f"""
-
-
-
- 404 Not found
- {self.favicon}
-
-
-
-
-
-
Error 404
-
-
- """.encode()
-
- def _get_schema_as_json(self, request: Request) -> str:
- """Get the schema encoded as a JSON string."""
-
- if not self._dumped_json_schema:
- schema = self.get_schema_from_request(request).to_schema()
- json_encoded_schema = encode_json(schema, request.route_handler.default_serializer)
- self._dumped_json_schema = json_encoded_schema.decode("utf-8")
-
- return self._dumped_json_schema
diff --git a/litestar/response/base.py b/litestar/response/base.py
index 92c7c1de6b..cc595c486d 100644
--- a/litestar/response/base.py
+++ b/litestar/response/base.py
@@ -17,7 +17,6 @@
if TYPE_CHECKING:
from typing import Optional
- from litestar.app import Litestar
from litestar.background_tasks import BackgroundTask, BackgroundTasks
from litestar.connection import Request
from litestar.types import (
@@ -397,7 +396,6 @@ def render(self, content: Any, media_type: str, enc_hook: Serializer = default_s
def to_asgi_response(
self,
- app: Litestar | None,
request: Request,
*,
background: BackgroundTask | BackgroundTasks | None = None,
@@ -412,7 +410,6 @@ def to_asgi_response(
"""Create an ASGIResponse from a Response instance.
Args:
- app: The :class:`Litestar <.app.Litestar>` application instance.
background: Background task(s) to be executed after the response is sent.
cookies: A list of cookies to be set on the response.
encoded_headers: A list of already encoded headers.
@@ -427,15 +424,6 @@ def to_asgi_response(
An ASGIResponse instance.
"""
- if app is not None:
- warn_deprecation(
- version="2.1",
- deprecated_name="app",
- kind="parameter",
- removal_in="3.0.0",
- alternative="request.app",
- )
-
headers = {**headers, **self.headers} if headers is not None else self.headers
cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies)
diff --git a/litestar/response/file.py b/litestar/response/file.py
index 2cc60ec126..fe7940759c 100644
--- a/litestar/response/file.py
+++ b/litestar/response/file.py
@@ -13,7 +13,6 @@
from litestar.file_system import BaseLocalFileSystem, FileSystemAdapter
from litestar.response.base import Response
from litestar.response.streaming import ASGIStreamingResponse
-from litestar.utils.deprecation import warn_deprecation
from litestar.utils.helpers import get_enum_string_value
if TYPE_CHECKING:
@@ -22,7 +21,6 @@
from anyio import Path
- from litestar.app import Litestar
from litestar.background_tasks import BackgroundTask, BackgroundTasks
from litestar.connection import Request
from litestar.datastructures.cookie import Cookie
@@ -319,7 +317,6 @@ def __init__(
def to_asgi_response(
self,
- app: Litestar | None,
request: Request,
*,
background: BackgroundTask | BackgroundTasks | None = None,
@@ -348,14 +345,6 @@ def to_asgi_response(
Returns:
A low-level ASGI file response.
"""
- if app is not None:
- warn_deprecation(
- version="2.1",
- deprecated_name="app",
- kind="parameter",
- removal_in="3.0.0",
- alternative="request.app",
- )
headers = {**headers, **self.headers} if headers is not None else self.headers
cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies)
diff --git a/litestar/response/redirect.py b/litestar/response/redirect.py
index 6a070769e9..bd60bb16a8 100644
--- a/litestar/response/redirect.py
+++ b/litestar/response/redirect.py
@@ -9,11 +9,9 @@
from litestar.response.base import ASGIResponse, Response
from litestar.status_codes import HTTP_302_FOUND
from litestar.utils import url_quote
-from litestar.utils.deprecation import warn_deprecation
from litestar.utils.helpers import get_enum_string_value
if TYPE_CHECKING:
- from litestar.app import Litestar
from litestar.background_tasks import BackgroundTask, BackgroundTasks
from litestar.connection import Request
from litestar.datastructures import Cookie
@@ -129,7 +127,6 @@ def __init__(
def to_asgi_response(
self,
- app: Litestar | None,
request: Request,
*,
background: BackgroundTask | BackgroundTasks | None = None,
@@ -145,15 +142,6 @@ def to_asgi_response(
cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies)
media_type = get_enum_string_value(self.media_type or media_type or MediaType.TEXT)
- if app is not None:
- warn_deprecation(
- version="2.1",
- deprecated_name="app",
- kind="parameter",
- removal_in="3.0.0",
- alternative="request.app",
- )
-
return ASGIRedirectResponse(
path=self.url,
background=self.background or background,
diff --git a/litestar/response/streaming.py b/litestar/response/streaming.py
index fc76522416..5643d8e3ca 100644
--- a/litestar/response/streaming.py
+++ b/litestar/response/streaming.py
@@ -9,12 +9,10 @@
from litestar.enums import MediaType
from litestar.response.base import ASGIResponse, Response
from litestar.types.helper_types import StreamType
-from litestar.utils.deprecation import warn_deprecation
from litestar.utils.helpers import get_enum_string_value
from litestar.utils.sync import AsyncIteratorWrapper
if TYPE_CHECKING:
- from litestar.app import Litestar
from litestar.background_tasks import BackgroundTask, BackgroundTasks
from litestar.connection import Request
from litestar.datastructures.cookie import Cookie
@@ -177,7 +175,6 @@ def __init__(
def to_asgi_response(
self,
- app: Litestar | None,
request: Request,
*,
background: BackgroundTask | BackgroundTasks | None = None,
@@ -192,7 +189,6 @@ def to_asgi_response(
"""Create an ASGIStreamingResponse from a StremaingResponse instance.
Args:
- app: The :class:`Litestar <.app.Litestar>` application instance.
background: Background task(s) to be executed after the response is sent.
cookies: A list of cookies to be set on the response.
encoded_headers: A list of already encoded headers.
@@ -206,14 +202,6 @@ def to_asgi_response(
Returns:
An ASGIStreamingResponse instance.
"""
- if app is not None:
- warn_deprecation(
- version="2.1",
- deprecated_name="app",
- kind="parameter",
- removal_in="3.0.0",
- alternative="request.app",
- )
headers = {**headers, **self.headers} if headers is not None else self.headers
cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies)
diff --git a/litestar/response/template.py b/litestar/response/template.py
index 06743a084f..935b036f58 100644
--- a/litestar/response/template.py
+++ b/litestar/response/template.py
@@ -9,12 +9,10 @@
from litestar.exceptions import ImproperlyConfiguredException
from litestar.response.base import ASGIResponse, Response
from litestar.status_codes import HTTP_200_OK
-from litestar.utils.deprecation import warn_deprecation
from litestar.utils.empty import value_or_default
from litestar.utils.scope.state import ScopeState
if TYPE_CHECKING:
- from litestar.app import Litestar
from litestar.background_tasks import BackgroundTask, BackgroundTasks
from litestar.connection import Request
from litestar.datastructures import Cookie
@@ -99,7 +97,6 @@ def create_template_context(self, request: Request) -> dict[str, Any]:
def to_asgi_response(
self,
- app: Litestar | None,
request: Request,
*,
background: BackgroundTask | BackgroundTasks | None = None,
@@ -111,15 +108,6 @@ def to_asgi_response(
status_code: int | None = None,
type_encoders: TypeEncodersMap | None = None,
) -> ASGIResponse:
- if app is not None:
- warn_deprecation(
- version="2.1",
- deprecated_name="app",
- kind="parameter",
- removal_in="3.0.0",
- alternative="request.app",
- )
-
if not (template_engine := request.app.template_engine):
raise ImproperlyConfiguredException("Template engine is not configured")
diff --git a/litestar/router.py b/litestar/router.py
index 6b9ca1c953..b5cf7fe1dd 100644
--- a/litestar/router.py
+++ b/litestar/router.py
@@ -9,6 +9,7 @@
from litestar.exceptions import ImproperlyConfiguredException
from litestar.handlers.asgi_handlers import ASGIRouteHandler
from litestar.handlers.http_handlers import HTTPRouteHandler
+from litestar.handlers.http_handlers._options import create_options_handler
from litestar.handlers.websocket_handlers import WebsocketListener, WebsocketRouteHandler
from litestar.routes import ASGIRoute, HTTPRoute, WebSocketRoute
from litestar.types.empty import Empty
@@ -241,11 +242,11 @@ def register(self, value: ControllerRouterHandler) -> list[BaseRoute]:
route: WebSocketRoute | ASGIRoute | HTTPRoute = HTTPRoute(
path=path,
- route_handlers=http_handlers,
+ route_handlers=_maybe_add_options_handler(path, http_handlers),
)
self.routes[existing_route_index] = route
else:
- route = HTTPRoute(path=path, route_handlers=http_handlers)
+ route = HTTPRoute(path=path, route_handlers=_maybe_add_options_handler(path, http_handlers))
self.routes.append(route)
routes.append(route)
@@ -264,17 +265,15 @@ def register(self, value: ControllerRouterHandler) -> list[BaseRoute]:
@property
def route_handler_method_map(self) -> dict[str, RouteHandlerMapItem]:
- """Map route paths to :class:`RouteHandlerMapItem `
+ """Map route paths to :class:`~litestar.types.internal_types.RouteHandlerMapItem`
Returns:
A dictionary mapping paths to route handlers
"""
- route_map: dict[str, RouteHandlerMapItem] = defaultdict(dict)
+ route_map: defaultdict[str, RouteHandlerMapItem] = defaultdict(dict)
for route in self.routes:
if isinstance(route, HTTPRoute):
- for route_handler in route.route_handlers:
- for method in route_handler.http_methods:
- route_map[route.path][method] = route_handler
+ route_map[route.path] = route.route_handler_map # type: ignore[assignment]
else:
route_map[route.path]["websocket" if isinstance(route, WebSocketRoute) else "asgi"] = (
route.route_handler
@@ -326,3 +325,12 @@ def _validate_registration_value(self, value: ControllerRouterHandler) -> RouteH
"If you passed in a function or method, "
"make sure to decorate it first with one of the routing decorators"
)
+
+
+def _maybe_add_options_handler(path: str, http_handlers: list[HTTPRouteHandler]) -> list[HTTPRouteHandler]:
+ handler_methods = {method for handler in http_handlers for method in handler.http_methods}
+ if "OPTIONS" not in handler_methods:
+ options_handler = create_options_handler(path=path, allow_methods={*handler_methods, "OPTIONS"}) # pyright: ignore
+ options_handler.owner = http_handlers[0].owner
+ return [*http_handlers, options_handler]
+ return http_handlers
diff --git a/litestar/routes/asgi.py b/litestar/routes/asgi.py
index a8564d0e61..fd95f4c20e 100644
--- a/litestar/routes/asgi.py
+++ b/litestar/routes/asgi.py
@@ -3,15 +3,15 @@
from typing import TYPE_CHECKING, Any
from litestar.connection import ASGIConnection
-from litestar.enums import ScopeType
from litestar.routes.base import BaseRoute
+from litestar.types import Scope
if TYPE_CHECKING:
from litestar.handlers.asgi_handlers import ASGIRouteHandler
- from litestar.types import Receive, Scope, Send
+ from litestar.types import Receive, Send
-class ASGIRoute(BaseRoute):
+class ASGIRoute(BaseRoute[Scope]):
"""An ASGI route, handling a single ``ASGIRouteHandler``"""
__slots__ = ("route_handler",)
@@ -29,11 +29,7 @@ def __init__(
route_handler: An instance of :class:`~.handlers.ASGIRouteHandler`.
"""
self.route_handler = route_handler
- super().__init__(
- path=path,
- scope_type=ScopeType.ASGI,
- handler_names=[route_handler.handler_name],
- )
+ super().__init__(path=path)
async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
"""ASGI app that authorizes the connection and then awaits the handler function.
@@ -46,9 +42,5 @@ async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
Returns:
None
"""
-
- if self.route_handler.resolve_guards():
- connection = ASGIConnection["ASGIRouteHandler", Any, Any, Any](scope=scope, receive=receive)
- await self.route_handler.authorize_connection(connection=connection)
-
- await self.route_handler.fn(scope=scope, receive=receive, send=send)
+ connection = ASGIConnection["ASGIRouteHandler", Any, Any, Any](scope=scope, receive=receive, send=send)
+ await self.route_handler.handle(connection=connection)
diff --git a/litestar/routes/base.py b/litestar/routes/base.py
index 40ecd706c6..df6756f5f8 100644
--- a/litestar/routes/base.py
+++ b/litestar/routes/base.py
@@ -5,7 +5,7 @@
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable
+from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar
from uuid import UUID
import msgspec
@@ -14,9 +14,10 @@
from litestar.types.internal_types import PathParameterDefinition
from litestar.utils import join_paths, normalize_path
+ScopeT = TypeVar("ScopeT", bound="BaseScope")
+
if TYPE_CHECKING:
- from litestar.enums import ScopeType
- from litestar.types import Method, Receive, Scope, Send
+ from litestar.types import BaseScope, Receive, Send
def _parse_datetime(value: str) -> datetime:
@@ -66,7 +67,7 @@ def _parse_timedelta(value: str) -> timedelta:
}
-class BaseRoute(ABC):
+class BaseRoute(ABC, Generic[ScopeT]):
"""Base Route class used by Litestar.
It's an abstract class meant to be extended.
@@ -86,26 +87,17 @@ class BaseRoute(ABC):
def __init__(
self,
*,
- handler_names: list[str],
path: str,
- scope_type: ScopeType,
- methods: list[Method] | None = None,
) -> None:
"""Initialize the route.
Args:
- handler_names: Names of the associated handler functions
path: Base path of the route
- scope_type: Type of the ASGI scope
- methods: Supported methods
"""
self.path, self.path_format, self.path_components, self.path_parameters = self._parse_path(path)
- self.handler_names = handler_names
- self.scope_type = scope_type
- self.methods = set(methods or [])
@abstractmethod
- async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
+ async def handle(self, scope: ScopeT, receive: Receive, send: Send) -> None:
"""ASGI App of the route.
Args:
diff --git a/litestar/routes/http.py b/litestar/routes/http.py
index 6ed17cfaa2..f2c8977c8e 100644
--- a/litestar/routes/http.py
+++ b/litestar/routes/http.py
@@ -1,28 +1,17 @@
from __future__ import annotations
-from itertools import chain
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Iterable
-from msgspec.msgpack import decode as _decode_msgpack_plain
-
-from litestar.datastructures.multi_dicts import FormMultiDict
-from litestar.enums import HttpMethod, MediaType, ScopeType
-from litestar.exceptions import ClientException, ImproperlyConfiguredException, SerializationException
-from litestar.handlers.http_handlers import HTTPRouteHandler
-from litestar.response import Response
+from litestar.exceptions import ImproperlyConfiguredException
from litestar.routes.base import BaseRoute
-from litestar.status_codes import HTTP_204_NO_CONTENT
-from litestar.types.empty import Empty
-from litestar.utils.scope.state import ScopeState
+from litestar.types import HTTPScope
if TYPE_CHECKING:
- from litestar._kwargs import KwargsModel
- from litestar._kwargs.cleanup import DependencyCleanupGroup
- from litestar.connection import Request
- from litestar.types import ASGIApp, HTTPScope, Method, Receive, Scope, Send
+ from litestar.handlers.http_handlers import HTTPRouteHandler
+ from litestar.types import Method, Receive, Send
-class HTTPRoute(BaseRoute):
+class HTTPRoute(BaseRoute[HTTPScope]):
"""An HTTP route, capable of handling multiple ``HTTPRouteHandler``\\ s.""" # noqa: D301
__slots__ = (
@@ -34,7 +23,7 @@ def __init__(
self,
*,
path: str,
- route_handlers: list[HTTPRouteHandler],
+ route_handlers: Iterable[HTTPRouteHandler],
) -> None:
"""Initialize ``HTTPRoute``.
@@ -42,24 +31,12 @@ def __init__(
path: The path for the route.
route_handlers: A list of :class:`~.handlers.HTTPRouteHandler`.
"""
- methods = list(chain.from_iterable([route_handler.http_methods for route_handler in route_handlers]))
- if "OPTIONS" not in methods:
- methods.append("OPTIONS")
- options_handler = self.create_options_handler(path)
- options_handler.owner = route_handlers[0].owner
- route_handlers.append(options_handler)
-
- self.route_handlers = route_handlers
- self.route_handler_map: dict[Method, tuple[HTTPRouteHandler, KwargsModel]] = {}
+ super().__init__(path=path)
+ self.route_handler_map: dict[Method, HTTPRouteHandler] = self.create_handler_map(route_handlers)
+ self.route_handlers = tuple(self.route_handler_map.values())
+ self.methods = tuple(self.route_handler_map)
- super().__init__(
- methods=methods,
- path=path,
- scope_type=ScopeType.HTTP,
- handler_names=[route_handler.handler_name for route_handler in self.route_handlers],
- )
-
- async def handle(self, scope: HTTPScope, receive: Receive, send: Send) -> None: # type: ignore[override]
+ async def handle(self, scope: HTTPScope, receive: Receive, send: Send) -> None:
"""ASGI app that creates a Request from the passed in args, determines which handler function to call and then
handles the call.
@@ -71,191 +48,20 @@ async def handle(self, scope: HTTPScope, receive: Receive, send: Send) -> None:
Returns:
None
"""
- route_handler, parameter_model = self.route_handler_map[scope["method"]]
- request: Request[Any, Any, Any] = route_handler.resolve_request_class()(scope=scope, receive=receive, send=send)
-
- if route_handler.resolve_guards():
- await route_handler.authorize_connection(connection=request)
-
- try:
- response = await self._get_response_for_request(
- scope=scope, request=request, route_handler=route_handler, parameter_model=parameter_model
- )
-
- await response(scope, receive, send)
-
- if after_response_handler := route_handler.resolve_after_response():
- await after_response_handler(request)
- finally:
- if (form_data := ScopeState.from_scope(scope).form) is not Empty:
- await FormMultiDict.from_form_data(form_data).close()
+ route_handler = self.route_handler_map[scope["method"]]
+ connection = route_handler.resolve_request_class()(scope=scope, receive=receive, send=send)
+ await route_handler.handle(connection=connection)
- def create_handler_map(self) -> None:
+ def create_handler_map(self, route_handlers: Iterable[HTTPRouteHandler]) -> dict[Method, HTTPRouteHandler]:
"""Parse the ``router_handlers`` of this route and return a mapping of
http- methods and route handlers.
"""
- for route_handler in self.route_handlers:
- kwargs_model = route_handler.create_kwargs_model(path_parameters=self.path_parameters)
+ handler_map = {}
+ for route_handler in route_handlers:
for http_method in route_handler.http_methods:
- if self.route_handler_map.get(http_method):
+ if http_method in handler_map:
raise ImproperlyConfiguredException(
f"Handler already registered for path {self.path!r} and http method {http_method}"
)
- self.route_handler_map[http_method] = (route_handler, kwargs_model)
-
- async def _get_response_for_request(
- self,
- scope: Scope,
- request: Request[Any, Any, Any],
- route_handler: HTTPRouteHandler,
- parameter_model: KwargsModel,
- ) -> ASGIApp:
- """Return a response for the request.
-
- If caching is enabled and a response exist in the cache, the cached response will be returned.
- If caching is enabled and a response does not exist in the cache, the newly created
- response will be cached.
-
- Args:
- scope: The Request's scope
- request: The Request instance
- route_handler: The HTTPRouteHandler instance
- parameter_model: The Handler's KwargsModel
-
- Returns:
- An instance of Response or a compatible ASGIApp or a subclass of it
- """
- if route_handler.cache and (
- response := await self._get_cached_response(request=request, route_handler=route_handler)
- ):
- return response
-
- return await self._call_handler_function(
- scope=scope, request=request, parameter_model=parameter_model, route_handler=route_handler
- )
-
- async def _call_handler_function(
- self, scope: Scope, request: Request, parameter_model: KwargsModel, route_handler: HTTPRouteHandler
- ) -> ASGIApp:
- """Call the before request handlers, retrieve any data required for the route handler, and call the route
- handler's ``to_response`` method.
-
- This is wrapped in a try except block - and if an exception is raised,
- it tries to pass it to an appropriate exception handler - if defined.
- """
- response_data: Any = None
- cleanup_group: DependencyCleanupGroup | None = None
-
- if before_request_handler := route_handler.resolve_before_request():
- response_data = await before_request_handler(request)
-
- if not response_data:
- response_data, cleanup_group = await self._get_response_data(
- route_handler=route_handler, parameter_model=parameter_model, request=request
- )
-
- response: ASGIApp = await route_handler.to_response(app=scope["app"], data=response_data, request=request)
-
- if cleanup_group:
- await cleanup_group.cleanup()
-
- return response
-
- @staticmethod
- async def _get_response_data(
- route_handler: HTTPRouteHandler, parameter_model: KwargsModel, request: Request
- ) -> tuple[Any, DependencyCleanupGroup | None]:
- """Determine what kwargs are required for the given route handler's ``fn`` and calls it."""
- parsed_kwargs: dict[str, Any] = {}
- cleanup_group: DependencyCleanupGroup | None = None
-
- if parameter_model.has_kwargs and route_handler.signature_model:
- try:
- kwargs = await parameter_model.to_kwargs(connection=request)
- except SerializationException as e:
- raise ClientException(str(e)) from e
-
- if kwargs.get("data") is Empty:
- del kwargs["data"]
-
- if parameter_model.dependency_batches:
- cleanup_group = await parameter_model.resolve_dependencies(request, kwargs)
-
- parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
- connection=request, kwargs=kwargs
- )
-
- if cleanup_group:
- async with cleanup_group:
- data = (
- route_handler.fn(**parsed_kwargs)
- if route_handler.has_sync_callable
- else await route_handler.fn(**parsed_kwargs)
- )
- elif route_handler.has_sync_callable:
- data = route_handler.fn(**parsed_kwargs)
- else:
- data = await route_handler.fn(**parsed_kwargs)
-
- return data, cleanup_group
-
- @staticmethod
- async def _get_cached_response(request: Request, route_handler: HTTPRouteHandler) -> ASGIApp | None:
- """Retrieve and un-pickle the cached response, if existing.
-
- Args:
- request: The :class:`Request ` instance
- route_handler: The :class:`~.handlers.HTTPRouteHandler` instance
-
- Returns:
- A cached response instance, if existing.
- """
-
- cache_config = request.app.response_cache_config
- cache_key = (route_handler.cache_key_builder or cache_config.key_builder)(request)
- store = cache_config.get_store_from_app(request.app)
-
- if not (cached_response_data := await store.get(key=cache_key)):
- return None
-
- # we use the regular msgspec.msgpack.decode here since we don't need any of
- # the added decoders
- messages = _decode_msgpack_plain(cached_response_data)
-
- async def cached_response(scope: Scope, receive: Receive, send: Send) -> None:
- ScopeState.from_scope(scope).is_cached = True
- for message in messages:
- await send(message)
-
- return cached_response
-
- def create_options_handler(self, path: str) -> HTTPRouteHandler:
- """Args:
- path: The route path
-
- Returns:
- An HTTP route handler for OPTIONS requests.
- """
-
- def options_handler(scope: Scope) -> Response:
- """Handler function for OPTIONS requests.
-
- Args:
- scope: The ASGI Scope.
-
- Returns:
- Response
- """
- return Response(
- content=None,
- status_code=HTTP_204_NO_CONTENT,
- headers={"Allow": ", ".join(sorted(self.methods))}, # pyright: ignore
- media_type=MediaType.TEXT,
- )
-
- return HTTPRouteHandler(
- path=path,
- http_method=[HttpMethod.OPTIONS],
- include_in_schema=False,
- sync_to_thread=False,
- )(options_handler)
+ handler_map[http_method] = route_handler
+ return handler_map
diff --git a/litestar/routes/websocket.py b/litestar/routes/websocket.py
index 67019b10b8..477c1da208 100644
--- a/litestar/routes/websocket.py
+++ b/litestar/routes/websocket.py
@@ -1,26 +1,19 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
-from litestar.enums import ScopeType
-from litestar.exceptions import ImproperlyConfiguredException
from litestar.routes.base import BaseRoute
+from litestar.types import WebSocketScope
if TYPE_CHECKING:
- from litestar._kwargs import KwargsModel
- from litestar._kwargs.cleanup import DependencyCleanupGroup
- from litestar.connection import WebSocket
from litestar.handlers.websocket_handlers import WebsocketRouteHandler
- from litestar.types import Receive, Send, WebSocketScope
+ from litestar.types import Receive, Send
-class WebSocketRoute(BaseRoute):
+class WebSocketRoute(BaseRoute[WebSocketScope]):
"""A websocket route, handling a single ``WebsocketRouteHandler``"""
- __slots__ = (
- "handler_parameter_model",
- "route_handler",
- )
+ __slots__ = ("route_handler",)
def __init__(
self,
@@ -35,15 +28,10 @@ def __init__(
route_handler: An instance of :class:`~.handlers.WebsocketRouteHandler`.
"""
self.route_handler = route_handler
- self.handler_parameter_model: KwargsModel | None = None
- super().__init__(
- path=path,
- scope_type=ScopeType.WEBSOCKET,
- handler_names=[route_handler.handler_name],
- )
+ super().__init__(path=path)
- async def handle(self, scope: WebSocketScope, receive: Receive, send: Send) -> None: # type: ignore[override]
+ async def handle(self, scope: WebSocketScope, receive: Receive, send: Send) -> None:
"""ASGI app that creates a WebSocket from the passed in args, and then awaits the handler function.
Args:
@@ -54,33 +42,5 @@ async def handle(self, scope: WebSocketScope, receive: Receive, send: Send) -> N
Returns:
None
"""
-
- if not self.handler_parameter_model: # pragma: no cover
- raise ImproperlyConfiguredException("handler parameter model not defined")
-
- websocket: WebSocket[Any, Any, Any] = self.route_handler.resolve_websocket_class()(
- scope=scope, receive=receive, send=send
- )
-
- if self.route_handler.resolve_guards():
- await self.route_handler.authorize_connection(connection=websocket)
-
- parsed_kwargs: dict[str, Any] = {}
- cleanup_group: DependencyCleanupGroup | None = None
-
- if self.handler_parameter_model.has_kwargs and self.route_handler.signature_model:
- parsed_kwargs = await self.handler_parameter_model.to_kwargs(connection=websocket)
-
- if self.handler_parameter_model.dependency_batches:
- cleanup_group = await self.handler_parameter_model.resolve_dependencies(websocket, parsed_kwargs)
-
- parsed_kwargs = self.route_handler.signature_model.parse_values_from_connection_kwargs(
- connection=websocket, kwargs=parsed_kwargs
- )
-
- if cleanup_group:
- async with cleanup_group:
- await self.route_handler.fn(**parsed_kwargs)
- await cleanup_group.cleanup()
- else:
- await self.route_handler.fn(**parsed_kwargs)
+ socket = self.route_handler.resolve_websocket_class()(scope=scope, receive=receive, send=send)
+ await self.route_handler.handle(connection=socket)
diff --git a/litestar/security/session_auth/middleware.py b/litestar/security/session_auth/middleware.py
index 1835dd31b5..1062fc7485 100644
--- a/litestar/security/session_auth/middleware.py
+++ b/litestar/security/session_auth/middleware.py
@@ -54,7 +54,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
scopes=self.config.scopes,
retrieve_user_handler=self.config.retrieve_user_handler, # type: ignore[arg-type]
)
- exception_middleware = ExceptionHandlerMiddleware(app=auth_middleware, debug=None)
+ exception_middleware = ExceptionHandlerMiddleware(app=auth_middleware)
self.app = self.config.session_backend_config.middleware.middleware(
app=exception_middleware,
backend=self.config.session_backend,
diff --git a/litestar/static_files.py b/litestar/static_files.py
new file mode 100644
index 0000000000..d8ee22c0ac
--- /dev/null
+++ b/litestar/static_files.py
@@ -0,0 +1,254 @@
+from __future__ import annotations
+
+import os
+from os.path import commonpath
+from pathlib import Path, PurePath
+from typing import TYPE_CHECKING, Any, Literal, Mapping, Sequence
+
+from litestar.exceptions import ImproperlyConfiguredException, NotFoundException
+from litestar.file_system import BaseLocalFileSystem, FileSystemAdapter
+from litestar.handlers import get, head
+from litestar.response.file import ASGIFileResponse
+from litestar.router import Router
+from litestar.status_codes import HTTP_404_NOT_FOUND
+from litestar.types import Empty, FileInfo
+from litestar.utils import normalize_path
+
+__all__ = ("create_static_files_router",)
+
+if TYPE_CHECKING:
+ from litestar.datastructures import CacheControlHeader
+ from litestar.openapi.spec import SecurityRequirement
+ from litestar.types import (
+ AfterRequestHookHandler,
+ AfterResponseHookHandler,
+ BeforeRequestHookHandler,
+ EmptyType,
+ ExceptionHandlersMap,
+ Guard,
+ Middleware,
+ PathType,
+ )
+
+
+def create_static_files_router(
+ path: str,
+ directories: Sequence[PathType],
+ file_system: Any = None,
+ send_as_attachment: bool = False,
+ html_mode: bool = False,
+ name: str = "static",
+ after_request: AfterRequestHookHandler | None = None,
+ after_response: AfterResponseHookHandler | None = None,
+ before_request: BeforeRequestHookHandler | None = None,
+ cache_control: CacheControlHeader | None = None,
+ exception_handlers: ExceptionHandlersMap | None = None,
+ guards: Sequence[Guard] | None = None,
+ include_in_schema: bool | EmptyType = Empty,
+ middleware: Sequence[Middleware] | None = None,
+ opt: Mapping[str, Any] | None = None,
+ security: Sequence[SecurityRequirement] | None = None,
+ tags: Sequence[str] | None = None,
+ router_class: type[Router] = Router,
+ resolve_symlinks: bool = True,
+) -> Router:
+ """Create a router with handlers to serve static files.
+
+ Args:
+ path: Path to serve static files under
+ directories: Directories to serve static files from
+ file_system: A *file system* implementing
+ :class:`~litestar.types.FileSystemProtocol`.
+ `fsspec `_ can be passed
+ here as well
+ send_as_attachment: Whether to send the file as an attachment
+ html_mode: When in HTML:
+ - Serve an ``index.html`` file from ``/``
+ - Serve ``404.html`` when a file could not be found
+ name: Name to pass to the generated handlers
+ after_request: ``after_request`` handlers passed to the router
+ after_response: ``after_response`` handlers passed to the router
+ before_request: ``before_request`` handlers passed to the router
+ cache_control: ``cache_control`` passed to the router
+ exception_handlers: Exception handlers passed to the router
+ guards: Guards passed to the router
+ include_in_schema: Include the routes / router in the OpenAPI schema
+ middleware: Middlewares passed to the router
+ opt: Opts passed to the router
+ security: Security options passed to the router
+ tags: ``tags`` passed to the router
+ router_class: The class used to construct a router from
+ resolve_symlinks: Resolve symlinks of ``directories``
+ """
+
+ if file_system is None:
+ file_system = BaseLocalFileSystem()
+
+ directories = tuple(os.path.normpath(Path(p).resolve() if resolve_symlinks else Path(p)) for p in directories)
+
+ _validate_config(path=path, directories=directories, file_system=file_system)
+ path = normalize_path(path)
+
+ headers = None
+ if cache_control:
+ headers = {cache_control.HEADER_NAME: cache_control.to_header()}
+
+ resolved_directories = tuple(Path(p).resolve() if resolve_symlinks else Path(p) for p in directories)
+ adapter = FileSystemAdapter(file_system)
+
+ @get("{file_path:path}", name=name)
+ async def get_handler(file_path: PurePath) -> ASGIFileResponse:
+ return await _handler(
+ path=file_path.as_posix(),
+ is_head_response=False,
+ directories=resolved_directories,
+ adapter=adapter,
+ is_html_mode=html_mode,
+ send_as_attachment=send_as_attachment,
+ headers=headers,
+ )
+
+ @head("/{file_path:path}", name=f"{name}/head")
+ async def head_handler(file_path: PurePath) -> ASGIFileResponse:
+ return await _handler(
+ path=file_path.as_posix(),
+ is_head_response=True,
+ directories=resolved_directories,
+ adapter=adapter,
+ is_html_mode=html_mode,
+ send_as_attachment=send_as_attachment,
+ headers=headers,
+ )
+
+ handlers = [get_handler, head_handler]
+
+ if html_mode:
+
+ @get("/", name=f"{name}/index")
+ async def index_handler() -> ASGIFileResponse:
+ return await _handler(
+ path="/",
+ is_head_response=False,
+ directories=resolved_directories,
+ adapter=adapter,
+ is_html_mode=True,
+ send_as_attachment=send_as_attachment,
+ headers=headers,
+ )
+
+ handlers.append(index_handler)
+
+ return router_class(
+ after_request=after_request,
+ after_response=after_response,
+ before_request=before_request,
+ cache_control=cache_control,
+ exception_handlers=exception_handlers,
+ guards=guards,
+ include_in_schema=include_in_schema,
+ middleware=middleware,
+ opt=opt,
+ path=path,
+ route_handlers=handlers,
+ security=security,
+ tags=tags,
+ )
+
+
+async def _handler(
+ *,
+ path: str,
+ is_head_response: bool,
+ directories: tuple[Path, ...],
+ send_as_attachment: bool,
+ adapter: FileSystemAdapter,
+ is_html_mode: bool,
+ headers: dict[str, str] | None,
+) -> ASGIFileResponse:
+ split_path = path.split("/")
+ filename = split_path[-1]
+ joined_path = Path(*split_path)
+ resolved_path, fs_info = await _get_fs_info(directories=directories, file_path=joined_path, adapter=adapter)
+ content_disposition_type: Literal["inline", "attachment"] = "attachment" if send_as_attachment else "inline"
+
+ if is_html_mode and fs_info and fs_info["type"] == "directory":
+ filename = "index.html"
+ resolved_path, fs_info = await _get_fs_info(
+ directories=directories,
+ file_path=Path(resolved_path or joined_path) / filename,
+ adapter=adapter,
+ )
+
+ if fs_info and fs_info["type"] == "file":
+ return ASGIFileResponse(
+ file_path=resolved_path or joined_path,
+ file_info=fs_info,
+ file_system=adapter.file_system,
+ filename=filename,
+ content_disposition_type=content_disposition_type,
+ is_head_response=is_head_response,
+ headers=headers,
+ )
+
+ if is_html_mode:
+ # for some reason coverage doesn't catch these two lines
+ filename = "404.html" # pragma: no cover
+ resolved_path, fs_info = await _get_fs_info( # pragma: no cover
+ directories=directories,
+ file_path=filename,
+ adapter=adapter,
+ )
+
+ if fs_info and fs_info["type"] == "file":
+ return ASGIFileResponse(
+ file_path=resolved_path or joined_path,
+ file_info=fs_info,
+ file_system=adapter.file_system,
+ filename=filename,
+ status_code=HTTP_404_NOT_FOUND,
+ content_disposition_type=content_disposition_type,
+ is_head_response=is_head_response,
+ headers=headers,
+ )
+
+ raise NotFoundException(
+ f"no file or directory match the path {resolved_path or joined_path} was found"
+ ) # pragma: no cover
+
+
+async def _get_fs_info(
+ directories: Sequence[PathType],
+ file_path: PathType,
+ adapter: FileSystemAdapter,
+) -> tuple[Path, FileInfo] | tuple[None, None]:
+ """Return the resolved path and a :class:`stat_result `"""
+ for directory in directories:
+ try:
+ joined_path = Path(directory, file_path)
+ file_info = await adapter.info(joined_path)
+ normalized_file_path = os.path.normpath(joined_path)
+ directory_path = str(directory)
+ if (
+ file_info
+ and commonpath([directory_path, file_info["name"], joined_path]) == directory_path
+ and os.path.commonpath([directory, normalized_file_path]) == directory_path
+ and (file_info := await adapter.info(joined_path))
+ ):
+ return joined_path, file_info
+ except FileNotFoundError:
+ continue
+ return None, None
+
+
+def _validate_config(path: str, directories: tuple[PathType, ...], file_system: Any) -> None:
+ if not path:
+ raise ImproperlyConfiguredException("path must be a non-zero length string")
+
+ if not directories or not any(bool(d) for d in directories):
+ raise ImproperlyConfiguredException("directories must include at least one path")
+
+ if "{" in path:
+ raise ImproperlyConfiguredException("path parameters are not supported for static files")
+
+ if not (callable(getattr(file_system, "info", None)) and callable(getattr(file_system, "open", None))):
+ raise ImproperlyConfiguredException("file_system must adhere to the FileSystemProtocol type")
diff --git a/litestar/static_files/__init__.py b/litestar/static_files/__init__.py
deleted file mode 100644
index 3cd45945f7..0000000000
--- a/litestar/static_files/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from litestar.static_files.base import StaticFiles
-from litestar.static_files.config import StaticFilesConfig, create_static_files_router
-
-__all__ = ("StaticFiles", "StaticFilesConfig", "create_static_files_router")
diff --git a/litestar/static_files/base.py b/litestar/static_files/base.py
index 896b2ebbbc..e69de29bb2 100644
--- a/litestar/static_files/base.py
+++ b/litestar/static_files/base.py
@@ -1,151 +0,0 @@
-# ruff: noqa: PTH118
-from __future__ import annotations
-
-import os.path
-from pathlib import Path
-from typing import TYPE_CHECKING, Literal, Sequence
-
-from litestar.enums import ScopeType
-from litestar.exceptions import MethodNotAllowedException, NotFoundException
-from litestar.file_system import FileSystemAdapter
-from litestar.response.file import ASGIFileResponse
-from litestar.status_codes import HTTP_404_NOT_FOUND
-
-__all__ = ("StaticFiles",)
-
-if TYPE_CHECKING:
- from litestar.types import Receive, Scope, Send
- from litestar.types.composite_types import PathType
- from litestar.types.file_types import FileInfo, FileSystemProtocol
-
-
-class StaticFiles:
- """ASGI App that handles file sending."""
-
- __slots__ = ("adapter", "directories", "headers", "is_html_mode", "send_as_attachment")
-
- def __init__(
- self,
- is_html_mode: bool,
- directories: Sequence[PathType],
- file_system: FileSystemProtocol,
- send_as_attachment: bool = False,
- resolve_symlinks: bool = True,
- headers: dict[str, str] | None = None,
- ) -> None:
- """Initialize the Application.
-
- Args:
- is_html_mode: Flag dictating whether serving html. If true, the default file will be ``index.html``.
- directories: A list of directories to serve files from.
- file_system: The file_system spec to use for serving files.
- send_as_attachment: Whether to send the file with a ``content-disposition`` header of
- ``attachment`` or ``inline``
- resolve_symlinks: Resolve symlinks to the directories
- headers: Headers that will be sent with every response.
- """
- self.adapter = FileSystemAdapter(file_system)
- self.directories = tuple(
- os.path.normpath(Path(p).resolve() if resolve_symlinks else Path(p)) for p in directories
- )
- self.is_html_mode = is_html_mode
- self.send_as_attachment = send_as_attachment
- self.headers = headers
-
- async def get_fs_info(
- self, directories: Sequence[PathType], file_path: PathType
- ) -> tuple[Path, FileInfo] | tuple[None, None]:
- """Return the resolved path and a :class:`stat_result `.
-
- .. versionchanged:: 2.8.3
-
- Prevent `CVE-2024-32982 `_
- by ensuring that the resolved path is within the configured directory as part of `advisory
- GHSA-83pv-qr33-2vcf `_.
-
- Args:
- directories: A list of directory paths.
- file_path: A file path to resolve
-
- Returns:
- A tuple with an optional resolved :class:`Path ` instance and an optional
- :class:`stat_result `.
- """
- for directory in directories:
- try:
- joined_path = Path(directory, file_path)
- normalized_file_path = os.path.normpath(joined_path)
- if os.path.commonpath([directory, normalized_file_path]) == str(directory) and (
- file_info := await self.adapter.info(joined_path)
- ):
- return joined_path, file_info
- except FileNotFoundError:
- continue
- return None, None
-
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
- """ASGI callable.
-
- Args:
- scope: ASGI scope
- receive: ASGI ``receive`` callable
- send: ASGI ``send`` callable
-
- Returns:
- None
- """
- if scope["type"] != ScopeType.HTTP or scope["method"] not in {"GET", "HEAD"}:
- raise MethodNotAllowedException()
-
- res = await self.handle(path=scope["path"], is_head_response=scope["method"] == "HEAD")
- await res(scope=scope, receive=receive, send=send)
-
- async def handle(self, path: str, is_head_response: bool) -> ASGIFileResponse:
- split_path = path.split("/")
- filename = split_path[-1]
- joined_path = Path(*split_path)
- resolved_path, fs_info = await self.get_fs_info(directories=self.directories, file_path=joined_path)
- content_disposition_type: Literal["inline", "attachment"] = (
- "attachment" if self.send_as_attachment else "inline"
- )
-
- if self.is_html_mode and fs_info and fs_info["type"] == "directory":
- filename = "index.html"
- resolved_path, fs_info = await self.get_fs_info(
- directories=self.directories,
- file_path=Path(resolved_path or joined_path) / filename,
- )
-
- if fs_info and fs_info["type"] == "file":
- return ASGIFileResponse(
- file_path=resolved_path or joined_path,
- file_info=fs_info,
- file_system=self.adapter.file_system,
- filename=filename,
- content_disposition_type=content_disposition_type,
- is_head_response=is_head_response,
- headers=self.headers,
- )
-
- if self.is_html_mode:
- # for some reason coverage doesn't catch these two lines
- filename = "404.html" # pragma: no cover
- resolved_path, fs_info = await self.get_fs_info( # pragma: no cover
- directories=self.directories, file_path=filename
- )
-
- if fs_info and fs_info["type"] == "file":
- return ASGIFileResponse(
- file_path=resolved_path or joined_path,
- file_info=fs_info,
- file_system=self.adapter.file_system,
- filename=filename,
- status_code=HTTP_404_NOT_FOUND,
- content_disposition_type=content_disposition_type,
- is_head_response=is_head_response,
- headers=self.headers,
- )
-
- raise NotFoundException(
- f"no file or directory match the path {resolved_path or joined_path} was found"
- ) # pragma: no cover
diff --git a/litestar/static_files/config.py b/litestar/static_files/config.py
index 7bd0c71728..e69de29bb2 100644
--- a/litestar/static_files/config.py
+++ b/litestar/static_files/config.py
@@ -1,223 +0,0 @@
-from __future__ import annotations
-
-from dataclasses import dataclass
-from pathlib import PurePath # noqa: TC003
-from typing import TYPE_CHECKING, Any, Sequence
-
-from litestar.exceptions import ImproperlyConfiguredException
-from litestar.file_system import BaseLocalFileSystem
-from litestar.handlers import asgi, get, head
-from litestar.response.file import ASGIFileResponse # noqa: TC001
-from litestar.router import Router
-from litestar.static_files.base import StaticFiles
-from litestar.utils import normalize_path, warn_deprecation
-
-__all__ = ("StaticFilesConfig",)
-
-if TYPE_CHECKING:
- from litestar.datastructures import CacheControlHeader
- from litestar.handlers.asgi_handlers import ASGIRouteHandler
- from litestar.openapi.spec import SecurityRequirement
- from litestar.types import (
- AfterRequestHookHandler,
- AfterResponseHookHandler,
- BeforeRequestHookHandler,
- EmptyType,
- ExceptionHandlersMap,
- Guard,
- Middleware,
- PathType,
- )
-
-
-@dataclass
-class StaticFilesConfig:
- """Configuration for static file service.
-
- To enable static files, pass an instance of this class to the :class:`Litestar ` constructor using
- the 'static_files_config' key.
- """
-
- path: str
- """Path to serve static files from.
-
- Note that the path cannot contain path parameters.
- """
- directories: list[PathType]
- """A list of directories to serve files from."""
- html_mode: bool = False
- """Flag dictating whether serving html.
-
- If true, the default file will be 'index.html'.
- """
- name: str | None = None
- """An optional string identifying the static files handler."""
- file_system: Any = BaseLocalFileSystem() # noqa: RUF009
- """The file_system spec to use for serving files.
-
- Notes:
- - A file_system is a class that adheres to the
- :class:`FileSystemProtocol `.
- - You can use any of the file systems exported from the
- [fsspec](https://filesystem-spec.readthedocs.io/en/latest/) library for this purpose.
- """
- opt: dict[str, Any] | None = None
- """A string key dictionary of arbitrary values that will be added to the static files handler."""
- guards: list[Guard] | None = None
- """A list of :class:`Guard ` callables."""
- exception_handlers: ExceptionHandlersMap | None = None
- """A dictionary that maps handler functions to status codes and/or exception types."""
- send_as_attachment: bool = False
- """Whether to send the file as an attachment."""
-
- def __post_init__(self) -> None:
- _validate_config(path=self.path, directories=self.directories, file_system=self.file_system)
- self.path = normalize_path(self.path)
- warn_deprecation(
- "2.6.0",
- kind="class",
- deprecated_name="StaticFilesConfig",
- removal_in="3.0",
- alternative="create_static_files_router",
- info='Replace static_files_config=[StaticFilesConfig(path="/static", directories=["assets"])] with '
- 'route_handlers=[..., create_static_files_router(path="/static", directories=["assets"])]',
- )
-
- def to_static_files_app(self) -> ASGIRouteHandler:
- """Return an ASGI app serving static files based on the config.
-
- Returns:
- :class:`StaticFiles `
- """
- static_files = StaticFiles(
- is_html_mode=self.html_mode,
- directories=self.directories,
- file_system=self.file_system,
- send_as_attachment=self.send_as_attachment,
- )
- return asgi(
- path=self.path,
- name=self.name,
- is_static=True,
- opt=self.opt,
- guards=self.guards,
- exception_handlers=self.exception_handlers,
- )(static_files)
-
-
-def create_static_files_router(
- path: str,
- directories: list[PathType],
- file_system: Any = None,
- send_as_attachment: bool = False,
- html_mode: bool = False,
- name: str = "static",
- after_request: AfterRequestHookHandler | None = None,
- after_response: AfterResponseHookHandler | None = None,
- before_request: BeforeRequestHookHandler | None = None,
- cache_control: CacheControlHeader | None = None,
- exception_handlers: ExceptionHandlersMap | None = None,
- guards: list[Guard] | None = None,
- include_in_schema: bool | EmptyType = False,
- middleware: Sequence[Middleware] | None = None,
- opt: dict[str, Any] | None = None,
- security: Sequence[SecurityRequirement] | None = None,
- tags: Sequence[str] | None = None,
- router_class: type[Router] = Router,
- resolve_symlinks: bool = True,
-) -> Router:
- """Create a router with handlers to serve static files.
-
- Args:
- path: Path to serve static files under
- directories: Directories to serve static files from
- file_system: A *file system* implementing
- :class:`~litestar.types.FileSystemProtocol`.
- `fsspec `_ can be passed
- here as well
- send_as_attachment: Whether to send the file as an attachment
- html_mode: When in HTML:
- - Serve an ``index.html`` file from ``/``
- - Serve ``404.html`` when a file could not be found
- name: Name to pass to the generated handlers
- after_request: ``after_request`` handlers passed to the router
- after_response: ``after_response`` handlers passed to the router
- before_request: ``before_request`` handlers passed to the router
- cache_control: ``cache_control`` passed to the router
- exception_handlers: Exception handlers passed to the router
- guards: Guards passed to the router
- include_in_schema: Include the routes / router in the OpenAPI schema
- middleware: Middlewares passed to the router
- opt: Opts passed to the router
- security: Security options passed to the router
- tags: ``tags`` passed to the router
- router_class: The class used to construct a router from
- resolve_symlinks: Resolve symlinks of ``directories``
- """
-
- if file_system is None:
- file_system = BaseLocalFileSystem()
-
- _validate_config(path=path, directories=directories, file_system=file_system)
- path = normalize_path(path)
-
- headers = None
- if cache_control:
- headers = {cache_control.HEADER_NAME: cache_control.to_header()}
-
- static_files = StaticFiles(
- is_html_mode=html_mode,
- directories=directories,
- file_system=file_system,
- send_as_attachment=send_as_attachment,
- resolve_symlinks=resolve_symlinks,
- headers=headers,
- )
-
- @get("{file_path:path}", name=name)
- async def get_handler(file_path: PurePath) -> ASGIFileResponse:
- return await static_files.handle(path=file_path.as_posix(), is_head_response=False)
-
- @head("/{file_path:path}", name=f"{name}/head")
- async def head_handler(file_path: PurePath) -> ASGIFileResponse:
- return await static_files.handle(path=file_path.as_posix(), is_head_response=True)
-
- handlers = [get_handler, head_handler]
-
- if html_mode:
-
- @get("/", name=f"{name}/index")
- async def index_handler() -> ASGIFileResponse:
- return await static_files.handle(path="/", is_head_response=False)
-
- handlers.append(index_handler)
-
- return router_class(
- after_request=after_request,
- after_response=after_response,
- before_request=before_request,
- cache_control=cache_control,
- exception_handlers=exception_handlers,
- guards=guards,
- include_in_schema=include_in_schema,
- middleware=middleware,
- opt=opt,
- path=path,
- route_handlers=handlers,
- security=security,
- tags=tags,
- )
-
-
-def _validate_config(path: str, directories: list[PathType], file_system: Any) -> None:
- if not path:
- raise ImproperlyConfiguredException("path must be a non-zero length string,")
-
- if not directories or not any(bool(d) for d in directories):
- raise ImproperlyConfiguredException("directories must include at least one path.")
-
- if "{" in path:
- raise ImproperlyConfiguredException("path parameters are not supported for static files")
-
- if not (callable(getattr(file_system, "info", None)) and callable(getattr(file_system, "open", None))):
- raise ImproperlyConfiguredException("file_system must adhere to the FileSystemProtocol type")
diff --git a/litestar/template/base.py b/litestar/template/base.py
index 3474717ed4..e3373a3190 100644
--- a/litestar/template/base.py
+++ b/litestar/template/base.py
@@ -19,7 +19,6 @@
"TemplateProtocol",
"csrf_token",
"url_for",
- "url_for_static_asset",
)
@@ -70,23 +69,6 @@ def csrf_token(context: Mapping[str, Any], /) -> str:
return value_or_default(ScopeState.from_scope(scope).csrf_token, "")
-def url_for_static_asset(context: Mapping[str, Any], /, name: str, file_path: str) -> str:
- """Wrap :meth:`url_for_static_asset ` to be used in templates.
-
- Args:
- context: The template context object.
- name: A static handler unique name.
- file_path: a string containing path to an asset.
-
- Raises:
- NoRouteMatchFoundException: If static files handler with ``name`` does not exist.
-
- Returns:
- A url path to the asset.
- """
- return _get_request_from_context(context).app.url_for_static_asset(name, file_path)
-
-
class TemplateProtocol(Protocol):
"""Protocol Defining a ``Template``.
diff --git a/litestar/testing/helpers.py b/litestar/testing/helpers.py
index 0635ef6531..d2b394d959 100644
--- a/litestar/testing/helpers.py
+++ b/litestar/testing/helpers.py
@@ -27,7 +27,6 @@
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.spec import SecurityRequirement
from litestar.plugins import PluginProtocol
- from litestar.static_files.config import StaticFilesConfig
from litestar.stores.base import Store
from litestar.stores.registry import StoreRegistry
from litestar.template.config import TemplateConfig
@@ -103,7 +102,6 @@ def create_test_client(
signature_namespace: Mapping[str, Any] | None = None,
signature_types: Sequence[Any] | None = None,
state: State | None = None,
- static_files_config: Sequence[StaticFilesConfig] | None = None,
stores: StoreRegistry | dict[str, Store] | None = None,
tags: Sequence[str] | None = None,
template_config: TemplateConfig | None = None,
@@ -226,7 +224,6 @@ def test_my_handler() -> None:
signature_types: A sequence of types for use in forward reference resolution during signature modeling.
These types will be added to the signature namespace using their ``__name__`` attribute.
state: An optional :class:`State <.datastructures.State>` for application state.
- static_files_config: A sequence of :class:`StaticFilesConfig <.static_files.StaticFilesConfig>`
stores: Central registry of :class:`Store <.stores.base.Store>` that will be available throughout the
application. If this is a dictionary to it will be passed to a
:class:`StoreRegistry <.stores.registry.StoreRegistry>`. If it is a
@@ -292,7 +289,6 @@ def test_my_handler() -> None:
signature_namespace=signature_namespace,
signature_types=signature_types,
state=state,
- static_files_config=static_files_config,
stores=stores,
tags=tags,
template_config=template_config,
@@ -364,7 +360,6 @@ def create_async_test_client(
signature_namespace: Mapping[str, Any] | None = None,
signature_types: Sequence[Any] | None = None,
state: State | None = None,
- static_files_config: Sequence[StaticFilesConfig] | None = None,
stores: StoreRegistry | dict[str, Store] | None = None,
tags: Sequence[str] | None = None,
template_config: TemplateConfig | None = None,
@@ -487,7 +482,6 @@ async def test_my_handler() -> None:
signature_types: A sequence of types for use in forward reference resolution during signature modeling.
These types will be added to the signature namespace using their ``__name__`` attribute.
state: An optional :class:`State <.datastructures.State>` for application state.
- static_files_config: A sequence of :class:`StaticFilesConfig <.static_files.StaticFilesConfig>`
stores: Central registry of :class:`Store <.stores.base.Store>` that will be available throughout the
application. If this is a dictionary to it will be passed to a
:class:`StoreRegistry <.stores.registry.StoreRegistry>`. If it is a
@@ -552,7 +546,6 @@ async def test_my_handler() -> None:
signature_namespace=signature_namespace,
signature_types=signature_types,
state=state,
- static_files_config=static_files_config,
stores=stores,
tags=tags,
template_config=template_config,
diff --git a/litestar/testing/request_factory.py b/litestar/testing/request_factory.py
index e25b6b0956..9592992b77 100644
--- a/litestar/testing/request_factory.py
+++ b/litestar/testing/request_factory.py
@@ -14,7 +14,7 @@
from litestar.enums import HttpMethod, ParamType, RequestEncodingType, ScopeType
from litestar.handlers.http_handlers import get
from litestar.serialization import decode_json, default_serializer, encode_json
-from litestar.types import DataContainerType, HTTPScope, RouteHandlerType
+from litestar.types import DataContainerType, HTTPHandlerDecorator, HTTPScope, RouteHandlerType
from litestar.types.asgi_types import ASGIVersion
from litestar.utils import get_serializer_from_scope
from litestar.utils.scope.state import ScopeState
@@ -25,7 +25,7 @@
from litestar.datastructures.cookie import Cookie
from litestar.handlers.http_handlers import HTTPRouteHandler
-_decorator_http_method_map: dict[HttpMethod, type[HTTPRouteHandler]] = {
+_decorator_http_method_map: dict[HttpMethod, HTTPHandlerDecorator] = {
HttpMethod.GET: get,
HttpMethod.POST: post,
HttpMethod.DELETE: delete,
diff --git a/litestar/types/__init__.py b/litestar/types/__init__.py
index ef91444b88..1c17ae69ef 100644
--- a/litestar/types/__init__.py
+++ b/litestar/types/__init__.py
@@ -54,6 +54,7 @@
ExceptionHandler,
GetLogger,
Guard,
+ HTTPHandlerDecorator,
LifespanHook,
OnAppInitHandler,
OperationIDCreator,
@@ -105,6 +106,7 @@
"GetLogger",
"Guard",
"HTTPDisconnectEvent",
+ "HTTPHandlerDecorator",
"HTTPReceiveMessage",
"HTTPRequestEvent",
"HTTPResponseBodyEvent",
diff --git a/litestar/types/callable_types.py b/litestar/types/callable_types.py
index 36055d7199..0f07295cc4 100644
--- a/litestar/types/callable_types.py
+++ b/litestar/types/callable_types.py
@@ -38,3 +38,4 @@
OnAppInitHandler: TypeAlias = "Callable[[AppConfig], AppConfig]"
OperationIDCreator: TypeAlias = "Callable[[HTTPRouteHandler, Method, list[str | PathParameterDefinition]], str]"
Serializer: TypeAlias = Callable[[Any], Any]
+HTTPHandlerDecorator: TypeAlias = "Callable[..., Callable[[AnyCallable], HTTPRouteHandler]]"
diff --git a/litestar/utils/__init__.py b/litestar/utils/__init__.py
index af86b83850..4cc9d1429b 100644
--- a/litestar/utils/__init__.py
+++ b/litestar/utils/__init__.py
@@ -1,11 +1,8 @@
-from typing import Any
-
from litestar.utils.deprecation import deprecated, warn_deprecation
from .helpers import get_enum_string_value, get_name, unique_name_for_scope, url_quote
from .path import join_paths, normalize_path
from .predicates import (
- _is_sync_or_async_generator,
is_annotated_type,
is_any,
is_async_callable,
@@ -21,10 +18,7 @@
is_undefined_sentinel,
is_union,
)
-from .scope import ( # type: ignore[attr-defined]
- _delete_litestar_scope_state,
- _get_litestar_scope_state,
- _set_litestar_scope_state,
+from .scope import (
get_serializer_from_scope,
)
from .sequence import find_index, unique
@@ -62,23 +56,3 @@
"url_quote",
"warn_deprecation",
)
-
-_deprecated_names = {
- "get_litestar_scope_state": _get_litestar_scope_state,
- "set_litestar_scope_state": _set_litestar_scope_state,
- "delete_litestar_scope_state": _delete_litestar_scope_state,
- "is_sync_or_async_generator": _is_sync_or_async_generator,
-}
-
-
-def __getattr__(name: str) -> Any:
- if name in _deprecated_names:
- warn_deprecation(
- deprecated_name=f"litestar.utils.{name}",
- version="2.4",
- kind="import",
- removal_in="3.0",
- info=f"'litestar.utils.{name}' is deprecated.",
- )
- return globals()["_deprecated_names"][name]
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # pragma: no cover
diff --git a/litestar/utils/predicates.py b/litestar/utils/predicates.py
index 4a01a87480..4a110fcba9 100644
--- a/litestar/utils/predicates.py
+++ b/litestar/utils/predicates.py
@@ -4,7 +4,7 @@
from collections import defaultdict, deque
from collections.abc import Iterable as CollectionsIterable
from dataclasses import is_dataclass
-from inspect import isasyncgenfunction, isclass, isgeneratorfunction
+from inspect import isclass
from typing import (
TYPE_CHECKING,
Any,
@@ -35,12 +35,10 @@
from litestar.constants import UNDEFINED_SENTINELS
from litestar.types.builtin_types import NoneType, UnionTypes
-from litestar.utils.deprecation import warn_deprecation
from litestar.utils.helpers import unwrap_partial
from litestar.utils.typing import get_origin_or_inner_type
if TYPE_CHECKING:
- from litestar.types.callable_types import AnyGenerator
from litestar.types.protocols import DataclassProtocol
@@ -186,7 +184,7 @@ def is_non_string_sequence(annotation: Any) -> TypeGuard[Sequence[Any]]:
annotation: A type.
Returns:
- A typeguard determining whether the type can be cast as :class`Sequence ` that is not a string.
+ A typeguard determining whether the type can be cast as :class:`Sequence ` that is not a string.
"""
origin = get_origin_or_inner_type(annotation)
if not origin and not isclass(annotation):
@@ -269,18 +267,6 @@ def is_class_var(annotation: Any) -> bool:
return annotation is ClassVar
-def _is_sync_or_async_generator(obj: Any) -> TypeGuard[AnyGenerator]:
- """Check if the given annotation is a sync or async generator.
-
- Args:
- obj: type to be tested for sync or async generator.
-
- Returns:
- A boolean.
- """
- return isgeneratorfunction(obj) or isasyncgenfunction(obj)
-
-
def is_annotated_type(annotation: Any) -> bool:
"""Check if the given annotation is an Annotated.
@@ -303,19 +289,3 @@ def is_undefined_sentinel(value: Any) -> bool:
A boolean.
"""
return any(v is value for v in UNDEFINED_SENTINELS)
-
-
-_deprecated_names = {"is_sync_or_async_generator": _is_sync_or_async_generator}
-
-
-def __getattr__(name: str) -> Any:
- if name in _deprecated_names:
- warn_deprecation(
- deprecated_name=f"litestar.utils.scope.{name}",
- version="2.4",
- kind="import",
- removal_in="3.0",
- info=f"'litestar.utils.predicates.{name}' is deprecated.",
- )
- return globals()["_deprecated_names"][name]
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # pragma: no cover
diff --git a/litestar/utils/scope/__init__.py b/litestar/utils/scope/__init__.py
index e5757d3983..1191339701 100644
--- a/litestar/utils/scope/__init__.py
+++ b/litestar/utils/scope/__init__.py
@@ -1,12 +1,8 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from litestar.serialization import get_serializer
-from litestar.utils.deprecation import warn_deprecation
-from litestar.utils.scope.state import delete_litestar_scope_state as _delete_litestar_scope_state
-from litestar.utils.scope.state import get_litestar_scope_state as _get_litestar_scope_state
-from litestar.utils.scope.state import set_litestar_scope_state as _set_litestar_scope_state
if TYPE_CHECKING:
from litestar.types import Scope, Serializer
@@ -39,24 +35,3 @@ def get_serializer_from_scope(scope: Scope) -> Serializer:
type_encoders = {**type_encoders, **(response_class.type_encoders or {})}
return get_serializer(type_encoders)
-
-
-_deprecated_names = {
- "get_litestar_scope_state": _get_litestar_scope_state,
- "set_litestar_scope_state": _set_litestar_scope_state,
- "delete_litestar_scope_state": _delete_litestar_scope_state,
-}
-
-
-def __getattr__(name: str) -> Any:
- if name in _deprecated_names:
- warn_deprecation(
- deprecated_name=f"litestar.utils.scope.{name}",
- version="2.4",
- kind="import",
- removal_in="3.0",
- info=f"'litestar.utils.scope.{name}' is deprecated. The Litestar scope state is private and should not be "
- f"used. Plugin authors should maintain their own scope state namespace.",
- )
- return globals()["_deprecated_names"][name]
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # pragma: no cover
diff --git a/litestar/utils/scope/state.py b/litestar/utils/scope/state.py
index 14824ef102..003750a65e 100644
--- a/litestar/utils/scope/state.py
+++ b/litestar/utils/scope/state.py
@@ -4,7 +4,6 @@
from typing import TYPE_CHECKING, Any, Final
from litestar.types import Empty, EmptyType
-from litestar.utils.empty import value_or_default
if TYPE_CHECKING:
from typing_extensions import Self
@@ -72,7 +71,6 @@ def __init__(self) -> None:
self.response_started = False
self.session_id = Empty
self.url = Empty
- self._compat_ns: dict[str, Any] = {}
accept: Accept | EmptyType
base_url: URL | EmptyType
@@ -95,7 +93,6 @@ def __init__(self) -> None:
response_started: bool
session_id: str | None | EmptyType
url: URL | EmptyType
- _compat_ns: dict[str, Any]
@classmethod
def from_scope(cls, scope: Scope) -> Self:
@@ -113,56 +110,3 @@ def from_scope(cls, scope: Scope) -> Self:
if (state := base_scope_state.get(CONNECTION_STATE_KEY)) is None:
state = base_scope_state[CONNECTION_STATE_KEY] = cls()
return state
-
-
-def get_litestar_scope_state(scope: Scope, key: str, default: Any = None, pop: bool = False) -> Any:
- """Get an internal value from connection scope state.
-
- Args:
- scope: The connection scope.
- key: Key to get from internal namespace in scope state.
- default: Default value to return.
- pop: Boolean flag dictating whether the value should be deleted from the state.
-
- Returns:
- Value mapped to ``key`` in internal connection scope namespace.
- """
- scope_state = ScopeState.from_scope(scope)
- try:
- val = value_or_default(getattr(scope_state, key), default)
- if pop:
- setattr(scope_state, key, Empty)
- return val
- except AttributeError:
- if pop:
- return scope_state._compat_ns.pop(key, default)
- return scope_state._compat_ns.get(key, default)
-
-
-def set_litestar_scope_state(scope: Scope, key: str, value: Any) -> None:
- """Set an internal value in connection scope state.
-
- Args:
- scope: The connection scope.
- key: Key to set under internal namespace in scope state.
- value: Value for key.
- """
- scope_state = ScopeState.from_scope(scope)
- if hasattr(scope_state, key):
- setattr(scope_state, key, value)
- else:
- scope_state._compat_ns[key] = value
-
-
-def delete_litestar_scope_state(scope: Scope, key: str) -> None:
- """Delete an internal value from connection scope state.
-
- Args:
- scope: The connection scope.
- key: Key to set under internal namespace in scope state.
- """
- scope_state = ScopeState.from_scope(scope)
- if hasattr(scope_state, key):
- setattr(scope_state, key, Empty)
- else:
- del scope_state._compat_ns[key]
diff --git a/pyproject.toml b/pyproject.toml
index 98709540fa..3cfbeeb242 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -147,8 +147,12 @@ docs = [
"sphinx-click>=4.4.0",
"sphinxcontrib-mermaid>=0.9.2",
"auto-pytabs[sphinx]>=0.5.0",
- "litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git",
"sphinx-paramlinks>=0.6.0",
+# "litestar-sphinx-theme @ {root:uri}/../litestar-sphinx-theme", # only needed when working on the theme
+ "litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git@v3",
+ "litestar[full]",
+ "asyncpg",
+ "psycopg",
]
linting = [
"ruff>=0.2.1",
diff --git a/test_apps/static_files_test_app/main.py b/test_apps/static_files_test_app/main.py
index 1d086bbd40..e6dea75135 100644
--- a/test_apps/static_files_test_app/main.py
+++ b/test_apps/static_files_test_app/main.py
@@ -1,7 +1,7 @@
from pathlib import Path
from litestar import Litestar, get
-from litestar.static_files.config import StaticFilesConfig
+from litestar.static_files.config import create_static_files_router
@get("/")
@@ -10,9 +10,8 @@ async def handler() -> dict[str, str]:
app = Litestar(
- route_handlers=[],
- static_files_config=[
- StaticFilesConfig(directories=[Path(__file__).parent / "public"], path="/", html_mode=True),
+ route_handlers=[
+ create_static_files_router(directories=[Path(__file__).parent / "public"], path="/", html_mode=True),
],
)
diff --git a/tests/e2e/test_router_registration.py b/tests/e2e/test_router_registration.py
index e966471f27..357411108f 100644
--- a/tests/e2e/test_router_registration.py
+++ b/tests/e2e/test_router_registration.py
@@ -14,8 +14,11 @@
put,
websocket,
)
-from litestar import route as route_decorator
+from litestar import (
+ route as route_decorator,
+)
from litestar.exceptions import ImproperlyConfiguredException
+from litestar.routes import HTTPRoute
@pytest.fixture
@@ -46,12 +49,13 @@ def test_register_with_controller_class(controller: Type[Controller]) -> None:
router = Router(path="/base", route_handlers=[controller])
assert len(router.routes) == 3
for route in router.routes:
- if len(route.methods) == 2:
- assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore
- assert route.path == "/base/test/{id:int}"
- elif len(route.methods) == 3:
- assert sorted(route.methods) == sorted(["GET", "POST", "OPTIONS"]) # pyright: ignore
- assert route.path == "/base/test"
+ if isinstance(route, HTTPRoute):
+ if len(route.methods) == 2:
+ assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore
+ assert route.path == "/base/test/{id:int}"
+ elif len(route.methods) == 3:
+ assert sorted(route.methods) == sorted(["GET", "POST", "OPTIONS"]) # pyright: ignore
+ assert route.path == "/base/test"
def test_register_controller_on_different_routers(controller: Type[Controller]) -> None:
@@ -82,12 +86,13 @@ def test_register_with_router_instance(controller: Type[Controller]) -> None:
assert len(base_router.routes) == 3
for route in base_router.routes:
- if len(route.methods) == 2:
- assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore
- assert route.path == "/base/top-level/test/{id:int}"
- elif len(route.methods) == 3:
- assert sorted(route.methods) == sorted(["GET", "POST", "OPTIONS"]) # pyright: ignore
- assert route.path == "/base/top-level/test"
+ if isinstance(route, HTTPRoute):
+ if len(route.methods) == 2:
+ assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore
+ assert route.path == "/base/top-level/test/{id:int}"
+ elif len(route.methods) == 3:
+ assert sorted(route.methods) == sorted(["GET", "POST", "OPTIONS"]) # pyright: ignore
+ assert route.path == "/base/top-level/test"
def test_register_with_route_handler_functions() -> None:
@@ -106,13 +111,14 @@ def third_route_handler() -> None:
router = Router(path="/base", route_handlers=[first_route_handler, second_route_handler, third_route_handler])
assert len(router.routes) == 2
for route in router.routes:
- if len(route.methods) == 2:
- assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore
- assert route.path == "/base/second"
- else:
- assert sorted(route.methods) == sorted(["GET", "POST", "PATCH", "OPTIONS"]) # pyright: ignore
- assert route.path == "/base/first"
- assert route.path == "/base/first"
+ if isinstance(route, HTTPRoute):
+ if len(route.methods) == 2:
+ assert sorted(route.methods) == sorted(["GET", "OPTIONS"]) # pyright: ignore
+ assert route.path == "/base/second"
+ else:
+ assert sorted(route.methods) == sorted(["GET", "POST", "PATCH", "OPTIONS"]) # pyright: ignore
+ assert route.path == "/base/first"
+ assert route.path == "/base/first"
def test_register_validation_wrong_class() -> None:
diff --git a/tests/e2e/test_routing/test_asset_url_path.py b/tests/e2e/test_routing/test_asset_url_path.py
deleted file mode 100644
index 1427315ac1..0000000000
--- a/tests/e2e/test_routing/test_asset_url_path.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from typing import TYPE_CHECKING
-
-import pytest
-
-from litestar import Litestar, get
-from litestar.exceptions import NoRouteMatchFoundException
-from litestar.static_files.config import StaticFilesConfig
-
-if TYPE_CHECKING:
- from pathlib import Path
-
-
-def test_url_for_static_asset(tmp_path: "Path") -> None:
- app = Litestar(
- route_handlers=[],
- static_files_config=[StaticFilesConfig(path="/static/path", directories=[tmp_path], name="asset")],
- )
- url_path = app.url_for_static_asset("asset", "abc/def.css")
- assert url_path == "/static/path/abc/def.css"
-
-
-def test_url_for_static_asset_doesnt_work_with_http_handler_name(tmp_path: "Path") -> None:
- @get("/handler", name="handler")
- def handler() -> None:
- pass
-
- app = Litestar(
- route_handlers=[handler],
- static_files_config=[StaticFilesConfig(path="/static/path", directories=[tmp_path], name="asset")],
- )
-
- with pytest.raises(NoRouteMatchFoundException):
- app.url_for_static_asset("handler", "abc/def.css")
-
-
-def test_url_for_static_asset_validates_name(tmp_path: "Path") -> None:
- app = Litestar(
- route_handlers=[],
- static_files_config=[StaticFilesConfig(path="/static/path", directories=[tmp_path], name="asset")],
- )
-
- with pytest.raises(NoRouteMatchFoundException):
- app.url_for_static_asset("non-existing-name", "abc/def.css")
diff --git a/tests/e2e/test_routing/test_path_resolution.py b/tests/e2e/test_routing/test_path_resolution.py
index abea4681c0..4ffdc004a8 100644
--- a/tests/e2e/test_routing/test_path_resolution.py
+++ b/tests/e2e/test_routing/test_path_resolution.py
@@ -1,5 +1,5 @@
from pathlib import Path
-from typing import Any, Callable, List, Optional, Type
+from typing import Any, Callable, List, Optional
import httpx
import pytest
@@ -75,29 +75,27 @@ def mixed_params(path_param: int, value: int) -> str:
@pytest.mark.parametrize(
- "decorator, test_path, decorator_path, delete_handler",
+ "test_path, decorator_path, delete_handler",
[
- (get, "", "/something", None),
- (get, "/", "/something", None),
- (get, "", "/", None),
- (get, "/", "/", None),
- (get, "", "", None),
- (get, "/", "", None),
- (get, "", "/something", root_delete_handler),
- (get, "/", "/something", root_delete_handler),
- (get, "", "/", root_delete_handler),
- (get, "/", "/", root_delete_handler),
- (get, "", "", root_delete_handler),
- (get, "/", "", root_delete_handler),
+ ("", "/something", None),
+ ("/", "/something", None),
+ ("", "/", None),
+ ("/", "/", None),
+ ("", "", None),
+ ("/", "", None),
+ ("", "/something", root_delete_handler),
+ ("/", "/something", root_delete_handler),
+ ("", "/", root_delete_handler),
+ ("/", "/", root_delete_handler),
+ ("", "", root_delete_handler),
+ ("/", "", root_delete_handler),
],
)
-def test_root_route_handler(
- decorator: Type[get], test_path: str, decorator_path: str, delete_handler: Optional[Callable]
-) -> None:
+def test_root_route_handler(test_path: str, decorator_path: str, delete_handler: Optional[Callable]) -> None:
class MyController(Controller):
path = test_path
- @decorator(path=decorator_path)
+ @get(path=decorator_path)
def test_method(self) -> str:
return "hello"
@@ -350,7 +348,7 @@ def test_server_root_path_handling_empty_path(
from typing import Optional
@get(path=["/", "/{path:path}"])
-async def pathfinder(path: Optional[Path]) -> str:
+async def pathfinder(path: Optional[Path] = None) -> str:
return str(path)
app = Litestar(route_handlers=[pathfinder], debug=True)
diff --git a/tests/e2e/test_routing/test_route_indexing.py b/tests/e2e/test_routing/test_route_indexing.py
index c09f15f48a..f56c4ecc89 100644
--- a/tests/e2e/test_routing/test_route_indexing.py
+++ b/tests/e2e/test_routing/test_route_indexing.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Any, Type
+from typing import TYPE_CHECKING, Any
import pytest
@@ -15,16 +15,15 @@
websocket,
)
from litestar.exceptions import ImproperlyConfiguredException
-from litestar.handlers.http_handlers import HTTPRouteHandler
-from litestar.static_files.config import StaticFilesConfig
+from litestar.types import HTTPHandlerDecorator
if TYPE_CHECKING:
from pathlib import Path
@pytest.mark.parametrize("decorator", [get, post, patch, put, delete])
-def test_indexes_handlers(decorator: Type[HTTPRouteHandler]) -> None:
- @decorator("/path-one/{param:str}", name="handler-name") # type: ignore[call-arg]
+def test_indexes_handlers(decorator: HTTPHandlerDecorator) -> None:
+ @decorator("/path-one/{param:str}", name="handler-name")
def handler() -> None:
return None
@@ -58,19 +57,19 @@ async def websocket_handler(socket: Any) -> None:
@pytest.mark.parametrize("decorator", [get, post, patch, put, delete])
-def test_default_indexes_handlers(decorator: Type[HTTPRouteHandler]) -> None:
- @decorator("/handler") # type: ignore[call-arg]
+def test_default_indexes_handlers(decorator: HTTPHandlerDecorator) -> None:
+ @decorator("/handler")
def handler() -> None:
pass
- @decorator("/named_handler", name="named_handler") # type: ignore[call-arg]
+ @decorator("/named_handler", name="named_handler")
def named_handler() -> None:
pass
class MyController(Controller):
path = "/test"
- @decorator() # type: ignore[call-arg]
+ @decorator()
def handler(self) -> None:
pass
@@ -94,12 +93,12 @@ def handler(self) -> None:
@pytest.mark.parametrize("decorator", [get, post, patch, put, delete])
-def test_indexes_handlers_with_multiple_paths(decorator: Type[HTTPRouteHandler]) -> None:
- @decorator(["/path-one", "/path-one/{param:str}"], name="handler") # type: ignore[call-arg]
+def test_indexes_handlers_with_multiple_paths(decorator: HTTPHandlerDecorator) -> None:
+ @decorator(["/path-one", "/path-one/{param:str}"], name="handler")
def handler() -> None:
return None
- @decorator(["/path-two"], name="handler-two") # type: ignore[call-arg]
+ @decorator(["/path-two"], name="handler-two")
def handler_two() -> None:
return None
@@ -130,9 +129,3 @@ def handler_two() -> None:
with pytest.raises(ImproperlyConfiguredException):
Litestar(route_handlers=[handler_one, handler_two])
-
- with pytest.raises(ImproperlyConfiguredException):
- Litestar(
- route_handlers=[handler_one],
- static_files_config=[StaticFilesConfig(path="/static", directories=[tmp_path], name="same-name")],
- )
diff --git a/tests/e2e/test_routing/test_route_reverse.py b/tests/e2e/test_routing/test_route_reverse.py
index 0a8d914883..32b948cdc2 100644
--- a/tests/e2e/test_routing/test_route_reverse.py
+++ b/tests/e2e/test_routing/test_route_reverse.py
@@ -1,35 +1,34 @@
from datetime import time
-from typing import Type
import pytest
from litestar import Litestar, Router, delete, get, patch, post, put
from litestar.exceptions import NoRouteMatchFoundException
-from litestar.handlers.http_handlers import HTTPRouteHandler
+from litestar.types import HTTPHandlerDecorator
@pytest.mark.parametrize("decorator", [get, post, patch, put, delete])
-def test_route_reverse(decorator: Type[HTTPRouteHandler]) -> None:
- @decorator("/path-one/{param:str}", name="handler-name") # type: ignore[call-arg]
+def test_route_reverse(decorator: HTTPHandlerDecorator) -> None:
+ @decorator("/path-one/{param:str}", name="handler-name")
def handler() -> None:
return None
- @decorator("/path-two", name="handler-no-params") # type: ignore[call-arg]
+ @decorator("/path-two", name="handler-no-params")
def handler_no_params() -> None:
return None
- @decorator("/multiple/{str_param:str}/params/{int_param:int}/", name="multiple-params-handler-name") # type: ignore[call-arg]
+ @decorator("/multiple/{str_param:str}/params/{int_param:int}/", name="multiple-params-handler-name")
def handler2() -> None:
return None
@decorator(
["/handler3", "/handler3/{str_param:str}/", "/handler3/{str_param:str}/{int_param:int}/"],
name="multiple-default-params",
- ) # type: ignore[call-arg]
+ )
def handler3(str_param: str = "default", int_param: int = 0) -> None:
return None
- @decorator(["/handler4/int/{int_param:int}", "/handler4/str/{str_param:str}"], name="handler4") # type: ignore[call-arg]
+ @decorator(["/handler4/int/{int_param:int}", "/handler4/str/{str_param:str}"], name="handler4")
def handler4(int_param: int = 1, str_param: str = "str") -> None:
return None
diff --git a/tests/e2e/test_routing/test_validations.py b/tests/e2e/test_routing/test_validations.py
index 8411104691..91d1363de3 100644
--- a/tests/e2e/test_routing/test_validations.py
+++ b/tests/e2e/test_routing/test_validations.py
@@ -2,9 +2,8 @@
import pytest
-from litestar import Controller, Litestar, WebSocket, get, post, websocket
+from litestar import Controller, Litestar, WebSocket, get, websocket
from litestar.exceptions import ImproperlyConfiguredException
-from litestar.static_files import StaticFilesConfig
from litestar.status_codes import HTTP_200_OK
from litestar.testing import create_test_client
@@ -67,19 +66,3 @@ async def websocket_handler(self, socket: "WebSocket[Any, Any, Any]") -> None:
with client.websocket_connect("/") as ws:
ws_response = ws.receive_json()
assert ws_response == {"hello": "world"}
-
-
-def test_validate_static_files_with_same_path_in_handler() -> None:
- # make sure this works and does not lead to a recursion error
- # https://github.com/litestar-org/litestar/issues/2629
-
- @post("/uploads")
- async def handler() -> None:
- pass
-
- Litestar(
- [handler],
- static_files_config=[
- StaticFilesConfig(directories=["uploads"], path="/uploads"),
- ],
- )
diff --git a/tests/examples/test_static_files.py b/tests/examples/test_static_files.py
index 7d48603992..537e23d8dd 100644
--- a/tests/examples/test_static_files.py
+++ b/tests/examples/test_static_files.py
@@ -58,12 +58,3 @@ def test_send_as_attachment(tmp_path: Path, assets_file: str) -> None:
res = client.get("/static/test.txt")
assert res.text == assets_file
assert res.headers["content-disposition"].startswith("attachment")
-
-
-def test_upgrade_from_static(tmp_path: Path, assets_file: str) -> None:
- from docs.examples.static_files import upgrade_from_static_1, upgrade_from_static_2
-
- for app in [upgrade_from_static_1.app, upgrade_from_static_2.app]:
- with TestClient(app) as client:
- res = client.get("/static/test.txt")
- assert res.text == assets_file
diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py
index 084e0572fa..0a32cbe8d2 100644
--- a/tests/unit/test_app.py
+++ b/tests/unit/test_app.py
@@ -74,7 +74,6 @@ def app_config_object() -> AppConfig:
response_headers=[],
route_handlers=[],
security=[],
- static_files_config=[],
tags=[],
template_config=None,
websocket_class=None,
diff --git a/tests/unit/test_asgi/test_asgi_router.py b/tests/unit/test_asgi/test_asgi_router.py
index dc5c9f053e..2d1fdf0889 100644
--- a/tests/unit/test_asgi/test_asgi_router.py
+++ b/tests/unit/test_asgi/test_asgi_router.py
@@ -29,7 +29,7 @@ async def handler(scope: Scope, receive: Receive, send: Send) -> None:
return None
with pytest.raises(ImproperlyConfiguredException):
- Litestar(route_handlers=[asgi("/mount-path", is_static=True)(handler), asgi("/mount-path/{id:str}")(handler)])
+ Litestar(route_handlers=[asgi("/mount-path", is_mount=True)(handler), asgi("/mount-path/{id:str}")(handler)])
class _LifeSpanCallable:
diff --git a/tests/unit/test_cli/test_core_commands.py b/tests/unit/test_cli/test_core_commands.py
index 2cf815c164..cff3815303 100644
--- a/tests/unit/test_cli/test_core_commands.py
+++ b/tests/unit/test_cli/test_core_commands.py
@@ -578,12 +578,12 @@ def test_run_command_with_server_lifespan_plugin(
2,
id="schema-disabled_exclude",
),
- pytest.param(APP_FILE_CONTENT_ROUTES_EXAMPLE, True, (), 13, id="schema-enabled_no-exclude"),
+ pytest.param(APP_FILE_CONTENT_ROUTES_EXAMPLE, True, (), 7, id="schema-enabled_no-exclude"),
pytest.param(
APP_FILE_CONTENT_ROUTES_EXAMPLE,
True,
("/foo", "/destroy/.*", "/java", "/haskell"),
- 12,
+ 6,
id="schema-enabled_exclude",
),
],
@@ -643,7 +643,7 @@ def test_remove_default_schema_routes() -> None:
http_routes.append(http_route)
api_config = MagicMock()
- api_config.openapi_controller.path = "/schema"
+ api_config.openapi_router.path = "/schema"
results = _utils.remove_default_schema_routes(http_routes, api_config) # type: ignore[arg-type]
assert len(results) == 3
diff --git a/tests/unit/test_connection/test_request.py b/tests/unit/test_connection/test_request.py
index 688f5250a7..a6164ae92a 100644
--- a/tests/unit/test_connection/test_request.py
+++ b/tests/unit/test_connection/test_request.py
@@ -23,13 +23,10 @@
from litestar.middleware import MiddlewareProtocol
from litestar.response.base import ASGIResponse
from litestar.serialization import encode_json, encode_msgpack
-from litestar.static_files.config import StaticFilesConfig
from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_413_REQUEST_ENTITY_TOO_LARGE
from litestar.testing import TestClient, create_test_client
if TYPE_CHECKING:
- from pathlib import Path
-
from litestar.types import ASGIApp, Receive, Scope, Send
@@ -104,26 +101,6 @@ def test_none(request: Request[Any, Any, State]) -> dict[str, str]:
assert response.status_code == 500
-def test_request_asset_url(tmp_path: Path) -> None:
- @get(path="/resolver", signature_namespace={"dict": Dict})
- def resolver(request: Request[Any, Any, State]) -> dict[str, str]:
- return {"url": request.url_for_static_asset("js", "main.js")}
-
- @get(path="/resolver-none", signature_namespace={"dict": Dict})
- def resolver_none(request: Request[Any, Any, State]) -> dict[str, str]:
- return {"url": request.url_for_static_asset("none", "main.js")}
-
- with create_test_client(
- route_handlers=[resolver, resolver_none],
- static_files_config=[StaticFilesConfig(path="/static/js", directories=[tmp_path], name="js")],
- ) as client:
- response = client.get("/resolver")
- assert response.json() == {"url": "http://testserver.local/static/js/main.js"}
-
- response = client.get("/resolver-none")
- assert response.status_code == 500
-
-
def test_route_handler_property() -> None:
value: Any = {}
diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py
index e049608f24..cc9b15d2a9 100644
--- a/tests/unit/test_controller.py
+++ b/tests/unit/test_controller.py
@@ -1,4 +1,4 @@
-from typing import Any, Type, Union
+from typing import Any
import msgspec
import pytest
@@ -19,6 +19,7 @@
from litestar.exceptions import ImproperlyConfiguredException
from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT
from litestar.testing import create_test_client
+from litestar.types import HTTPHandlerDecorator
from tests.models import DataclassPerson, DataclassPersonFactory
@@ -40,7 +41,7 @@
],
)
async def test_controller_http_method(
- decorator: Union[Type[get], Type[post], Type[put], Type[patch], Type[delete]],
+ decorator: HTTPHandlerDecorator,
http_method: HttpMethod,
expected_status_code: int,
return_value: Any,
@@ -51,7 +52,7 @@ async def test_controller_http_method(
class MyController(Controller):
path = test_path
- @decorator() # type: ignore[misc]
+ @decorator()
def test_method(self) -> return_annotation:
return return_value
diff --git a/tests/unit/test_deprecations.py b/tests/unit/test_deprecations.py
index 0842c24e30..15aa9a897a 100644
--- a/tests/unit/test_deprecations.py
+++ b/tests/unit/test_deprecations.py
@@ -2,8 +2,6 @@
import pytest
-from litestar.types.asgi_types import ASGIApp
-
@pytest.mark.parametrize(
"import_path, import_name",
@@ -117,115 +115,3 @@ def test_litestar_templates_template_context_deprecation() -> None:
def test_minijinja_from_state_deprecation() -> None:
with pytest.warns(DeprecationWarning):
from litestar.contrib.minijinja import minijinja_from_state # noqa: F401
-
-
-def test_constants_deprecations() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.constants import SCOPE_STATE_NAMESPACE # noqa: F401
-
-
-def test_utils_deprecations() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.utils import ( # noqa: F401
- delete_litestar_scope_state,
- get_litestar_scope_state,
- set_litestar_scope_state,
- )
-
-
-def test_utils_scope_deprecations() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.utils.scope import ( # noqa: F401
- delete_litestar_scope_state,
- get_litestar_scope_state,
- set_litestar_scope_state,
- )
-
-
-def test_is_sync_or_async_generator_deprecation() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.utils.predicates import is_sync_or_async_generator # noqa: F401
-
- with pytest.warns(DeprecationWarning):
- from litestar.utils import is_sync_or_async_generator as _ # noqa: F401
-
-
-def test_openapi_config_openapi_controller_deprecation() -> None:
- from litestar.openapi.config import OpenAPIConfig
- from litestar.openapi.controller import OpenAPIController
-
- with pytest.warns(DeprecationWarning):
- OpenAPIConfig(title="API", version="1.0", openapi_controller=OpenAPIController)
-
-
-def test_openapi_config_root_schema_site_deprecation() -> None:
- from litestar.openapi.config import OpenAPIConfig
-
- with pytest.warns(DeprecationWarning):
- OpenAPIConfig(title="API", version="1.0", root_schema_site="redoc")
-
-
-def test_openapi_config_enabled_endpoints_deprecation() -> None:
- from litestar.openapi.config import OpenAPIConfig
-
- with pytest.warns(DeprecationWarning):
- OpenAPIConfig(title="API", version="1.0", enabled_endpoints={"redoc"})
-
-
-def test_cors_middleware_public_interface_deprecation() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.middleware.cors import CORSMiddleware # noqa: F401
-
-
-def test_exception_handler_middleware_debug_deprecation(mock_asgi_app: ASGIApp) -> None:
- from litestar.middleware._internal.exceptions import ExceptionHandlerMiddleware
-
- with pytest.warns(DeprecationWarning):
- ExceptionHandlerMiddleware(mock_asgi_app, debug=True)
-
-
-def test_exception_handler_middleware_exception_handlers_deprecation(mock_asgi_app: ASGIApp) -> None:
- from litestar.middleware._internal.exceptions import ExceptionHandlerMiddleware
-
- with pytest.warns(DeprecationWarning):
- ExceptionHandlerMiddleware(mock_asgi_app, debug=None, exception_handlers={})
-
-
-def test_deprecate_exception_handler_middleware() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.middleware.exceptions import ExceptionHandlerMiddleware # noqa: F401
-
- with pytest.raises(ImportError):
- from litestar.middleware.exceptions.middleware import OtherName # noqa: F401
-
-
-def test_deprecate_exception_handler_middleware_2() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.middleware.exceptions.middleware import ExceptionHandlerMiddleware # noqa: F401
-
- with pytest.raises(ImportError):
- from litestar.middleware.exceptions import OtherName # noqa: F401
-
-
-def test_deprecate_create_debug_response() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.middleware.exceptions._debug_response import create_debug_response # noqa: F401
-
- with pytest.raises(ImportError):
- from litestar.middleware.exceptions._debug_response import OtherName # noqa: F401
-
-
-def test_deprecate_create_exception_response() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.middleware.exceptions.middleware import create_exception_response # noqa: F401
-
- with pytest.raises(ImportError):
- from litestar.middleware.exceptions.middleware import OtherName # noqa: F401
-
-
-def test_deprecate_exception_response_content() -> None:
- with pytest.warns(DeprecationWarning):
- from litestar.middleware.exceptions.middleware import ExceptionResponseContent # noqa: F401
-
- with pytest.raises(ImportError):
- from litestar.middleware.exceptions.middleware import OtherName # noqa: F401
diff --git a/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py b/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py
index 84d9320c98..1230399cef 100644
--- a/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py
+++ b/tests/unit/test_handlers/test_asgi_handlers/test_handle_asgi.py
@@ -1,5 +1,6 @@
from litestar import Controller, MediaType, asgi
from litestar.enums import ScopeType
+from litestar.handlers import ASGIRouteHandler
from litestar.response.base import ASGIResponse
from litestar.status_codes import HTTP_200_OK
from litestar.testing import create_test_client
@@ -51,3 +52,14 @@ async def root_asgi_handler(
response = client.get("/asgi")
assert response.status_code == HTTP_200_OK
assert response.text == "/asgi"
+
+
+def test_custom_handler_class() -> None:
+ class MyHandlerClass(ASGIRouteHandler):
+ pass
+
+ @asgi("/", handler_class=MyHandlerClass)
+ async def handler() -> None:
+ pass
+
+ assert isinstance(handler, MyHandlerClass)
diff --git a/tests/unit/test_handlers/test_asgi_handlers/test_validations.py b/tests/unit/test_handlers/test_asgi_handlers/test_validations.py
index dcebc1eb17..3e1e245fa8 100644
--- a/tests/unit/test_handlers/test_asgi_handlers/test_validations.py
+++ b/tests/unit/test_handlers/test_asgi_handlers/test_validations.py
@@ -4,6 +4,7 @@
from litestar import Litestar, asgi
from litestar.exceptions import ImproperlyConfiguredException
+from litestar.routes import ASGIRoute
from litestar.testing import create_test_client
if TYPE_CHECKING:
@@ -15,25 +16,29 @@ async def fn_without_scope_arg(receive: "Receive", send: "Send") -> None:
pass
with pytest.raises(ImproperlyConfiguredException):
- asgi(path="/")(fn_without_scope_arg).on_registration(Litestar())
+ handler = asgi(path="/")(fn_without_scope_arg)
+ handler.on_registration(Litestar(), ASGIRoute(path="/", route_handler=handler))
async def fn_without_receive_arg(scope: "Scope", send: "Send") -> None:
pass
with pytest.raises(ImproperlyConfiguredException):
- asgi(path="/")(fn_without_receive_arg).on_registration(Litestar())
+ handler = asgi(path="/")(fn_without_receive_arg)
+ handler.on_registration(Litestar(), ASGIRoute(path="/", route_handler=handler))
async def fn_without_send_arg(scope: "Scope", receive: "Receive") -> None:
pass
with pytest.raises(ImproperlyConfiguredException):
- asgi(path="/")(fn_without_send_arg).on_registration(Litestar())
+ handler = asgi(path="/")(fn_without_send_arg)
+ handler.on_registration(Litestar(), ASGIRoute(path="/", route_handler=handler))
async def fn_with_return_annotation(scope: "Scope", receive: "Receive", send: "Send") -> dict:
return {}
with pytest.raises(ImproperlyConfiguredException):
- asgi(path="/")(fn_with_return_annotation).on_registration(Litestar())
+ handler = asgi(path="/")(fn_with_return_annotation)
+ handler.on_registration(Litestar(), ASGIRoute(path="/", route_handler=handler))
asgi_handler_with_no_fn = asgi(path="/")
@@ -44,4 +49,5 @@ def sync_fn(scope: "Scope", receive: "Receive", send: "Send") -> None:
return None
with pytest.raises(ImproperlyConfiguredException):
- asgi(path="/")(sync_fn).on_registration(Litestar()) # type: ignore[arg-type]
+ handler = asgi(path="/")(sync_fn) # type: ignore[arg-type]
+ handler.on_registration(Litestar(), ASGIRoute(path="/", route_handler=handler))
diff --git a/tests/unit/test_handlers/test_base_handlers/test_opt.py b/tests/unit/test_handlers/test_base_handlers/test_opt.py
index 453eb00889..60558ff2f6 100644
--- a/tests/unit/test_handlers/test_base_handlers/test_opt.py
+++ b/tests/unit/test_handlers/test_base_handlers/test_opt.py
@@ -17,7 +17,7 @@
if TYPE_CHECKING:
from litestar import WebSocket
- from litestar.types import Receive, RouteHandlerType, Scope, Send
+ from litestar.types import AnyCallable, Receive, RouteHandlerType, Scope, Send
def regular_handler() -> None: ...
@@ -41,9 +41,11 @@ async def socket_handler(socket: "WebSocket") -> None: ...
(websocket, socket_handler),
],
)
-def test_opt_settings(decorator: "RouteHandlerType", handler: Callable) -> None:
+def test_opt_settings(
+ decorator: Callable[..., Callable[["AnyCallable"], "RouteHandlerType"]], handler: "Callable"
+) -> None:
base_opt = {"base": 1, "kwarg_value": 0}
- result = decorator("/", opt=base_opt, kwarg_value=2)(handler) # type: ignore[arg-type, call-arg]
+ result = decorator("/", opt=base_opt, kwarg_value=2)(handler)
assert result.opt == {"base": 1, "kwarg_value": 2}
diff --git a/tests/unit/test_handlers/test_base_handlers/test_validations.py b/tests/unit/test_handlers/test_base_handlers/test_validations.py
index a0b168a230..e4f9cc5e06 100644
--- a/tests/unit/test_handlers/test_base_handlers/test_validations.py
+++ b/tests/unit/test_handlers/test_base_handlers/test_validations.py
@@ -5,14 +5,6 @@
from litestar import Litestar, post
from litestar.dto import DTOData
from litestar.exceptions import ImproperlyConfiguredException
-from litestar.handlers.base import BaseRouteHandler
-
-
-def test_raise_no_fn_validation() -> None:
- handler = BaseRouteHandler(path="/")
-
- with pytest.raises(ImproperlyConfiguredException):
- handler.fn
def test_dto_data_annotation_with_no_resolved_dto() -> None:
diff --git a/tests/unit/test_handlers/test_http_handlers/test_custom_handler_class.py b/tests/unit/test_handlers/test_http_handlers/test_custom_handler_class.py
new file mode 100644
index 0000000000..3df1d6954f
--- /dev/null
+++ b/tests/unit/test_handlers/test_http_handlers/test_custom_handler_class.py
@@ -0,0 +1,30 @@
+from typing import Callable
+
+import pytest
+
+from litestar.handlers import HTTPRouteHandler
+from litestar.handlers.http_handlers import delete, get, patch, post, put, route
+from litestar.types import AnyCallable
+
+
+@pytest.mark.parametrize("handler_decorator", [get, put, delete, post, patch])
+def test_custom_handler_class(handler_decorator: Callable[..., Callable[[AnyCallable], HTTPRouteHandler]]) -> None:
+ class MyHandlerClass(HTTPRouteHandler):
+ pass
+
+ @handler_decorator("/", handler_class=MyHandlerClass)
+ async def handler() -> None:
+ pass
+
+ assert isinstance(handler, MyHandlerClass)
+
+
+def test_custom_handler_class_route() -> None:
+ class MyHandlerClass(HTTPRouteHandler):
+ pass
+
+ @route("/", handler_class=MyHandlerClass, http_method="GET")
+ async def handler() -> None:
+ pass
+
+ assert isinstance(handler, MyHandlerClass)
diff --git a/tests/unit/test_handlers/test_http_handlers/test_defaults.py b/tests/unit/test_handlers/test_http_handlers/test_defaults.py
index 06c0387f98..314e131ccc 100644
--- a/tests/unit/test_handlers/test_http_handlers/test_defaults.py
+++ b/tests/unit/test_handlers/test_http_handlers/test_defaults.py
@@ -37,5 +37,5 @@
],
)
def test_route_handler_default_status_code(http_method: Any, expected_status_code: int) -> None:
- route_handler = HTTPRouteHandler(http_method=http_method)
+ route_handler = HTTPRouteHandler(http_method=http_method, fn=lambda: None)
assert route_handler.status_code == expected_status_code
diff --git a/tests/unit/test_handlers/test_http_handlers/test_deprecation.py b/tests/unit/test_handlers/test_http_handlers/test_deprecation.py
index 1cac197d2b..e69de29bb2 100644
--- a/tests/unit/test_handlers/test_http_handlers/test_deprecation.py
+++ b/tests/unit/test_handlers/test_http_handlers/test_deprecation.py
@@ -1,28 +0,0 @@
-from __future__ import annotations
-
-from importlib import reload
-from warnings import catch_warnings, simplefilter
-
-import pytest
-
-from litestar.handlers import delete, get, head, patch, post, put
-
-
-@pytest.mark.parametrize("handler_cls", [get, post, put, patch, delete, head])
-def test_subclass_warns_deprecation(handler_cls: get | post | put | patch | delete | head) -> None:
- with pytest.warns(DeprecationWarning):
-
- class SubClass(handler_cls): # type: ignore[valid-type, misc]
- pass
-
-
-def test_default_no_warns() -> None:
- with catch_warnings(record=True) as warnings:
- simplefilter("always")
- import litestar.handlers.http_handlers.decorators
-
- reload(litestar.handlers.http_handlers.decorators)
- assert len(warnings) == 0
-
- # revert to previous filter
- simplefilter("default")
diff --git a/tests/unit/test_handlers/test_http_handlers/test_head.py b/tests/unit/test_handlers/test_http_handlers/test_head.py
index 4f8983c6d6..af17b045c9 100644
--- a/tests/unit/test_handlers/test_http_handlers/test_head.py
+++ b/tests/unit/test_handlers/test_http_handlers/test_head.py
@@ -3,9 +3,10 @@
import pytest
-from litestar import HttpMethod, Litestar, Response, head
+from litestar import Litestar, Response, head
from litestar.exceptions import ImproperlyConfiguredException
from litestar.response.file import ASGIFileResponse, File
+from litestar.routes import HTTPRoute
from litestar.status_codes import HTTP_200_OK
from litestar.testing import create_test_client
@@ -27,6 +28,7 @@ def test_head_decorator_raises_validation_error_if_body_is_declared() -> None:
def handler() -> dict:
return {}
+ handler.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[handler]))
Litestar(route_handlers=[handler])
@@ -48,16 +50,6 @@ def handler_subclass() -> MyResponse[None]:
Litestar(route_handlers=[handler, handler_subclass])
-def test_head_decorator_raises_validation_error_if_method_is_passed() -> None:
- with pytest.raises(ImproperlyConfiguredException):
-
- @head("/", http_method=HttpMethod.HEAD)
- def handler() -> None:
- return
-
- handler.on_registration(Litestar())
-
-
def test_head_decorator_does_not_raise_for_file_response() -> None:
@head("/")
def handler() -> "File":
@@ -65,7 +57,7 @@ def handler() -> "File":
Litestar(route_handlers=[handler])
- handler.on_registration(Litestar())
+ handler.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[handler]))
def test_head_decorator_does_not_raise_for_asgi_file_response() -> None:
@@ -75,4 +67,4 @@ def handler() -> ASGIFileResponse:
Litestar(route_handlers=[handler])
- handler.on_registration(Litestar())
+ handler.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[handler]))
diff --git a/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py b/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py
index 73ed586b50..326972ecac 100644
--- a/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py
+++ b/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py
@@ -4,11 +4,10 @@
from hypothesis import given
from hypothesis import strategies as st
-from litestar import HttpMethod, MediaType, Response, delete, get, patch, post, put
+from litestar import HttpMethod, MediaType, Response
from litestar.exceptions import ImproperlyConfiguredException
from litestar.handlers.http_handlers import HTTPRouteHandler
from litestar.handlers.http_handlers._utils import get_default_status_code
-from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT
from litestar.utils import normalize_path
@@ -36,9 +35,9 @@ def test_route_handler_kwarg_handling(
) -> None:
if not http_method:
with pytest.raises(ImproperlyConfiguredException):
- HTTPRouteHandler(http_method=http_method)
+ HTTPRouteHandler(http_method=http_method, fn=dummy_method)
else:
- decorator = HTTPRouteHandler(
+ result = HTTPRouteHandler(
http_method=http_method,
media_type=media_type,
include_in_schema=include_in_schema,
@@ -46,8 +45,8 @@ def test_route_handler_kwarg_handling(
response_headers=response_headers,
status_code=status_code,
path=path,
+ fn=dummy_method,
)
- result = decorator(dummy_method)
if isinstance(http_method, list):
assert all(method in result.http_methods for method in http_method)
else:
@@ -61,24 +60,3 @@ def test_route_handler_kwarg_handling(
else:
assert next(iter(result.paths)) == normalize_path(path)
assert result.status_code == status_code or get_default_status_code(http_methods=result.http_methods)
-
-
-@pytest.mark.parametrize(
- "sub, http_method, expected_status_code",
- [
- (post, HttpMethod.POST, HTTP_201_CREATED),
- (delete, HttpMethod.DELETE, HTTP_204_NO_CONTENT),
- (get, HttpMethod.GET, HTTP_200_OK),
- (put, HttpMethod.PUT, HTTP_200_OK),
- (patch, HttpMethod.PATCH, HTTP_200_OK),
- ],
-)
-def test_semantic_route_handlers_disallow_http_method_assignment(
- sub: Any, http_method: Any, expected_status_code: int
-) -> None:
- result = sub()(dummy_method)
- assert http_method in result.http_methods
- assert result.status_code == expected_status_code
-
- with pytest.raises(ImproperlyConfiguredException):
- sub(http_method=HttpMethod.GET if http_method != HttpMethod.GET else HttpMethod.POST)
diff --git a/tests/unit/test_handlers/test_http_handlers/test_media_type.py b/tests/unit/test_handlers/test_http_handlers/test_media_type.py
index 306049e4d9..a2ff10b60e 100644
--- a/tests/unit/test_handlers/test_http_handlers/test_media_type.py
+++ b/tests/unit/test_handlers/test_http_handlers/test_media_type.py
@@ -4,6 +4,7 @@
import pytest
from litestar import Litestar, MediaType, get
+from litestar.routes import HTTPRoute
from tests.models import DataclassPerson
@@ -38,5 +39,5 @@ def handler() -> annotation:
Litestar(route_handlers=[handler])
- handler.on_registration(Litestar())
+ handler.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[handler]))
assert handler.media_type == expected_media_type
diff --git a/tests/unit/test_handlers/test_http_handlers/test_resolution.py b/tests/unit/test_handlers/test_http_handlers/test_resolution.py
index 2f328005ea..f294599b39 100644
--- a/tests/unit/test_handlers/test_http_handlers/test_resolution.py
+++ b/tests/unit/test_handlers/test_http_handlers/test_resolution.py
@@ -26,7 +26,7 @@ def controller_handler(self) -> None:
assert router_handler.resolve_request_max_body_size() == 1
assert app_handler.resolve_request_max_body_size() == 3
assert (
- next(r for r in app.routes if r.path == "/3").route_handler_map["POST"][0].resolve_request_max_body_size() == 2 # type: ignore[union-attr]
+ next(r for r in app.routes if r.path == "/3").route_handler_map["POST"].resolve_request_max_body_size() == 2 # type: ignore[union-attr]
)
diff --git a/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py b/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py
index eff63bf2b0..112af4ed24 100644
--- a/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py
+++ b/tests/unit/test_handlers/test_http_handlers/test_signature_namespace.py
@@ -6,17 +6,25 @@
from litestar import Controller, Router, delete, get, patch, post, put
from litestar.testing import create_test_client
+from litestar.types import HTTPHandlerDecorator
@pytest.mark.parametrize(
- ("method", "decorator"), [("GET", get), ("PUT", put), ("POST", post), ("PATCH", patch), ("DELETE", delete)]
+ ("method", "decorator"),
+ [
+ ("GET", get),
+ ("PUT", put),
+ ("POST", post),
+ ("PATCH", patch),
+ ("DELETE", delete),
+ ],
)
-def test_websocket_signature_namespace(method: str, decorator: type[get | put | post | patch | delete]) -> None:
+def test_websocket_signature_namespace(method: str, decorator: HTTPHandlerDecorator) -> None:
class MyController(Controller):
path = "/"
signature_namespace = {"c": float}
- @decorator(path="/", signature_namespace={"d": List[str], "dict": Dict}, status_code=200) # type:ignore[misc]
+ @decorator(path="/", signature_namespace={"d": List[str], "dict": Dict}, status_code=200)
async def simple_handler(
self,
a: a, # type:ignore[name-defined] # noqa: F821
diff --git a/tests/unit/test_handlers/test_http_handlers/test_validations.py b/tests/unit/test_handlers/test_http_handlers/test_validations.py
index d7be2958d4..94c2efe7c5 100644
--- a/tests/unit/test_handlers/test_http_handlers/test_validations.py
+++ b/tests/unit/test_handlers/test_http_handlers/test_validations.py
@@ -10,6 +10,7 @@
from litestar.handlers.http_handlers import HTTPRouteHandler
from litestar.params import Body
from litestar.response import File, Redirect
+from litestar.routes import HTTPRoute
from litestar.status_codes import (
HTTP_100_CONTINUE,
HTTP_200_OK,
@@ -22,19 +23,19 @@
def test_route_handler_validation_http_method() -> None:
# doesn't raise for http methods
for value in (*list(HttpMethod), *[x.upper() for x in list(HttpMethod)]):
- assert route(http_method=value) # type: ignore[arg-type, truthy-bool]
+ assert route(http_method=value) # type: ignore[arg-type, truthy-function]
# raises for invalid values
with pytest.raises(ValidationException):
- HTTPRouteHandler(http_method="deleze") # type: ignore[arg-type]
+ HTTPRouteHandler(http_method="deleze", fn=lambda: None) # type: ignore[arg-type]
# also when passing an empty list
with pytest.raises(ImproperlyConfiguredException):
- route(http_method=[], status_code=HTTP_200_OK)
+ HTTPRouteHandler(http_method=[], status_code=HTTP_200_OK, fn=lambda: None)
# also when passing malformed tokens
with pytest.raises(ValidationException):
- route(http_method=[HttpMethod.GET, "poft"], status_code=HTTP_200_OK) # type: ignore[list-item]
+ HTTPRouteHandler(http_method=[HttpMethod.GET, "poft"], status_code=HTTP_200_OK, fn=lambda: None) # type: ignore[list-item]
async def test_function_validation() -> None:
@@ -46,7 +47,9 @@ def method_with_no_annotation(): # type: ignore[no-untyped-def]
Litestar(route_handlers=[method_with_no_annotation])
- method_with_no_annotation.on_registration(Litestar())
+ method_with_no_annotation.on_registration(
+ Litestar(), HTTPRoute(path="/", route_handlers=[method_with_no_annotation])
+ )
with pytest.raises(ImproperlyConfiguredException):
@@ -56,7 +59,7 @@ def method_with_no_content() -> Dict[str, str]:
Litestar(route_handlers=[method_with_no_content])
- method_with_no_content.on_registration(Litestar())
+ method_with_no_content.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[method_with_no_content]))
with pytest.raises(ImproperlyConfiguredException):
@@ -66,7 +69,9 @@ def method_with_not_modified() -> Dict[str, str]:
Litestar(route_handlers=[method_with_not_modified])
- method_with_not_modified.on_registration(Litestar())
+ method_with_not_modified.on_registration(
+ Litestar(), HTTPRoute(path="/", route_handlers=[method_with_not_modified])
+ )
with pytest.raises(ImproperlyConfiguredException):
@@ -76,7 +81,9 @@ def method_with_status_lower_than_200() -> Dict[str, str]:
Litestar(route_handlers=[method_with_status_lower_than_200])
- method_with_status_lower_than_200.on_registration(Litestar())
+ method_with_status_lower_than_200.on_registration(
+ Litestar(), HTTPRoute(path="/", route_handlers=[method_with_status_lower_than_200])
+ )
@get(path="/", status_code=HTTP_307_TEMPORARY_REDIRECT)
def redirect_method() -> Redirect:
@@ -84,7 +91,7 @@ def redirect_method() -> Redirect:
Litestar(route_handlers=[redirect_method])
- redirect_method.on_registration(Litestar())
+ redirect_method.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[redirect_method]))
@get(path="/")
def file_method() -> File:
@@ -92,7 +99,7 @@ def file_method() -> File:
Litestar(route_handlers=[file_method])
- file_method.on_registration(Litestar())
+ file_method.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[file_method]))
assert not file_method.media_type
@@ -102,7 +109,7 @@ def file_method() -> File:
def test_function_1(socket: WebSocket) -> None:
return None
- test_function_1.on_registration(Litestar())
+ test_function_1.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[test_function_1]))
with pytest.raises(ImproperlyConfiguredException):
@@ -112,7 +119,7 @@ def test_function_2(self, data: DataclassPerson) -> None: # type: ignore[no-unt
Litestar(route_handlers=[test_function_2])
- test_function_2.on_registration(Litestar())
+ test_function_2.on_registration(Litestar(), HTTPRoute(path="/", route_handlers=[test_function_2]))
@pytest.mark.parametrize(
diff --git a/tests/unit/test_handlers/test_websocket_handlers/test_custom_handler_class.py b/tests/unit/test_handlers/test_websocket_handlers/test_custom_handler_class.py
new file mode 100644
index 0000000000..e4b3afcb89
--- /dev/null
+++ b/tests/unit/test_handlers/test_websocket_handlers/test_custom_handler_class.py
@@ -0,0 +1,12 @@
+from litestar.handlers import WebsocketRouteHandler, websocket
+
+
+def test_custom_handler_class() -> None:
+ class MyHandlerClass(WebsocketRouteHandler):
+ pass
+
+ @websocket("/", handler_class=MyHandlerClass)
+ async def handler() -> None:
+ pass
+
+ assert isinstance(handler, MyHandlerClass)
diff --git a/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py b/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py
index f6afec0a2f..a8e8c29934 100644
--- a/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py
+++ b/tests/unit/test_handlers/test_websocket_handlers/test_listeners.py
@@ -11,7 +11,9 @@
from litestar.di import Provide
from litestar.dto import DataclassDTO, dto_field
from litestar.exceptions import ImproperlyConfiguredException
+from litestar.handlers import WebsocketListenerRouteHandler
from litestar.handlers.websocket_handlers import WebsocketListener, websocket_listener
+from litestar.routes import WebSocketRoute
from litestar.testing import create_test_client
from litestar.types.asgi_types import WebSocketMode
@@ -27,21 +29,21 @@ def on_receive(self, data: str) -> str: # pyright: ignore
@pytest.fixture
-def sync_listener_callable(mock: MagicMock) -> websocket_listener:
+def sync_listener_callable(mock: MagicMock) -> WebsocketListenerRouteHandler:
def listener(data: str) -> str:
mock(data)
return data
- return websocket_listener("/")(listener)
+ return WebsocketListenerRouteHandler("/", fn=listener)
@pytest.fixture
-def async_listener_callable(mock: MagicMock) -> websocket_listener:
+def async_listener_callable(mock: MagicMock) -> WebsocketListenerRouteHandler:
async def listener(data: str) -> str:
mock(data)
return data
- return websocket_listener("/")(listener)
+ return WebsocketListenerRouteHandler("/", fn=listener)
@pytest.mark.parametrize(
@@ -52,7 +54,9 @@ async def listener(data: str) -> str:
lf("listener_class"),
],
)
-def test_basic_listener(mock: MagicMock, listener: Union[websocket_listener, Type[WebsocketListener]]) -> None:
+def test_basic_listener(
+ mock: MagicMock, listener: Union[WebsocketListenerRouteHandler, Type[WebsocketListener]]
+) -> None:
client = create_test_client([listener])
with client.websocket_connect("/") as ws:
ws.send_text("foo")
@@ -235,7 +239,7 @@ def test_listener_callback_no_data_arg_raises() -> None:
@websocket_listener("/")
def handler() -> None: ...
- handler.on_registration(Litestar())
+ handler.on_registration(Litestar(), WebSocketRoute(path="/", route_handler=handler))
def test_listener_callback_request_and_body_arg_raises() -> None:
@@ -244,14 +248,14 @@ def test_listener_callback_request_and_body_arg_raises() -> None:
@websocket_listener("/")
def handler_request(data: str, request: Request) -> None: ...
- handler_request.on_registration(Litestar())
+ handler_request.on_registration(Litestar(), WebSocketRoute(path="/", route_handler=handler_request))
with pytest.raises(ImproperlyConfiguredException):
@websocket_listener("/")
def handler_body(data: str, body: bytes) -> None: ...
- handler_body.on_registration(Litestar())
+ handler_body.on_registration(Litestar(), WebSocketRoute(path="/", route_handler=handler_body))
def test_listener_accept_connection_callback() -> None:
diff --git a/tests/unit/test_handlers/test_websocket_handlers/test_validations.py b/tests/unit/test_handlers/test_websocket_handlers/test_validations.py
index b1002d3a29..57b8f19ce6 100644
--- a/tests/unit/test_handlers/test_websocket_handlers/test_validations.py
+++ b/tests/unit/test_handlers/test_websocket_handlers/test_validations.py
@@ -4,6 +4,7 @@
from litestar import Litestar, WebSocket, websocket
from litestar.exceptions import ImproperlyConfiguredException
+from litestar.routes import WebSocketRoute
from litestar.testing import create_test_client
@@ -12,7 +13,8 @@ def fn_without_socket_arg(websocket: WebSocket) -> None:
pass
with pytest.raises(ImproperlyConfiguredException):
- websocket(path="/")(fn_without_socket_arg).on_registration(Litestar()) # type: ignore[arg-type]
+ handler = websocket(path="/")(fn_without_socket_arg) # type: ignore[arg-type]
+ handler.on_registration(Litestar(), WebSocketRoute(path="/", route_handler=handler))
def test_raises_for_return_annotation() -> None:
@@ -20,7 +22,8 @@ async def fn_with_return_annotation(socket: WebSocket) -> dict:
return {}
with pytest.raises(ImproperlyConfiguredException):
- websocket(path="/")(fn_with_return_annotation).on_registration(Litestar())
+ handler = websocket(path="/")(fn_with_return_annotation)
+ handler.on_registration(Litestar(), WebSocketRoute(path="/", route_handler=handler))
def test_raises_when_no_function() -> None:
@@ -36,7 +39,9 @@ def test_raises_when_sync_handler_user() -> None:
@websocket(path="/") # type: ignore[arg-type]
def sync_websocket_handler(socket: WebSocket) -> None: ...
- sync_websocket_handler.on_registration(Litestar())
+ sync_websocket_handler.on_registration(
+ Litestar(), WebSocketRoute(path="/", route_handler=sync_websocket_handler)
+ )
def test_raises_when_data_kwarg_is_used() -> None:
@@ -45,7 +50,9 @@ def test_raises_when_data_kwarg_is_used() -> None:
@websocket(path="/")
async def websocket_handler_with_data_kwarg(socket: WebSocket, data: Any) -> None: ...
- websocket_handler_with_data_kwarg.on_registration(Litestar())
+ websocket_handler_with_data_kwarg.on_registration(
+ Litestar(), WebSocketRoute(path="/", route_handler=websocket_handler_with_data_kwarg)
+ )
def test_raises_when_request_kwarg_is_used() -> None:
@@ -54,7 +61,9 @@ def test_raises_when_request_kwarg_is_used() -> None:
@websocket(path="/")
async def websocket_handler_with_request_kwarg(socket: WebSocket, request: Any) -> None: ...
- websocket_handler_with_request_kwarg.on_registration(Litestar())
+ websocket_handler_with_request_kwarg.on_registration(
+ Litestar(), WebSocketRoute(path="/", route_handler=websocket_handler_with_request_kwarg)
+ )
def test_raises_when_body_kwarg_is_used() -> None:
@@ -63,4 +72,6 @@ def test_raises_when_body_kwarg_is_used() -> None:
@websocket(path="/")
async def websocket_handler_with_request_kwarg(socket: WebSocket, body: bytes) -> None: ...
- websocket_handler_with_request_kwarg.on_registration(Litestar())
+ websocket_handler_with_request_kwarg.on_registration(
+ Litestar(), WebSocketRoute(path="/", route_handler=websocket_handler_with_request_kwarg)
+ )
diff --git a/tests/unit/test_kwargs/test_cookie_params.py b/tests/unit/test_kwargs/test_cookie_params.py
index 7111fe0f70..45ee2f7e69 100644
--- a/tests/unit/test_kwargs/test_cookie_params.py
+++ b/tests/unit/test_kwargs/test_cookie_params.py
@@ -15,13 +15,18 @@
(
Optional[str],
{},
- Parameter(cookie="special-cookie", min_length=1, max_length=2, required=False),
+ Parameter(cookie="special-cookie", min_length=1, max_length=2, required=False, default=None),
HTTP_200_OK,
),
(int, {"special-cookie": "123"}, Parameter(cookie="special-cookie", ge=100, le=201), HTTP_200_OK),
(int, {"special-cookie": "123"}, Parameter(cookie="special-cookie", ge=100, le=120), HTTP_400_BAD_REQUEST),
(int, {}, Parameter(cookie="special-cookie", ge=100, le=120), HTTP_400_BAD_REQUEST),
- (Optional[int], {}, Parameter(cookie="special-cookie", ge=100, le=120, required=False), HTTP_200_OK),
+ (
+ Optional[int],
+ {},
+ Parameter(cookie="special-cookie", ge=100, le=120, required=False, default=None),
+ HTTP_200_OK,
+ ),
],
)
def test_cookie_params(t_type: Type, param_dict: dict, param: ParameterKwarg, expected_code: int) -> None:
diff --git a/tests/unit/test_kwargs/test_header_params.py b/tests/unit/test_kwargs/test_header_params.py
index f281647bcc..fee0685ee4 100644
--- a/tests/unit/test_kwargs/test_header_params.py
+++ b/tests/unit/test_kwargs/test_header_params.py
@@ -15,11 +15,16 @@
(str, {"special-header": "123"}, Parameter(header="special-header", min_length=1, max_length=3), False),
(str, {"special-header": "123"}, Parameter(header="special-header", min_length=1, max_length=2), True),
(str, {}, Parameter(header="special-header", min_length=1, max_length=2), True),
- (Optional[str], {}, Parameter(header="special-header", min_length=1, max_length=2, required=False), False),
+ (
+ Optional[str],
+ {},
+ Parameter(header="special-header", min_length=1, max_length=2, required=False, default=None),
+ False,
+ ),
(int, {"special-header": "123"}, Parameter(header="special-header", ge=100, le=201), False),
(int, {"special-header": "123"}, Parameter(header="special-header", ge=100, le=120), True),
(int, {}, Parameter(header="special-header", ge=100, le=120), True),
- (Optional[int], {}, Parameter(header="special-header", ge=100, le=120, required=False), False),
+ (Optional[int], {}, Parameter(header="special-header", ge=100, le=120, required=False, default=None), False),
],
)
def test_header_params(
diff --git a/tests/unit/test_kwargs/test_path_params.py b/tests/unit/test_kwargs/test_path_params.py
index fb72fa9569..99b1993243 100644
--- a/tests/unit/test_kwargs/test_path_params.py
+++ b/tests/unit/test_kwargs/test_path_params.py
@@ -187,7 +187,7 @@ def post_greeting(title: str) -> str:
def test_optional_path_parameter() -> None:
@get(path=["/", "/{message:str}"], media_type=MediaType.TEXT, sync_to_thread=False)
- def handler(message: Optional[str]) -> str:
+ def handler(message: Optional[str] = None) -> str:
return message or "no message"
with create_test_client(route_handlers=[handler]) as client:
diff --git a/tests/unit/test_kwargs/test_validations.py b/tests/unit/test_kwargs/test_validations.py
index f0d524b0ec..0853be05c6 100644
--- a/tests/unit/test_kwargs/test_validations.py
+++ b/tests/unit/test_kwargs/test_validations.py
@@ -48,17 +48,17 @@ def test_raises_when_reserved_kwargs_are_misused(reserved_kwarg: str) -> None:
decorator = post if reserved_kwarg != "socket" else websocket
exec(f"async def test_fn({reserved_kwarg}: int) -> None: pass")
- handler_with_path_param = decorator("/{" + reserved_kwarg + ":int}")(locals()["test_fn"])
+ handler_with_path_param = decorator("/{" + reserved_kwarg + ":int}")(locals()["test_fn"]) # type: ignore[operator]
with pytest.raises(ImproperlyConfiguredException):
Litestar(route_handlers=[handler_with_path_param])
exec(f"async def test_fn({reserved_kwarg}: int) -> None: pass")
- handler_with_dependency = decorator("/", dependencies={reserved_kwarg: Provide(my_dependency)})(locals()["test_fn"])
+ handler_with_dependency = decorator("/", dependencies={reserved_kwarg: Provide(my_dependency)})(locals()["test_fn"]) # type: ignore[operator]
with pytest.raises(ImproperlyConfiguredException):
Litestar(route_handlers=[handler_with_dependency])
exec(f"async def test_fn({reserved_kwarg}: int = Parameter(query='my_param')) -> None: pass")
- handler_with_aliased_param = decorator("/")(locals()["test_fn"])
+ handler_with_aliased_param = decorator("/")(locals()["test_fn"]) # type: ignore[operator]
with pytest.raises(ImproperlyConfiguredException):
Litestar(route_handlers=[handler_with_aliased_param])
diff --git a/tests/unit/test_middleware/test_exception_handler_middleware.py b/tests/unit/test_middleware/test_exception_handler_middleware.py
index 81b402015e..8cc8433d57 100644
--- a/tests/unit/test_middleware/test_exception_handler_middleware.py
+++ b/tests/unit/test_middleware/test_exception_handler_middleware.py
@@ -48,7 +48,7 @@ def app() -> Litestar:
@pytest.fixture()
def middleware() -> ExceptionHandlerMiddleware:
- return ExceptionHandlerMiddleware(dummy_app, None)
+ return ExceptionHandlerMiddleware(dummy_app)
@pytest.fixture()
@@ -416,7 +416,7 @@ async def asgi_app(scope: Scope, receive: Receive, send: Send) -> None:
await send(start_message)
raise RuntimeError("Test exception")
- mw = ExceptionHandlerMiddleware(asgi_app, None)
+ mw = ExceptionHandlerMiddleware(asgi_app)
with pytest.raises(LitestarException):
await mw(scope, mock_receive, mock_send)
diff --git a/tests/unit/test_middleware/test_rate_limit_middleware.py b/tests/unit/test_middleware/test_rate_limit_middleware.py
index c3f452efee..48d1a78539 100644
--- a/tests/unit/test_middleware/test_rate_limit_middleware.py
+++ b/tests/unit/test_middleware/test_rate_limit_middleware.py
@@ -6,14 +6,15 @@
from time_machine import travel
from litestar import Litestar, Request, get
+from litestar.handlers import ASGIRouteHandler
from litestar.middleware.rate_limit import (
DURATION_VALUES,
CacheObject,
DurationUnit,
RateLimitConfig,
)
+from litestar.response.base import ASGIResponse
from litestar.serialization import decode_json, encode_json
-from litestar.static_files.config import StaticFilesConfig
from litestar.status_codes import HTTP_200_OK, HTTP_429_TOO_MANY_REQUESTS
from litestar.stores.base import Store
from litestar.testing import TestClient, create_test_client
@@ -224,20 +225,19 @@ def handler() -> None:
path1 = tmpdir / "test.css"
path1.write_text("styles content", "utf-8")
- static_files_config = StaticFilesConfig(directories=[tmpdir], path="/src/static") # pyright: ignore
+ asgi_handler = ASGIRouteHandler("/asgi", is_mount=True, fn=ASGIResponse(body="something"))
+
rate_limit_config = RateLimitConfig(rate_limit=("minute", 1), exclude=[r"^/src.*$"])
- with create_test_client(
- [handler], static_files_config=[static_files_config], middleware=[rate_limit_config.middleware]
- ) as client:
+ with create_test_client([handler, asgi_handler], middleware=[rate_limit_config.middleware]) as client:
response = client.get("/not-excluded")
assert response.status_code == HTTP_200_OK
response = client.get("/not-excluded")
assert response.status_code == HTTP_429_TOO_MANY_REQUESTS
- response = client.get("/src/static/test.css")
+ response = client.get("/asgi")
assert response.status_code == HTTP_200_OK
- assert response.text == "styles content"
+ assert response.text == "something"
async def test_rate_limiting_works_with_cache() -> None:
diff --git a/tests/unit/test_openapi/conftest.py b/tests/unit/test_openapi/conftest.py
index 6ec46f8d86..66af44dd6b 100644
--- a/tests/unit/test_openapi/conftest.py
+++ b/tests/unit/test_openapi/conftest.py
@@ -6,7 +6,6 @@
from litestar import Controller, MediaType, delete, get, patch, post, put
from litestar.datastructures import ResponseHeader, State
from litestar.dto import DataclassDTO, DTOConfig, DTOData
-from litestar.openapi.controller import OpenAPIController
from litestar.openapi.spec.example import Example
from litestar.params import Parameter
from tests.models import DataclassPerson, DataclassPersonFactory, DataclassPet
@@ -145,8 +144,3 @@ def person_controller() -> Type[Controller]:
@pytest.mark.usefixtures("disable_warn_implicit_sync_to_thread")
def pet_controller() -> Type[Controller]:
return create_pet_controller()
-
-
-@pytest.fixture(params=[OpenAPIController, None])
-def openapi_controller(request: pytest.FixtureRequest) -> Optional[Type[OpenAPIController]]:
- return request.param # type: ignore[no-any-return]
diff --git a/tests/unit/test_openapi/test_config.py b/tests/unit/test_openapi/test_config.py
index c16eeb9a41..17951395e5 100644
--- a/tests/unit/test_openapi/test_config.py
+++ b/tests/unit/test_openapi/test_config.py
@@ -5,7 +5,6 @@
from litestar import Litestar, get
from litestar.exceptions import ImproperlyConfiguredException
from litestar.openapi.config import OpenAPIConfig
-from litestar.openapi.controller import OpenAPIController
from litestar.openapi.plugins import RedocRenderPlugin, SwaggerRenderPlugin
from litestar.openapi.spec import Components, Example, OpenAPIHeader, OpenAPIType, Schema
@@ -80,19 +79,6 @@ def handler_2() -> None:
}
-def test_allows_customization_of_path() -> None:
- app = Litestar(
- openapi_config=OpenAPIConfig(
- title="my title", version="1.0.0", openapi_controller=OpenAPIController, path="/custom_schema_path"
- ),
- )
-
- assert app.openapi_config
- assert app.openapi_config.path == "/custom_schema_path"
- assert app.openapi_config.openapi_controller is not None
- assert app.openapi_config.openapi_controller.path == "/custom_schema_path"
-
-
def test_raises_exception_when_no_config_in_place() -> None:
with pytest.raises(ImproperlyConfiguredException):
Litestar(route_handlers=[], openapi_config=None).update_openapi_schema()
@@ -101,7 +87,7 @@ def test_raises_exception_when_no_config_in_place() -> None:
@pytest.mark.parametrize(
("plugins", "exp"),
[
- ((), RedocRenderPlugin),
+ ((), type(None)),
([RedocRenderPlugin()], RedocRenderPlugin),
([SwaggerRenderPlugin(), RedocRenderPlugin()], SwaggerRenderPlugin),
([RedocRenderPlugin(), SwaggerRenderPlugin(path="/")], SwaggerRenderPlugin),
@@ -110,8 +96,3 @@ def test_raises_exception_when_no_config_in_place() -> None:
def test_default_plugin(plugins: "List[OpenAPIRenderPlugin]", exp: "Type[OpenAPIRenderPlugin]") -> None:
config = OpenAPIConfig(title="my title", version="1.0.0", render_plugins=plugins)
assert isinstance(config.default_plugin, exp)
-
-
-def test_default_plugin_legacy() -> None:
- config = OpenAPIConfig(title="my title", version="1.0.0", openapi_controller=OpenAPIController)
- assert config.default_plugin is None
diff --git a/tests/unit/test_openapi/test_endpoints.py b/tests/unit/test_openapi/test_endpoints.py
index 6e0230a7e3..8d28d0fdba 100644
--- a/tests/unit/test_openapi/test_endpoints.py
+++ b/tests/unit/test_openapi/test_endpoints.py
@@ -1,11 +1,12 @@
-from typing import List, Optional, Type
+from collections.abc import Callable
+from typing import List, Sequence, Type
import pytest
+from typing_extensions import ParamSpec, TypeAlias
from litestar import Controller
from litestar.enums import MediaType
from litestar.openapi.config import OpenAPIConfig
-from litestar.openapi.controller import OpenAPIController
from litestar.openapi.plugins import (
JsonRenderPlugin,
OpenAPIRenderPlugin,
@@ -20,24 +21,32 @@
root_paths: List[str] = ["", "/part1", "/part1/part2"]
+P = ParamSpec("P")
+ConfigFactoryType: TypeAlias = "Callable[[Sequence[OpenAPIRenderPlugin]], OpenAPIConfig]"
+
@pytest.fixture()
-def config(openapi_controller: Optional[Type[OpenAPIController]]) -> OpenAPIConfig:
- return OpenAPIConfig(title="Litestar API", version="1.0.0", openapi_controller=openapi_controller)
+def config_factory() -> ConfigFactoryType:
+ def factory(render_plugins: Sequence[OpenAPIRenderPlugin]) -> OpenAPIConfig:
+ return OpenAPIConfig(title="Litestar API", version="1.0.0", render_plugins=list(render_plugins))
+
+ return factory
def test_default_redoc_cdn_urls(
- person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
default_redoc_version = "next"
default_redoc_js_bundle = f"https://cdn.jsdelivr.net/npm/redoc@{default_redoc_version}/bundles/redoc.standalone.js"
- with create_test_client([person_controller, pet_controller], openapi_config=config) as client:
+ with create_test_client(
+ [person_controller, pet_controller], openapi_config=config_factory((RedocRenderPlugin(),))
+ ) as client:
response = client.get("/schema/redoc")
assert default_redoc_js_bundle in response.text
def test_default_swagger_ui_cdn_urls(
- person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
default_swagger_ui_version = "5.18.2"
default_swagger_bundles = [
@@ -45,59 +54,55 @@ def test_default_swagger_ui_cdn_urls(
f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{default_swagger_ui_version}/swagger-ui-bundle.js",
f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{default_swagger_ui_version}/swagger-ui-standalone-preset.js",
]
- with create_test_client([person_controller, pet_controller], openapi_config=config) as client:
+ with create_test_client(
+ [person_controller, pet_controller], openapi_config=config_factory((SwaggerRenderPlugin(),))
+ ) as client:
response = client.get("/schema/swagger")
assert all(cdn_url in response.text for cdn_url in default_swagger_bundles)
def test_default_stoplight_elements_cdn_urls(
- person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
default_stoplight_elements_version = "7.7.18"
default_stoplight_elements_bundles = [
f"https://unpkg.com/@stoplight/elements@{default_stoplight_elements_version}/styles.min.css",
f"https://unpkg.com/@stoplight/elements@{default_stoplight_elements_version}/web-components.min.js",
]
- with create_test_client([person_controller, pet_controller], openapi_config=config) as client:
+ with create_test_client(
+ [person_controller, pet_controller], openapi_config=config_factory((StoplightRenderPlugin(),))
+ ) as client:
response = client.get("/schema/elements")
assert all(cdn_url in response.text for cdn_url in default_stoplight_elements_bundles)
def test_default_rapidoc_elements_cdn_urls(
- person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
default_rapidoc_version = "9.3.4"
default_rapidoc_bundles = [f"https://unpkg.com/rapidoc@{default_rapidoc_version}/dist/rapidoc-min.js"]
- with create_test_client([person_controller, pet_controller], openapi_config=config) as client:
+ with create_test_client(
+ [person_controller, pet_controller], openapi_config=config_factory((RapidocRenderPlugin(),))
+ ) as client:
response = client.get("/schema/rapidoc")
assert all(cdn_url in response.text for cdn_url in default_rapidoc_bundles)
def test_redoc_with_google_fonts(
- person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
google_font_cdn = "https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
- with create_test_client([person_controller, pet_controller], openapi_config=config) as client:
+ with create_test_client(
+ [person_controller, pet_controller], openapi_config=config_factory((RedocRenderPlugin(),))
+ ) as client:
response = client.get("/schema/redoc")
assert google_font_cdn in response.text
-@pytest.mark.parametrize(
- ("openapi_controller", "render_plugins"),
- [
- (type("OfflineOpenAPIController", (OpenAPIController,), {"redoc_google_fonts": False}), []),
- (None, [RedocRenderPlugin(google_fonts=False)]),
- ],
-)
def test_redoc_without_google_fonts(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
- render_plugins: List[OpenAPIRenderPlugin],
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
- offline_config = OpenAPIConfig(
- title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins
- )
+ offline_config = config_factory((RedocRenderPlugin(google_fonts=False),))
with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client:
response = client.get("/schema/redoc")
assert "fonts.googleapis.com" not in response.text
@@ -108,62 +113,26 @@ def test_redoc_without_google_fonts(
OFFLINE_LOCATION_OTHER_URL = "https://offline_location/bundle.other"
-@pytest.mark.parametrize(
- ("openapi_controller", "render_plugins"),
- [
- (type("OfflineOpenAPIController", (OpenAPIController,), {"redoc_js_url": OFFLINE_LOCATION_JS_URL}), []),
- (None, [RedocRenderPlugin(js_url=OFFLINE_LOCATION_JS_URL)]),
- ],
-)
def test_openapi_redoc_offline(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
- render_plugins: List[OpenAPIRenderPlugin],
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
- offline_config = OpenAPIConfig(
- title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins
- )
+ offline_config = config_factory((RedocRenderPlugin(js_url=OFFLINE_LOCATION_JS_URL),))
with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client:
response = client.get("/schema/redoc")
assert OFFLINE_LOCATION_JS_URL in response.text
-@pytest.mark.parametrize(
- ("openapi_controller", "render_plugins"),
- [
- (
- type(
- "OfflineOpenAPIController",
- (OpenAPIController,),
- {
- "swagger_ui_bundle_js_url": OFFLINE_LOCATION_JS_URL,
- "swagger_css_url": OFFLINE_LOCATION_CSS_URL,
- "swagger_ui_standalone_preset_js_url": OFFLINE_LOCATION_OTHER_URL,
- },
- ),
- [],
- ),
- (
- None,
- [
- SwaggerRenderPlugin(
- js_url=OFFLINE_LOCATION_JS_URL,
- css_url=OFFLINE_LOCATION_CSS_URL,
- standalone_preset_js_url=OFFLINE_LOCATION_OTHER_URL,
- )
- ],
- ),
- ],
-)
def test_openapi_swagger_offline(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
- render_plugins: List[OpenAPIRenderPlugin],
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
- offline_config = OpenAPIConfig(
- title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins
+ offline_config = config_factory(
+ (
+ SwaggerRenderPlugin(
+ js_url=OFFLINE_LOCATION_JS_URL,
+ css_url=OFFLINE_LOCATION_CSS_URL,
+ standalone_preset_js_url=OFFLINE_LOCATION_OTHER_URL,
+ ),
+ )
)
with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client:
response = client.get("/schema/swagger")
@@ -173,129 +142,79 @@ def test_openapi_swagger_offline(
)
-@pytest.mark.parametrize(
- ("openapi_controller", "render_plugins"),
- [
- (
- type(
- "OfflineOpenAPIController",
- (OpenAPIController,),
- {
- "stoplight_elements_css_url": OFFLINE_LOCATION_CSS_URL,
- "stoplight_elements_js_url": OFFLINE_LOCATION_JS_URL,
- },
- ),
- [],
- ),
- (
- None,
- [
- StoplightRenderPlugin(
- js_url=OFFLINE_LOCATION_JS_URL,
- css_url=OFFLINE_LOCATION_CSS_URL,
- )
- ],
- ),
- ],
-)
def test_openapi_stoplight_elements_offline(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
- render_plugins: List[OpenAPIRenderPlugin],
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
- offline_config = OpenAPIConfig(
- title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins
+ offline_config = config_factory(
+ (StoplightRenderPlugin(js_url=OFFLINE_LOCATION_JS_URL, css_url=OFFLINE_LOCATION_CSS_URL),)
)
with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client:
response = client.get("/schema/elements")
assert all(offline_url in response.text for offline_url in [OFFLINE_LOCATION_JS_URL, OFFLINE_LOCATION_CSS_URL])
-@pytest.mark.parametrize(
- ("openapi_controller", "render_plugins"),
- [
- (
- None,
- [
- ScalarRenderPlugin(
- js_url=OFFLINE_LOCATION_JS_URL,
- css_url=OFFLINE_LOCATION_CSS_URL,
- )
- ],
- ),
- ],
-)
def test_openapi_scalar_offline(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
- render_plugins: List[OpenAPIRenderPlugin],
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
- offline_config = OpenAPIConfig(
- title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins
+ offline_config = config_factory(
+ (ScalarRenderPlugin(js_url=OFFLINE_LOCATION_JS_URL, css_url=OFFLINE_LOCATION_CSS_URL),)
)
with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client:
response = client.get("/schema/scalar")
assert all(offline_url in response.text for offline_url in [OFFLINE_LOCATION_JS_URL, OFFLINE_LOCATION_CSS_URL])
-@pytest.mark.parametrize(
- ("openapi_controller", "render_plugins"),
- [
- (type("OfflineOpenAPIController", (OpenAPIController,), {"rapidoc_js_url": OFFLINE_LOCATION_JS_URL}), []),
- (None, [RapidocRenderPlugin(js_url=OFFLINE_LOCATION_JS_URL)]),
- ],
-)
def test_openapi_rapidoc_offline(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
- render_plugins: List[OpenAPIRenderPlugin],
+ person_controller: Type[Controller], pet_controller: Type[Controller], config_factory: ConfigFactoryType
) -> None:
- offline_config = OpenAPIConfig(
- title="Litestar API", version="1.0.0", openapi_controller=openapi_controller, render_plugins=render_plugins
- )
+ offline_config = config_factory((RapidocRenderPlugin(js_url=OFFLINE_LOCATION_JS_URL),))
with create_test_client([person_controller, pet_controller], openapi_config=offline_config) as client:
response = client.get("/schema/rapidoc")
assert OFFLINE_LOCATION_JS_URL in response.text
@pytest.mark.parametrize("root_path", root_paths)
-def test_openapi_root(
- root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
+@pytest.mark.parametrize(
+ ("plugin", "path"),
+ [
+ (RedocRenderPlugin(), "/schema/redoc"),
+ (SwaggerRenderPlugin(), "/schema/swagger"),
+ (StoplightRenderPlugin(), "/schema/elements"),
+ (ScalarRenderPlugin(), "/schema/scalar"),
+ (RapidocRenderPlugin(), "/schema/rapidoc"),
+ ],
+)
+def test_openapi_plugins(
+ root_path: str,
+ plugin: OpenAPIRenderPlugin,
+ path: str,
+ person_controller: Type[Controller],
+ pet_controller: Type[Controller],
+ config_factory: ConfigFactoryType,
) -> None:
- with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client:
+ with create_test_client(
+ [person_controller, pet_controller], root_path=root_path, openapi_config=config_factory((plugin,))
+ ) as client:
response = client.get("/schema")
assert response.status_code == HTTP_200_OK
assert response.headers["content-type"].startswith(MediaType.HTML.value)
-
-
-@pytest.mark.parametrize("root_path", root_paths)
-def test_openapi_redoc(
- root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
-) -> None:
- with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client:
- response = client.get("/schema/redoc")
- assert response.status_code == HTTP_200_OK
- assert response.headers["content-type"].startswith(MediaType.HTML.value)
-
-
-@pytest.mark.parametrize("root_path", root_paths)
-def test_openapi_swagger(
- root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
-) -> None:
- with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client:
- response = client.get("/schema/swagger")
+ response = client.get(path)
assert response.status_code == HTTP_200_OK
assert response.headers["content-type"].startswith(MediaType.HTML.value)
@pytest.mark.parametrize("root_path", root_paths)
def test_openapi_swagger_caching_schema(
- root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
+ root_path: str,
+ person_controller: Type[Controller],
+ pet_controller: Type[Controller],
+ config_factory: ConfigFactoryType,
) -> None:
- with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client:
+ with create_test_client(
+ [person_controller, pet_controller],
+ root_path=root_path,
+ openapi_config=config_factory((SwaggerRenderPlugin(),)),
+ ) as client:
# Make sure that the schema is tweaked for swagger as the openapi version is changed.
# Because schema can get cached, make sure that getting a different schema type before works.
client.get("/schema/redoc") # Cache the schema
@@ -306,114 +225,12 @@ def test_openapi_swagger_caching_schema(
assert response.headers["content-type"].startswith(MediaType.HTML.value)
-@pytest.mark.parametrize("root_path", root_paths)
-def test_openapi_stoplight_elements(
- root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
-) -> None:
- with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client:
- response = client.get("/schema/elements/")
- assert response.status_code == HTTP_200_OK
- assert response.headers["content-type"].startswith(MediaType.HTML.value)
-
-
-@pytest.mark.parametrize("root_path", root_paths)
-def test_openapi_rapidoc(
- root_path: str, person_controller: Type[Controller], pet_controller: Type[Controller], config: OpenAPIConfig
-) -> None:
- with create_test_client([person_controller, pet_controller], root_path=root_path, openapi_config=config) as client:
- response = client.get("/schema/rapidoc")
- assert response.status_code == HTTP_200_OK
- assert response.headers["content-type"].startswith(MediaType.HTML.value)
-
-
-def test_openapi_root_not_allowed(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
-) -> None:
+def test_openapi_plugin_not_found(person_controller: Type[Controller], pet_controller: Type[Controller]) -> None:
with create_test_client(
[person_controller, pet_controller],
openapi_config=OpenAPIConfig(
title="Litestar API",
version="1.0.0",
- enabled_endpoints={"swagger", "elements", "openapi.json", "openapi.yaml", "openapi.yml"},
- openapi_controller=openapi_controller,
- ),
- ) as client:
- response = client.get("/schema")
- assert response.status_code == HTTP_404_NOT_FOUND
- assert response.headers["content-type"].startswith(MediaType.HTML.value)
-
-
-def test_openapi_redoc_not_allowed(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
-) -> None:
- with create_test_client(
- [person_controller, pet_controller],
- openapi_config=OpenAPIConfig(
- title="Litestar API",
- version="1.0.0",
- enabled_endpoints={"swagger", "elements", "openapi.json", "openapi.yaml", "openapi.yml"},
- openapi_controller=openapi_controller,
- ),
- ) as client:
- response = client.get("/schema/redoc")
- assert response.status_code == HTTP_404_NOT_FOUND
- assert response.headers["content-type"].startswith(MediaType.HTML.value)
-
-
-def test_openapi_swagger_not_allowed(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
-) -> None:
- with create_test_client(
- [person_controller, pet_controller],
- openapi_config=OpenAPIConfig(
- title="Litestar API",
- version="1.0.0",
- enabled_endpoints={"redoc", "elements", "openapi.json", "openapi.yaml", "openapi.yml"},
- openapi_controller=openapi_controller,
- ),
- ) as client:
- response = client.get("/schema/swagger")
- assert response.status_code == HTTP_404_NOT_FOUND
- assert response.headers["content-type"].startswith(MediaType.HTML.value)
-
-
-def test_openapi_stoplight_elements_not_allowed(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
-) -> None:
- with create_test_client(
- [person_controller, pet_controller],
- openapi_config=OpenAPIConfig(
- title="Litestar API",
- version="1.0.0",
- enabled_endpoints={"redoc", "swagger", "openapi.json", "openapi.yaml", "openapi.yml"},
- openapi_controller=openapi_controller,
- ),
- ) as client:
- response = client.get("/schema/elements/")
- assert response.status_code == HTTP_404_NOT_FOUND
- assert response.headers["content-type"].startswith(MediaType.HTML.value)
-
-
-def test_openapi_rapidoc_not_allowed(
- person_controller: Type[Controller],
- pet_controller: Type[Controller],
- openapi_controller: Optional[Type[OpenAPIController]],
-) -> None:
- with create_test_client(
- [person_controller, pet_controller],
- openapi_config=OpenAPIConfig(
- title="Litestar API",
- version="1.0.0",
- enabled_endpoints={"swagger", "elements", "openapi.json", "openapi.yaml", "openapi.yml"},
- openapi_controller=openapi_controller,
),
) as client:
response = client.get("/schema/rapidoc")
@@ -425,8 +242,8 @@ def test_openapi_rapidoc_not_allowed(
("render_plugins",),
[
([],),
- ([RedocRenderPlugin()],),
- ([RedocRenderPlugin(), JsonRenderPlugin()],),
+ ([ScalarRenderPlugin()],),
+ ([ScalarRenderPlugin(), JsonRenderPlugin()],),
([JsonRenderPlugin(path="/custom_path")],),
([JsonRenderPlugin(path=["/openapi.json", "/custom_path"])],),
],
@@ -444,7 +261,7 @@ def test_json_plugin_always_enabled(render_plugins: List["OpenAPIRenderPlugin"])
assert response.status_code == HTTP_200_OK
-def test_default_plugin_explicit_path() -> None:
+def test_plugin_explicit_root_path() -> None:
config = OpenAPIConfig(title="my title", version="1.0.0", render_plugins=[SwaggerRenderPlugin(path="/")])
with create_test_client([], openapi_config=config) as client:
response = client.get("/schema/")
@@ -454,30 +271,17 @@ def test_default_plugin_explicit_path() -> None:
assert response.status_code == HTTP_404_NOT_FOUND
-def test_default_plugin_backward_compatibility() -> None:
+def test_default_plugin() -> None:
config = OpenAPIConfig(title="my title", version="1.0.0")
with create_test_client([], openapi_config=config) as client:
response = client.get("/schema/")
assert response.status_code == HTTP_200_OK
- response = client.get("/schema/redoc")
- assert response.status_code == HTTP_200_OK
-
-
-def test_default_plugin_backward_compatibility_not_found() -> None:
- config = OpenAPIConfig(title="my title", version="1.0.0", enabled_endpoints={"redoc"}, root_schema_site="swagger")
- with create_test_client([], openapi_config=config) as client:
- response = client.get("/schema/")
- assert response.status_code == HTTP_404_NOT_FOUND
-
- response = client.get("/schema/swagger")
- assert response.status_code == HTTP_404_NOT_FOUND
-
- response = client.get("/schema/redoc")
+ response = client.get("/schema/scalar")
assert response.status_code == HTTP_200_OK
-def test_default_plugin_future_compatibility() -> None:
+def test_explicit_plugin() -> None:
config = OpenAPIConfig(title="my title", version="1.0.0", render_plugins=[SwaggerRenderPlugin()])
with create_test_client([], openapi_config=config) as client:
response = client.get("/schema/")
diff --git a/tests/unit/test_openapi/test_integration.py b/tests/unit/test_openapi/test_integration.py
index 92f6cc118a..fe5f9f3c5d 100644
--- a/tests/unit/test_openapi/test_integration.py
+++ b/tests/unit/test_openapi/test_integration.py
@@ -10,10 +10,11 @@
import yaml
from typing_extensions import Annotated
-from litestar import Controller, Litestar, Router, delete, get, patch, post
+from litestar import Controller, Litestar, delete, get, patch, post
from litestar._openapi.plugin import OpenAPIPlugin
from litestar.enums import MediaType, OpenAPIMediaType, ParamType
-from litestar.openapi import OpenAPIConfig, OpenAPIController
+from litestar.openapi import OpenAPIConfig
+from litestar.openapi.plugins import YamlRenderPlugin
from litestar.openapi.spec import Parameter as OpenAPIParameter
from litestar.params import Parameter
from litestar.serialization.msgspec_hooks import decode_json, encode_json, get_serializer
@@ -34,10 +35,9 @@ def test_openapi(
pet_controller: type[Controller],
create_examples: bool,
schema_path: str,
- openapi_controller: type[OpenAPIController] | None,
) -> None:
openapi_config = OpenAPIConfig(
- "Example API", "1.0.0", create_examples=create_examples, openapi_controller=openapi_controller
+ "Example API", "1.0.0", create_examples=create_examples, render_plugins=[YamlRenderPlugin()]
)
with create_test_client([person_controller, pet_controller], openapi_config=openapi_config) as client:
assert client.app.openapi_schema
@@ -53,14 +53,9 @@ def test_openapi(
def test_openapi_json(
- person_controller: type[Controller],
- pet_controller: type[Controller],
- create_examples: bool,
- openapi_controller: type[OpenAPIController] | None,
+ person_controller: type[Controller], pet_controller: type[Controller], create_examples: bool
) -> None:
- openapi_config = OpenAPIConfig(
- "Example API", "1.0.0", create_examples=create_examples, openapi_controller=openapi_controller
- )
+ openapi_config = OpenAPIConfig("Example API", "1.0.0", create_examples=create_examples)
with create_test_client([person_controller, pet_controller], openapi_config=openapi_config) as client:
assert client.app.openapi_schema
openapi_schema = client.app.openapi_schema
@@ -77,15 +72,10 @@ def test_openapi_json(
"endpoint, schema_path", [("openapi.yaml", "/schema/openapi.yaml"), ("openapi.yml", "/schema/openapi.yml")]
)
def test_openapi_yaml_not_allowed(
- endpoint: str,
- schema_path: str,
- person_controller: type[Controller],
- pet_controller: type[Controller],
- openapi_controller: type[OpenAPIController] | None,
+ endpoint: str, schema_path: str, person_controller: type[Controller], pet_controller: type[Controller]
) -> None:
- openapi_config = OpenAPIConfig(
- "Example API", "1.0.0", enabled_endpoints=set(), openapi_controller=openapi_controller
- )
+ openapi_config = OpenAPIConfig("Example API", "1.0.0")
+ assert not any(isinstance(plugin, YamlRenderPlugin) for plugin in openapi_config.render_plugins)
with create_test_client([person_controller, pet_controller], openapi_config=openapi_config) as client:
assert client.app.openapi_schema
@@ -95,23 +85,6 @@ def test_openapi_yaml_not_allowed(
assert response.status_code == HTTP_404_NOT_FOUND
-def test_openapi_json_not_allowed(person_controller: type[Controller], pet_controller: type[Controller]) -> None:
- # only tested with the OpenAPIController, b/c new router based approach always serves `openapi.json`.
- openapi_config = OpenAPIConfig(
- "Example API",
- "1.0.0",
- enabled_endpoints=set(),
- openapi_controller=OpenAPIController,
- )
-
- with create_test_client([person_controller, pet_controller], openapi_config=openapi_config) as client:
- assert client.app.openapi_schema
- openapi_schema = client.app.openapi_schema
- assert openapi_schema.paths
- response = client.get("/schema/openapi.json")
- assert response.status_code == HTTP_404_NOT_FOUND
-
-
@pytest.mark.parametrize(
"schema_paths",
[
@@ -120,7 +93,7 @@ def test_openapi_json_not_allowed(person_controller: type[Controller], pet_contr
],
)
def test_openapi_controller_internal_schema_conversion(schema_paths: list[str]) -> None:
- openapi_config = OpenAPIConfig("Example API", "1.0.0", openapi_controller=OpenAPIController)
+ openapi_config = OpenAPIConfig("Example API", "1.0.0", render_plugins=(YamlRenderPlugin(),))
with create_test_client([], openapi_config=openapi_config) as client:
for schema_path in schema_paths:
@@ -129,10 +102,8 @@ def test_openapi_controller_internal_schema_conversion(schema_paths: list[str])
assert "Example API" in response.text
-def test_openapi_custom_path(openapi_controller: type[OpenAPIController] | None) -> None:
- openapi_config = OpenAPIConfig(
- title="my title", version="1.0.0", path="/custom_schema_path", openapi_controller=openapi_controller
- )
+def test_openapi_custom_path() -> None:
+ openapi_config = OpenAPIConfig(title="my title", version="1.0.0", path="/custom_schema_path")
with create_test_client([], openapi_config=openapi_config) as client:
response = client.get("/schema")
assert response.status_code == HTTP_404_NOT_FOUND
@@ -144,10 +115,8 @@ def test_openapi_custom_path(openapi_controller: type[OpenAPIController] | None)
assert response.status_code == HTTP_200_OK
-def test_openapi_normalizes_custom_path(openapi_controller: type[OpenAPIController] | None) -> None:
- openapi_config = OpenAPIConfig(
- title="my title", version="1.0.0", path="custom_schema_path", openapi_controller=openapi_controller
- )
+def test_openapi_normalizes_custom_path() -> None:
+ openapi_config = OpenAPIConfig(title="my title", version="1.0.0", path="custom_schema_path")
with create_test_client([], openapi_config=openapi_config) as client:
response = client.get("/custom_schema_path/openapi.json")
assert response.status_code == HTTP_200_OK
@@ -156,41 +125,7 @@ def test_openapi_normalizes_custom_path(openapi_controller: type[OpenAPIControll
assert response.status_code == HTTP_200_OK
-def test_openapi_custom_path_avoids_override() -> None:
- class CustomOpenAPIController(OpenAPIController):
- path = "/custom_docs"
-
- openapi_config = OpenAPIConfig(title="my title", version="1.0.0", openapi_controller=CustomOpenAPIController)
- with create_test_client([], openapi_config=openapi_config) as client:
- response = client.get("/schema")
- assert response.status_code == HTTP_404_NOT_FOUND
-
- response = client.get("/custom_docs/openapi.json")
- assert response.status_code == HTTP_200_OK
-
- response = client.get("/custom_docs/openapi.json")
- assert response.status_code == HTTP_200_OK
-
-
-def test_openapi_custom_path_overrides_custom_controller_path() -> None:
- class CustomOpenAPIController(OpenAPIController):
- path = "/custom_docs"
-
- openapi_config = OpenAPIConfig(
- title="my title", version="1.0.0", openapi_controller=CustomOpenAPIController, path="/override_docs_path"
- )
- with create_test_client([], openapi_config=openapi_config) as client:
- response = client.get("/custom_docs")
- assert response.status_code == HTTP_404_NOT_FOUND
-
- response = client.get("/override_docs_path/openapi.json")
- assert response.status_code == HTTP_200_OK
-
- response = client.get("/override_docs_path/openapi.json")
- assert response.status_code == HTTP_200_OK
-
-
-def test_msgspec_schema_generation(create_examples: bool, openapi_controller: type[OpenAPIController] | None) -> None:
+def test_msgspec_schema_generation(create_examples: bool) -> None:
class Lookup(msgspec.Struct):
id: Annotated[
str,
@@ -208,12 +143,7 @@ async def example_route() -> Lookup:
with create_test_client(
route_handlers=[example_route],
- openapi_config=OpenAPIConfig(
- title="Example API",
- version="1.0.0",
- create_examples=create_examples,
- openapi_controller=openapi_controller,
- ),
+ openapi_config=OpenAPIConfig(title="Example API", version="1.0.0", create_examples=create_examples),
signature_types=[Lookup],
) as client:
response = client.get("/schema/openapi.json")
@@ -268,19 +198,14 @@ async def handler() -> SomeModel:
assert schema.properties["field_c"].default is None # type: ignore[union-attr, index]
-def test_schema_for_optional_path_parameter(openapi_controller: type[OpenAPIController] | None) -> None:
+def test_schema_for_optional_path_parameter() -> None:
@get(path=["/", "/{test_message:str}"], media_type=MediaType.TEXT, sync_to_thread=False)
def handler(test_message: Optional[str]) -> str: # noqa: UP007
return test_message or "no message"
with create_test_client(
route_handlers=[handler],
- openapi_config=OpenAPIConfig(
- title="Example API",
- version="1.0.0",
- create_examples=True,
- openapi_controller=openapi_controller,
- ),
+ openapi_config=OpenAPIConfig(title="Example API", version="1.0.0", create_examples=True),
) as client:
response = client.get("/schema/openapi.json")
assert response.status_code == HTTP_200_OK
@@ -299,7 +224,7 @@ class Foo(Generic[T]):
foo: T
-def test_with_generic_class(openapi_controller: type[OpenAPIController] | None) -> None:
+def test_with_generic_class() -> None:
@get("/foo-str", sync_to_thread=False)
def handler_foo_str() -> Foo[str]:
return Foo("")
@@ -310,11 +235,7 @@ def handler_foo_int() -> Foo[int]:
with create_test_client(
route_handlers=[handler_foo_str, handler_foo_int],
- openapi_config=OpenAPIConfig(
- title="Example API",
- version="1.0.0",
- openapi_controller=openapi_controller,
- ),
+ openapi_config=OpenAPIConfig(title="Example API", version="1.0.0"),
) as client:
response = client.get("/schema/openapi.json")
assert response.status_code == HTTP_200_OK
@@ -520,40 +441,3 @@ def delete_handler(self, data: B) -> None: ...
"test_components_schemas_in_alphabetical_order.C",
]
assert list(openapi.components.schemas.keys()) == expected_keys
-
-
-def test_openapi_controller_and_openapi_router_on_same_app() -> None:
- """Test that OpenAPIController and OpenAPIRouter can coexist on the same app.
-
- As part of backward compatibility with new plugin-based OpenAPI router approach, we did not consider
- the case where an OpenAPIController is registered on the application by means other than via the
- OpenAPIConfig object. This is an approach that has been used to serve the openapi both under the
- `/schema` and `/some-prefix/schema` paths. This test ensures that the OpenAPIController and OpenAPIRouter
- can coexist on the same app.
-
- See: https://github.com/litestar-org/litestar/issues/3337
- """
- router = Router(path="/abc", route_handlers=[OpenAPIController])
- openapi_config = OpenAPIConfig("Litestar", "v0.0.1") # no openapi_controller specified means we use the router
- app = Litestar([router], openapi_config=openapi_config)
- assert sorted(r.path for r in app.routes) == [
- "/abc/schema",
- "/abc/schema/elements",
- "/abc/schema/oauth2-redirect.html",
- "/abc/schema/openapi.json",
- "/abc/schema/openapi.yaml",
- "/abc/schema/openapi.yml",
- "/abc/schema/rapidoc",
- "/abc/schema/redoc",
- "/abc/schema/swagger",
- "/schema",
- "/schema/elements",
- "/schema/oauth2-redirect.html",
- "/schema/openapi.json",
- "/schema/openapi.yaml",
- "/schema/openapi.yml",
- "/schema/rapidoc",
- "/schema/redoc",
- "/schema/swagger",
- "/schema/{path:str}",
- ]
diff --git a/tests/unit/test_openapi/test_parameters.py b/tests/unit/test_openapi/test_parameters.py
index 8ef236e12b..21c424053c 100644
--- a/tests/unit/test_openapi/test_parameters.py
+++ b/tests/unit/test_openapi/test_parameters.py
@@ -40,7 +40,7 @@ def create_factory(route: BaseRoute, handler: HTTPRouteHandler) -> ParameterFact
def _create_parameters(app: Litestar, path: str) -> List["OpenAPIParameter"]:
index = find_index(app.routes, lambda x: x.path_format == path)
route = app.routes[index]
- route_handler = route.route_handler_map["GET"][0] # type: ignore[union-attr]
+ route_handler = route.route_handler_map["GET"] # type: ignore[union-attr]
handler = route_handler.fn
assert callable(handler)
return create_factory(route, route_handler).create_parameters_for_handler()
diff --git a/tests/unit/test_openapi/test_path_item.py b/tests/unit/test_openapi/test_path_item.py
index 1f0dadc5b3..a2401b1c86 100644
--- a/tests/unit/test_openapi/test_path_item.py
+++ b/tests/unit/test_openapi/test_path_item.py
@@ -8,12 +8,11 @@
import pytest
from typing_extensions import TypeAlias
-from litestar import Controller, HttpMethod, Litestar, Request, Router, delete, get
+from litestar import Controller, HttpMethod, Litestar, Request, Router, delete, get, route
from litestar._openapi.datastructures import OpenAPIContext
from litestar._openapi.path_item import PathItemFactory, merge_path_item_operations
from litestar._openapi.utils import default_operation_id_creator
from litestar.exceptions import ImproperlyConfiguredException
-from litestar.handlers.http_handlers import HTTPRouteHandler
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.spec import Operation, PathItem
from litestar.utils import find_index
@@ -23,7 +22,7 @@
@pytest.fixture()
-def route(person_controller: type[Controller]) -> HTTPRoute:
+def http_route(person_controller: type[Controller]) -> HTTPRoute:
app = Litestar(route_handlers=[person_controller], openapi_config=None)
index = find_index(app.routes, lambda x: x.path_format == "/{service_id}/person/{person_id}")
return cast("HTTPRoute", app.routes[index])
@@ -59,8 +58,8 @@ def factory(route: HTTPRoute) -> PathItemFactory:
return factory
-def test_create_path_item(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None:
- schema = create_factory(route).create_path_item()
+def test_create_path_item(http_route: HTTPRoute, create_factory: CreateFactoryFixture) -> None:
+ schema = create_factory(http_route).create_path_item()
assert schema.delete
assert schema.delete.operation_id == "ServiceIdPersonPersonIdDeletePerson"
assert schema.delete.summary == "DeletePerson"
@@ -79,7 +78,7 @@ def test_unique_operation_ids_for_multiple_http_methods(create_factory: CreateFa
class MultipleMethodsRouteController(Controller):
path = "/"
- @HTTPRouteHandler("/", http_method=["GET", "HEAD"])
+ @route("/", http_method=["GET", "HEAD"])
async def root(self, *, request: Request[str, str, Any]) -> None:
pass
@@ -100,7 +99,7 @@ def test_unique_operation_ids_for_multiple_http_methods_with_handler_level_opera
class MultipleMethodsRouteController(Controller):
path = "/"
- @HTTPRouteHandler("/", http_method=["GET", "HEAD"], operation_id=default_operation_id_creator)
+ @route("/", http_method=["GET", "HEAD"], operation_id=default_operation_id_creator)
async def root(self, *, request: Request[str, str, Any]) -> None:
pass
@@ -128,8 +127,10 @@ def test_routes_with_different_paths_should_generate_unique_operation_ids(
assert schema_v1.get.operation_id != schema_v2.get.operation_id
-def test_create_path_item_use_handler_docstring_false(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None:
- factory = create_factory(route)
+def test_create_path_item_use_handler_docstring_false(
+ http_route: HTTPRoute, create_factory: CreateFactoryFixture
+) -> None:
+ factory = create_factory(http_route)
assert not factory.context.openapi_config.use_handler_docstrings
schema = factory.create_path_item()
assert schema.get
@@ -138,8 +139,10 @@ def test_create_path_item_use_handler_docstring_false(route: HTTPRoute, create_f
assert schema.patch.description == "Description in decorator"
-def test_create_path_item_use_handler_docstring_true(route: HTTPRoute, create_factory: CreateFactoryFixture) -> None:
- factory = create_factory(route)
+def test_create_path_item_use_handler_docstring_true(
+ http_route: HTTPRoute, create_factory: CreateFactoryFixture
+) -> None:
+ factory = create_factory(http_route)
factory.context.openapi_config.use_handler_docstrings = True
schema = factory.create_path_item()
assert schema.get
diff --git a/tests/unit/test_openapi/test_request_body.py b/tests/unit/test_openapi/test_request_body.py
index 05b196ebfc..7a1452aa96 100644
--- a/tests/unit/test_openapi/test_request_body.py
+++ b/tests/unit/test_openapi/test_request_body.py
@@ -50,7 +50,7 @@ def _factory(route_handler: BaseRouteHandler, data_field: FieldDefinition) -> Re
def test_create_request_body(person_controller: Type[Controller], create_request: RequestBodyFactory) -> None:
for route in Litestar(route_handlers=[person_controller]).routes:
- for route_handler, _ in route.route_handler_map.values(): # type: ignore[union-attr]
+ for route_handler in route.route_handler_map.values(): # type: ignore[union-attr]
handler_fields = route_handler.parsed_fn_signature.parameters
if "data" in handler_fields:
request_body = create_request(route_handler, handler_fields["data"])
diff --git a/tests/unit/test_openapi/test_responses.py b/tests/unit/test_openapi/test_responses.py
index bb5c213397..b618a5df3a 100644
--- a/tests/unit/test_openapi/test_responses.py
+++ b/tests/unit/test_openapi/test_responses.py
@@ -73,7 +73,7 @@ def test_create_responses(
) -> None:
for route in Litestar(route_handlers=[person_controller]).routes:
assert isinstance(route, HTTPRoute)
- for route_handler, _ in route.route_handler_map.values():
+ for route_handler in route.route_handler_map.values():
if route_handler.resolve_include_in_schema():
responses = create_factory(route_handler).create_responses(True)
assert responses
diff --git a/tests/unit/test_params.py b/tests/unit/test_params.py
index b332871359..07a70d8a89 100644
--- a/tests/unit/test_params.py
+++ b/tests/unit/test_params.py
@@ -83,22 +83,17 @@ def handler(dep: int = Dependency(skip_validation=True)) -> int:
assert response.text == "null"
-@pytest.mark.parametrize(
- "dependency, expected",
- [
- (Dependency(), None),
- (Dependency(default=None), None),
- (Dependency(default=13), 13),
- ],
-)
-def test_dependency_defaults(dependency: Any, expected: Optional[int]) -> None:
+@pytest.mark.parametrize(("default",), [(None,), (13,)])
+def test_dependency_defaults(default: Any) -> None:
@get("/")
- def handler(value: Optional[int] = dependency) -> Dict[str, Optional[int]]:
- return {"value": value}
+ def handler(
+ value_1: Optional[int] = Dependency(default=default), value_2: Annotated[Optional[int], Dependency()] = default
+ ) -> Dict[str, Optional[int]]:
+ return {"value_1": value_1, "value_2": value_2}
with create_test_client(route_handlers=[handler]) as client:
resp = client.get("/")
- assert resp.json() == {"value": expected}
+ assert resp.json() == {"value_1": default, "value_2": default}
def test_dependency_non_optional_with_default() -> None:
@@ -238,8 +233,10 @@ def handle_optional_annotated(param: Annotated[Optional[str], Parameter(query="k
def test_optional_query_parameter_consistency_no_default_queried_without_param(
optional_no_default_client: TestClient,
) -> None:
- assert optional_no_default_client.get("/optional-no-default", params={}).json() == {"key": None}
- assert optional_no_default_client.get("/optional-annotated-no-default", params={}).json() == {"key": None}
+ assert optional_no_default_client.get("/optional-no-default", params={}).status_code == HTTP_400_BAD_REQUEST
+ assert (
+ optional_no_default_client.get("/optional-annotated-no-default", params={}).status_code == HTTP_400_BAD_REQUEST
+ )
def test_optional_query_parameter_consistency_no_default_queried_with_expected_param(
@@ -252,10 +249,14 @@ def test_optional_query_parameter_consistency_no_default_queried_with_expected_p
def test_optional_query_parameter_consistency_no_default_queried_with_other_param(
optional_no_default_client: TestClient,
) -> None:
- assert optional_no_default_client.get("/optional-no-default", params={"param": "a"}).json() == {"key": None}
- assert optional_no_default_client.get("/optional-annotated-no-default", params={"param": "a"}).json() == {
- "key": None
- }
+ assert (
+ optional_no_default_client.get("/optional-no-default", params={"param": "a"}).status_code
+ == HTTP_400_BAD_REQUEST
+ )
+ assert (
+ optional_no_default_client.get("/optional-annotated-no-default", params={"param": "a"}).status_code
+ == HTTP_400_BAD_REQUEST
+ )
@pytest.fixture(name="optional_default_client")
diff --git a/tests/unit/test_response/test_response_cookies.py b/tests/unit/test_response/test_response_cookies.py
index b1627aa7a8..3c5deaa2e6 100644
--- a/tests/unit/test_response/test_response_cookies.py
+++ b/tests/unit/test_response/test_response_cookies.py
@@ -37,7 +37,7 @@ def test_method(self) -> None:
response_cookies=[app_first, app_second],
route_handlers=[first_router, second_router],
)
- route_handler, _ = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr]
+ route_handler = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr]
response_cookies = {cookie.key: cookie.value for cookie in route_handler.resolve_response_cookies()}
assert response_cookies["first"] == local_first.value
assert response_cookies["second"] == controller_second.value
diff --git a/tests/unit/test_response/test_response_headers.py b/tests/unit/test_response/test_response_headers.py
index adffade596..d7f7393da0 100644
--- a/tests/unit/test_response/test_response_headers.py
+++ b/tests/unit/test_response/test_response_headers.py
@@ -38,7 +38,7 @@ def test_method(self) -> None:
route_handlers=[first_router, second_router],
)
- route_handler, _ = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr]
+ route_handler = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr]
resolved_headers = {header.name: header for header in route_handler.resolve_response_headers()}
assert resolved_headers["first"].value == local_first.value
assert resolved_headers["second"].value == controller_second.value
@@ -184,6 +184,6 @@ def my_handler() -> None:
app = Litestar(route_handlers=[my_handler])
- route_handler, _ = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr]
+ route_handler = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr]
resolved_headers = {header.name: header for header in route_handler.resolve_response_headers()}
assert resolved_headers[header.HEADER_NAME].value == header.to_header()
diff --git a/tests/unit/test_response/test_response_to_asgi_response.py b/tests/unit/test_response/test_response_to_asgi_response.py
index b05da1c98d..4a23f69986 100644
--- a/tests/unit/test_response/test_response_to_asgi_response.py
+++ b/tests/unit/test_response/test_response_to_asgi_response.py
@@ -89,8 +89,7 @@ async def handler(data: DataclassPerson) -> DataclassPerson:
response = await handler.to_response(
data=handler.fn(data=person_instance),
- app=Litestar(route_handlers=[handler]),
- request=RequestFactory().get(route_handler=handler),
+ request=RequestFactory(app=Litestar(route_handlers=[handler])).get(route_handler=handler),
)
assert loads(response.body) == msgspec.to_builtins(person_instance) # type: ignore[attr-defined]
@@ -103,9 +102,7 @@ def handler() -> Response:
with create_test_client(handler) as client:
http_route: HTTPRoute = client.app.routes[0]
route_handler = http_route.route_handlers[0]
- response = await route_handler.to_response(
- data=route_handler.fn(), app=client.app, request=RequestFactory().get()
- )
+ response = await route_handler.to_response(data=route_handler.fn(), request=RequestFactory().get())
assert isinstance(response, ASGIResponse)
@@ -128,9 +125,7 @@ def handler() -> StarletteResponse:
with create_test_client(handler) as client:
http_route: HTTPRoute = client.app.routes[0]
route_handler = http_route.route_handlers[0]
- response = await route_handler.to_response(
- data=route_handler.fn(), app=client.app, request=RequestFactory().get()
- )
+ response = await route_handler.to_response(data=route_handler.fn(), request=RequestFactory().get())
assert isinstance(response, StarletteResponse)
assert response is expected_response
@@ -155,9 +150,7 @@ def handler() -> Redirect:
with create_test_client(handler) as client:
route: HTTPRoute = client.app.routes[0]
route_handler = route.route_handlers[0]
- response = await route_handler.to_response(
- data=route_handler.fn(), app=client.app, request=RequestFactory().get()
- )
+ response = await route_handler.to_response(data=route_handler.fn(), request=RequestFactory().get())
encoded_headers = response.encode_headers() # type: ignore[attr-defined]
assert isinstance(response, ASGIResponse)
@@ -209,9 +202,7 @@ def handler() -> File:
with create_test_client(handler) as client:
route: HTTPRoute = client.app.routes[0]
route_handler = route.route_handlers[0]
- response = await route_handler.to_response(
- data=route_handler.fn(), app=client.app, request=RequestFactory().get()
- )
+ response = await route_handler.to_response(data=route_handler.fn(), request=RequestFactory().get())
assert isinstance(response, ASGIFileResponse)
assert response.file_info
if iscoroutine(response.file_info):
@@ -267,11 +258,9 @@ def handler() -> Stream:
route_handler = route.route_handlers[0]
if should_raise:
with pytest.raises(TypeError):
- await route_handler.to_response(data=route_handler.fn(), app=client.app, request=RequestFactory().get())
+ await route_handler.to_response(data=route_handler.fn(), request=RequestFactory().get())
else:
- response = await route_handler.to_response(
- data=route_handler.fn(), app=client.app, request=RequestFactory().get()
- )
+ response = await route_handler.to_response(data=route_handler.fn(), request=RequestFactory().get())
assert isinstance(response, ASGIStreamingResponse)
encoded_headers = response.encode_headers()
assert (b"local-header", b"123") in encoded_headers
@@ -315,9 +304,7 @@ def handler() -> Template:
) as client:
route: HTTPRoute = client.app.routes[0]
route_handler = route.route_handlers[0]
- response = await route_handler.to_response(
- data=route_handler.fn(), app=client.app, request=RequestFactory(app=app).get()
- )
+ response = await route_handler.to_response(data=route_handler.fn(), request=RequestFactory(app=app).get())
assert isinstance(response, ASGIResponse)
encoded_headers = response.encode_headers()
@@ -366,9 +353,7 @@ def handler() -> ServerSentEvent:
with create_test_client(handler) as client:
route: HTTPRoute = client.app.routes[0]
route_handler = route.route_handlers[0]
- response = await route_handler.to_response(
- data=route_handler.fn(), app=client.app, request=RequestFactory().get()
- )
+ response = await route_handler.to_response(data=route_handler.fn(), request=RequestFactory().get())
encoded_headers = response.encode_headers() # type: ignore[attr-defined]
assert isinstance(response, ASGIStreamingResponse)
@@ -411,9 +396,7 @@ def handler() -> ServerSentEvent:
with create_test_client(handler) as client:
route: HTTPRoute = client.app.routes[0]
route_handler = route.route_handlers[0]
- response = await route_handler.to_response(
- data=route_handler.fn(), app=client.app, request=RequestFactory().get()
- )
+ response = await route_handler.to_response(data=route_handler.fn(), request=RequestFactory().get())
assert isinstance(response, ASGIStreamingResponse)
async for value in response.iterator:
events.append(cast("bytes", value))
diff --git a/tests/unit/test_response/test_type_encoders.py b/tests/unit/test_response/test_type_encoders.py
index 8c11c68f68..ec59ee37a0 100644
--- a/tests/unit/test_response/test_type_encoders.py
+++ b/tests/unit/test_response/test_type_encoders.py
@@ -32,7 +32,7 @@ def handler(self) -> Any: ...
router = Router("/router", type_encoders={router_type: router_encoder}, route_handlers=[MyController])
app = Litestar([router], type_encoders={app_type: app_encoder})
- route_handler = app.routes[0].route_handler_map[HttpMethod.GET][0] # type: ignore[union-attr]
+ route_handler = app.routes[0].route_handler_map[HttpMethod.GET] # type: ignore[union-attr]
encoders = route_handler.resolve_type_encoders()
assert encoders.get(handler_type) == handler_encoder
assert encoders.get(controller_type) == controller_encoder
diff --git a/tests/unit/test_signature/test_validation.py b/tests/unit/test_signature/test_validation.py
index acc8f97cae..4fb9a62e66 100644
--- a/tests/unit/test_signature/test_validation.py
+++ b/tests/unit/test_signature/test_validation.py
@@ -66,7 +66,7 @@ def test_validation_failure_raises_400() -> None:
dependencies = {"dep": Provide(lambda: 13, sync_to_thread=False)}
@get("/")
- def test(dep: int, param: int, optional_dep: Optional[int] = Dependency()) -> None: ...
+ def test(dep: int, param: int, optional_dep: Optional[int] = Dependency(default=None)) -> None: ...
with create_test_client(route_handlers=[test], dependencies=dependencies) as client:
response = client.get("/?param=thirteen")
@@ -120,7 +120,7 @@ def handler(data: Parent) -> None:
model = SignatureModel.create(
dependency_name_set=set(),
- fn=handler,
+ fn=handler, # type: ignore[arg-type]
data_dto=None,
parsed_signature=ParsedSignature.from_fn(handler.fn, {}),
type_decoders=[],
diff --git a/tests/unit/test_static_files/conftest.py b/tests/unit/test_static_files/conftest.py
index 01d70d2bb1..b2996a7c1e 100644
--- a/tests/unit/test_static_files/conftest.py
+++ b/tests/unit/test_static_files/conftest.py
@@ -1,30 +1,12 @@
from __future__ import annotations
-from dataclasses import asdict
-from typing import Callable
-
import pytest
from _pytest.fixtures import FixtureRequest
from fsspec.implementations.local import LocalFileSystem
-from typing_extensions import TypeAlias
-from litestar import Router
from litestar.file_system import BaseLocalFileSystem
-from litestar.static_files import StaticFilesConfig, create_static_files_router
from litestar.types import FileSystemProtocol
-MakeConfig: TypeAlias = "Callable[[StaticFilesConfig], tuple[list[StaticFilesConfig], list[Router]]]"
-
-
-@pytest.fixture(params=["config", "handlers"])
-def make_config(request: FixtureRequest) -> MakeConfig:
- def make(config: StaticFilesConfig) -> tuple[list[StaticFilesConfig], list[Router]]:
- if request.param == "config":
- return [config], []
- return [], [create_static_files_router(**asdict(config))]
-
- return make
-
@pytest.fixture(params=[BaseLocalFileSystem(), LocalFileSystem()])
def file_system(request: FixtureRequest) -> FileSystemProtocol:
diff --git a/tests/unit/test_static_files/test_file_serving_resolution.py b/tests/unit/test_static_files/test_file_serving_resolution.py
index 435526470c..798b47f20d 100644
--- a/tests/unit/test_static_files/test_file_serving_resolution.py
+++ b/tests/unit/test_static_files/test_file_serving_resolution.py
@@ -3,28 +3,28 @@
import gzip
import mimetypes
from pathlib import Path
-from typing import TYPE_CHECKING, Callable
+from typing import TYPE_CHECKING
import brotli
import pytest
-from typing_extensions import TypeAlias
-from litestar import MediaType, Router, get
-from litestar.static_files import StaticFiles, StaticFilesConfig, create_static_files_router
+from litestar import MediaType, get
+from litestar.file_system import FileSystemAdapter
+from litestar.static_files import _get_fs_info, create_static_files_router
from litestar.status_codes import HTTP_200_OK
from litestar.testing import create_test_client
-from tests.unit.test_static_files.conftest import MakeConfig
if TYPE_CHECKING:
from litestar.types import FileSystemProtocol
-def test_default_static_files_config(tmpdir: Path, make_config: MakeConfig) -> None:
+def test_default_static_files_router(
+ tmpdir: Path,
+) -> None:
path = tmpdir / "test.txt"
path.write_text("content", "utf-8")
- static_files_config, router = make_config(StaticFilesConfig(path="/static", directories=[tmpdir]))
- with create_test_client(router, static_files_config=static_files_config) as client:
+ with create_test_client([create_static_files_router(path="/static", directories=[tmpdir])]) as client:
response = client.get("/static/test.txt")
assert response.status_code == HTTP_200_OK, response.text
assert response.text == "content"
@@ -43,31 +43,15 @@ def setup_dirs(tmpdir: Path) -> tuple[Path, Path]:
return paths[0], paths[1]
-MakeConfigs: TypeAlias = (
- "Callable[[StaticFilesConfig, StaticFilesConfig], tuple[list[StaticFilesConfig], list[Router]]]"
-)
-
-
-@pytest.fixture()
-def make_configs(make_config: MakeConfig) -> MakeConfigs:
- def make(
- first_config: StaticFilesConfig, second_config: StaticFilesConfig
- ) -> tuple[list[StaticFilesConfig], list[Router]]:
- configs_1, routers_1 = make_config(first_config)
- configs_2, routers_2 = make_config(second_config)
- return [*configs_1, *configs_2], [*routers_1, *routers_2]
-
- return make
-
-
-def test_multiple_static_files_configs(setup_dirs: tuple[Path, Path], make_configs: MakeConfigs) -> None:
+def test_multiple_static_files_routers(setup_dirs: tuple[Path, Path]) -> None:
root1, root2 = setup_dirs
- configs, handlers = make_configs(
- StaticFilesConfig(path="/static_first", directories=[root1]), # pyright: ignore
- StaticFilesConfig(path="/static_second", directories=[root2]), # pyright: ignore
- )
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client(
+ [
+ create_static_files_router(path="/static_first", directories=[root1]),
+ create_static_files_router(path="/static_second", directories=[root2]),
+ ]
+ ) as client:
response = client.get("/static_first/test_1.txt")
assert response.status_code == HTTP_200_OK
assert response.text == "content1"
@@ -77,17 +61,17 @@ def test_multiple_static_files_configs(setup_dirs: tuple[Path, Path], make_confi
assert response.text == "content2"
-def test_static_files_configs_with_mixed_file_systems(
- file_system: FileSystemProtocol, setup_dirs: tuple[Path, Path], make_configs: MakeConfigs
+def test_static_files_routers_with_mixed_file_systems(
+ file_system: FileSystemProtocol, setup_dirs: tuple[Path, Path]
) -> None:
root1, root2 = setup_dirs
- configs, handlers = make_configs(
- StaticFilesConfig(path="/static_first", directories=[root1], file_system=file_system), # pyright: ignore
- StaticFilesConfig(path="/static_second", directories=[root2]), # pyright: ignore
- )
-
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client(
+ [
+ create_static_files_router(path="/static_first", directories=[root1], file_system=file_system),
+ create_static_files_router(path="/static_second", directories=[root2]),
+ ]
+ ) as client:
response = client.get("/static_first/test_1.txt")
assert response.status_code == HTTP_200_OK
assert response.text == "content1"
@@ -97,15 +81,14 @@ def test_static_files_configs_with_mixed_file_systems(
assert response.text == "content2"
-def test_static_files_config_with_multiple_directories(
- file_system: FileSystemProtocol, setup_dirs: tuple[Path, Path], make_config: MakeConfig
+def test_static_files_routers_with_multiple_directories(
+ file_system: FileSystemProtocol, setup_dirs: tuple[Path, Path]
) -> None:
root1, root2 = setup_dirs
- configs, handlers = make_config(
- StaticFilesConfig(path="/static", directories=[root1, root2], file_system=file_system)
- )
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client(
+ [create_static_files_router(path="/static", directories=[root1, root2], file_system=file_system)]
+ ) as client:
response = client.get("/static/test_1.txt")
assert response.status_code == HTTP_200_OK
assert response.text == "content1"
@@ -115,31 +98,31 @@ def test_static_files_config_with_multiple_directories(
assert response.text == "content2"
-def test_staticfiles_for_slash_path_regular_mode(tmpdir: Path, make_config: MakeConfig) -> None:
+def test_staticfiles_for_slash_path_regular_mode(tmpdir: Path) -> None:
path = tmpdir / "text.txt"
path.write_text("content", "utf-8")
- configs, handlers = make_config(StaticFilesConfig(path="/", directories=[tmpdir]))
-
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client([create_static_files_router(path="/", directories=[tmpdir])]) as client:
response = client.get("/text.txt")
assert response.status_code == HTTP_200_OK
assert response.text == "content"
-def test_staticfiles_for_slash_path_html_mode(tmpdir: Path, make_config: MakeConfig) -> None:
+def test_staticfiles_for_slash_path_html_mode(
+ tmpdir: Path,
+) -> None:
path = tmpdir / "index.html"
path.write_text("", "utf-8")
- configs, handlers = make_config(StaticFilesConfig(path="/", directories=[tmpdir], html_mode=True))
-
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client([create_static_files_router(path="/", directories=[tmpdir], html_mode=True)]) as client:
response = client.get("/")
assert response.status_code == HTTP_200_OK
assert response.text == ""
-def test_sub_path_under_static_path(tmpdir: Path, make_config: MakeConfig) -> None:
+def test_sub_path_under_static_path(
+ tmpdir: Path,
+) -> None:
path = tmpdir / "test.txt"
path.write_text("content", "utf-8")
@@ -147,10 +130,7 @@ def test_sub_path_under_static_path(tmpdir: Path, make_config: MakeConfig) -> No
def handler(f: str) -> str:
return f
- configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir]))
- handlers.append(handler) # type: ignore[arg-type]
-
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client([create_static_files_router(path="/static", directories=[tmpdir]), handler]) as client:
response = client.get("/static/test.txt")
assert response.status_code == HTTP_200_OK
@@ -158,26 +138,29 @@ def handler(f: str) -> str:
assert response.status_code == HTTP_200_OK
-def test_static_substring_of_self(tmpdir: Path, make_config: MakeConfig) -> None:
+def test_static_substring_of_self(
+ tmpdir: Path,
+) -> None:
path = tmpdir.mkdir("static_part").mkdir("static") / "test.txt" # type: ignore[arg-type, func-returns-value]
path.write_text("content", "utf-8")
- configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir]))
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client([create_static_files_router(path="/static", directories=[tmpdir])]) as client:
response = client.get("/static/static_part/static/test.txt")
assert response.status_code == HTTP_200_OK
assert response.text == "content"
@pytest.mark.parametrize("extension", ["css", "js", "html", "json"])
-def test_static_files_response_mimetype(tmpdir: Path, extension: str, make_config: MakeConfig) -> None:
+def test_static_files_response_mimetype(
+ tmpdir: Path,
+ extension: str,
+) -> None:
fn = f"test.{extension}"
path = tmpdir / fn
path.write_text("content", "utf-8")
- configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir]))
expected_mime_type = mimetypes.guess_type(fn)[0]
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client([create_static_files_router(path="/static", directories=[tmpdir])]) as client:
response = client.get(f"/static/{fn}")
assert expected_mime_type
assert response.status_code == HTTP_200_OK
@@ -185,7 +168,10 @@ def test_static_files_response_mimetype(tmpdir: Path, extension: str, make_confi
@pytest.mark.parametrize("extension", ["gz", "br"])
-def test_static_files_response_encoding(tmp_path: Path, extension: str, make_config: MakeConfig) -> None:
+def test_static_files_response_encoding(
+ tmp_path: Path,
+ extension: str,
+) -> None:
fn = f"test.js.{extension}"
path = tmp_path / fn
compressed_data = None
@@ -196,9 +182,7 @@ def test_static_files_response_encoding(tmp_path: Path, extension: str, make_con
path.write_bytes(compressed_data) # type: ignore[arg-type]
expected_encoding_type = mimetypes.guess_type(fn)[1]
- configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmp_path]))
-
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client([create_static_files_router(path="/static", directories=[tmp_path])]) as client:
response = client.get(f"/static/{fn}")
assert expected_encoding_type
assert response.status_code == HTTP_200_OK
@@ -207,78 +191,51 @@ def test_static_files_response_encoding(tmp_path: Path, extension: str, make_con
@pytest.mark.parametrize("send_as_attachment,disposition", [(True, "attachment"), (False, "inline")])
def test_static_files_content_disposition(
- tmpdir: Path, send_as_attachment: bool, disposition: str, make_config: MakeConfig
+ tmpdir: Path,
+ send_as_attachment: bool,
+ disposition: str,
) -> None:
path = tmpdir.mkdir("static_part").mkdir("static") / "test.txt" # type: ignore[arg-type, func-returns-value]
path.write_text("content", "utf-8")
- configs, handlers = make_config(
- StaticFilesConfig(path="/static", directories=[tmpdir], send_as_attachment=send_as_attachment)
- )
-
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client(
+ [create_static_files_router(path="/static", directories=[tmpdir], send_as_attachment=send_as_attachment)]
+ ) as client:
response = client.get("/static/static_part/static/test.txt")
assert response.status_code == HTTP_200_OK
assert response.headers["content-disposition"].startswith(disposition)
-def test_service_from_relative_path_using_string(tmpdir: Path, make_config: MakeConfig) -> None:
+def test_service_from_relative_path_using_string(
+ tmpdir: Path,
+) -> None:
sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore[arg-type, func-returns-value]
path = tmpdir / "test.txt"
path.write_text("content", "utf-8")
- configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[f"{sub_dir}/.."]))
-
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client([create_static_files_router(path="/static", directories=[f"{sub_dir}/.."])]) as client:
response = client.get("/static/test.txt")
assert response.status_code == HTTP_200_OK
assert response.text == "content"
-def test_service_from_relative_path_using_path(tmpdir: Path, make_config: MakeConfig) -> None:
+def test_service_from_relative_path_using_path(
+ tmpdir: Path,
+) -> None:
sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore[arg-type, func-returns-value]
path = tmpdir / "test.txt"
path.write_text("content", "utf-8")
- configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[Path(f"{sub_dir}/..")]))
-
- with create_test_client(handlers, static_files_config=configs) as client:
+ with create_test_client(
+ [create_static_files_router(path="/static", directories=[Path(f"{sub_dir}/..")])]
+ ) as client:
response = client.get("/static/test.txt")
assert response.status_code == HTTP_200_OK
assert response.text == "content"
-def test_service_from_base_path_using_string(tmpdir: Path) -> None:
- sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore[arg-type, func-returns-value]
-
- path = tmpdir / "test.txt"
- path.write_text("content", "utf-8")
-
- @get("/", media_type=MediaType.TEXT)
- def index_handler() -> str:
- return "index"
-
- @get("/sub")
- def sub_handler() -> dict:
- return {"hello": "world"}
-
- static_files_config = StaticFilesConfig(path="/", directories=[f"{sub_dir}/.."])
- with create_test_client([index_handler, sub_handler], static_files_config=[static_files_config]) as client:
- response = client.get("/test.txt")
- assert response.status_code == HTTP_200_OK
- assert response.text == "content"
-
- response = client.get("/")
- assert response.status_code == HTTP_200_OK
- assert response.text == "index"
-
- response = client.get("/sub")
- assert response.status_code == HTTP_200_OK
- assert response.json() == {"hello": "world"}
-
-
@pytest.mark.parametrize("resolve", [True, False])
def test_resolve_symlinks(tmp_path: Path, resolve: bool) -> None:
source_dir = tmp_path / "foo"
@@ -305,8 +262,7 @@ async def test_staticfiles_get_fs_info_no_access_to_non_static_directory(
assets.mkdir()
index = tmp_path / "index.html"
index.write_text("content", "utf-8")
- static_files = StaticFiles(is_html_mode=False, directories=[assets], file_system=file_system)
- path, info = await static_files.get_fs_info([assets], "../index.html")
+ path, info = await _get_fs_info([assets], "../index.html", adapter=FileSystemAdapter(file_system))
assert path is None
assert info is None
@@ -319,7 +275,6 @@ async def test_staticfiles_get_fs_info_no_access_to_non_static_file_with_prefix(
static.mkdir()
private_file = tmp_path / "staticsecrets.env"
private_file.write_text("content", "utf-8")
- static_files = StaticFiles(is_html_mode=False, directories=[static], file_system=file_system)
- path, info = await static_files.get_fs_info([static], "../staticsecrets.env")
+ path, info = await _get_fs_info([static], "../staticsecrets.env", adapter=FileSystemAdapter(file_system))
assert path is None
assert info is None
diff --git a/tests/unit/test_static_files/test_html_mode.py b/tests/unit/test_static_files/test_html_mode.py
index c1f792d55a..93a04b86f9 100644
--- a/tests/unit/test_static_files/test_html_mode.py
+++ b/tests/unit/test_static_files/test_html_mode.py
@@ -2,10 +2,9 @@
from typing import TYPE_CHECKING
-from litestar.static_files import StaticFilesConfig
+from litestar.static_files import create_static_files_router
from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND
from litestar.testing import create_test_client
-from tests.unit.test_static_files.conftest import MakeConfig
if TYPE_CHECKING:
from pathlib import Path
@@ -13,14 +12,13 @@
from litestar.types import FileSystemProtocol
-def test_staticfiles_is_html_mode(tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig) -> None:
+def test_staticfiles_is_html_mode(tmpdir: Path, file_system: FileSystemProtocol) -> None:
path = tmpdir / "index.html"
path.write_text("content", "utf-8")
- static_files_config, handlers = make_config(
- StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system)
- )
- with create_test_client(handlers, static_files_config=static_files_config) as client:
+ with create_test_client(
+ [create_static_files_router(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system)]
+ ) as client:
response = client.get("/static")
assert response.status_code == HTTP_200_OK
assert response.text == "content"
@@ -28,15 +26,13 @@ def test_staticfiles_is_html_mode(tmpdir: Path, file_system: FileSystemProtocol,
assert response.headers["content-disposition"].startswith("inline")
-def test_staticfiles_is_html_mode_serves_404_when_present(
- tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig
-) -> None:
+def test_staticfiles_is_html_mode_serves_404_when_present(tmpdir: Path, file_system: FileSystemProtocol) -> None:
path = tmpdir / "404.html"
path.write_text("not found", "utf-8")
- static_files_config, handlers = make_config(
- StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system)
- )
- with create_test_client(handlers, static_files_config=static_files_config) as client:
+
+ with create_test_client(
+ [create_static_files_router(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system)]
+ ) as client:
response = client.get("/static")
assert response.status_code == HTTP_404_NOT_FOUND
assert response.text == "not found"
@@ -44,12 +40,11 @@ def test_staticfiles_is_html_mode_serves_404_when_present(
def test_staticfiles_is_html_mode_raises_exception_when_no_404_html_is_present(
- tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig
+ tmpdir: Path, file_system: FileSystemProtocol
) -> None:
- static_files_config, handlers = make_config(
- StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system)
- )
- with create_test_client(handlers, static_files_config=static_files_config) as client:
+ with create_test_client(
+ [create_static_files_router(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system)]
+ ) as client:
response = client.get("/static")
assert response.status_code == HTTP_404_NOT_FOUND
assert response.json() == {"status_code": 404, "detail": "no file or directory match the path . was found"}
diff --git a/tests/unit/test_static_files/test_static_files_validation.py b/tests/unit/test_static_files/test_static_files_validation.py
index a7682f60b5..57ef6a4b59 100644
--- a/tests/unit/test_static_files/test_static_files_validation.py
+++ b/tests/unit/test_static_files/test_static_files_validation.py
@@ -1,53 +1,44 @@
-import asyncio
from pathlib import Path
-from typing import TYPE_CHECKING, Any, List, cast
import pytest
-from litestar import HttpMethod, Litestar, MediaType, get
+from litestar import HttpMethod
from litestar.exceptions import ImproperlyConfiguredException
-from litestar.static_files import StaticFilesConfig, create_static_files_router
+from litestar.static_files import create_static_files_router
from litestar.status_codes import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_405_METHOD_NOT_ALLOWED
from litestar.testing import create_test_client
-if TYPE_CHECKING:
- from litestar.static_files import StaticFiles
-
-@pytest.mark.parametrize("directories", [[], [""]])
-@pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router])
-def test_config_validation_of_directories(func: Any, directories: List[str]) -> None:
+def test_validation_of_directories() -> None:
with pytest.raises(ImproperlyConfiguredException):
- func(path="/static", directories=directories)
+ create_static_files_router(path="/static", directories=[])
-@pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router])
-def test_config_validation_of_path(tmpdir: "Path", func: Any) -> None:
+def test_validation_of_path(tmpdir: "Path") -> None:
path = tmpdir / "text.txt"
path.write_text("content", "utf-8")
with pytest.raises(ImproperlyConfiguredException):
- func(path="", directories=[tmpdir])
+ create_static_files_router(path="", directories=[tmpdir])
with pytest.raises(ImproperlyConfiguredException):
- func(path="/{param:int}", directories=[tmpdir])
+ create_static_files_router(path="/{param:int}", directories=[tmpdir])
-@pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router])
-def test_config_validation_of_file_system(tmpdir: "Path", func: Any) -> None:
+def test_validation_of_file_system(tmpdir: "Path") -> None:
class FSWithoutOpen:
def info(self) -> None:
return
with pytest.raises(ImproperlyConfiguredException):
- func(path="/static", directories=[tmpdir], file_system=FSWithoutOpen())
+ create_static_files_router(path="/static", directories=[tmpdir], file_system=FSWithoutOpen())
class FSWithoutInfo:
def open(self) -> None:
return
with pytest.raises(ImproperlyConfiguredException):
- func(path="/static", directories=[tmpdir], file_system=FSWithoutInfo())
+ create_static_files_router(path="/static", directories=[tmpdir], file_system=FSWithoutInfo())
class ImplementedFS:
def info(self) -> None:
@@ -56,44 +47,7 @@ def info(self) -> None:
def open(self) -> None:
return
- assert func(path="/static", directories=[tmpdir], file_system=ImplementedFS())
-
-
-def test_runtime_validation_of_static_path_and_path_parameter(tmpdir: "Path") -> None:
- path = tmpdir / "test.txt"
- path.write_text("content", "utf-8")
-
- @get("/static/{f:str}", media_type=MediaType.TEXT)
- def handler(f: str) -> str:
- return f
-
- with pytest.raises(ImproperlyConfiguredException):
- Litestar(
- route_handlers=[handler], static_files_config=[StaticFilesConfig(path="/static", directories=[tmpdir])]
- )
-
-
-@pytest.mark.parametrize(
- "method, expected",
- (
- (HttpMethod.GET, HTTP_200_OK),
- (HttpMethod.HEAD, HTTP_200_OK),
- (HttpMethod.PUT, HTTP_405_METHOD_NOT_ALLOWED),
- (HttpMethod.PATCH, HTTP_405_METHOD_NOT_ALLOWED),
- (HttpMethod.POST, HTTP_405_METHOD_NOT_ALLOWED),
- (HttpMethod.DELETE, HTTP_405_METHOD_NOT_ALLOWED),
- (HttpMethod.OPTIONS, HTTP_405_METHOD_NOT_ALLOWED),
- ),
-)
-def test_runtime_validation_of_request_method_legacy_config(tmpdir: "Path", method: HttpMethod, expected: int) -> None:
- path = tmpdir / "test.txt"
- path.write_text("content", "utf-8")
-
- with create_test_client(
- [], static_files_config=[StaticFilesConfig(path="/static", directories=[tmpdir])]
- ) as client:
- response = client.request(method, "/static/test.txt")
- assert response.status_code == expected
+ assert create_static_files_router(path="/static", directories=[tmpdir], file_system=ImplementedFS())
@pytest.mark.parametrize(
@@ -115,37 +69,3 @@ def test_runtime_validation_of_request_method_create_handler(tmpdir: "Path", met
with create_test_client(create_static_files_router(path="/static", directories=[tmpdir])) as client:
response = client.request(method, "/static/test.txt")
assert response.status_code == expected
-
-
-def test_config_validation_of_path_prevents_directory_traversal(tmpdir: "Path") -> None:
- # Setup: Create a 'secret.txt' outside the static directory to simulate sensitive file
- secret_path = Path(tmpdir) / "../secret.txt"
- secret_path.write_text("This is a secret file.", encoding="utf-8")
-
- # Setup: Create 'test.txt' inside the static directory
- test_file_path = Path(tmpdir) / "test.txt"
- test_file_path.write_text("This is a test file.", encoding="utf-8")
-
- # Get StaticFiles handler
- config = StaticFilesConfig(path="/static", directories=[tmpdir])
- asgi_router = config.to_static_files_app()
- static_files_handler = cast("StaticFiles", asgi_router.fn)
-
- # Resolve file path with the StaticFiles handler
- string_path = Path("../secret.txt").as_posix()
-
- coroutine = static_files_handler.get_fs_info(directories=static_files_handler.directories, file_path=string_path)
- resolved_path, fs_info = asyncio.run(coroutine)
-
- assert resolved_path is None # Because the resolved path is outside the static directory
- assert fs_info is None # Because the file doesn't exist, so there is no info
-
- # Resolve file path with the StaticFiles handler
- string_path = Path("test.txt").as_posix()
-
- coroutine = static_files_handler.get_fs_info(directories=static_files_handler.directories, file_path=string_path)
- resolved_path, fs_info = asyncio.run(coroutine)
-
- expected_resolved_path = tmpdir / "test.txt"
- assert resolved_path == expected_resolved_path # Because the resolved path is inside the static directory
- assert fs_info is not None # Because the file exists, so there is info
diff --git a/tests/unit/test_template/test_builtin_functions.py b/tests/unit/test_template/test_builtin_functions.py
index 1e0f889418..d04705104f 100644
--- a/tests/unit/test_template/test_builtin_functions.py
+++ b/tests/unit/test_template/test_builtin_functions.py
@@ -9,7 +9,6 @@
from litestar.contrib.mako import MakoTemplateEngine
from litestar.contrib.minijinja import MiniJinjaTemplateEngine
from litestar.response.template import Template
-from litestar.static_files.config import StaticFilesConfig
from litestar.status_codes import HTTP_200_OK, HTTP_500_INTERNAL_SERVER_ERROR
from litestar.template.config import TemplateConfig
from litestar.testing import create_test_client
@@ -75,77 +74,6 @@ def complex_handler() -> None:
assert response.status_code == 500
-# TODO: use some other flaky test technique, probably re-running flaky tests?
-@pytest.mark.xfail(sys.platform == "win32", reason="For some reason this is flaky on windows", strict=False)
-def test_jinja_url_for_static_asset(tmp_path: Path) -> None:
- template_config = TemplateConfig(engine=JinjaTemplateEngine, directory=tmp_path)
-
- @get(path="/", name="tpl_renderer")
- def tpl_renderer() -> Template:
- return Template(template_name="tpl.html")
-
- with create_test_client(
- route_handlers=[tpl_renderer],
- template_config=template_config,
- static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")],
- ) as client:
- Path(tmp_path / "tpl.html").write_text("{{ url_for_static_asset('css', 'main/main.css') }}")
-
- response = client.get("/")
- assert response.status_code == 200
- assert response.text == "/static/css/main/main.css"
-
- with create_test_client(
- route_handlers=[tpl_renderer],
- template_config=template_config,
- static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")],
- ) as client:
- Path(tmp_path / "tpl.html").write_text("{{ url_for_static_asset('non-existent', 'main.css') }}")
-
- response = client.get("/")
- assert response.status_code == 500
-
- with create_test_client(
- route_handlers=[tpl_renderer],
- template_config=template_config,
- static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")],
- ) as client:
- Path(tmp_path / "tpl.html").write_text("{{ url_for_static_asset('tpl_renderer', 'main.css') }}")
-
- response = client.get("/")
- assert response.status_code == 500
-
-
-@pytest.mark.parametrize(
- "builtin, expected_status, expected_text",
- (
- ("${url_for_static_asset('css', 'main/main.css')}", HTTP_200_OK, "/static/css/main/main.css"),
- ("${url_for_static_asset('non-existent', 'main.css')}", HTTP_500_INTERNAL_SERVER_ERROR, None),
- ("${url_for_static_asset('tpl_renderer', 'main.css')}", HTTP_500_INTERNAL_SERVER_ERROR, None),
- ),
-)
-def test_mako_url_for_static_asset(
- tmp_path: Path, builtin: str, expected_status: int, expected_text: Optional[str]
-) -> None:
- template_config = TemplateConfig(engine=MakoTemplateEngine, directory=tmp_path)
-
- @get(path="/", name="tpl_renderer")
- def tpl_renderer() -> Template:
- return Template(template_name="tpl.html")
-
- with create_test_client(
- route_handlers=[tpl_renderer],
- template_config=template_config,
- static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")],
- ) as client:
- Path(tmp_path / "tpl.html").write_text(builtin)
-
- response = client.get("/")
- assert response.status_code == expected_status
- if expected_text:
- assert response.text == expected_text
-
-
@pytest.mark.parametrize(
"builtin, expected_status, expected_text",
(
@@ -243,43 +171,3 @@ def complex_handler() -> None:
response = client.get("/non_existent.html")
assert response.status_code == 500
-
-
-@pytest.mark.xfail(sys.platform == "win32", reason="For some reason this is flaky on windows", strict=False)
-def test_minijinja_url_for_static_asset(tmp_path: Path) -> None:
- template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory=tmp_path)
-
- @get(path="/{path:path}", name="tpl_renderer")
- def tpl_renderer(path: Path) -> Template:
- return Template(template_name=path.name)
-
- with create_test_client(
- route_handlers=[tpl_renderer],
- template_config=template_config,
- static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")],
- ) as client:
- Path(tmp_path / "working.html").write_text("{{ url_for_static_asset('css', 'main/main.css') }}")
-
- response = client.get("/working.html")
- assert response.status_code == 200
- assert response.text == "/static/css/main/main.css"
-
- with create_test_client(
- route_handlers=[tpl_renderer],
- template_config=template_config,
- static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")],
- ) as client:
- Path(tmp_path / "non_existent.html").write_text("{{ url_for_static_asset('non-existent', 'main.css') }}")
-
- response = client.get("/non_existent.html")
- assert response.status_code == 500
-
- with create_test_client(
- route_handlers=[tpl_renderer],
- template_config=template_config,
- static_files_config=[StaticFilesConfig(path="/static/css", directories=[tmp_path], name="css")],
- ) as client:
- Path(tmp_path / "self.html").write_text("{{ url_for_static_asset('tpl_renderer', 'main.css') }}")
-
- response = client.get("/self.html")
- assert response.status_code == 500
diff --git a/tests/unit/test_utils/test_scope.py b/tests/unit/test_utils/test_scope.py
index 2a7e2f43fc..dd65a9fe15 100644
--- a/tests/unit/test_utils/test_scope.py
+++ b/tests/unit/test_utils/test_scope.py
@@ -1,78 +1,9 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Callable
-
-import pytest
-
-from litestar.types.empty import Empty
-from litestar.utils import (
- delete_litestar_scope_state,
- get_litestar_scope_state,
- set_litestar_scope_state,
-)
from litestar.utils.scope.state import CONNECTION_STATE_KEY, ScopeState
-if TYPE_CHECKING:
- from litestar.types.asgi_types import Scope
-
-
-@pytest.fixture()
-def scope(create_scope: Callable[..., Scope]) -> Scope:
- return create_scope()
-
def test_from_scope_without_state() -> None:
scope = {} # type: ignore[var-annotated]
state = ScopeState.from_scope(scope) # type: ignore[arg-type]
assert scope["state"][CONNECTION_STATE_KEY] is state
-
-
-@pytest.mark.parametrize(("pop",), [(True,), (False,)])
-def test_get_litestar_scope_state_arbitrary_value(pop: bool, scope: Scope) -> None:
- key = "test"
- value = {"key": "value"}
- connection_state = ScopeState.from_scope(scope)
- connection_state._compat_ns[key] = value
- retrieved_value = get_litestar_scope_state(scope, key, pop=pop)
- assert retrieved_value == value
- if pop:
- assert connection_state._compat_ns.get(key) is None
- else:
- assert connection_state._compat_ns.get(key) == value
-
-
-@pytest.mark.parametrize(("pop",), [(True,), (False,)])
-def test_get_litestar_scope_state_defined_value(pop: bool, scope: Scope) -> None:
- connection_state = ScopeState.from_scope(scope)
- connection_state.is_cached = True
- assert get_litestar_scope_state(scope, "is_cached", pop=pop) is True
- if pop:
- assert connection_state.is_cached is Empty # type: ignore[comparison-overlap]
- else:
- assert connection_state.is_cached is True
-
-
-def test_set_litestar_scope_state_arbitrary_value(scope: Scope) -> None:
- connection_state = ScopeState.from_scope(scope)
- set_litestar_scope_state(scope, "key", "value")
- assert connection_state._compat_ns["key"] == "value"
-
-
-def test_set_litestar_scope_state_defined_value(scope: Scope) -> None:
- connection_state = ScopeState.from_scope(scope)
- set_litestar_scope_state(scope, "is_cached", True)
- assert connection_state.is_cached is True
-
-
-def test_delete_litestar_scope_state_arbitrary_value(scope: Scope) -> None:
- connection_state = ScopeState.from_scope(scope)
- connection_state._compat_ns["key"] = "value"
- delete_litestar_scope_state(scope, "key")
- assert "key" not in connection_state._compat_ns
-
-
-def test_delete_litestar_scope_state_defined_value(scope: Scope) -> None:
- connection_state = ScopeState.from_scope(scope)
- connection_state.is_cached = True
- delete_litestar_scope_state(scope, "is_cached")
- assert connection_state.is_cached is Empty # type: ignore[comparison-overlap]
diff --git a/tests/unit/test_utils/test_signature.py b/tests/unit/test_utils/test_signature.py
index 4926147e8e..6895ed79f5 100644
--- a/tests/unit/test_utils/test_signature.py
+++ b/tests/unit/test_utils/test_signature.py
@@ -14,8 +14,7 @@
from litestar import Controller, Router, post
from litestar.exceptions import ImproperlyConfiguredException
from litestar.exceptions.base_exceptions import LitestarWarning
-from litestar.file_system import BaseLocalFileSystem
-from litestar.static_files import StaticFiles
+from litestar.response.base import ASGIResponse
from litestar.types.asgi_types import Receive, Scope, Send
from litestar.types.builtin_types import NoneType
from litestar.types.empty import Empty
@@ -30,8 +29,8 @@ class ConcreteT: ...
def test_get_fn_type_hints_asgi_app() -> None:
- app = StaticFiles(is_html_mode=False, directories=[], file_system=BaseLocalFileSystem())
- assert get_fn_type_hints(app) == {"scope": Scope, "receive": Receive, "send": Send, "return": NoneType}
+ app = ASGIResponse()
+ assert get_fn_type_hints(app.__call__) == {"scope": Scope, "receive": Receive, "send": Send, "return": NoneType}
def func(a: int, b: str, c: float) -> None: ...
diff --git a/tools/build_docs.py b/tools/build_docs.py
index 5b0078495f..1f8a44c0e2 100644
--- a/tools/build_docs.py
+++ b/tools/build_docs.py
@@ -27,6 +27,7 @@
parser = argparse.ArgumentParser()
parser.add_argument("--version", required=False)
+parser.add_argument("--environment", required=False)
parser.add_argument("output")
@@ -49,12 +50,15 @@ def load_version_spec() -> VersionSpec:
return {"versions": [], "latest": ""}
-def build(output_dir: str, version: str | None) -> None:
+def build(output_dir: str, version: str | None, environment: str = "local") -> None:
if version is None:
version = importlib.metadata.version("litestar").rsplit(".")[0]
else:
os.environ["_LITESTAR_DOCS_BUILD_VERSION"] = version
+ if environment is not None:
+ os.environ["_LITESTAR_DOCS_BUILD_ENVIRONMENT"] = environment
+
subprocess.run(["make", "docs"], check=True) # noqa: S603 S607
output_dir = Path(output_dir)
@@ -83,7 +87,7 @@ def build(output_dir: str, version: str | None) -> None:
def main() -> None:
args = parser.parse_args()
- build(output_dir=args.output, version=args.version)
+ build(output_dir=args.output, version=args.version, environment=args.environment)
if __name__ == "__main__":
diff --git a/uv.lock b/uv.lock
index 25bba1d003..f4f789601c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -7,18 +7,6 @@ resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'win32'",
]
-[[package]]
-name = "accessible-pygments"
-version = "0.0.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b2/50/7055ebd9b7928eca202768bcf5f8f69d8d69dec1767c956c08f055c5b31e/accessible-pygments-0.0.4.tar.gz", hash = "sha256:e7b57a9b15958e9601c7e9eb07a440c813283545a20973f2574a5f453d0e953e", size = 11650 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/20/d7/45cfa326d945e411c7e02764206845b05f8f5766aa7ebc812ef3bc4138cd/accessible_pygments-0.0.4-py2.py3-none-any.whl", hash = "sha256:416c6d8c1ea1c5ad8701903a20fcedf953c6e720d64f33dc47bfb2d3f2fa4e8d", size = 29320 },
-]
-
[[package]]
name = "advanced-alchemy"
version = "0.25.0"
@@ -374,6 +362,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504 },
]
+[[package]]
+name = "blacken-docs"
+version = "1.18.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "black" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/12/a9/99fe0283575297466df65eb54873a892f0db5fd24459abef4c2691db883a/blacken_docs-1.18.0.tar.gz", hash = "sha256:47bed628679d008a8eb55d112df950582e68d0f57615223929e366348d935444", size = 14743 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/7e/532f6c8b81346c2337c54b0e16f49f2968d86b0c535d6318b3f21f3ff4cf/blacken_docs-1.18.0-py3-none-any.whl", hash = "sha256:64f592246784131e9f84dad1db397f44eeddc77fdf01726bab920a3f00a3815c", size = 8243 },
+]
+
[[package]]
name = "brotli"
version = "1.1.0"
@@ -851,7 +851,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 },
{ url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 },
{ url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 },
- { url = "https://files.pythonhosted.org/packages/4e/d5/9cc182bf24c86f542129565976c21301d4ac397e74bf5a16e48241aab8a6/cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385", size = 4164756 },
{ url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 },
{ url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 },
{ url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 },
@@ -862,7 +861,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 },
{ url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 },
{ url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 },
- { url = "https://files.pythonhosted.org/packages/31/d9/90409720277f88eb3ab72f9a32bfa54acdd97e94225df699e7713e850bd4/cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba", size = 4165207 },
{ url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 },
{ url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 },
{ url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 },
@@ -1741,28 +1739,70 @@ structlog = [
[package.dev-dependencies]
dev = [
+ { name = "advanced-alchemy" },
{ name = "aiosqlite" },
+ { name = "annotated-types" },
{ name = "asyncpg" },
+ { name = "attrs" },
{ name = "beanie" },
{ name = "beautifulsoup4" },
+ { name = "brotli" },
+ { name = "cryptography" },
{ name = "daphne" },
+ { name = "email-validator" },
+ { name = "fast-query-parsers" },
{ name = "fsspec" },
{ name = "greenlet" },
{ name = "httpx-sse" },
{ name = "hypercorn" },
{ name = "hypothesis" },
- { name = "litestar", extra = ["full"] },
+ { name = "jinja2" },
+ { name = "jsbeautifier" },
+ { name = "mako" },
+ { name = "minijinja" },
+ { name = "opentelemetry-instrumentation-asgi" },
{ name = "opentelemetry-sdk" },
+ { name = "piccolo" },
+ { name = "picologging" },
+ { name = "prometheus-client" },
{ name = "psutil" },
{ name = "psycopg", extra = ["binary", "pool"] },
{ name = "psycopg2-binary" },
+ { name = "pydantic" },
+ { name = "pydantic-extra-types" },
+ { name = "pyjwt" },
{ name = "python-dotenv" },
+ { name = "redis", extra = ["hiredis"] },
{ name = "starlette" },
+ { name = "structlog" },
{ name = "trio" },
+ { name = "uvicorn", extra = ["standard"] },
+ { name = "uvloop", marker = "sys_platform != 'win32'" },
]
docs = [
+ { name = "advanced-alchemy" },
+ { name = "annotated-types" },
+ { name = "asyncpg" },
+ { name = "attrs" },
{ name = "auto-pytabs", extra = ["sphinx"] },
+ { name = "brotli" },
+ { name = "cryptography" },
+ { name = "email-validator" },
+ { name = "fast-query-parsers" },
+ { name = "jinja2" },
+ { name = "jsbeautifier" },
{ name = "litestar-sphinx-theme" },
+ { name = "mako" },
+ { name = "minijinja" },
+ { name = "opentelemetry-instrumentation-asgi" },
+ { name = "piccolo" },
+ { name = "picologging" },
+ { name = "prometheus-client" },
+ { name = "psycopg" },
+ { name = "pydantic" },
+ { name = "pydantic-extra-types" },
+ { name = "pyjwt" },
+ { name = "redis", extra = ["hiredis"] },
{ name = "sphinx" },
{ name = "sphinx-autobuild" },
{ name = "sphinx-click" },
@@ -1771,6 +1811,9 @@ docs = [
{ name = "sphinx-paramlinks" },
{ name = "sphinx-toolbox" },
{ name = "sphinxcontrib-mermaid" },
+ { name = "structlog" },
+ { name = "uvicorn", extra = ["standard"] },
+ { name = "uvloop", marker = "sys_platform != 'win32'" },
]
linting = [
{ name = "asyncpg-stubs" },
@@ -1867,8 +1910,11 @@ dev = [
{ name = "trio" },
]
docs = [
+ { name = "asyncpg" },
{ name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" },
- { name = "litestar-sphinx-theme", git = "https://github.com/litestar-org/litestar-sphinx-theme.git" },
+ { name = "litestar", extras = ["full"] },
+ { name = "litestar-sphinx-theme", git = "https://github.com/litestar-org/litestar-sphinx-theme.git?rev=v3" },
+ { name = "psycopg" },
{ name = "sphinx", specifier = ">=7.1.2" },
{ name = "sphinx-autobuild", specifier = ">=2021.3.14" },
{ name = "sphinx-click", specifier = ">=4.4.0" },
@@ -1915,11 +1961,16 @@ wheels = [
[[package]]
name = "litestar-sphinx-theme"
-version = "0.2.0"
-source = { git = "https://github.com/litestar-org/litestar-sphinx-theme.git#76b1d0e4c8afff1ad135b1917fe09cf6c1cc6c9b" }
+version = "0.3.1"
+source = { git = "https://github.com/litestar-org/litestar-sphinx-theme.git?rev=v3#e5f1df5af973eafafbc683e56d0f5762b2e0c315" }
dependencies = [
- { name = "pydata-sphinx-theme" },
+ { name = "blacken-docs" },
+ { name = "shibuya" },
+ { name = "sphinx-autobuild" },
+ { name = "sphinx-copybutton" },
{ name = "sphinx-design" },
+ { name = "sphinx-togglebutton" },
+ { name = "sphinx-toolbox" },
]
[[package]]
@@ -2948,25 +2999,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/41/0b0cc8b59c31a04bdfde2ae71fccbb13c11fadafc8bd41a2af3e76db7e44/pydantic_extra_types-2.10.0-py3-none-any.whl", hash = "sha256:b19943914e6286548254f5079d1da094e9c0583ee91a8e611e9df24bfd07dbcd", size = 34185 },
]
-[[package]]
-name = "pydata-sphinx-theme"
-version = "0.14.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "accessible-pygments" },
- { name = "babel" },
- { name = "beautifulsoup4" },
- { name = "docutils" },
- { name = "packaging" },
- { name = "pygments" },
- { name = "sphinx" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/de/47/1bc31c4bc8b395cd37d8ceaf720abe10cf64c857fb9ce55856a6dd958484/pydata_sphinx_theme-0.14.4.tar.gz", hash = "sha256:f5d7a2cb7a98e35b9b49d3b02cec373ad28958c2ed5c9b1ffe6aff6c56e9de5b", size = 2410500 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1b/bf/3f8dc653e3015fa0656587e101013754d9bf926f395cbe0892f7e87158dd/pydata_sphinx_theme-0.14.4-py3-none-any.whl", hash = "sha256:ac15201f4c2e2e7042b0cad8b30251433c1f92be762ddcefdb4ae68811d918d9", size = 4682140 },
-]
-
[[package]]
name = "pygments"
version = "2.18.0"
@@ -3441,6 +3473,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/90/12/282ee9bce8b58130cb762fbc9beabd531549952cac11fc56add11dcb7ea0/setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", size = 1251070 },
]
+[[package]]
+name = "shibuya"
+version = "2024.10.15"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "sphinx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ed/ae/31d3755354c0903fbbce4a094575cada36afcb5fdc2df729afc69a7bea9d/shibuya-2024.10.15.tar.gz", hash = "sha256:fc057a32a2dcdca9f0f85616f9fff5b3007500bf42ff4eeb1d68fae124b1a51d", size = 80522 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/11/19d5407d5828e2d6dc45baff6ad813f70f6a10acea9a678db1f41ccee5c5/shibuya-2024.10.15-py3-none-any.whl", hash = "sha256:46d32c4dc7f244bfe130e710f477f4bda64706e5610916089371509992cae5e6", size = 96363 },
+]
+
[[package]]
name = "six"
version = "1.16.0"
@@ -3656,6 +3700,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/9f/4ac7dbb9f23a2ff5a10903a4f9e9f43e0ff051f63a313e989c962526e305/sphinx_tabs-3.4.5-py3-none-any.whl", hash = "sha256:92cc9473e2ecf1828ca3f6617d0efc0aa8acb06b08c56ba29d1413f2f0f6cf09", size = 9904 },
]
+[[package]]
+name = "sphinx-togglebutton"
+version = "0.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docutils" },
+ { name = "setuptools" },
+ { name = "sphinx" },
+ { name = "wheel" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f0/df/d151dfbbe588116e450ca7e898750cb218dca6b2e557ced8de6f9bd7242b/sphinx-togglebutton-0.3.2.tar.gz", hash = "sha256:ab0c8b366427b01e4c89802d5d078472c427fa6e9d12d521c34fa0442559dc7a", size = 8324 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/18/267ce39f29d26cdc7177231428ba823fe5ca94db8c56d1bed69033b364c8/sphinx_togglebutton-0.3.2-py3-none-any.whl", hash = "sha256:9647ba7874b7d1e2d43413d8497153a85edc6ac95a3fea9a75ef9c1e08aaae2b", size = 8249 },
+]
+
[[package]]
name = "sphinx-toolbox"
version = "3.8.1"
@@ -3870,7 +3929,7 @@ name = "taskgroup"
version = "0.0.0a4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "exceptiongroup" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0a/40/02753c40fa30dfdde7567c1daeefbf957dcf8c99e6534a80afb438adf07e/taskgroup-0.0.0a4.tar.gz", hash = "sha256:eb08902d221e27661950f2a0320ddf3f939f579279996f81fe30779bca3a159c", size = 8553 }
wheels = [
@@ -4506,6 +4565,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134 },
]
+[[package]]
+name = "wheel"
+version = "0.45.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 },
+]
+
[[package]]
name = "wrapt"
version = "1.17.0"