What’s changed in 2.0?#
This document is an overview of the changes between version 1.51 and 2.0. For a detailed list of all changes, including changes between versions leading up to the 2.0 release, consult the 2.x Changelog.
Starlite → Litestar#
We’re thrilled to introduce some exciting changes in our latest release, version 2! The most noteworthy transformation you will notice is the rebranding of our project, previously known as Starlite, now stepping into the limelight as Litestar.
The name “Starlite” was chosen as an homage to Starlette, the ASGI framework and toolkit Starlite was initially based on. Over the course of its development, Starlite grew more independent and relied less on Starlette, up to the point were Starlette was officially removed as a dependency in November 2022, with the release of v1.39.0.
After careful consideration, it was decided that with the release of 2.0, Starlite would be renamed to Litestar. There were many factors contributing to this decision, but it was mainly driven by concerns from within and outside the community about the possible confusion of the names Starlette and Starlite which - not incidentally - bore a lot of resemblance, which now had outlived its purpose.
Aside from the name, Litestar 2.0 is a direct successor of Starlite 1.x, and the regular release cycle will continue. It was determined that making the first 2.0 release under the new name and continuing with the version scheme from Starlite would cause the least friction. Following that decision, the first release under the new name was v2.0.0alpha3, following the last alpha release of Starlite 2.0, v2.0.0alpha2.
Note
The 1.51 release line is unaffected by this change
Imports#
|
|
---|---|
|
|
|
replaced with DTOs |
Enums |
|
|
|
|
|
|
|
Datastructures |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Configuration |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Provide |
|
|
|
Pagination |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Response Containers |
|
|
|
|
|
|
|
|
|
|
|
Exceptions |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Testing |
|
|
|
|
|
|
|
OpenAPI |
|
|
|
|
|
Middleware |
|
|
|
|
|
|
|
|
|
|
|
Security |
|
|
|
Route Handlers |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Routes |
|
|
|
|
|
|
|
|
|
Parameters |
|
|
|
|
Response headers#
Response header can now be set using either a Sequence
of
ResponseHeader
, or by using a
plain Mapping[str, str]
. The typing of
ResponseHeader
was also
changed to be more strict and now only allows string values.
from starlite import ResponseHeader, get
@get(response_headers={"my-header": ResponseHeader(value="header-value")})
async def handler() -> str: ...
from litestar import ResponseHeader, get
@get(response_headers=[ResponseHeader(name="my-header", value="header-value")])
async def handler() -> str: ...
# or
@get(response_headers={"my-header": "header-value"})
async def handler() -> str: ...
SQLAlchemy Plugin#
Support for SQLAlchemy 1 has been dropped and the new plugin will now support SQLAlchemy 2 only.
TODO: Migration instructions
See also
The SQLAlchemy usage documentation and the sqlalchemy API reference
Removal of Pydantic models#
Several Pydantic models used for configuration have been replaced with dataclasses or plain classes. If you relied on implicit data conversion from these models or subclassed them, you might need to adjust your code accordingly.
Plugin protocols#
The plugin protocol has been split into three distinct protocols, covering different use cases:
litestar.plugins.InitPluginProtocol
Hook into an application’s initialization process
litestar.plugins.SerializationPluginProtocol
Extend the serialization and deserialization capabilities of an application
litestar.plugins.OpenAPISchemaPluginProtocol
Extend OpenAPI schema generation
Plugins that made use of all features of the previous API should simply inherit from all three base classes.
Remove 2 argument before_send
#
The 2 argument for of before_send
hook handlers has been removed. Existing handlers
should be changed to include an additional scope
parameter.
async def before_send(message: Message, state: State) -> None: ...
async def before_send(message: Message, state: State, scope: Scope) -> None: ...
See also
Change: Remove support for 2 argument form of before_send
and
the before_send API reference
initial_state
application parameter#
The initial_state
argument to Litestar
has been replaced
with a state
keyword argument, accepting an optional
State
instance.
Existing code using this keyword argument will need to be changed from
app = Starlite(..., initial_state={"some": "key"})
to
app = Litestar(..., state=State({"some": "key"}))
Stores#
A new module, litestar.stores
has been introduced, which replaces the previously
used starlite.cache.Cache
and server-side session storage backends.
These stores provide a low-level, asynchronous interface for common key/value stores such as Redis and an in-memory implementation. They are currently used for server-side sessions, caching and rate limiting.
Stores are integrated into the Litestar
application object via the
StoreRegistry
, which can be used to register and access
stores as well as provide defaults.
from litestar.stores.memory import MemoryStore
store = MemoryStore()
async def main() -> None:
value = await store.get("key")
print(value) # this will print 'None', as no store with this key has been defined yet
await store.set("key", b"value")
value = await store.get("key")
print(value)
from litestar import Litestar
from litestar.stores.redis import RedisStore
root_store = RedisStore.with_client()
cache_store = root_store.with_namespace("cache")
session_store = root_store.with_namespace("sessions")
async def before_shutdown() -> None:
await cache_store.delete_all()
app = Litestar(before_shutdown=[before_shutdown])
from litestar import Litestar
from litestar.stores.memory import MemoryStore
app = Litestar([], stores={"memory": MemoryStore()})
memory_store = app.stores.get("memory")
# this is the previously defined store
some_other_store = app.stores.get("something_else")
# this will be a newly created instance
assert app.stores.get("something_else") is some_other_store
# but subsequent requests will return the same instance
See also
The Stores usage documentation
Usage of the stores
for caching and other integrations#
The newly introduced stores have superseded the removed
starlite.cache
module in various places.
The following now make use of stores:
The following attributes have been renamed to reduce ambiguity:
Starlite.cache_config
>Litestar.response_cache_config
AppConfig.cache_config
>response_cache_config
In addition, the ASGIConnection.cache
property has been removed. It can be replaced
by accessing the store directly as described in stores
DTOs#
Data Transfer Objects are now defined using the dto
and return_dto
arguments to handlers/controllers/routers and the application.
A DTO is any type that inherits from litestar.dto.base_dto.AbstractDTO
.
Litestar provides a suite of types that implement the AbstractDTO
abstract class
and can be used to define DTOs:
For example, to define a DTO from a dataclass:
from dataclasses import dataclass
from litestar import get
from litestar.dto import DTOConfig, DataclassDTO
@dataclass
class MyType:
some_field: str
another_field: int
class MyDTO(DataclassDTO[MyType]):
config = DTOConfig(exclude={"another_field"})
@get(dto=MyDTO)
async def handler() -> MyType:
return MyType(some_field="some value", another_field=42)
from litestar import post
from .models import User, UserDTO, UserReturnDTO
@post(dto=UserDTO, return_dto=UserReturnDTO)
def create_user(data: User) -> User:
return data
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
from typing_extensions import Annotated
from litestar import Litestar, post
from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
from litestar.dto import DTOConfig, dto_field
from .my_lib import Base
class User(Base):
name: Mapped[str]
password: Mapped[str] = mapped_column(info=dto_field("private"))
created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only"))
config = DTOConfig(rename_fields={"name": "userName"})
UserDTO = SQLAlchemyDTO[Annotated[User, config]]
@post("/users", dto=UserDTO, sync_to_thread=False)
def create_user(data: User) -> User:
assert data.name == "Litestar User"
data.created_at = datetime.min
return data
app = Litestar(route_handlers=[create_user])
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
from typing import Annotated
from litestar import Litestar, post
from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
from litestar.dto import DTOConfig, dto_field
from .my_lib import Base
class User(Base):
name: Mapped[str]
password: Mapped[str] = mapped_column(info=dto_field("private"))
created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only"))
config = DTOConfig(rename_fields={"name": "userName"})
UserDTO = SQLAlchemyDTO[Annotated[User, config]]
@post("/users", dto=UserDTO, sync_to_thread=False)
def create_user(data: User) -> User:
assert data.name == "Litestar User"
data.created_at = datetime.min
return data
app = Litestar(route_handlers=[create_user])
Run it
> curl http://127.0.0.1:8000/users -H Content-Type: application/json -d {"userName":"Litestar User","password":"xyz","created_at":"2023-04-24T00:00:00Z"}
{"created_at":"0001-01-01T00:00:00","id":"2349a563-bc97-4003-8c0b-6950eaea86e4","userName":"Litestar User"}
from datetime import datetime
from typing import List
from uuid import UUID
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing_extensions import Annotated
from litestar import Litestar, post
from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
from litestar.dto import DTOConfig, dto_field
from .my_lib import Base
class Address(Base):
street: Mapped[str]
city: Mapped[str]
state: Mapped[str]
zip: Mapped[str]
class Pets(Base):
name: Mapped[str]
user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id"))
class User(Base):
name: Mapped[str]
password: Mapped[str] = mapped_column(info=dto_field("private"))
created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only"))
address_id: Mapped[UUID] = mapped_column(ForeignKey("address.id"), info=dto_field("private"))
address: Mapped[Address] = relationship(info=dto_field("read-only"))
pets: Mapped[List[Pets]] = relationship(info=dto_field("read-only"))
UserDTO = SQLAlchemyDTO[User]
config = DTOConfig(
exclude={
"id",
"address.id",
"address.street",
"pets.0.id",
"pets.0.user_id",
}
)
ReadUserDTO = SQLAlchemyDTO[Annotated[User, config]]
@post("/users", dto=UserDTO, return_dto=ReadUserDTO, sync_to_thread=False)
def create_user(data: User) -> User:
data.created_at = datetime.min
data.address = Address(street="123 Main St", city="Anytown", state="NY", zip="12345")
data.pets = [Pets(id=1, name="Fido"), Pets(id=2, name="Spot")]
return data
app = Litestar(route_handlers=[create_user])
from datetime import datetime
from uuid import UUID
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import Annotated
from litestar import Litestar, post
from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
from litestar.dto import DTOConfig, dto_field
from .my_lib import Base
class Address(Base):
street: Mapped[str]
city: Mapped[str]
state: Mapped[str]
zip: Mapped[str]
class Pets(Base):
name: Mapped[str]
user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id"))
class User(Base):
name: Mapped[str]
password: Mapped[str] = mapped_column(info=dto_field("private"))
created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only"))
address_id: Mapped[UUID] = mapped_column(ForeignKey("address.id"), info=dto_field("private"))
address: Mapped[Address] = relationship(info=dto_field("read-only"))
pets: Mapped[list[Pets]] = relationship(info=dto_field("read-only"))
UserDTO = SQLAlchemyDTO[User]
config = DTOConfig(
exclude={
"id",
"address.id",
"address.street",
"pets.0.id",
"pets.0.user_id",
}
)
ReadUserDTO = SQLAlchemyDTO[Annotated[User, config]]
@post("/users", dto=UserDTO, return_dto=ReadUserDTO, sync_to_thread=False)
def create_user(data: User) -> User:
data.created_at = datetime.min
data.address = Address(street="123 Main St", city="Anytown", state="NY", zip="12345")
data.pets = [Pets(id=1, name="Fido"), Pets(id=2, name="Spot")]
return data
app = Litestar(route_handlers=[create_user])
Run it
> curl http://127.0.0.1:8000/users -H Content-Type: application/json -d {"name":"Litestar User","password":"xyz","created_at":"2023-04-24T00:00:00Z"}
{"created_at":"0001-01-01T00:00:00","address":{"city":"Anytown","state":"NY","zip":"12345"},"pets":[{"name":"Fido"},{"name":"Spot"}],"name":"Litestar User"}
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
from litestar import Litestar, post
from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
from litestar.dto import dto_field
from .my_lib import Base
class User(Base):
name: Mapped[str]
password: Mapped[str] = mapped_column(info=dto_field("private"))
created_at: Mapped[datetime] = mapped_column(info=dto_field("read-only"))
UserDTO = SQLAlchemyDTO[User]
@post("/users", dto=UserDTO, sync_to_thread=False)
def create_user(data: User) -> User:
# even though the client sent the password and created_at field, it is not in the data object
assert "password" not in vars(data)
assert "created_at" not in vars(data)
# normally the database would set the created_at timestamp
data.created_at = datetime.min
return data # the response includes the created_at field
app = Litestar(route_handlers=[create_user])
Run it
> curl http://127.0.0.1:8000/users -H Content-Type: application/json -d {"name":"Litestar User","password":"xyz","created_at":"2023-04-24T00:00:00Z"}
{"created_at":"0001-01-01T00:00:00","id":"a19cc31b-672d-4e24-ab97-80cad0c7965e","name":"Litestar User"}
See also
The Data Transfer Object (DTO) usage documentation
Application lifespan hooks#
All application lifespan hooks have been merged into on_startup
and on_shutdown
.
The following hooks have been removed:
before_startup
after_startup
before_shutdown
after_shutdown
on_startup
and on_shutdown
now optionally receive the application instance as
their first parameter. If your on_startup
and on_shutdown
hooks made use of the
application state, they will now have to access it through the provided application
instance.
def on_startup(state: State) -> None:
print(state.something)
def on_startup(app: Litestar) -> None:
print(app.state.something)
Dependencies without Provide
#
Dependencies may now be declared without Provide
, by passing the
callable directly. This can be advantageous in places where the configuration options
of Provide
are not needed.
async def some_dependency() -> str: ...
app = Litestar(dependencies={"some": Provide(some_dependency)})
is equivalent to
async def some_dependency() -> str: ...
app = Litestar(dependencies={"some": some_dependency})
sync_to_thread
#
The sync_to_thread
option can be used to run a synchronous callable provided to a
route handler or Provide
inside a thread pool. Since synchronous
functions may block the main thread when not used with sync_to_thread=True
, a
warning will be raised in these cases. If the synchronous function should not be run in
a thread pool, passing sync_to_thread=False
will also silence the warning.
Tip
The warning can be disabled entirely by setting the environment variable
LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0
@get()
def handler() -> None: ...
@get(sync_to_thread=False)
def handler() -> None: ...
or
@get(sync_to_thread=True)
def handler() -> None: ...
See also
The Sync vs. Async topic guide
HTMX#
Basic support for HTMX requests and responses was added with the
litestar.contrib.htmx
module.
See also
The HTMX usage documentation
Event bus#
A simple event bus system for Litestar, supporting synchronous and asynchronous listeners and emitters, providing a similar interface to handlers. It currently features a simple in-memory, process-local backend.
Enhanced WebSocket support#
A new set of features for handling WebSockets, including automatic connection handling, (de)serialization of incoming and outgoing data analogous to route handlers, OOP based event dispatching, data iterators and more.
from litestar import Litestar, WebSocket
from litestar.handlers import WebsocketListener
class Handler(WebsocketListener):
path = "/"
def on_accept(self, socket: WebSocket) -> None:
print("Connection accepted")
def on_disconnect(self, socket: WebSocket) -> None:
print("Connection closed")
def on_receive(self, data: str) -> str:
return data
app = Litestar([Handler])
from litestar import Litestar, websocket_listener
@websocket_listener("/", send_mode="text")
async def handler(data: str) -> str:
return data
app = Litestar([handler])
from dataclasses import dataclass
from datetime import datetime
from litestar import Litestar, websocket_listener
@dataclass
class Message:
content: str
timestamp: float
@websocket_listener("/")
async def handler(data: str) -> Message:
return Message(content=data, timestamp=datetime.now().timestamp())
app = Litestar([handler])
from sqlalchemy.orm import Mapped
from litestar import Litestar, websocket_listener
from litestar.contrib.sqlalchemy.base import UUIDBase
from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
class User(UUIDBase):
name: Mapped[str]
UserDTO = SQLAlchemyDTO[User]
@websocket_listener("/", dto=UserDTO)
async def handler(data: User) -> User:
return data
app = Litestar([handler])
from litestar import websocket, WebSocket
@websocket("/")
async def handler(socket: WebSocket) -> None:
await socket.accept()
async for message in socket.iter_data(mode):
await socket.send_msgpack(message)
Attrs signature modelling#
TBD
Annotated
support in route handlers#
Annotated
can now be used in route handler and
dependencies to specify additional information about the fields
@get("/")
def index(param: int = Parameter(gt=5)) -> dict[str, int]: ...
@get("/")
def index(param: Annotated[int, Parameter(gt=5)]) -> dict[str, int]: ...
Channels#
Channels are a general purpose event streaming module, which can for example be used to broadcast messages via WebSockets and includes functionalities such as automatically generating WebSocket route handlers to broadcast messages.
from litestar import Litestar, WebSocket, websocket
from litestar.channels import ChannelsPlugin
from litestar.channels.backends.memory import MemoryChannelsBackend
@websocket("/ws")
async def handler(socket: WebSocket, channels: ChannelsPlugin) -> None:
await socket.accept()
async with channels.start_subscription(["some_channel"]) as subscriber, subscriber.run_in_background(
socket.send_text
):
while True:
response = await socket.receive_text()
await socket.send_text(response)
app = Litestar(
[handler],
plugins=[ChannelsPlugin(backend=MemoryChannelsBackend(), channels=["some_channel"])],
)
See also
The channels usage documentation
Application lifespan context managers#
A new lifespan
argument has been added to 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.
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import create_async_engine
from litestar import Litestar
@asynccontextmanager
async def db_connection(app: Litestar) -> AsyncGenerator[None, None]:
engine = getattr(app.state, "engine", None)
if engine is None:
engine = create_async_engine("postgresql+asyncpg://postgres:mysecretpassword@pg.db:5432/db")
app.state.engine = engine
try:
yield
finally:
await engine.dispose()
app = Litestar(lifespan=[db_connection])
from contextlib import asynccontextmanager
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import create_async_engine
from litestar import Litestar
@asynccontextmanager
async def db_connection(app: Litestar) -> AsyncGenerator[None, None]:
engine = getattr(app.state, "engine", None)
if engine is None:
engine = create_async_engine("postgresql+asyncpg://postgres:mysecretpassword@pg.db:5432/db")
app.state.engine = engine
try:
yield
finally:
await engine.dispose()
app = Litestar(lifespan=[db_connection])
Response types#
Starlite had the concept of Response Containers, which were datatypes used to indicate
the type of response returned by a handler. These included File
, Redirect
,
Template
and Stream
types. These types abstracted the interface of responses
from the underlying response itself.
In Litestar, these types still exist, however they are now subclasses of
Response
and are imported from the litestar.response
module. In contrast to Starlite’s Response Containers, these types have more utility
for interacting with the outgoing response, such as methods to add headers and
cookies. Otherwise, their usage should remain very similar to Starlite.
Litestar also introduces a new layer of ASGI response type, based on
ASGIResponse
. These types 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, however they are
low-level, and lack the utility of the higher-level response types.