diff --git a/docs/examples/application_state/injecting_application_state.py b/docs/examples/application_state/injecting_application_state.py new file mode 100644 index 0000000000..78097cd532 --- /dev/null +++ b/docs/examples/application_state/injecting_application_state.py @@ -0,0 +1,6 @@ +from litestar import get +from litestar.datastructures import State + + +@get("/") +def handler(state: State) -> None: ... diff --git a/docs/examples/caching/__init__.py b/docs/examples/caching/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/caching/caching_duration.py b/docs/examples/caching/caching_duration.py new file mode 100644 index 0000000000..4f7f5171ab --- /dev/null +++ b/docs/examples/caching/caching_duration.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get("/cached-path", cache=120) # seconds +def my_cached_handler() -> str: ... diff --git a/docs/examples/caching/caching_forever.py b/docs/examples/caching/caching_forever.py new file mode 100644 index 0000000000..7fc0c37831 --- /dev/null +++ b/docs/examples/caching/caching_forever.py @@ -0,0 +1,6 @@ +from litestar import get +from litestar.config.response_cache import CACHE_FOREVER + + +@get("/cached-path", cache=CACHE_FOREVER) # seconds +def my_cached_handler() -> str: ... diff --git a/docs/examples/caching/caching_key_builder.py b/docs/examples/caching/caching_key_builder.py new file mode 100644 index 0000000000..2d9c966196 --- /dev/null +++ b/docs/examples/caching/caching_key_builder.py @@ -0,0 +1,9 @@ +from litestar import Litestar, Request +from litestar.config.cache import ResponseCacheConfig + + +def key_builder(request: Request) -> str: + return request.url.path + request.headers.get("my-header", "") + + +app = Litestar([], cache_config=ResponseCacheConfig(key_builder=key_builder)) diff --git a/docs/examples/caching/caching_key_builder_specific_route.py b/docs/examples/caching/caching_key_builder_specific_route.py new file mode 100644 index 0000000000..1951ea55ee --- /dev/null +++ b/docs/examples/caching/caching_key_builder_specific_route.py @@ -0,0 +1,9 @@ +from litestar import Request, get + + +def key_builder(request: Request) -> str: + return request.url.path + request.headers.get("my-header", "") + + +@get("/cached-path", cache=True, cache_key_builder=key_builder) +def cached_handler() -> str: ... diff --git a/docs/examples/caching/caching_response.py b/docs/examples/caching/caching_response.py new file mode 100644 index 0000000000..45ec4b3e5e --- /dev/null +++ b/docs/examples/caching/caching_response.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get("/cached-path", cache=True) +def my_cached_handler() -> str: ... diff --git a/docs/examples/caching/caching_storage_redis.py b/docs/examples/caching/caching_storage_redis.py new file mode 100644 index 0000000000..852f9fa6c0 --- /dev/null +++ b/docs/examples/caching/caching_storage_redis.py @@ -0,0 +1,6 @@ +from litestar.config.cache import ResponseCacheConfig +from litestar.stores.redis import RedisStore + +redis_store = RedisStore(url="redis://localhost/", port=6379, db=0) + +cache_config = ResponseCacheConfig(store=redis_store) diff --git a/docs/examples/channels/__init__.py b/docs/examples/channels/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/channels/allowing_arbitrary_channels.py b/docs/examples/channels/allowing_arbitrary_channels.py new file mode 100644 index 0000000000..0bd178d5c5 --- /dev/null +++ b/docs/examples/channels/allowing_arbitrary_channels.py @@ -0,0 +1,3 @@ +from litestar.channels import ChannelsPlugin + +channels_plugin = ChannelsPlugin(..., arbitrary_channels_allowed=True) diff --git a/docs/examples/channels/backoff_strategy.py b/docs/examples/channels/backoff_strategy.py new file mode 100644 index 0000000000..35fd2556c9 --- /dev/null +++ b/docs/examples/channels/backoff_strategy.py @@ -0,0 +1,8 @@ +from litestar.channels import ChannelsPlugin +from litestar.channels.memory import MemoryChannelsBackend + +channels = ChannelsPlugin( + backend=MemoryChannelsBackend(), + max_backlog=1000, + backlog_strategy="backoff", +) diff --git a/docs/examples/channels/eviction_strategy.py b/docs/examples/channels/eviction_strategy.py new file mode 100644 index 0000000000..a77d778dbf --- /dev/null +++ b/docs/examples/channels/eviction_strategy.py @@ -0,0 +1,8 @@ +from litestar.channels import ChannelsPlugin +from litestar.channels.memory import MemoryChannelsBackend + +channels = ChannelsPlugin( + backend=MemoryChannelsBackend(), + max_backlog=1000, + backlog_strategy="dropleft", +) diff --git a/docs/examples/channels/passing_channels_explicitly.py b/docs/examples/channels/passing_channels_explicitly.py new file mode 100644 index 0000000000..81b5caaead --- /dev/null +++ b/docs/examples/channels/passing_channels_explicitly.py @@ -0,0 +1,3 @@ +from litestar.channels import ChannelsPlugin + +channels_plugin = ChannelsPlugin(..., channels=["foo", "bar"]) diff --git a/docs/examples/channels/publish_data.py b/docs/examples/channels/publish_data.py new file mode 100644 index 0000000000..3ba2221deb --- /dev/null +++ b/docs/examples/channels/publish_data.py @@ -0,0 +1 @@ +channels.publish({"message": "Hello"}, "general") diff --git a/docs/examples/channels/subscribe_method_context_manager.py b/docs/examples/channels/subscribe_method_context_manager.py new file mode 100644 index 0000000000..42b58ba3bf --- /dev/null +++ b/docs/examples/channels/subscribe_method_context_manager.py @@ -0,0 +1,2 @@ +async with channels.start_subscription(["foo", "bar"]) as subscriber: + ... # do some stuff here diff --git a/docs/examples/channels/subscribe_method_manually.py b/docs/examples/channels/subscribe_method_manually.py new file mode 100644 index 0000000000..2efce67722 --- /dev/null +++ b/docs/examples/channels/subscribe_method_manually.py @@ -0,0 +1,5 @@ +subscriber = await channels.subscribe(["foo", "bar"]) +try: + ... # do some stuff here +finally: + await channels.unsubscribe(subscriber) diff --git a/docs/examples/channels/unsubscribe_method_context_manager.py b/docs/examples/channels/unsubscribe_method_context_manager.py new file mode 100644 index 0000000000..6e7d8d22d2 --- /dev/null +++ b/docs/examples/channels/unsubscribe_method_context_manager.py @@ -0,0 +1,3 @@ +async with channels.start_subscription(["foo", "bar"]) as subscriber: + # do some stuff here + await channels.unsubscribe(subscriber, ["foo"]) diff --git a/docs/examples/channels/unsubscribe_method_manually.py b/docs/examples/channels/unsubscribe_method_manually.py new file mode 100644 index 0000000000..4da06367cc --- /dev/null +++ b/docs/examples/channels/unsubscribe_method_manually.py @@ -0,0 +1,3 @@ +subscriber = await channels.subscribe(["foo", "bar"]) +# do some stuff here +await channels.unsubscribe(subscriber, ["foo"]) diff --git a/docs/examples/cli/__init__.py b/docs/examples/cli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/cli/app_instance.py b/docs/examples/cli/app_instance.py new file mode 100644 index 0000000000..65f85aa84f --- /dev/null +++ b/docs/examples/cli/app_instance.py @@ -0,0 +1,7 @@ +import click + +from litestar import Litestar + + +@click.command() +def my_command(app: Litestar) -> None: ... diff --git a/docs/examples/cli/entry_points.py b/docs/examples/cli/entry_points.py new file mode 100644 index 0000000000..538c2a1af4 --- /dev/null +++ b/docs/examples/cli/entry_points.py @@ -0,0 +1,8 @@ +from setuptools import setup + +setup( + name="my-litestar-plugin", + entry_points={ + "litestar.commands": ["my_command=my_litestar_plugin.cli:main"], + }, +) diff --git a/docs/examples/cli/info.sh b/docs/examples/cli/info.sh new file mode 100644 index 0000000000..4694357f1d --- /dev/null +++ b/docs/examples/cli/info.sh @@ -0,0 +1 @@ +litestar info diff --git a/docs/examples/cli/openapi_schema.sh b/docs/examples/cli/openapi_schema.sh new file mode 100644 index 0000000000..6c8571b821 --- /dev/null +++ b/docs/examples/cli/openapi_schema.sh @@ -0,0 +1 @@ +litestar schema openapi --output my-specs.yml diff --git a/docs/examples/cli/plugin.py b/docs/examples/cli/plugin.py new file mode 100644 index 0000000000..40ac6cea46 --- /dev/null +++ b/docs/examples/cli/plugin.py @@ -0,0 +1,14 @@ +from click import Group + +from litestar import Litestar +from litestar.plugins import CLIPluginProtocol + + +class CLIPlugin(CLIPluginProtocol): + def on_cli_init(self, cli: Group) -> None: + @cli.command() + def is_debug_mode(app: Litestar): + print(app.debug) + + +app = Litestar(plugins=[CLIPlugin()]) diff --git a/docs/examples/cli/poetry.toml b/docs/examples/cli/poetry.toml new file mode 100644 index 0000000000..f28e195c66 --- /dev/null +++ b/docs/examples/cli/poetry.toml @@ -0,0 +1,2 @@ +[tool.poetry.plugins."litestar.commands"] +my_command = "my_litestar_plugin.cli:main" diff --git a/docs/examples/cli/reload_dir.sh b/docs/examples/cli/reload_dir.sh new file mode 100644 index 0000000000..f9d14f3576 --- /dev/null +++ b/docs/examples/cli/reload_dir.sh @@ -0,0 +1 @@ +litestar run --reload-dir=. --reload-dir=../other-library/src diff --git a/docs/examples/cli/reload_dir_multiple_directories.sh b/docs/examples/cli/reload_dir_multiple_directories.sh new file mode 100644 index 0000000000..3695807d21 --- /dev/null +++ b/docs/examples/cli/reload_dir_multiple_directories.sh @@ -0,0 +1 @@ + LITESTAR_RELOAD_DIRS=.,../other-library/src diff --git a/docs/examples/cli/reload_exclude.sh b/docs/examples/cli/reload_exclude.sh new file mode 100644 index 0000000000..f77b1d2d57 --- /dev/null +++ b/docs/examples/cli/reload_exclude.sh @@ -0,0 +1 @@ + litestar run --reload-exclude="*.py" --reload-exclude="*.yml" diff --git a/docs/examples/cli/reload_exclude_multiple_directories.sh b/docs/examples/cli/reload_exclude_multiple_directories.sh new file mode 100644 index 0000000000..6957d0711b --- /dev/null +++ b/docs/examples/cli/reload_exclude_multiple_directories.sh @@ -0,0 +1 @@ +LITESTAR_RELOAD_EXCLUDES=*.py,*.yml diff --git a/docs/examples/cli/reload_include.sh b/docs/examples/cli/reload_include.sh new file mode 100644 index 0000000000..ed31f538fd --- /dev/null +++ b/docs/examples/cli/reload_include.sh @@ -0,0 +1 @@ +litestar run --reload-include="*.rst" --reload-include="*.yml" diff --git a/docs/examples/cli/reload_include_multiple_directories.sh b/docs/examples/cli/reload_include_multiple_directories.sh new file mode 100644 index 0000000000..b59339d0bc --- /dev/null +++ b/docs/examples/cli/reload_include_multiple_directories.sh @@ -0,0 +1 @@ +LITESTAR_RELOAD_INCLUDES=*.rst,*.yml diff --git a/docs/examples/cli/routes.sh b/docs/examples/cli/routes.sh new file mode 100644 index 0000000000..ada6d5ad1b --- /dev/null +++ b/docs/examples/cli/routes.sh @@ -0,0 +1 @@ +litestar routes diff --git a/docs/examples/cli/run.sh b/docs/examples/cli/run.sh new file mode 100644 index 0000000000..9338a7a357 --- /dev/null +++ b/docs/examples/cli/run.sh @@ -0,0 +1 @@ +litestar run diff --git a/docs/examples/cli/sessions_clear.sh b/docs/examples/cli/sessions_clear.sh new file mode 100644 index 0000000000..7a586f9ed3 --- /dev/null +++ b/docs/examples/cli/sessions_clear.sh @@ -0,0 +1 @@ +litestar sessions clear diff --git a/docs/examples/cli/sessions_delete.sh b/docs/examples/cli/sessions_delete.sh new file mode 100644 index 0000000000..1b5cc4660a --- /dev/null +++ b/docs/examples/cli/sessions_delete.sh @@ -0,0 +1 @@ +litestar sessions delete cc3debc7-1ab6-4dc8-a220-91934a473717 diff --git a/docs/examples/cli/ssl.sh b/docs/examples/cli/ssl.sh new file mode 100644 index 0000000000..bbeccdf798 --- /dev/null +++ b/docs/examples/cli/ssl.sh @@ -0,0 +1 @@ +litestar run --ssl-certfile=certs/cert.pem --ssl-keyfile=certs/key.pem diff --git a/docs/examples/cli/ssl_self_signed.sh b/docs/examples/cli/ssl_self_signed.sh new file mode 100644 index 0000000000..2d460a0d1f --- /dev/null +++ b/docs/examples/cli/ssl_self_signed.sh @@ -0,0 +1 @@ +litestar run --ssl-certfile=certs/cert.pem --ssl-keyfile=certs/key.pem --create-self-signed-cert diff --git a/docs/examples/cli/typescript_schema.sh b/docs/examples/cli/typescript_schema.sh new file mode 100644 index 0000000000..ee21ebf878 --- /dev/null +++ b/docs/examples/cli/typescript_schema.sh @@ -0,0 +1 @@ +litestar schema typescript diff --git a/docs/examples/cli/typescript_schema.ts b/docs/examples/cli/typescript_schema.ts new file mode 100644 index 0000000000..e853017624 --- /dev/null +++ b/docs/examples/cli/typescript_schema.ts @@ -0,0 +1,3 @@ +export namespace API { + // ... +} diff --git a/docs/examples/cli/typescript_schema_namespace.sh b/docs/examples/cli/typescript_schema_namespace.sh new file mode 100644 index 0000000000..73c252cb4e --- /dev/null +++ b/docs/examples/cli/typescript_schema_namespace.sh @@ -0,0 +1 @@ +litestar schema typescript --namespace MyNamespace diff --git a/docs/examples/cli/typescript_schema_namespace.ts b/docs/examples/cli/typescript_schema_namespace.ts new file mode 100644 index 0000000000..848d65311a --- /dev/null +++ b/docs/examples/cli/typescript_schema_namespace.ts @@ -0,0 +1,3 @@ +export namespace MyNamespace { + // ... +} diff --git a/docs/examples/cli/typescript_schema_path.sh b/docs/examples/cli/typescript_schema_path.sh new file mode 100644 index 0000000000..750575df55 --- /dev/null +++ b/docs/examples/cli/typescript_schema_path.sh @@ -0,0 +1 @@ +litestar schema typescript --output my-types.ts diff --git a/docs/examples/data_transfer_objects/define_dto_from_dataclass.py b/docs/examples/data_transfer_objects/define_dto_from_dataclass.py new file mode 100644 index 0000000000..0131ca6a1e --- /dev/null +++ b/docs/examples/data_transfer_objects/define_dto_from_dataclass.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +from litestar import get +from litestar.dto import DataclassDTO, DTOConfig + + +@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) diff --git a/docs/examples/data_transfer_objects/disable_backend_selectively.py b/docs/examples/data_transfer_objects/disable_backend_selectively.py new file mode 100644 index 0000000000..bcd1025ab0 --- /dev/null +++ b/docs/examples/data_transfer_objects/disable_backend_selectively.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +from litestar.dto import DataclassDTO, DTOConfig + + +@dataclass +class Foo: + name: str + + +class FooDTO(DataclassDTO[Foo]): + config = DTOConfig(experimental_codegen_backend=False) diff --git a/docs/examples/data_transfer_objects/enabling_backend.py b/docs/examples/data_transfer_objects/enabling_backend.py new file mode 100644 index 0000000000..6055d977cc --- /dev/null +++ b/docs/examples/data_transfer_objects/enabling_backend.py @@ -0,0 +1,4 @@ +from litestar import Litestar +from litestar.config.app import ExperimentalFeatures + +app = Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN]) diff --git a/docs/examples/data_transfer_objects/factory/enveloping_return_data_1.py b/docs/examples/data_transfer_objects/factory/enveloping_return_data_1.py new file mode 100644 index 0000000000..1d4fe107cf --- /dev/null +++ b/docs/examples/data_transfer_objects/factory/enveloping_return_data_1.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from typing import Generic, List, TypeVar + +T = TypeVar("T") + + +@dataclass +class WithCount(Generic[T]): + count: int + data: List[T] diff --git a/docs/examples/data_transfer_objects/factory/enveloping_return_data_2.py b/docs/examples/data_transfer_objects/factory/enveloping_return_data_2.py new file mode 100644 index 0000000000..01be48130f --- /dev/null +++ b/docs/examples/data_transfer_objects/factory/enveloping_return_data_2.py @@ -0,0 +1,7 @@ +from advanced_alchemy.dto import SQLAlchemyDTO + +from litestar.dto import DTOConfig + + +class UserDTO(SQLAlchemyDTO[User]): + config = DTOConfig(exclude={"password", "created_at"}) diff --git a/docs/examples/data_transfer_objects/factory/enveloping_return_data_3.py b/docs/examples/data_transfer_objects/factory/enveloping_return_data_3.py new file mode 100644 index 0000000000..490da0a99c --- /dev/null +++ b/docs/examples/data_transfer_objects/factory/enveloping_return_data_3.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Generic, TypeVar + +from sqlalchemy.orm import Mapped + +from litestar import get +from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO +from litestar.dto import DTOConfig + +from .my_lib import Base + +T = TypeVar("T") + + +@dataclass +class WithCount(Generic[T]): + count: int + data: list[T] + + +class User(Base): + name: Mapped[str] + password: Mapped[str] + created_at: Mapped[datetime] + + +class UserDTO(SQLAlchemyDTO[User]): + config = DTOConfig(exclude={"password", "created_at"}) + + +@get("/users", dto=UserDTO, sync_to_thread=False) +def get_users() -> WithCount[User]: + return WithCount( + count=1, + data=[ + User( + id=1, + name="Litestar User", + password="xyz", + created_at=datetime.now(), + ), + ], + ) diff --git a/docs/examples/data_transfer_objects/factory/excluding_fields_2.py b/docs/examples/data_transfer_objects/factory/excluding_fields_2.py new file mode 100644 index 0000000000..71cc132622 --- /dev/null +++ b/docs/examples/data_transfer_objects/factory/excluding_fields_2.py @@ -0,0 +1,9 @@ +config = DTOConfig( + exclude={ + "id", + "address.id", + "address.street", + "pets.0.id", + "pets.0.user_id", + } +) diff --git a/docs/examples/data_transfer_objects/factory/paginated_return_data_1.py b/docs/examples/data_transfer_objects/factory/paginated_return_data_1.py new file mode 100644 index 0000000000..73c4f91d81 --- /dev/null +++ b/docs/examples/data_transfer_objects/factory/paginated_return_data_1.py @@ -0,0 +1,18 @@ +from datetime import datetime + +from advanced_alchemy.dto import SQLAlchemyDTO +from sqlalchemy.orm import Mapped + +from litestar.dto import DTOConfig + +from .my_lib import Base + + +class User(Base): + name: Mapped[str] + password: Mapped[str] + created_at: Mapped[datetime] + + +class UserDTO(SQLAlchemyDTO[User]): + config = DTOConfig(exclude={"password", "created_at"}) diff --git a/docs/examples/data_transfer_objects/factory/paginated_return_data_2.py b/docs/examples/data_transfer_objects/factory/paginated_return_data_2.py new file mode 100644 index 0000000000..b4cb294d98 --- /dev/null +++ b/docs/examples/data_transfer_objects/factory/paginated_return_data_2.py @@ -0,0 +1,37 @@ +from datetime import datetime + +from sqlalchemy.orm import Mapped + +from litestar import get +from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO +from litestar.dto import DTOConfig +from litestar.pagination import ClassicPagination + +from .my_lib import Base + + +class User(Base): + name: Mapped[str] + password: Mapped[str] + created_at: Mapped[datetime] + + +class UserDTO(SQLAlchemyDTO[User]): + config = DTOConfig(exclude={"password", "created_at"}) + + +@get("/users", dto=UserDTO, sync_to_thread=False) +def get_users() -> ClassicPagination[User]: + return ClassicPagination( + page_size=10, + total_pages=1, + current_page=1, + items=[ + User( + id=1, + name="Litestar User", + password="xyz", + created_at=datetime.now(), + ), + ], + ) diff --git a/docs/examples/data_transfer_objects/factory/response_return_data_1.py b/docs/examples/data_transfer_objects/factory/response_return_data_1.py new file mode 100644 index 0000000000..01be48130f --- /dev/null +++ b/docs/examples/data_transfer_objects/factory/response_return_data_1.py @@ -0,0 +1,7 @@ +from advanced_alchemy.dto import SQLAlchemyDTO + +from litestar.dto import DTOConfig + + +class UserDTO(SQLAlchemyDTO[User]): + config = DTOConfig(exclude={"password", "created_at"}) diff --git a/docs/examples/data_transfer_objects/factory/response_return_data_2.py b/docs/examples/data_transfer_objects/factory/response_return_data_2.py new file mode 100644 index 0000000000..add2aae928 --- /dev/null +++ b/docs/examples/data_transfer_objects/factory/response_return_data_2.py @@ -0,0 +1,32 @@ +from datetime import datetime + +from sqlalchemy.orm import Mapped + +from litestar import Response, get +from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO +from litestar.dto import DTOConfig + +from .my_lib import Base + + +class User(Base): + name: Mapped[str] + password: Mapped[str] + created_at: Mapped[datetime] + + +class UserDTO(SQLAlchemyDTO[User]): + config = DTOConfig(exclude={"password", "created_at"}) + + +@get("/users", dto=UserDTO, sync_to_thread=False) +def get_users() -> Response[User]: + return Response( + content=User( + id=1, + name="Litestar User", + password="xyz", + created_at=datetime.now(), + ), + headers={"X-Total-Count": "1"}, + ) diff --git a/docs/examples/data_transfer_objects/individual_dto.py b/docs/examples/data_transfer_objects/individual_dto.py new file mode 100644 index 0000000000..e8ae9ace57 --- /dev/null +++ b/docs/examples/data_transfer_objects/individual_dto.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +from litestar.dto import DataclassDTO, DTOConfig + + +@dataclass +class Foo: + name: str + + +class FooDTO(DataclassDTO[Foo]): + config = DTOConfig(experimental_codegen_backend=True) diff --git a/docs/examples/debugging/__init__.py b/docs/examples/debugging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/debugging/running_litestar_with_pdb_on_exception.py b/docs/examples/debugging/running_litestar_with_pdb_on_exception.py new file mode 100644 index 0000000000..bc74ffcb50 --- /dev/null +++ b/docs/examples/debugging/running_litestar_with_pdb_on_exception.py @@ -0,0 +1,3 @@ +from litestar import Litestar + +app = Litestar(pdb_on_exception=True) diff --git a/docs/examples/dependency_injection/dependency_base.py b/docs/examples/dependency_injection/dependency_base.py new file mode 100644 index 0000000000..547e8e21b0 --- /dev/null +++ b/docs/examples/dependency_injection/dependency_base.py @@ -0,0 +1,42 @@ +from litestar import Controller, Litestar, Router, get +from litestar.di import Provide + + +async def bool_fn() -> bool: ... + + +async def dict_fn() -> dict: ... + + +async def list_fn() -> list: ... + + +async def int_fn() -> int: ... + + +class MyController(Controller): + path = "/controller" + # on the controller + dependencies = {"controller_dependency": Provide(list_fn)} + + # on the route handler + @get(path="/handler", dependencies={"local_dependency": Provide(int_fn)}) + def my_route_handler( + self, + app_dependency: bool, + router_dependency: dict, + controller_dependency: list, + local_dependency: int, + ) -> None: ... + + # on the router + + +my_router = Router( + path="/router", + dependencies={"router_dependency": Provide(dict_fn)}, + route_handlers=[MyController], +) + +# on the app +app = Litestar(route_handlers=[my_router], dependencies={"app_dependency": Provide(bool_fn)}) diff --git a/docs/examples/dependency_injection/dependency_connection.py b/docs/examples/dependency_injection/dependency_connection.py new file mode 100644 index 0000000000..74c6996c31 --- /dev/null +++ b/docs/examples/dependency_injection/dependency_connection.py @@ -0,0 +1,7 @@ +from dependencies import CONNECTION, app + +from litestar.testing import TestClient + +with TestClient(app=app) as client: + print(client.get("/").json()) # {"open": True} + print(CONNECTION) # {"open": False} diff --git a/docs/examples/dependency_injection/dependency_keyword_arguments.py b/docs/examples/dependency_injection/dependency_keyword_arguments.py new file mode 100644 index 0000000000..857afda8fb --- /dev/null +++ b/docs/examples/dependency_injection/dependency_keyword_arguments.py @@ -0,0 +1,20 @@ +from pydantic import UUID4, BaseModel + +from litestar import Controller, patch +from litestar.di import Provide + + +class User(BaseModel): + id: UUID4 + name: str + + +async def retrieve_db_user(user_id: UUID4) -> User: ... + + +class UserController(Controller): + path = "/user" + dependencies = {"user": Provide(retrieve_db_user)} + + @patch(path="/{user_id:uuid}") + async def get_user(self, user: User) -> User: ... diff --git a/docs/examples/dependency_injection/dependency_overrides.py b/docs/examples/dependency_injection/dependency_overrides.py new file mode 100644 index 0000000000..79f1a7b214 --- /dev/null +++ b/docs/examples/dependency_injection/dependency_overrides.py @@ -0,0 +1,21 @@ +from litestar import Controller, get +from litestar.di import Provide + + +def bool_fn() -> bool: ... + + +def dict_fn() -> dict: ... + + +class MyController(Controller): + path = "/controller" + # on the controller + dependencies = {"some_dependency": Provide(dict_fn)} + + # on the route handler + @get(path="/handler", dependencies={"some_dependency": Provide(bool_fn)}) + def my_route_handler( + self, + some_dependency: bool, + ) -> None: ... diff --git a/docs/examples/dependency_injection/dependency_provide.py b/docs/examples/dependency_injection/dependency_provide.py new file mode 100644 index 0000000000..86842ccbe7 --- /dev/null +++ b/docs/examples/dependency_injection/dependency_provide.py @@ -0,0 +1,19 @@ +from random import randint + +from litestar import get +from litestar.di import Provide + + +def my_dependency() -> int: + return randint(1, 10) + + +@get( + "/some-path", + dependencies={ + "my_dep": Provide( + my_dependency, + ) + }, +) +def my_handler(my_dep: int) -> None: ... diff --git a/docs/examples/dependency_injection/dependency_within_dependency.py b/docs/examples/dependency_injection/dependency_within_dependency.py new file mode 100644 index 0000000000..95d7e5bec9 --- /dev/null +++ b/docs/examples/dependency_injection/dependency_within_dependency.py @@ -0,0 +1,26 @@ +from random import randint + +from litestar import Litestar, get +from litestar.di import Provide + + +def first_dependency() -> int: + return randint(1, 10) + + +def second_dependency(injected_integer: int) -> bool: + return injected_integer % 2 == 0 + + +@get("/true-or-false") +def true_or_false_handler(injected_bool: bool) -> str: + return "its true!" if injected_bool else "nope, its false..." + + +app = Litestar( + route_handlers=[true_or_false_handler], + dependencies={ + "injected_integer": Provide(first_dependency), + "injected_bool": Provide(second_dependency), + }, +) diff --git a/docs/examples/dependency_injection/dependency_yield_exceptions_state.py b/docs/examples/dependency_injection/dependency_yield_exceptions_state.py new file mode 100644 index 0000000000..bb3ed1561b --- /dev/null +++ b/docs/examples/dependency_injection/dependency_yield_exceptions_state.py @@ -0,0 +1,12 @@ +from dependencies import STATE, app + +from litestar.testing import TestClient + +with TestClient(app=app) as client: + response = client.get("/John") + print(response.json()) # {"John": "hello"} + print(STATE) # {"result": "OK", "connection": "closed"} + + response = client.get("/Peter") + print(response.status_code) # 500 + print(STATE) # {"result": "error", "connection": "closed"} diff --git a/docs/examples/dependency_injection/dependency_yield_exceptions_trap.py b/docs/examples/dependency_injection/dependency_yield_exceptions_trap.py new file mode 100644 index 0000000000..d2b3551139 --- /dev/null +++ b/docs/examples/dependency_injection/dependency_yield_exceptions_trap.py @@ -0,0 +1,5 @@ +def generator_dependency(): + try: + yield + finally: + ... # cleanup code diff --git a/docs/examples/deployment/__init__.py b/docs/examples/deployment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/deployment/docker.py b/docs/examples/deployment/docker.py new file mode 100644 index 0000000000..d1fdbf39cd --- /dev/null +++ b/docs/examples/deployment/docker.py @@ -0,0 +1,22 @@ +"""Minimal Litestar application.""" + +from asyncio import sleep +from typing import Any, Dict + +from litestar import Litestar, get + + +@get("/") +async def async_hello_world() -> Dict[str, Any]: + """Route Handler that outputs hello world.""" + await sleep(0.1) + return {"hello": "world"} + + +@get("/sync", sync_to_thread=False) +def sync_hello_world() -> Dict[str, Any]: + """Route Handler that outputs hello world.""" + return {"hello": "world"} + + +app = Litestar(route_handlers=[sync_hello_world, async_hello_world]) diff --git a/docs/examples/events/__init__.py b/docs/examples/events/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/events/argument_to_listener.py b/docs/examples/events/argument_to_listener.py new file mode 100644 index 0000000000..8e0cc8e367 --- /dev/null +++ b/docs/examples/events/argument_to_listener.py @@ -0,0 +1,4 @@ +from typing import Any + + +def emit(self, event_id: str, *args: Any, **kwargs: Any) -> None: ... diff --git a/docs/examples/events/event_base.py b/docs/examples/events/event_base.py new file mode 100644 index 0000000000..b056fc816f --- /dev/null +++ b/docs/examples/events/event_base.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass + +from db import user_repository + +from litestar import Litestar, Request, post +from litestar.events import listener +from utils.email import send_welcome_mail + + +@listener("user_created") +async def send_welcome_email_handler(email: str) -> None: + # do something here to send an email + await send_welcome_mail(email) + + +@dataclass +class CreateUserDTO: + first_name: str + last_name: str + email: str + + +@post("/users") +async def create_user_handler(data: UserDTO, request: Request) -> None: + # do something here to create a new user + # e.g. insert the user into a database + await user_repository.insert(data) + + # assuming we have now inserted a user, we want to send a welcome email. + # To do this in a none-blocking fashion, we will emit an event to a listener, which will send the email, + # using a different async block than the one where we are returning a response. + request.app.emit("user_created", email=data.email) + + +app = Litestar(route_handlers=[create_user_handler], listeners=[send_welcome_email_handler]) diff --git a/docs/examples/events/listener_exception.py b/docs/examples/events/listener_exception.py new file mode 100644 index 0000000000..b7032068f9 --- /dev/null +++ b/docs/examples/events/listener_exception.py @@ -0,0 +1,25 @@ +from litestar import post +from litestar.events import listener + + +@listener("user_deleted") +async def send_farewell_email_handler(email: str) -> None: + await send_farewell_email(email) + + +@listener("user_deleted") +async def notify_customer_support(reason: str) -> None: + # do something here to send an email + await client.post("some-url", reason) + + +@dataclass +class DeleteUserDTO: + email: str + reason: str + + +@post("/users") +async def delete_user_handler(data: UserDTO, request: Request) -> None: + await user_repository.delete({"email": data.email}) + request.app.emit("user_deleted", email=data.email, reason="deleted") diff --git a/docs/examples/events/listener_no_exception.py b/docs/examples/events/listener_no_exception.py new file mode 100644 index 0000000000..4cf4bb6942 --- /dev/null +++ b/docs/examples/events/listener_no_exception.py @@ -0,0 +1,11 @@ +from litestar.events import listener + + +@listener("user_deleted") +async def send_farewell_email_handler(email: str, **kwargs) -> None: + await send_farewell_email(email) + + +@listener("user_deleted") +async def notify_customer_support(reason: str, **kwargs) -> None: + await client.post("some-url", reason) diff --git a/docs/examples/events/multiple_events.py b/docs/examples/events/multiple_events.py new file mode 100644 index 0000000000..87d13297c1 --- /dev/null +++ b/docs/examples/events/multiple_events.py @@ -0,0 +1,8 @@ +from litestar.events import listener + + +@listener("user_created", "password_changed") +async def send_email_handler(email: str, message: str) -> None: + # do something here to send an email + + await send_email(email, message) diff --git a/docs/examples/events/multiple_listeners.py b/docs/examples/events/multiple_listeners.py new file mode 100644 index 0000000000..219ccfbc5f --- /dev/null +++ b/docs/examples/events/multiple_listeners.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass + +from db import user_repository + +from litestar import Request, post +from litestar.events import listener +from utils.client import client +from utils.email import send_farewell_email + + +@listener("user_deleted") +async def send_farewell_email_handler(email: str, **kwargs) -> None: + # do something here to send an email + await send_farewell_email(email) + + +@listener("user_deleted") +async def notify_customer_support(reason: str, **kwargs) -> None: + # do something here to send an email + await client.post("some-url", reason) + + +@dataclass +class DeleteUserDTO: + email: str + reason: str + + +@post("/users") +async def delete_user_handler(data: UserDTO, request: Request) -> None: + await user_repository.delete({"email": email}) + request.app.emit("user_deleted", email=data.email, reason="deleted") diff --git a/docs/examples/execution_order.py b/docs/examples/execution_order.py new file mode 100644 index 0000000000..87cc85cdf7 --- /dev/null +++ b/docs/examples/execution_order.py @@ -0,0 +1,3 @@ +from litestar import Litestar + +app = Litestar(lifespan=[ctx_a, ctx_b], on_shutdown=[hook_a, hook_b]) diff --git a/docs/examples/guards/__init__.py b/docs/examples/guards/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/guards/enum.py b/docs/examples/guards/enum.py new file mode 100644 index 0000000000..98d03f72c9 --- /dev/null +++ b/docs/examples/guards/enum.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class UserRole(str, Enum): + CONSUMER = "consumer" + ADMIN = "admin" diff --git a/docs/examples/guards/guard.py b/docs/examples/guards/guard.py new file mode 100644 index 0000000000..fe53d80d51 --- /dev/null +++ b/docs/examples/guards/guard.py @@ -0,0 +1,32 @@ +from enum import Enum + +from pydantic import UUID4, BaseModel + +from litestar import post +from litestar.connection import ASGIConnection +from litestar.exceptions import NotAuthorizedException +from litestar.handlers.base import BaseRouteHandler + + +class UserRole(str, Enum): + CONSUMER = "consumer" + ADMIN = "admin" + + +class User(BaseModel): + id: UUID4 + role: UserRole + + @property + def is_admin(self) -> bool: + """Determines whether the user is an admin user""" + return self.role == UserRole.ADMIN + + +def admin_user_guard(connection: ASGIConnection, _: BaseRouteHandler) -> None: + if not connection.user.is_admin: + raise NotAuthorizedException() + + +@post(path="/user", guards=[admin_user_guard]) +def create_user(data: User) -> User: ... diff --git a/docs/examples/guards/guard_scope.py b/docs/examples/guards/guard_scope.py new file mode 100644 index 0000000000..4da8f6f17e --- /dev/null +++ b/docs/examples/guards/guard_scope.py @@ -0,0 +1,19 @@ +from litestar import Controller, Litestar, Router +from litestar.connection import ASGIConnection +from litestar.handlers.base import BaseRouteHandler + + +def my_guard(connection: ASGIConnection, handler: BaseRouteHandler) -> None: ... + + +# controller +class UserController(Controller): + path = "/user" + guards = [my_guard] + + +# router +admin_router = Router(path="admin", route_handlers=[UserController], guards=[my_guard]) + +# app +app = Litestar(route_handlers=[admin_router], guards=[my_guard]) diff --git a/docs/examples/guards/model.py b/docs/examples/guards/model.py new file mode 100644 index 0000000000..f671224427 --- /dev/null +++ b/docs/examples/guards/model.py @@ -0,0 +1,18 @@ +from enum import Enum + +from pydantic import UUID4, BaseModel + + +class UserRole(str, Enum): + CONSUMER = "consumer" + ADMIN = "admin" + + +class User(BaseModel): + id: UUID4 + role: UserRole + + @property + def is_admin(self) -> bool: + """Determines whether the user is an admin user""" + return self.role == UserRole.ADMIN diff --git a/docs/examples/guards/route_handler.py b/docs/examples/guards/route_handler.py new file mode 100644 index 0000000000..d89345915a --- /dev/null +++ b/docs/examples/guards/route_handler.py @@ -0,0 +1,18 @@ +from os import environ + +from litestar import get +from litestar.connection import ASGIConnection +from litestar.exceptions import NotAuthorizedException +from litestar.handlers.base import BaseRouteHandler + + +def secret_token_guard(connection: ASGIConnection, route_handler: BaseRouteHandler) -> None: + if ( + route_handler.opt.get("secret") + and not connection.headers.get("Secret-Header", "") == route_handler.opt["secret"] + ): + raise NotAuthorizedException() + + +@get(path="/secret", guards=[secret_token_guard], opt={"secret": environ.get("SECRET")}) +def secret_endpoint() -> None: ... diff --git a/docs/examples/htmx/__init__.py b/docs/examples/htmx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/htmx/htmx_client_redirect.py b/docs/examples/htmx/htmx_client_redirect.py new file mode 100644 index 0000000000..f519634a17 --- /dev/null +++ b/docs/examples/htmx/htmx_client_redirect.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.contrib.htmx.response import ClientRedirect + + +@get("/") +def handler() -> ClientRedirect: + return ClientRedirect(redirect_to="/contact-us") diff --git a/docs/examples/htmx/htmx_client_refresh.py b/docs/examples/htmx/htmx_client_refresh.py new file mode 100644 index 0000000000..f7027e1df2 --- /dev/null +++ b/docs/examples/htmx/htmx_client_refresh.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.contrib.htmx.response import ClientRefresh + + +@get("/") +def handler() -> ClientRefresh: + return ClientRefresh() diff --git a/docs/examples/htmx/htmx_push_url.py b/docs/examples/htmx/htmx_push_url.py new file mode 100644 index 0000000000..b52345c54f --- /dev/null +++ b/docs/examples/htmx/htmx_push_url.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.contrib.htmx.response import PushUrl + + +@get("/about") +def handler() -> PushUrl: + return PushUrl(content="Success!", push_url="/about") diff --git a/docs/examples/htmx/htmx_replace_url.py b/docs/examples/htmx/htmx_replace_url.py new file mode 100644 index 0000000000..23d06f10e3 --- /dev/null +++ b/docs/examples/htmx/htmx_replace_url.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.contrib.htmx.response import ReplaceUrl + + +@get("/contact-us") +def handler() -> ReplaceUrl: + return ReplaceUrl(content="Success!", replace_url="/contact-us") diff --git a/docs/examples/htmx/htmx_request.py b/docs/examples/htmx/htmx_request.py new file mode 100644 index 0000000000..fbe312ffd7 --- /dev/null +++ b/docs/examples/htmx/htmx_request.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from litestar import Litestar, get +from litestar.contrib.htmx.request import HTMXRequest +from litestar.contrib.htmx.response import HTMXTemplate +from litestar.contrib.jinja import JinjaTemplateEngine +from litestar.response import Template +from litestar.template.config import TemplateConfig + + +@get(path="/form") +def get_form(request: HTMXRequest) -> Template: + htmx = request.htmx # if true will return HTMXDetails class object + if htmx: + print(htmx.current_url) + # OR + if request.htmx: + print(request.htmx.current_url) + return HTMXTemplate(template_name="partial.html", context=context, push_url="/form") + + +app = Litestar( + route_handlers=[get_form], + debug=True, + request_class=HTMXRequest, + template_config=TemplateConfig( + directory=Path("litestar_htmx/templates"), + engine=JinjaTemplateEngine, + ), +) diff --git a/docs/examples/htmx/htmx_response.py b/docs/examples/htmx/htmx_response.py new file mode 100644 index 0000000000..080441d449 --- /dev/null +++ b/docs/examples/htmx/htmx_response.py @@ -0,0 +1,22 @@ +from litestar import get +from litestar.contrib.htmx.request import HTMXRequest +from litestar.contrib.htmx.response import HTMXTemplate +from litestar.response import Template + + +@get(path="/form") +def get_form( + request: HTMXRequest, +) -> Template: # Return type is Template and not HTMXTemplate. + return HTMXTemplate( + template_name="partial.html", + context=context, + # Optional parameters + push_url="/form", # update browser history + re_swap="outerHTML", # change swapping method + re_target="#new-target", # change target element + trigger_event="showMessage", # trigger event name + params={"alert": "Confirm your Choice."}, # parameter to pass to the event + after="receive", # when to trigger event, + # possible values 'receive', 'settle', and 'swap' + ) diff --git a/docs/examples/htmx/htmx_response_change_dom.py b/docs/examples/htmx/htmx_response_change_dom.py new file mode 100644 index 0000000000..f0b6ea6c04 --- /dev/null +++ b/docs/examples/htmx/htmx_response_change_dom.py @@ -0,0 +1,16 @@ +from litestar import get +from litestar.contrib.htmx.response import HXLocation + +@get("/about") +def handler() -> HXLocation: + ... + return HXLocation( + redirect_to="/contact-us", + # Optional parameters + source, # the source element of the request. + event, # an event that "triggered" the request. + target="#target", # element id to target to. + swap="outerHTML", # swapping method to use. + hx_headers={"attr": "val"}, # headers to pass to htmx. + values={"val": "one"}, + ) # values to submit with response. diff --git a/docs/examples/htmx/htmx_response_no_dom_change.py b/docs/examples/htmx/htmx_response_no_dom_change.py new file mode 100644 index 0000000000..a29403b4a0 --- /dev/null +++ b/docs/examples/htmx/htmx_response_no_dom_change.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.contrib.htmx.response import HXStopPolling + + +@get("/") +def handler() -> HXStopPolling: + return HXStopPolling() diff --git a/docs/examples/htmx/htmx_reswap.py b/docs/examples/htmx/htmx_reswap.py new file mode 100644 index 0000000000..d5c324bb7c --- /dev/null +++ b/docs/examples/htmx/htmx_reswap.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.contrib.htmx.response import Reswap + + +@get("/contact-us") +def handler() -> Reswap: + return Reswap(content="Success!", method="beforebegin") diff --git a/docs/examples/htmx/htmx_retarget.py b/docs/examples/htmx/htmx_retarget.py new file mode 100644 index 0000000000..8e4913795c --- /dev/null +++ b/docs/examples/htmx/htmx_retarget.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.contrib.htmx.response import Retarget + + +@get("/contact-us") +def handler() -> Retarget: + return Retarget(content="Success!", target="#new-target") diff --git a/docs/examples/htmx/htmx_trigger_event.py b/docs/examples/htmx/htmx_trigger_event.py new file mode 100644 index 0000000000..46a9125418 --- /dev/null +++ b/docs/examples/htmx/htmx_trigger_event.py @@ -0,0 +1,12 @@ +from litestar import get +from litestar.contrib.htmx.response import TriggerEvent + + +@get("/contact-us") +def handler() -> TriggerEvent: + return TriggerEvent( + content="Success!", + name="showMessage", + params={"attr": "value"}, + after="receive", # possible values 'receive', 'settle', and 'swap' + ) diff --git a/docs/examples/index/__init__.py b/docs/examples/index/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/index/expanded_example_1.py b/docs/examples/index/expanded_example_1.py new file mode 100644 index 0000000000..02eeae8361 --- /dev/null +++ b/docs/examples/index/expanded_example_1.py @@ -0,0 +1,7 @@ +from pydantic import UUID4, BaseModel + + +class User(BaseModel): + first_name: str + last_name: str + id: UUID4 diff --git a/docs/examples/index/expanded_example_2.py b/docs/examples/index/expanded_example_2.py new file mode 100644 index 0000000000..4b7a7ebf78 --- /dev/null +++ b/docs/examples/index/expanded_example_2.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from uuid import UUID + +from litestar.dto import DataclassDTO, DTOConfig + + +@dataclass +class User: + first_name: str + last_name: str + id: UUID + + +class PartialUserDTO(DataclassDTO[User]): + config = DTOConfig(exclude={"id"}, partial=True) diff --git a/docs/examples/index/expanded_example_3.py b/docs/examples/index/expanded_example_3.py new file mode 100644 index 0000000000..3473bb6060 --- /dev/null +++ b/docs/examples/index/expanded_example_3.py @@ -0,0 +1,29 @@ +from typing import List + +from my_app.models import PartialUserDTO, User +from pydantic import UUID4 + +from litestar import Controller, delete, get, patch, post, put +from litestar.dto import DTOData + + +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: ... diff --git a/docs/examples/index/expanded_example_4.py b/docs/examples/index/expanded_example_4.py new file mode 100644 index 0000000000..16db6b864d --- /dev/null +++ b/docs/examples/index/expanded_example_4.py @@ -0,0 +1,5 @@ +from my_app.controllers.user import UserController + +from litestar import Litestar + +app = Litestar(route_handlers=[UserController]) diff --git a/docs/examples/index/minimal_example.py b/docs/examples/index/minimal_example.py new file mode 100644 index 0000000000..d9d8ea4f89 --- /dev/null +++ b/docs/examples/index/minimal_example.py @@ -0,0 +1,14 @@ +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]) diff --git a/docs/examples/logging/__init__.py b/docs/examples/logging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/logging/logging_base.py b/docs/examples/logging/logging_base.py new file mode 100644 index 0000000000..ca1a8b2ad4 --- /dev/null +++ b/docs/examples/logging/logging_base.py @@ -0,0 +1,18 @@ +import logging + +from litestar import Litestar, Request, get +from litestar.logging import LoggingConfig + + +@get("/") +def my_router_handler(request: Request) -> None: + request.logger.info("inside a request") + return None + + +logging_config = LoggingConfig( + root={"level": logging.getLevelName(logging.INFO), "handlers": ["console"]}, + formatters={"standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}}, +) + +app = Litestar(route_handlers=[my_router_handler], logging_config=logging_config) diff --git a/docs/examples/logging/logging_litestar_logging_config.py b/docs/examples/logging/logging_litestar_logging_config.py new file mode 100644 index 0000000000..94c38cc1d4 --- /dev/null +++ b/docs/examples/logging/logging_litestar_logging_config.py @@ -0,0 +1,28 @@ +import logging + +from litestar import Litestar, Request, get + + +def get_logger(mod_name: str) -> logging.Logger: + """Return logger object.""" + format = "%(asctime)s: %(name)s: %(levelname)s: %(message)s" + logger = logging.getLogger(mod_name) + # Writes to stdout + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + ch.setFormatter(logging.Formatter(format)) + logger.addHandler(ch) + return logger + + +logger = get_logger(__name__) + + +@get("/") +def my_router_handler(request: Request) -> None: + logger.info("logger inside a request") + + +app = Litestar( + route_handlers=[my_router_handler], +) diff --git a/docs/examples/logging/logging_standard_library.py b/docs/examples/logging/logging_standard_library.py new file mode 100644 index 0000000000..4871bca939 --- /dev/null +++ b/docs/examples/logging/logging_standard_library.py @@ -0,0 +1,23 @@ +import logging + +from litestar import Litestar, Request, get +from litestar.logging import LoggingConfig + +logging_config = LoggingConfig( + root={"level": logging.getLevelName(logging.INFO), "handlers": ["console"]}, + formatters={"standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}}, +) + +logger = logging_config.configure()() + + +@get("/") +def my_router_handler(request: Request) -> None: + request.logger.info("inside a request") + logger.info("here too") + + +app = Litestar( + route_handlers=[my_router_handler], + logging_config=logging_config, +) diff --git a/docs/examples/logging/logging_structlog.py b/docs/examples/logging/logging_structlog.py new file mode 100644 index 0000000000..7554a1327d --- /dev/null +++ b/docs/examples/logging/logging_structlog.py @@ -0,0 +1,13 @@ +from litestar import Litestar, Request, get +from litestar.plugins.structlog import StructlogPlugin + + +@get("/") +def my_router_handler(request: Request) -> None: + request.logger.info("inside a request") + return None + + +structlog_plugin = StructlogPlugin() + +app = Litestar(route_handlers=[my_router_handler], plugins=[StructlogPlugin()]) diff --git a/docs/examples/metrics/__init__.py b/docs/examples/metrics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/metrics/opentelemetry.py b/docs/examples/metrics/opentelemetry.py new file mode 100644 index 0000000000..aa444121b8 --- /dev/null +++ b/docs/examples/metrics/opentelemetry.py @@ -0,0 +1,6 @@ +from litestar import Litestar +from litestar.contrib.opentelemetry import OpenTelemetryConfig + +open_telemetry_config = OpenTelemetryConfig() + +app = Litestar(middleware=[open_telemetry_config.middleware]) diff --git a/docs/examples/middleware/builtin/__init__.py b/docs/examples/middleware/builtin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/middleware/builtin/allowed_hosts.py b/docs/examples/middleware/builtin/allowed_hosts.py new file mode 100644 index 0000000000..14649002e3 --- /dev/null +++ b/docs/examples/middleware/builtin/allowed_hosts.py @@ -0,0 +1,7 @@ +from litestar import Litestar +from litestar.config.allowed_hosts import AllowedHostsConfig + +app = Litestar( + route_handlers=[...], + allowed_hosts=AllowedHostsConfig(allowed_hosts=["*.example.com", "www.wikipedia.org"]), +) diff --git a/docs/examples/middleware/builtin/brotli.py b/docs/examples/middleware/builtin/brotli.py new file mode 100644 index 0000000000..2b6fa4faf3 --- /dev/null +++ b/docs/examples/middleware/builtin/brotli.py @@ -0,0 +1,7 @@ +from litestar import Litestar +from litestar.config.compression import CompressionConfig + +app = Litestar( + route_handlers=[...], + compression_config=CompressionConfig(backend="brotli", brotli_gzip_fallback=True), +) diff --git a/docs/examples/middleware/builtin/cors.py b/docs/examples/middleware/builtin/cors.py new file mode 100644 index 0000000000..25a6b98cf7 --- /dev/null +++ b/docs/examples/middleware/builtin/cors.py @@ -0,0 +1,6 @@ +from litestar import Litestar +from litestar.config.cors import CORSConfig + +cors_config = CORSConfig(allow_origins=["https://www.example.com"]) + +app = Litestar(route_handlers=[...], cors_config=cors_config) diff --git a/docs/examples/middleware/builtin/csrf.py b/docs/examples/middleware/builtin/csrf.py new file mode 100644 index 0000000000..d0859a2e33 --- /dev/null +++ b/docs/examples/middleware/builtin/csrf.py @@ -0,0 +1,19 @@ +from litestar import Litestar, get, post +from litestar.config.csrf import CSRFConfig + + +@get() +async def get_resource() -> str: + # GET is one of the safe methods + return "some_resource" + + +@post("{id:int}") +async def create_resource(id: int) -> bool: + # POST is one of the unsafe methods + return True + + +csrf_config = CSRFConfig(secret="my-secret") + +app = Litestar([get_resource, create_resource], csrf_config=csrf_config) diff --git a/docs/examples/middleware/builtin/csrf_cookies.py b/docs/examples/middleware/builtin/csrf_cookies.py new file mode 100644 index 0000000000..e758c4bb54 --- /dev/null +++ b/docs/examples/middleware/builtin/csrf_cookies.py @@ -0,0 +1,3 @@ +from litestar.config.csrf import CSRFConfig + +csrf_config = CSRFConfig(secret="my-secret", cookie_name="some-cookie-name", header_name="some-header-name") diff --git a/docs/examples/middleware/builtin/csrf_exclude_route.py b/docs/examples/middleware/builtin/csrf_exclude_route.py new file mode 100644 index 0000000000..ca1bb06814 --- /dev/null +++ b/docs/examples/middleware/builtin/csrf_exclude_route.py @@ -0,0 +1,5 @@ +from litestar import post + + +@post("/post", exclude_from_csrf=True) +def handler() -> None: ... diff --git a/docs/examples/middleware/builtin/csrf_httpx.py b/docs/examples/middleware/builtin/csrf_httpx.py new file mode 100644 index 0000000000..24fa6351a7 --- /dev/null +++ b/docs/examples/middleware/builtin/csrf_httpx.py @@ -0,0 +1,21 @@ +import httpx + +with httpx.Client() as client: + get_response = client.get("http://localhost:8000/") + + # "csrftoken" is the default cookie name + csrf = get_response.cookies["csrftoken"] + + # "x-csrftoken" is the default header name + post_response_using_header = client.post("http://localhost:8000/1", headers={"x-csrftoken": csrf}) + assert post_response_using_header.status_code == 201 + + # "_csrf_token" is the default *non* configurable form-data key + post_response_using_form_data = client.post("http://localhost:8000/1", data={"_csrf_token": csrf}) + assert post_response_using_form_data.status_code == 201 + + # despite the header being passed, this request will fail as it does not have a cookie in its session + # note the usage of ``httpx.post`` instead of ``client.post`` + post_response_with_no_persisted_cookie = httpx.post("http://localhost:8000/1", headers={"x-csrftoken": csrf}) + assert post_response_with_no_persisted_cookie.status_code == 403 + assert "CSRF token verification failed" in post_response_with_no_persisted_cookie.text diff --git a/docs/examples/middleware/builtin/gzip.py b/docs/examples/middleware/builtin/gzip.py new file mode 100644 index 0000000000..85e3143bdd --- /dev/null +++ b/docs/examples/middleware/builtin/gzip.py @@ -0,0 +1,7 @@ +from litestar import Litestar +from litestar.config.compression import CompressionConfig + +app = Litestar( + route_handlers=[...], + compression_config=CompressionConfig(backend="gzip", gzip_compress_level=9), +) diff --git a/docs/examples/middleware/builtin/loggin_middleware_obfuscating_output.py b/docs/examples/middleware/builtin/loggin_middleware_obfuscating_output.py new file mode 100644 index 0000000000..ccfa1582c7 --- /dev/null +++ b/docs/examples/middleware/builtin/loggin_middleware_obfuscating_output.py @@ -0,0 +1,8 @@ +from litestar.middleware.logging import LoggingMiddlewareConfig + +logging_middleware_config = LoggingMiddlewareConfig( + request_cookies_to_obfuscate={"my-custom-session-key"}, + response_cookies_to_obfuscate={"my-custom-session-key"}, + request_headers_to_obfuscate={"my-custom-header"}, + response_headers_to_obfuscate={"my-custom-header"}, +) diff --git a/docs/examples/middleware/creation/__init__.py b/docs/examples/middleware/creation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/middleware/creation/create_basic_middleware.py b/docs/examples/middleware/creation/create_basic_middleware.py new file mode 100644 index 0000000000..a39dcd00e5 --- /dev/null +++ b/docs/examples/middleware/creation/create_basic_middleware.py @@ -0,0 +1,9 @@ +from litestar.types import ASGIApp, Receive, Scope, Send + + +def middleware_factory(app: ASGIApp) -> ASGIApp: + async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: + # do something here + await app(scope, receive, send) + + return my_middleware diff --git a/docs/examples/middleware/creation/create_middleware_using_middleware_protocol_1.py b/docs/examples/middleware/creation/create_middleware_using_middleware_protocol_1.py new file mode 100644 index 0000000000..40ffa42e4f --- /dev/null +++ b/docs/examples/middleware/creation/create_middleware_using_middleware_protocol_1.py @@ -0,0 +1,9 @@ +from typing import Any, Protocol + +from litestar.types import ASGIApp, Receive, Scope, Send + + +class MiddlewareProtocol(Protocol): + def __init__(self, app: ASGIApp, **kwargs: Any) -> None: ... + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ... diff --git a/docs/examples/middleware/creation/create_middleware_using_middleware_protocol_2.py b/docs/examples/middleware/creation/create_middleware_using_middleware_protocol_2.py new file mode 100644 index 0000000000..9c1e69f938 --- /dev/null +++ b/docs/examples/middleware/creation/create_middleware_using_middleware_protocol_2.py @@ -0,0 +1,19 @@ +import logging + +from litestar import Request +from litestar.middleware.base import MiddlewareProtocol +from litestar.types import ASGIApp, Receive, Scope, Send + +logger = logging.getLogger(__name__) + + +class MyRequestLoggingMiddleware(MiddlewareProtocol): + def __init__(self, app: ASGIApp) -> None: + super().__init__(app) + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "http": + request = Request(scope) + logger.info("%s - %s" % request.method, request.url) + await self.app(scope, receive, send) diff --git a/docs/examples/middleware/creation/inheriting_abstract_middleware.py b/docs/examples/middleware/creation/inheriting_abstract_middleware.py new file mode 100644 index 0000000000..e8f63c5d0d --- /dev/null +++ b/docs/examples/middleware/creation/inheriting_abstract_middleware.py @@ -0,0 +1,27 @@ +from time import time +from typing import TYPE_CHECKING + +from litestar.datastructures import MutableScopeHeaders +from litestar.enums import ScopeType +from litestar.middleware import AbstractMiddleware + +if TYPE_CHECKING: + from litestar.types import Message, Receive, Scope, Send + + +class MyMiddleware(AbstractMiddleware): + scopes = {ScopeType.HTTP} + exclude = ["first_path", "second_path"] + exclude_opt_key = "exclude_from_middleware" + + async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: + start_time = time() + + async def send_wrapper(message: "Message") -> None: + if message["type"] == "http.response.start": + process_time = time() - start_time + headers = MutableScopeHeaders.from_message(message=message) + headers["X-Process-Time"] = str(process_time) + await send(message) + + await self.app(scope, receive, send_wrapper) diff --git a/docs/examples/middleware/creation/responding_using_middleware_protocol.py b/docs/examples/middleware/creation/responding_using_middleware_protocol.py new file mode 100644 index 0000000000..c5a978b9a7 --- /dev/null +++ b/docs/examples/middleware/creation/responding_using_middleware_protocol.py @@ -0,0 +1,17 @@ +from litestar import Request +from litestar.middleware.base import MiddlewareProtocol +from litestar.response.redirect import ASGIRedirectResponse +from litestar.types import ASGIApp, Receive, Scope, Send + + +class RedirectMiddleware(MiddlewareProtocol): + def __init__(self, app: ASGIApp) -> None: + super().__init__(app) + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if Request(scope).session is None: + response = ASGIRedirectResponse(path="/login") + await response(scope, receive, send) + else: + await self.app(scope, receive, send) diff --git a/docs/examples/middleware/creation/responding_using_middleware_protocol_asgi.py b/docs/examples/middleware/creation/responding_using_middleware_protocol_asgi.py new file mode 100644 index 0000000000..39f0ad0e8c --- /dev/null +++ b/docs/examples/middleware/creation/responding_using_middleware_protocol_asgi.py @@ -0,0 +1,26 @@ +import time + +from litestar.datastructures import MutableScopeHeaders +from litestar.middleware.base import MiddlewareProtocol +from litestar.types import ASGIApp, Message, Receive, Scope, Send + + +class ProcessTimeHeader(MiddlewareProtocol): + def __init__(self, app: ASGIApp) -> None: + super().__init__(app) + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "http": + start_time = time.time() + + async def send_wrapper(message: Message) -> None: + if message["type"] == "http.response.start": + process_time = time.time() - start_time + headers = MutableScopeHeaders.from_message(message=message) + headers["X-Process-Time"] = str(process_time) + await send(message) + + await self.app(scope, receive, send_wrapper) + else: + await self.app(scope, receive, send) diff --git a/docs/examples/middleware/creation/using_define_middleware.py b/docs/examples/middleware/creation/using_define_middleware.py new file mode 100644 index 0000000000..c28949d29b --- /dev/null +++ b/docs/examples/middleware/creation/using_define_middleware.py @@ -0,0 +1,17 @@ +from litestar import Litestar +from litestar.middleware import DefineMiddleware +from litestar.types import ASGIApp, Receive, Scope, Send + + +def middleware_factory(my_arg: int, *, app: ASGIApp, my_kwarg: str) -> ASGIApp: + async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: + # here we can use my_arg and my_kwarg for some purpose + await app(scope, receive, send) + + return my_middleware + + +app = Litestar( + route_handlers=[...], + middleware=[DefineMiddleware(middleware_factory, 1, my_kwarg="abc")], +) diff --git a/docs/examples/middleware/using_middleware_1.py b/docs/examples/middleware/using_middleware_1.py new file mode 100644 index 0000000000..a39dcd00e5 --- /dev/null +++ b/docs/examples/middleware/using_middleware_1.py @@ -0,0 +1,9 @@ +from litestar.types import ASGIApp, Receive, Scope, Send + + +def middleware_factory(app: ASGIApp) -> ASGIApp: + async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: + # do something here + await app(scope, receive, send) + + return my_middleware diff --git a/docs/examples/middleware/using_middleware_2.py b/docs/examples/middleware/using_middleware_2.py new file mode 100644 index 0000000000..b47bbef1c9 --- /dev/null +++ b/docs/examples/middleware/using_middleware_2.py @@ -0,0 +1,13 @@ +from litestar import Litestar +from litestar.types import ASGIApp, Receive, Scope, Send + + +def middleware_factory(app: ASGIApp) -> ASGIApp: + async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: + # do something here + await app(scope, receive, send) + + return my_middleware + + +app = Litestar(route_handlers=[...], middleware=[middleware_factory]) diff --git a/docs/examples/migrations/__init__.py b/docs/examples/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/migrations/fastapi/authentication_fastapi.py b/docs/examples/migrations/fastapi/authentication_fastapi.py new file mode 100644 index 0000000000..a0a53a5e7f --- /dev/null +++ b/docs/examples/migrations/fastapi/authentication_fastapi.py @@ -0,0 +1,11 @@ +from fastapi import Depends, FastAPI, Request + + +async def authenticate(request: Request) -> None: ... + + +app = FastAPI() + + +@app.get("/", dependencies=[Depends(authenticate)]) +async def index() -> dict[str, str]: ... diff --git a/docs/examples/migrations/fastapi/authentication_litestar.py b/docs/examples/migrations/fastapi/authentication_litestar.py new file mode 100644 index 0000000000..9ef1c38077 --- /dev/null +++ b/docs/examples/migrations/fastapi/authentication_litestar.py @@ -0,0 +1,8 @@ +from litestar import ASGIConnection, BaseRouteHandler, get + + +async def authenticate(connection: ASGIConnection, route_handler: BaseRouteHandler) -> None: ... + + +@get("/", guards=[authenticate]) +async def index() -> dict[str, str]: ... diff --git a/docs/examples/migrations/fastapi/di_fastapi.py b/docs/examples/migrations/fastapi/di_fastapi.py new file mode 100644 index 0000000000..912654cd43 --- /dev/null +++ b/docs/examples/migrations/fastapi/di_fastapi.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, Depends, FastAPI + + +async def route_dependency() -> bool: ... + + +async def nested_dependency() -> str: ... + + +async def router_dependency() -> int: ... + + +async def app_dependency(data: str = Depends(nested_dependency)) -> int: ... + + +router = APIRouter(dependencies=[Depends(router_dependency)]) +app = FastAPI(dependencies=[Depends(nested_dependency)]) +app.include_router(router) + + +@app.get("/") +async def handler( + val_route: bool = Depends(route_dependency), + val_router: int = Depends(router_dependency), + val_nested: str = Depends(nested_dependency), + val_app: int = Depends(app_dependency), +) -> None: ... diff --git a/docs/examples/migrations/fastapi/di_litestar.py b/docs/examples/migrations/fastapi/di_litestar.py new file mode 100644 index 0000000000..0f43b0b0c8 --- /dev/null +++ b/docs/examples/migrations/fastapi/di_litestar.py @@ -0,0 +1,27 @@ +from litestar import Litestar, Provide, Router, get + + +async def route_dependency() -> bool: ... + + +async def nested_dependency() -> str: ... + + +async def router_dependency() -> int: ... + + +async def app_dependency(nested: str) -> int: ... + + +@get("/", dependencies={"val_route": Provide(route_dependency)}) +async def handler(val_route: bool, val_router: int, val_nested: str, val_app: int) -> None: ... + + +router = Router(dependencies={"val_router": Provide(router_dependency)}) +app = Litestar( + route_handlers=[handler], + dependencies={ + "val_app": Provide(app_dependency), + "val_nested": Provide(nested_dependency), + }, +) diff --git a/docs/examples/migrations/fastapi/routing_fastapi.py b/docs/examples/migrations/fastapi/routing_fastapi.py new file mode 100644 index 0000000000..dc417ae3ff --- /dev/null +++ b/docs/examples/migrations/fastapi/routing_fastapi.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def index() -> dict[str, str]: ... diff --git a/docs/examples/migrations/fastapi/routing_litestar.py b/docs/examples/migrations/fastapi/routing_litestar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/migrations/fastapi/routing_starlette.py b/docs/examples/migrations/fastapi/routing_starlette.py new file mode 100644 index 0000000000..ae835e34d3 --- /dev/null +++ b/docs/examples/migrations/fastapi/routing_starlette.py @@ -0,0 +1,10 @@ +from starlette.applications import Starlette +from starlette.routing import Route + + +async def index(request): ... + + +routes = [Route("/", endpoint=index)] + +app = Starlette(routes=routes) diff --git a/docs/examples/migrations/flask/cookies_headers_flask.py b/docs/examples/migrations/flask/cookies_headers_flask.py new file mode 100644 index 0000000000..2d47e75c27 --- /dev/null +++ b/docs/examples/migrations/flask/cookies_headers_flask.py @@ -0,0 +1,11 @@ +from flask import Flask, make_response + +app = Flask(__name__) + + +@app.get("/") +def index(): + response = make_response("hello") + response.set_cookie("my-cookie", "cookie-value") + response.headers["my-header"] = "header-value" + return response diff --git a/docs/examples/migrations/flask/cookies_headers_litestar.py b/docs/examples/migrations/flask/cookies_headers_litestar.py new file mode 100644 index 0000000000..524a2210b8 --- /dev/null +++ b/docs/examples/migrations/flask/cookies_headers_litestar.py @@ -0,0 +1,22 @@ +from litestar import Response, get +from litestar.datastructures import Cookie, ResponseHeader + + +@get( + "/static", + response_headers={"my-header": ResponseHeader(value="header-value")}, + response_cookies=[Cookie("my-cookie", "cookie-value")], +) +def static() -> str: + # you can set headers and cookies when defining handlers + ... + + +@get("/dynamic") +def dynamic() -> Response[str]: + # or dynamically, by returning an instance of Response + return Response( + "hello", + headers={"my-header": "header-value"}, + cookies=[Cookie("my-cookie", "cookie-value")], + ) diff --git a/docs/examples/migrations/flask/error_handling_flask.py b/docs/examples/migrations/flask/error_handling_flask.py new file mode 100644 index 0000000000..27e4f7ce18 --- /dev/null +++ b/docs/examples/migrations/flask/error_handling_flask.py @@ -0,0 +1,8 @@ +from flask import Flask +from werkzeug.exceptions import HTTPException + +app = Flask(__name__) + + +@app.errorhandler(HTTPException) +def handle_exception(e): ... diff --git a/docs/examples/migrations/flask/error_handling_litestar.py b/docs/examples/migrations/flask/error_handling_litestar.py new file mode 100644 index 0000000000..11de426741 --- /dev/null +++ b/docs/examples/migrations/flask/error_handling_litestar.py @@ -0,0 +1,8 @@ +from litestar import Litestar, Request, Response +from litestar.exceptions import HTTPException + + +def handle_exception(request: Request, exception: Exception) -> Response: ... + + +app = Litestar([], exception_handlers={HTTPException: handle_exception}) diff --git a/docs/examples/migrations/flask/errors_flask.py b/docs/examples/migrations/flask/errors_flask.py new file mode 100644 index 0000000000..8e3f3eb275 --- /dev/null +++ b/docs/examples/migrations/flask/errors_flask.py @@ -0,0 +1,8 @@ +from flask import Flask, abort + +app = Flask(__name__) + + +@app.get("/") +def index(): + abort(400, "this did not work") diff --git a/docs/examples/migrations/flask/errors_litestar.py b/docs/examples/migrations/flask/errors_litestar.py new file mode 100644 index 0000000000..8b14b5db80 --- /dev/null +++ b/docs/examples/migrations/flask/errors_litestar.py @@ -0,0 +1,10 @@ +from litestar import Litestar, get +from litestar.exceptions import HTTPException + + +@get("/") +def index() -> None: + raise HTTPException(status_code=400, detail="this did not work") + + +app = Litestar([index]) diff --git a/docs/examples/migrations/flask/path_parameters_flask.py b/docs/examples/migrations/flask/path_parameters_flask.py new file mode 100644 index 0000000000..47131ab01f --- /dev/null +++ b/docs/examples/migrations/flask/path_parameters_flask.py @@ -0,0 +1,18 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/user/") +def show_user_profile(username): + return f"User {username}" + + +@app.route("/post/") +def show_post(post_id): + return f"Post {post_id}" + + +@app.route("/path/") +def show_subpath(subpath): + return f"Subpath {subpath}" diff --git a/docs/examples/migrations/flask/path_parameters_litestar.py b/docs/examples/migrations/flask/path_parameters_litestar.py new file mode 100644 index 0000000000..d4d67399f7 --- /dev/null +++ b/docs/examples/migrations/flask/path_parameters_litestar.py @@ -0,0 +1,21 @@ +from pathlib import Path + +from litestar import Litestar, get + + +@get("/user/{username:str}") +def show_user_profile(username: str) -> str: + return f"User {username}" + + +@get("/post/{post_id:int}") +def show_post(post_id: int) -> str: + return f"Post {post_id}" + + +@get("/path/{subpath:path}") +def show_subpath(subpath: Path) -> str: + return f"Subpath {subpath}" + + +app = Litestar([show_user_profile, show_post, show_subpath]) diff --git a/docs/examples/migrations/flask/redirects_flask.py b/docs/examples/migrations/flask/redirects_flask.py new file mode 100644 index 0000000000..6fd4c3b615 --- /dev/null +++ b/docs/examples/migrations/flask/redirects_flask.py @@ -0,0 +1,13 @@ +from flask import Flask, redirect, url_for + +app = Flask(__name__) + + +@app.get("/") +def index(): + return "hello" + + +@app.get("/hello") +def hello(): + return redirect(url_for("index")) diff --git a/docs/examples/migrations/flask/redirects_litestar.py b/docs/examples/migrations/flask/redirects_litestar.py new file mode 100644 index 0000000000..92130776e5 --- /dev/null +++ b/docs/examples/migrations/flask/redirects_litestar.py @@ -0,0 +1,15 @@ +from litestar import Litestar, get +from litestar.response import Redirect + + +@get("/") +def index() -> str: + return "hello" + + +@get("/hello") +def hello() -> Redirect: + return Redirect(path="index") + + +app = Litestar([index, hello]) diff --git a/docs/examples/migrations/flask/request_object_flask.py b/docs/examples/migrations/flask/request_object_flask.py new file mode 100644 index 0000000000..c01b630195 --- /dev/null +++ b/docs/examples/migrations/flask/request_object_flask.py @@ -0,0 +1,8 @@ +from flask import Flask, request + +app = Flask(__name__) + + +@app.get("/") +def index(): + print(request.method) diff --git a/docs/examples/migrations/flask/request_object_litestar.py b/docs/examples/migrations/flask/request_object_litestar.py new file mode 100644 index 0000000000..b4fc0bab47 --- /dev/null +++ b/docs/examples/migrations/flask/request_object_litestar.py @@ -0,0 +1,6 @@ +from litestar import Request, get + + +@get("/") +def index(request: Request) -> None: + print(request.method) diff --git a/docs/examples/migrations/flask/routing_flask.py b/docs/examples/migrations/flask/routing_flask.py new file mode 100644 index 0000000000..59141fb919 --- /dev/null +++ b/docs/examples/migrations/flask/routing_flask.py @@ -0,0 +1,13 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def index(): + return "Index Page" + + +@app.route("/hello") +def hello(): + return "Hello, World" diff --git a/docs/examples/migrations/flask/routing_litestar.py b/docs/examples/migrations/flask/routing_litestar.py new file mode 100644 index 0000000000..b3704a22cd --- /dev/null +++ b/docs/examples/migrations/flask/routing_litestar.py @@ -0,0 +1,14 @@ +from litestar import Litestar, get + + +@get("/") +def index() -> str: + return "Index Page" + + +@get("/hello") +def hello() -> str: + return "Hello, World" + + +app = Litestar([index, hello]) diff --git a/docs/examples/migrations/flask/serialization_flask.py b/docs/examples/migrations/flask/serialization_flask.py new file mode 100644 index 0000000000..93f32dcf6c --- /dev/null +++ b/docs/examples/migrations/flask/serialization_flask.py @@ -0,0 +1,18 @@ +from flask import Flask, Response + +app = Flask(__name__) + + +@app.get("/json") +def get_json(): + return {"hello": "world"} + + +@app.get("/text") +def get_text(): + return "hello, world!" + + +@app.get("/html") +def get_html(): + return Response("hello, world", mimetype="text/html") diff --git a/docs/examples/migrations/flask/serialization_litestar.py b/docs/examples/migrations/flask/serialization_litestar.py new file mode 100644 index 0000000000..9e6b3a0177 --- /dev/null +++ b/docs/examples/migrations/flask/serialization_litestar.py @@ -0,0 +1,19 @@ +from litestar import Litestar, MediaType, get + + +@get("/json") +def get_json() -> dict[str, str]: + return {"hello": "world"} + + +@get("/text", media_type=MediaType.TEXT) +def get_text() -> str: + return "hello, world" + + +@get("/html", media_type=MediaType.HTML) +def get_html() -> str: + return "hello, world" + + +app = Litestar([get_json, get_text, get_html]) diff --git a/docs/examples/migrations/flask/static_files.py b/docs/examples/migrations/flask/static_files.py new file mode 100644 index 0000000000..a00b5ef6c2 --- /dev/null +++ b/docs/examples/migrations/flask/static_files.py @@ -0,0 +1,4 @@ +from litestar import Litestar +from litestar.static_files import StaticFilesConfig + +app = Litestar([], static_files_config=[StaticFilesConfig(path="/static", directories=["static"])]) diff --git a/docs/examples/migrations/flask/status_codes_flask.py b/docs/examples/migrations/flask/status_codes_flask.py new file mode 100644 index 0000000000..3d30d6e50a --- /dev/null +++ b/docs/examples/migrations/flask/status_codes_flask.py @@ -0,0 +1,8 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.get("/") +def index(): + return "not found", 404 diff --git a/docs/examples/migrations/flask/status_codes_litestar.py b/docs/examples/migrations/flask/status_codes_litestar.py new file mode 100644 index 0000000000..f21e5a6860 --- /dev/null +++ b/docs/examples/migrations/flask/status_codes_litestar.py @@ -0,0 +1,14 @@ +from litestar import Litestar, Response, get + + +@get("/static", status_code=404) +def static_status() -> str: + return "not found" + + +@get("/dynamic") +def dynamic_status() -> Response[str]: + return Response("not found", status_code=404) + + +app = Litestar([static_status, dynamic_status]) diff --git a/docs/examples/migrations/flask/templates_flask.py b/docs/examples/migrations/flask/templates_flask.py new file mode 100644 index 0000000000..c89aa3c052 --- /dev/null +++ b/docs/examples/migrations/flask/templates_flask.py @@ -0,0 +1,8 @@ +from flask import Flask, render_template + +app = Flask(__name__) + + +@app.route("/hello/") +def hello(name): + return render_template("hello.html", name=name) diff --git a/docs/examples/migrations/flask/templates_litestar.py b/docs/examples/migrations/flask/templates_litestar.py new file mode 100644 index 0000000000..05f7d3aa1a --- /dev/null +++ b/docs/examples/migrations/flask/templates_litestar.py @@ -0,0 +1,15 @@ +from litestar import Litestar, get +from litestar.contrib.jinja import JinjaTemplateEngine +from litestar.response import Template +from litestar.template.config import TemplateConfig + + +@get("/hello/{name:str}") +def hello(name: str) -> Template: + return Template(response_name="hello.html", context={"name": name}) + + +app = Litestar( + [hello], + template_config=TemplateConfig(directory="templates", engine=JinjaTemplateEngine), +) diff --git a/docs/examples/openapi/accessing_schema_in_code.py b/docs/examples/openapi/accessing_schema_in_code.py new file mode 100644 index 0000000000..3cff111840 --- /dev/null +++ b/docs/examples/openapi/accessing_schema_in_code.py @@ -0,0 +1,7 @@ +from litestar import Request, get + + +@get(path="/") +def my_route_handler(request: Request) -> dict: + schema = request.app.openapi_schema + return schema.dict() diff --git a/docs/examples/openapi/configure_schema_generation_on_route_1.py b/docs/examples/openapi/configure_schema_generation_on_route_1.py new file mode 100644 index 0000000000..da6e3ec629 --- /dev/null +++ b/docs/examples/openapi/configure_schema_generation_on_route_1.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get(path="/some-path", include_in_schema=False) +def my_route_handler() -> None: ... diff --git a/docs/examples/openapi/configure_schema_generation_on_route_2.py b/docs/examples/openapi/configure_schema_generation_on_route_2.py new file mode 100644 index 0000000000..a238514e76 --- /dev/null +++ b/docs/examples/openapi/configure_schema_generation_on_route_2.py @@ -0,0 +1,22 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + +from litestar import get +from litestar.openapi.datastructures import ResponseSpec + + +class Item(BaseModel): ... + + +class ItemNotFound(BaseModel): + was_removed: bool + removed_at: Optional[datetime] + + +@get( + path="/items/{pk:int}", + responses={404: ResponseSpec(data_container=ItemNotFound, description="Item was removed or not found")}, +) +def retrieve_item(pk: int) -> Item: ... diff --git a/docs/examples/openapi/configure_schema_generation_on_route_3.py b/docs/examples/openapi/configure_schema_generation_on_route_3.py new file mode 100644 index 0000000000..2eba7866e6 --- /dev/null +++ b/docs/examples/openapi/configure_schema_generation_on_route_3.py @@ -0,0 +1,38 @@ +from litestar import Litestar, get +from litestar.openapi import OpenAPIConfig +from litestar.openapi.spec import Components, SecurityScheme, Tag + + +@get( + "/public", + tags=["public"], + security=[{}], # this endpoint is marked as having optional security +) +def public_path_handler() -> dict[str, str]: + return {"hello": "world"} + + +@get("/other", tags=["internal"], security=[{"apiKey": []}]) +def internal_path_handler() -> None: ... + + +app = Litestar( + route_handlers=[public_path_handler, internal_path_handler], + openapi_config=OpenAPIConfig( + title="my api", + version="1.0.0", + tags=[ + Tag(name="public", description="This endpoint is for external users"), + Tag(name="internal", description="This endpoint is for internal users"), + ], + security=[{"BearerToken": []}], + components=Components( + security_schemes={ + "BearerToken": SecurityScheme( + type="http", + scheme="bearer", + ) + }, + ), + ), +) diff --git a/docs/examples/openapi/disable_schema_generation.py b/docs/examples/openapi/disable_schema_generation.py new file mode 100644 index 0000000000..5fee165c7b --- /dev/null +++ b/docs/examples/openapi/disable_schema_generation.py @@ -0,0 +1,3 @@ +from litestar import Litestar + +app = Litestar(route_handlers=[...], openapi_config=None) diff --git a/docs/examples/openapi/schema_generation.py b/docs/examples/openapi/schema_generation.py new file mode 100644 index 0000000000..a7508c87dd --- /dev/null +++ b/docs/examples/openapi/schema_generation.py @@ -0,0 +1,4 @@ +from litestar import Litestar +from litestar.openapi import OpenAPIConfig + +app = Litestar(route_handlers=[...], openapi_config=OpenAPIConfig(title="My API", version="1.0.0")) diff --git a/docs/examples/responses/class_asgi_application.py b/docs/examples/responses/class_asgi_application.py new file mode 100644 index 0000000000..1af730e5ac --- /dev/null +++ b/docs/examples/responses/class_asgi_application.py @@ -0,0 +1,7 @@ +from litestar.types import Receive, Scope, Send + + +class ASGIApp: + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + # do something here + ... diff --git a/docs/examples/responses/file_response.py b/docs/examples/responses/file_response.py new file mode 100644 index 0000000000..6b5321b4e2 --- /dev/null +++ b/docs/examples/responses/file_response.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from litestar import get +from litestar.response import File + + +@get(path="/file-download") +def handle_file_download() -> File: + return File( + path=Path(Path(__file__).resolve().parent, "report").with_suffix(".pdf"), + filename="report.pdf", + ) diff --git a/docs/examples/responses/file_response_2.py b/docs/examples/responses/file_response_2.py new file mode 100644 index 0000000000..7faec2780a --- /dev/null +++ b/docs/examples/responses/file_response_2.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from litestar import get +from litestar.response import File + + +@get(path="/file-download", media_type="application/pdf") +def handle_file_download() -> File: + return File( + path=Path(Path(__file__).resolve().parent, "report").with_suffix(".pdf"), + filename="report.pdf", + ) diff --git a/docs/examples/responses/function_asgi_application.py b/docs/examples/responses/function_asgi_application.py new file mode 100644 index 0000000000..5e2d642ba1 --- /dev/null +++ b/docs/examples/responses/function_asgi_application.py @@ -0,0 +1,6 @@ +from litestar.types import Receive, Scope, Send + + +async def my_asgi_app_function(scope: Scope, receive: Receive, send: Send) -> None: + # do something here + ... diff --git a/docs/examples/responses/method_asgi_application.py b/docs/examples/responses/method_asgi_application.py new file mode 100644 index 0000000000..6f28259d74 --- /dev/null +++ b/docs/examples/responses/method_asgi_application.py @@ -0,0 +1,7 @@ +from litestar.types import Receive, Scope, Send + + +class MyClass: + async def my_asgi_app_method(self, scope: Scope, receive: Receive, send: Send) -> None: + # do something here + ... diff --git a/docs/examples/responses/redirect_response.py b/docs/examples/responses/redirect_response.py new file mode 100644 index 0000000000..33c9786c34 --- /dev/null +++ b/docs/examples/responses/redirect_response.py @@ -0,0 +1,11 @@ +from litestar import get +from litestar.response import Redirect +from litestar.status_codes import HTTP_302_FOUND + + +@get(path="/some-path", status_code=HTTP_302_FOUND) +def redirect() -> Redirect: + # do some stuff here + # ... + # finally return redirect + return Redirect(path="/other-path") diff --git a/docs/examples/responses/response_cookies_dict.py b/docs/examples/responses/response_cookies_dict.py new file mode 100644 index 0000000000..2ce14f7995 --- /dev/null +++ b/docs/examples/responses/response_cookies_dict.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get(response_cookies={"my-cookie": "cookie-value"}) +async def handler() -> str: ... diff --git a/docs/examples/responses/response_headers_attributes.py b/docs/examples/responses/response_headers_attributes.py new file mode 100644 index 0000000000..97e2d9ca4a --- /dev/null +++ b/docs/examples/responses/response_headers_attributes.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get(response_headers={"my-header": "header-value"}) +async def handler() -> str: ... diff --git a/docs/examples/responses/response_html.py b/docs/examples/responses/response_html.py new file mode 100644 index 0000000000..508c299df1 --- /dev/null +++ b/docs/examples/responses/response_html.py @@ -0,0 +1,14 @@ +from litestar import MediaType, get + + +@get(path="/page", media_type=MediaType.HTML) +def health_check() -> str: + return """ + + +
+ Hello World! +
+ + + """ diff --git a/docs/examples/responses/response_media_type.py b/docs/examples/responses/response_media_type.py new file mode 100644 index 0000000000..574f2aa355 --- /dev/null +++ b/docs/examples/responses/response_media_type.py @@ -0,0 +1,6 @@ +from litestar import MediaType, get + + +@get("/resources", media_type=MediaType.TEXT) +def retrieve_resource() -> str: + return "The rumbling rabbit ran around the rock" diff --git a/docs/examples/responses/response_messagepack.py b/docs/examples/responses/response_messagepack.py new file mode 100644 index 0000000000..5212419168 --- /dev/null +++ b/docs/examples/responses/response_messagepack.py @@ -0,0 +1,8 @@ +from typing import Dict + +from litestar import MediaType, get + + +@get(path="/health-check", media_type=MediaType.MESSAGEPACK) +def health_check() -> Dict[str, str]: + return {"hello": "world"} diff --git a/docs/examples/responses/response_plaintext.py b/docs/examples/responses/response_plaintext.py new file mode 100644 index 0000000000..f214343f03 --- /dev/null +++ b/docs/examples/responses/response_plaintext.py @@ -0,0 +1,6 @@ +from litestar import MediaType, get + + +@get(path="/health-check", media_type=MediaType.TEXT) +def health_check() -> str: + return "healthy" diff --git a/docs/examples/responses/response_simple.py b/docs/examples/responses/response_simple.py new file mode 100644 index 0000000000..7d20add70b --- /dev/null +++ b/docs/examples/responses/response_simple.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + +from litestar import get + + +class Resource(BaseModel): + id: int + name: str + + +@get("/resources") +def retrieve_resource() -> Resource: + return Resource(id=1, name="my resource") diff --git a/docs/examples/responses/response_status_codes.py b/docs/examples/responses/response_status_codes.py new file mode 100644 index 0000000000..e1e75c8b16 --- /dev/null +++ b/docs/examples/responses/response_status_codes.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + +from litestar import get +from litestar.status_codes import HTTP_202_ACCEPTED + + +class Resource(BaseModel): + id: int + name: str + + +@get("/resources", status_code=HTTP_202_ACCEPTED) +def retrieve_resource() -> Resource: + return Resource(id=1, name="my resource") diff --git a/docs/examples/responses/returning_asgi_applications.py b/docs/examples/responses/returning_asgi_applications.py new file mode 100644 index 0000000000..bea94f6f00 --- /dev/null +++ b/docs/examples/responses/returning_asgi_applications.py @@ -0,0 +1,9 @@ +from litestar import get +from litestar.types import ASGIApp, Receive, Scope, Send + + +@get("/") +def handler() -> ASGIApp: + async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: ... + + return my_asgi_app diff --git a/docs/examples/responses/returning_responses_from_third_party.py b/docs/examples/responses/returning_responses_from_third_party.py new file mode 100644 index 0000000000..da44153dfc --- /dev/null +++ b/docs/examples/responses/returning_responses_from_third_party.py @@ -0,0 +1,9 @@ +from starlette.responses import JSONResponse + +from litestar import get +from litestar.types import ASGIApp + + +@get("/") +def handler() -> ASGIApp: + return JSONResponse(content={"hello": "world"}) # type: ignore diff --git a/docs/examples/responses/template_responses.py b/docs/examples/responses/template_responses.py new file mode 100644 index 0000000000..d83c09f96a --- /dev/null +++ b/docs/examples/responses/template_responses.py @@ -0,0 +1,7 @@ +from litestar import Request, get +from litestar.response import Template + + +@get(path="/info") +def info(request: Request) -> Template: + return Template(template_name="info.html", context={"user": request.user}) diff --git a/docs/examples/routing/declaring_multiple_paths.py b/docs/examples/routing/declaring_multiple_paths.py new file mode 100644 index 0000000000..44ad45a891 --- /dev/null +++ b/docs/examples/routing/declaring_multiple_paths.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get(["/some-path", "/some-other-path"]) +async def my_route_handler() -> None: ... diff --git a/docs/examples/routing/declaring_path.py b/docs/examples/routing/declaring_path.py new file mode 100644 index 0000000000..adc74f9535 --- /dev/null +++ b/docs/examples/routing/declaring_path.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get(path="/some-path") +async def my_route_handler() -> None: ... diff --git a/docs/examples/routing/declaring_path_argument.py b/docs/examples/routing/declaring_path_argument.py new file mode 100644 index 0000000000..d01be94226 --- /dev/null +++ b/docs/examples/routing/declaring_path_argument.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get("/some-path") +async def my_route_handler() -> None: ... diff --git a/docs/examples/routing/declaring_path_optional_parameter.py b/docs/examples/routing/declaring_path_optional_parameter.py new file mode 100644 index 0000000000..ec18b5b1ee --- /dev/null +++ b/docs/examples/routing/declaring_path_optional_parameter.py @@ -0,0 +1,7 @@ +from litestar import get + + +@get( + ["/some-path", "/some-path/{some_id:int}"], +) +async def my_route_handler(some_id: int = 1) -> None: ... diff --git a/docs/examples/routing/handler.py b/docs/examples/routing/handler.py new file mode 100644 index 0000000000..633bff1bf5 --- /dev/null +++ b/docs/examples/routing/handler.py @@ -0,0 +1,6 @@ +from litestar import get + + +@get("/") +def greet() -> str: + return "hello world" diff --git a/docs/examples/routing/handler_asgi_1.py b/docs/examples/routing/handler_asgi_1.py new file mode 100644 index 0000000000..a1929a1c71 --- /dev/null +++ b/docs/examples/routing/handler_asgi_1.py @@ -0,0 +1,14 @@ +from litestar import Response, asgi +from litestar.status_codes import HTTP_400_BAD_REQUEST +from litestar.types import Receive, Scope, Send + + +@asgi(path="/my-asgi-app") +async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "http": + if scope["method"] == "GET": + response = Response({"hello": "world"}) + await response(scope=scope, receive=receive, send=send) + return + response = Response({"detail": "unsupported request"}, status_code=HTTP_400_BAD_REQUEST) + await response(scope=scope, receive=receive, send=send) diff --git a/docs/examples/routing/handler_asgi_2.py b/docs/examples/routing/handler_asgi_2.py new file mode 100644 index 0000000000..51e953f08f --- /dev/null +++ b/docs/examples/routing/handler_asgi_2.py @@ -0,0 +1,26 @@ +from litestar import Response, asgi +from litestar.handlers.asgi_handlers import ASGIRouteHandler +from litestar.status_codes import HTTP_400_BAD_REQUEST +from litestar.types import Receive, Scope, 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": + response = Response({"hello": "world"}) + await response(scope=scope, receive=receive, send=send) + return + response = Response({"detail": "unsupported request"}, status_code=HTTP_400_BAD_REQUEST) + await response(scope=scope, receive=receive, send=send) + + +@asgi(path="/my-asgi-app") +async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "http": + if scope["method"] == "GET": + response = Response({"hello": "world"}) + await response(scope=scope, receive=receive, send=send) + return + response = Response({"detail": "unsupported request"}, status_code=HTTP_400_BAD_REQUEST) + await response(scope=scope, receive=receive, send=send) diff --git a/docs/examples/routing/handler_decorator.py b/docs/examples/routing/handler_decorator.py new file mode 100644 index 0000000000..b2c93266c4 --- /dev/null +++ b/docs/examples/routing/handler_decorator.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel + +from litestar import delete, get, head, patch, post, put +from litestar.contrib.pydantic import PydanticDTO +from litestar.dto import DTOConfig, DTOData + + +class Resource(BaseModel): ... + + +class PartialResourceDTO(PydanticDTO[Resource]): + config = DTOConfig(partial=True) + + +@get(path="/resources") +async def list_resources() -> list[Resource]: ... + + +@post(path="/resources") +async def create_resource(data: Resource) -> Resource: ... + + +@get(path="/resources/{pk:int}") +async def retrieve_resource(pk: int) -> Resource: ... + + +@head(path="/resources/{pk:int}") +async def retrieve_resource_head(pk: int) -> None: ... + + +@put(path="/resources/{pk:int}") +async def update_resource(data: Resource, pk: int) -> Resource: ... + + +@patch(path="/resources/{pk:int}", dto=PartialResourceDTO) +async def partially_update_resource(data: DTOData[PartialResourceDTO], pk: int) -> Resource: ... + + +@delete(path="/resources/{pk:int}") +async def delete_resource(pk: int) -> None: ... diff --git a/docs/examples/routing/handler_indexing_1.py b/docs/examples/routing/handler_indexing_1.py new file mode 100644 index 0000000000..f0a2c74a4b --- /dev/null +++ b/docs/examples/routing/handler_indexing_1.py @@ -0,0 +1,40 @@ +from litestar import Litestar, Request, get +from litestar.exceptions import NotFoundException +from litestar.response import Redirect + + +@get("/abc", name="one") +def handler_one() -> None: + pass + + +@get("/xyz", name="two") +def handler_two() -> None: + pass + + +@get("/def/{param:int}", name="three") +def handler_three(param: int) -> None: + pass + + +@get("/{handler_name:str}", name="four") +def handler_four(request: Request, name: str) -> Redirect: + handler_index = request.app.get_handler_index_by_name(name) + if not handler_index: + raise NotFoundException(f"no handler matching the name {name} was found") + + # handler_index == { "paths": ["/"], "handler": ..., "qualname": ... } + # do something with the handler index below, e.g. send a redirect response to the handler, or access + # handler.opt and some values stored there etc. + + return Redirect(path=handler_index[0]) + + +@get("/redirect/{param_value:int}", name="five") +def handler_five(request: Request, param_value: int) -> Redirect: + path = request.app.route_reverse("three", param=param_value) + return Redirect(path=path) + + +app = Litestar(route_handlers=[handler_one, handler_two, handler_three]) diff --git a/docs/examples/routing/handler_indexing_2.py b/docs/examples/routing/handler_indexing_2.py new file mode 100644 index 0000000000..905c30113e --- /dev/null +++ b/docs/examples/routing/handler_indexing_2.py @@ -0,0 +1,22 @@ +from litestar import Request, get + + +@get( + ["/some-path", "/some-path/{id:int}", "/some-path/{id:int}/{val:str}"], + name="handler_name", +) +def handler(id: int = 1, val: str = "default") -> None: ... + + +@get("/path-info") +def path_info(request: Request) -> str: + path_optional = request.app.route_reverse("handler_name") + # /some-path` + + path_partial = request.app.route_reverse("handler_name", id=100) + # /some-path/100 + + path_full = request.app.route_reverse("handler_name", id=100, val="value") + # /some-path/100/value` + + return f"{path_optional} {path_partial} {path_full}" diff --git a/docs/examples/routing/handler_metadata_1.py b/docs/examples/routing/handler_metadata_1.py new file mode 100644 index 0000000000..90b697912a --- /dev/null +++ b/docs/examples/routing/handler_metadata_1.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get("/", opt={"my_key": "some-value"}) +def handler() -> None: ... diff --git a/docs/examples/routing/handler_metadata_2.py b/docs/examples/routing/handler_metadata_2.py new file mode 100644 index 0000000000..b9f893e17b --- /dev/null +++ b/docs/examples/routing/handler_metadata_2.py @@ -0,0 +1,8 @@ +from litestar import get + + +@get("/", my_key="some-value") +def handler() -> None: ... + + +assert handler.opt["my_key"] == "some-value" diff --git a/docs/examples/routing/handler_websocket_1.py b/docs/examples/routing/handler_websocket_1.py new file mode 100644 index 0000000000..b5504dd44c --- /dev/null +++ b/docs/examples/routing/handler_websocket_1.py @@ -0,0 +1,8 @@ +from litestar import WebSocket, websocket + + +@websocket(path="/socket") +async def my_websocket_handler(socket: WebSocket) -> None: + await socket.accept() + await socket.send_json({...}) + await socket.close() diff --git a/docs/examples/routing/handler_websocket_2.py b/docs/examples/routing/handler_websocket_2.py new file mode 100644 index 0000000000..94d7d75146 --- /dev/null +++ b/docs/examples/routing/handler_websocket_2.py @@ -0,0 +1,9 @@ +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() diff --git a/docs/examples/routing/registering_controller.py b/docs/examples/routing/registering_controller.py new file mode 100644 index 0000000000..bf7eb7b57f --- /dev/null +++ b/docs/examples/routing/registering_controller.py @@ -0,0 +1,31 @@ +from pydantic import UUID4, BaseModel + +from litestar.contrib.pydantic import PydanticDTO +from litestar.controller import Controller +from litestar.dto import DTOConfig, DTOData +from litestar.handlers import delete, get, patch, post + + +class UserOrder(BaseModel): + user_id: int + order: str + + +class PartialUserOrderDTO(PydanticDTO[UserOrder]): + config = DTOConfig(partial=True) + + +class UserOrderController(Controller): + path = "/user-order" + + @post() + async def create_user_order(self, data: UserOrder) -> UserOrder: ... + + @get(path="/{order_id:uuid}") + async def retrieve_user_order(self, order_id: UUID4) -> UserOrder: ... + + @patch(path="/{order_id:uuid}", dto=PartialUserOrderDTO) + async def update_user_order(self, order_id: UUID4, data: DTOData[PartialUserOrderDTO]) -> UserOrder: ... + + @delete(path="/{order_id:uuid}") + async def delete_user_order(self, order_id: UUID4) -> None: ... diff --git a/docs/examples/routing/registering_controller_1.py b/docs/examples/routing/registering_controller_1.py new file mode 100644 index 0000000000..0093cf47d3 --- /dev/null +++ b/docs/examples/routing/registering_controller_1.py @@ -0,0 +1,13 @@ +from litestar import Controller, Router, get + + +class MyController(Controller): + path = "/controller" + + @get() + def handler(self) -> None: ... + + +internal_router = Router(path="/internal", route_handlers=[MyController]) +partner_router = Router(path="/partner", route_handlers=[MyController]) +consumer_router = Router(path="/consumer", route_handlers=[MyController]) diff --git a/docs/examples/routing/registering_route_1.py b/docs/examples/routing/registering_route_1.py new file mode 100644 index 0000000000..8869ba4a87 --- /dev/null +++ b/docs/examples/routing/registering_route_1.py @@ -0,0 +1,12 @@ +from litestar import Litestar, get + + +@get("/sub-path") +def sub_path_handler() -> None: ... + + +@get() +def root_handler() -> None: ... + + +app = Litestar(route_handlers=[root_handler, sub_path_handler]) diff --git a/docs/examples/routing/registering_route_2.py b/docs/examples/routing/registering_route_2.py new file mode 100644 index 0000000000..e4995a6d17 --- /dev/null +++ b/docs/examples/routing/registering_route_2.py @@ -0,0 +1,8 @@ +from litestar import Litestar, get + + +@get(["/", "/sub-path"]) +def handler() -> None: ... + + +app = Litestar(route_handlers=[handler]) diff --git a/docs/examples/routing/registering_route_3.py b/docs/examples/routing/registering_route_3.py new file mode 100644 index 0000000000..e30b1aec0a --- /dev/null +++ b/docs/examples/routing/registering_route_3.py @@ -0,0 +1,15 @@ +from litestar import Litestar, get + + +@get() +def root_handler() -> None: ... + + +app = Litestar(route_handlers=[root_handler]) + + +@get("/sub-path") +def sub_path_handler() -> None: ... + + +app.register(sub_path_handler) diff --git a/docs/examples/routing/registering_route_4.py b/docs/examples/routing/registering_route_4.py new file mode 100644 index 0000000000..82cfe8d8cc --- /dev/null +++ b/docs/examples/routing/registering_route_4.py @@ -0,0 +1,14 @@ +from typing import Any + +from litestar import Litestar, Request, get + + +@get("/some-path") +def route_handler(request: Request[Any, Any]) -> None: + @get("/sub-path") + def sub_path_handler() -> None: ... + + request.app.register(sub_path_handler) + + +app = Litestar(route_handlers=[route_handler]) diff --git a/docs/examples/routing/registering_route_5.py b/docs/examples/routing/registering_route_5.py new file mode 100644 index 0000000000..81d26f0164 --- /dev/null +++ b/docs/examples/routing/registering_route_5.py @@ -0,0 +1,10 @@ +from litestar import Litestar, Router, get + + +@get("/{order_id:int}") +def order_handler(order_id: int) -> None: ... + + +order_router = Router(path="/orders", route_handlers=[order_handler]) +base_router = Router(path="/base", route_handlers=[order_router]) +app = Litestar(route_handlers=[base_router]) diff --git a/docs/examples/routing/registering_route_handlers_multiple_times.py b/docs/examples/routing/registering_route_handlers_multiple_times.py new file mode 100644 index 0000000000..2c133f6968 --- /dev/null +++ b/docs/examples/routing/registering_route_handlers_multiple_times.py @@ -0,0 +1,12 @@ +from litestar import Litestar, Router, get + + +@get(path="/handler") +def my_route_handler() -> None: ... + + +internal_router = Router(path="/internal", route_handlers=[my_route_handler]) +partner_router = Router(path="/partner", route_handlers=[my_route_handler]) +consumer_router = Router(path="/consumer", route_handlers=[my_route_handler]) + +Litestar(route_handlers=[internal_router, partner_router, consumer_router]) diff --git a/docs/examples/routing/reserved_keyword_argument.py b/docs/examples/routing/reserved_keyword_argument.py new file mode 100644 index 0000000000..6c42d6cb23 --- /dev/null +++ b/docs/examples/routing/reserved_keyword_argument.py @@ -0,0 +1,14 @@ +from typing import Any, Dict + +from litestar import Request, get +from litestar.datastructures import State + + +@get(path="/") +async def my_request_handler( + state: State, + request: Request, + headers: Dict[str, str], + query: Dict[str, Any], + cookies: Dict[str, Any], +) -> None: ... diff --git a/docs/examples/routing/route_handler_http_1.py b/docs/examples/routing/route_handler_http_1.py new file mode 100644 index 0000000000..84cc839e31 --- /dev/null +++ b/docs/examples/routing/route_handler_http_1.py @@ -0,0 +1,5 @@ +from litestar import HttpMethod, route + + +@route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST]) +async def my_endpoint() -> None: ... diff --git a/docs/examples/routing/route_handler_http_2.py b/docs/examples/routing/route_handler_http_2.py new file mode 100644 index 0000000000..776b8a1b33 --- /dev/null +++ b/docs/examples/routing/route_handler_http_2.py @@ -0,0 +1,6 @@ +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: ... diff --git a/docs/examples/security/exclude_from_auth.py b/docs/examples/security/exclude_from_auth.py new file mode 100644 index 0000000000..bb23072842 --- /dev/null +++ b/docs/examples/security/exclude_from_auth.py @@ -0,0 +1,11 @@ +from typing import Any + +from litestar import get + + +@get("/secured") +def secured_route() -> Any: ... + + +@get("/unsecured", exclude_from_auth=True) +def unsecured_route() -> Any: ... diff --git a/docs/examples/security/exclude_from_auth_with_key.py b/docs/examples/security/exclude_from_auth_with_key.py new file mode 100644 index 0000000000..b0251dd336 --- /dev/null +++ b/docs/examples/security/exclude_from_auth_with_key.py @@ -0,0 +1,25 @@ +from typing import Any + +from litestar import get +from litestar.middleware.session.server_side import ServerSideSessionBackend, ServerSideSessionConfig +from litestar.security.session_auth import SessionAuth + + +@get("/secured") +def secured_route() -> Any: ... + + +@get("/unsecured", no_auth=True) +def unsecured_route() -> Any: ... + + +session_auth = SessionAuth[User, ServerSideSessionBackend]( + retrieve_user_handler=retrieve_user_handler, + # we must pass a config for a session backend. + # all session backends are supported + session_backend_config=ServerSideSessionConfig(), + # exclude any URLs that should not have authentication. + # We exclude the documentation URLs, signup and login. + exclude=["/login", "/signup", "/schema"], + exclude_opt_key="no_auth", # default value is `exclude_from_auth` +) diff --git a/docs/examples/security/excluding_routes.py b/docs/examples/security/excluding_routes.py new file mode 100644 index 0000000000..aabc747188 --- /dev/null +++ b/docs/examples/security/excluding_routes.py @@ -0,0 +1,23 @@ +from uuid import UUID + +from pydantic import BaseModel, EmailStr + +from litestar.middleware.session.server_side import ServerSideSessionBackend, ServerSideSessionConfig +from litestar.security.session_auth import SessionAuth + + +class User(BaseModel): + id: UUID + name: str + email: EmailStr + + +session_auth = SessionAuth[User, ServerSideSessionBackend]( + retrieve_user_handler=retrieve_user_handler, + # we must pass a config for a session backend. + # all session backends are supported + session_backend_config=ServerSideSessionConfig(), + # exclude any URLs that should not have authentication. + # We exclude the documentation URLs, signup and login. + exclude=["/login", "/signup", "/schema"], +) diff --git a/docs/examples/security/including_routes.py b/docs/examples/security/including_routes.py new file mode 100644 index 0000000000..ec7587972d --- /dev/null +++ b/docs/examples/security/including_routes.py @@ -0,0 +1,23 @@ +from uuid import UUID + +from pydantic import BaseModel, EmailStr + +from litestar.middleware.session.server_side import ServerSideSessionBackend, ServerSideSessionConfig +from litestar.security.session_auth import SessionAuth + + +class User(BaseModel): + id: UUID + name: str + email: EmailStr + + +session_auth = SessionAuth[User, ServerSideSessionBackend]( + retrieve_user_handler=retrieve_user_handler, + # we must pass a config for a session backend. + # all session backends are supported + session_backend_config=ServerSideSessionConfig(), + # exclude any URLs that should not have authentication. + # We exclude the documentation URLs, signup and login. + exclude=[r"^(?!.*\/secured$).*$"], +) diff --git a/docs/examples/security/middleware/__init__.py b/docs/examples/security/middleware/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/security/middleware/auth_middleware_1.py b/docs/examples/security/middleware/auth_middleware_1.py new file mode 100644 index 0000000000..d0f8c5babb --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_1.py @@ -0,0 +1,11 @@ +from litestar.connection import ASGIConnection +from litestar.middleware import ( + AbstractAuthenticationMiddleware, + AuthenticationResult, +) + + +class MyAuthenticationMiddleware(AbstractAuthenticationMiddleware): + async def authenticate_request(self, connection: ASGIConnection) -> AuthenticationResult: + # do something here. + ... diff --git a/docs/examples/security/middleware/auth_middleware_creation.py b/docs/examples/security/middleware/auth_middleware_creation.py new file mode 100644 index 0000000000..d98c90f3c3 --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_creation.py @@ -0,0 +1,41 @@ +from typing import TYPE_CHECKING, cast + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.models import User +from app.security.jwt import decode_jwt_token +from litestar.connection import ASGIConnection +from litestar.exceptions import NotAuthorizedException +from litestar.middleware import ( + AbstractAuthenticationMiddleware, + AuthenticationResult, +) + +if TYPE_CHECKING: + from sqlalchemy.ext.asyncio import AsyncEngine + +API_KEY_HEADER = "X-API-KEY" + + +class JWTAuthenticationMiddleware(AbstractAuthenticationMiddleware): + async def authenticate_request(self, connection: ASGIConnection) -> AuthenticationResult: + """ + Given a request, parse the request api key stored in the header and retrieve the user correlating to the token from the DB + """ + + # retrieve the auth header + auth_header = connection.headers.get(API_KEY_HEADER) + if not auth_header: + raise NotAuthorizedException() + + # decode the token, the result is a ``Token`` model instance + token = decode_jwt_token(encoded_token=auth_header) + + engine = cast("AsyncEngine", connection.app.state.postgres_connection) + async with AsyncSession(engine) as async_session: + async with async_session.begin(): + user = await async_session.execute(select(User).where(User.id == token.sub)) + if not user: + raise NotAuthorizedException() + return AuthenticationResult(user=user, auth=token) diff --git a/docs/examples/security/middleware/auth_middleware_dependencies.py b/docs/examples/security/middleware/auth_middleware_dependencies.py new file mode 100644 index 0000000000..2c8558effc --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_dependencies.py @@ -0,0 +1,17 @@ +from typing import Any + +from my_app.db.models import User +from my_app.security.jwt import Token + +from litestar import Provide, Request, Router +from litestar.datastructures import State + + +async def my_dependency(request: Request[User, Token, State]) -> Any: + user = request.user # correctly typed as User + auth = request.auth # correctly typed as Token + assert isinstance(user, User) + assert isinstance(auth, Token) + + +my_router = Router(path="sub-path/", dependencies={"some_dependency": Provide(my_dependency)}) diff --git a/docs/examples/security/middleware/auth_middleware_exclude_route.py b/docs/examples/security/middleware/auth_middleware_exclude_route.py new file mode 100644 index 0000000000..b59fbf1e9b --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_exclude_route.py @@ -0,0 +1,26 @@ +import anyio +from my_app.security.authentication_middleware import JWTAuthenticationMiddleware + +from litestar import Litestar, MediaType, Response, get +from litestar.exceptions import NotFoundException +from litestar.middleware.base import DefineMiddleware + +# you can optionally exclude certain paths from authentication. +# the following excludes all routes mounted at or under `/schema*` +# additionally, +# you can modify the default exclude key of "exclude_from_auth", by overriding the `exclude_from_auth_key` parameter on the Authentication Middleware +auth_mw = DefineMiddleware(JWTAuthenticationMiddleware, exclude="schema") + + +@get(path="/", exclude_from_auth=True) +async def site_index() -> Response: + """Site index""" + exists = await anyio.Path("index.html").exists() + if exists: + async with await anyio.open_file(anyio.Path("index.html")) as file: + content = await file.read() + return Response(content=content, status_code=200, media_type=MediaType.HTML) + raise NotFoundException("Site index was not found") + + +app = Litestar(route_handlers=[site_index], middleware=[auth_mw]) diff --git a/docs/examples/security/middleware/auth_middleware_jwt.py b/docs/examples/security/middleware/auth_middleware_jwt.py new file mode 100644 index 0000000000..3b0b532ed7 --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_jwt.py @@ -0,0 +1,40 @@ +from datetime import datetime, timedelta +from uuid import UUID + +from jose import JWTError, jwt +from pydantic import UUID4, BaseModel + +from app.config import settings +from litestar.exceptions import NotAuthorizedException + +DEFAULT_TIME_DELTA = timedelta(days=1) +ALGORITHM = "HS256" + + +class Token(BaseModel): + exp: datetime + iat: datetime + sub: UUID4 + + +def decode_jwt_token(encoded_token: str) -> Token: + """ + Helper function that decodes a jwt token and returns the value stored under the ``sub`` key + + If the token is invalid or expired (i.e. the value stored under the exp key is in the past) an exception is raised + """ + try: + payload = jwt.decode(token=encoded_token, key=settings.JWT_SECRET, algorithms=[ALGORITHM]) + return Token(**payload) + except JWTError as e: + raise NotAuthorizedException("Invalid token") from e + + +def encode_jwt_token(user_id: UUID, expiration: timedelta = DEFAULT_TIME_DELTA) -> str: + """Helper function that encodes a JWT token with expiration and a given user_id""" + token = Token( + exp=datetime.now() + expiration, + iat=datetime.now(), + sub=user_id, + ) + return jwt.encode(token.dict(), settings.JWT_SECRET, algorithm=ALGORITHM) diff --git a/docs/examples/security/middleware/auth_middleware_model.py b/docs/examples/security/middleware/auth_middleware_model.py new file mode 100644 index 0000000000..8d8bbe1bfa --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_model.py @@ -0,0 +1,12 @@ +import uuid + +from sqlalchemy import Column +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import declarative_base + +Base = declarative_base() + + +class User(Base): + id: uuid.UUID | None = Column(UUID(as_uuid=True), default=uuid.uuid4, primary_key=True) + # ... other fields follow, but we only require id for this example diff --git a/docs/examples/security/middleware/auth_middleware_route.py b/docs/examples/security/middleware/auth_middleware_route.py new file mode 100644 index 0000000000..399e1d350a --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_route.py @@ -0,0 +1,13 @@ +from my_app.db.models import User +from my_app.security.jwt import Token + +from litestar import Request, get +from litestar.datastructures import State + + +@get("/") +def my_route_handler(request: Request[User, Token, State]) -> None: + user = request.user # correctly typed as User + auth = request.auth # correctly typed as Token + assert isinstance(user, User) + assert isinstance(auth, Token) diff --git a/docs/examples/security/middleware/auth_middleware_to_app.py b/docs/examples/security/middleware/auth_middleware_to_app.py new file mode 100644 index 0000000000..29426d0f06 --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_to_app.py @@ -0,0 +1,10 @@ +from my_app.security.authentication_middleware import JWTAuthenticationMiddleware + +from litestar import Litestar +from litestar.middleware.base import DefineMiddleware + +# you can optionally exclude certain paths from authentication. +# the following excludes all routes mounted at or under `/schema*` +auth_mw = DefineMiddleware(JWTAuthenticationMiddleware, exclude="schema") + +app = Litestar(route_handlers=[...], middleware=[auth_mw]) diff --git a/docs/examples/security/middleware/auth_middleware_websocket.py b/docs/examples/security/middleware/auth_middleware_websocket.py new file mode 100644 index 0000000000..caddacb149 --- /dev/null +++ b/docs/examples/security/middleware/auth_middleware_websocket.py @@ -0,0 +1,13 @@ +from my_app.db.models import User +from my_app.security.jwt import Token + +from litestar import WebSocket, websocket +from litestar.datastructures import State + + +@websocket("/") +async def my_route_handler(socket: WebSocket[User, Token, State]) -> None: + user = socket.user # correctly typed as User + auth = socket.auth # correctly typed as Token + assert isinstance(user, User) + assert isinstance(auth, Token) diff --git a/docs/examples/signature_namespace/handler_signature_1.py b/docs/examples/signature_namespace/handler_signature_1.py new file mode 100644 index 0000000000..0fb1ddb6be --- /dev/null +++ b/docs/examples/signature_namespace/handler_signature_1.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from litestar import Controller, post + +if TYPE_CHECKING: + from domain import Model + + +class MyController(Controller): + @post() + def create_item(data: Model) -> Model: + return data diff --git a/docs/examples/signature_namespace/handler_signature_2.py b/docs/examples/signature_namespace/handler_signature_2.py new file mode 100644 index 0000000000..9efa889709 --- /dev/null +++ b/docs/examples/signature_namespace/handler_signature_2.py @@ -0,0 +1,4 @@ +from __future__ import annotations + + +# Choose the appropriate noqa directive according to your linter diff --git a/docs/examples/templating/engine_custom.py b/docs/examples/templating/engine_custom.py new file mode 100644 index 0000000000..03489e1a0d --- /dev/null +++ b/docs/examples/templating/engine_custom.py @@ -0,0 +1,16 @@ +from typing import List, Protocol, Union + +from pydantic import DirectoryPath + +# the template class of the respective library +from some_lib import SomeTemplate + + +class TemplateEngineProtocol(Protocol[SomeTemplate]): + def __init__(self, directory: Union[DirectoryPath, List[DirectoryPath]]) -> None: + """Builds a template engine.""" + ... + + def get_template(self, template_name: str) -> SomeTemplate: + """Loads the template with template_name and returns it.""" + ... diff --git a/docs/examples/templating/passing_template_context.py b/docs/examples/templating/passing_template_context.py new file mode 100644 index 0000000000..3e817728b1 --- /dev/null +++ b/docs/examples/templating/passing_template_context.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.response import Template + + +@get(path="/info") +def info() -> Template: + return Template(template_name="info.html", context={"numbers": "1234567890"}) diff --git a/docs/examples/templating/registering_new_template.py b/docs/examples/templating/registering_new_template.py new file mode 100644 index 0000000000..011ad94889 --- /dev/null +++ b/docs/examples/templating/registering_new_template.py @@ -0,0 +1,8 @@ +from jinja2 import DictLoader, Environment + +from litestar import Litestar +from litestar.contrib.jinja import JinjaTemplateEngine +from litestar.template import TemplateConfig + +my_custom_env = Environment(loader=DictLoader({"index.html": "Hello {{name}}!"})) +app = Litestar(template_config=TemplateConfig(instance=JinjaTemplateEngine.from_environment(my_custom_env))) diff --git a/docs/examples/templating/template_file.py b/docs/examples/templating/template_file.py new file mode 100644 index 0000000000..2567243e34 --- /dev/null +++ b/docs/examples/templating/template_file.py @@ -0,0 +1,7 @@ +from litestar import get +from litestar.response import Template + + +@get() +async def example() -> Template: + return Template(template_name="test.html", context={"hello": "world"}) diff --git a/docs/examples/templating/template_string.py b/docs/examples/templating/template_string.py new file mode 100644 index 0000000000..79db11143d --- /dev/null +++ b/docs/examples/templating/template_string.py @@ -0,0 +1,8 @@ +from litestar import get +from litestar.response import Template + + +@get() +async def example() -> Template: + template_string = "{{ hello }}" + return Template(template_str=template_string, context={"hello": "world"}) diff --git a/docs/examples/templating/templates/csrf_inputs.html.jinja2 b/docs/examples/templating/templates/csrf_inputs.html.jinja2 new file mode 100644 index 0000000000..ff26373157 --- /dev/null +++ b/docs/examples/templating/templates/csrf_inputs.html.jinja2 @@ -0,0 +1,13 @@ + + +
+
+ {{ csrf_input | safe }} +
+ +
+ +
+
+ + diff --git a/docs/examples/templating/templates/csrf_inputs.html.mako b/docs/examples/templating/templates/csrf_inputs.html.mako new file mode 100644 index 0000000000..4e2d6444ff --- /dev/null +++ b/docs/examples/templating/templates/csrf_inputs.html.mako @@ -0,0 +1,13 @@ + + +
+
+ ${csrf_input | n} +
+ +
+ +
+
+ + diff --git a/docs/examples/templating/templates/csrf_inputs.html.minijinja b/docs/examples/templating/templates/csrf_inputs.html.minijinja new file mode 100644 index 0000000000..54336d57a7 --- /dev/null +++ b/docs/examples/templating/templates/csrf_inputs.html.minijinja @@ -0,0 +1,13 @@ + + +
+
+ {{ csrf_input | safe}} +
+ +
+ +
+
+ + diff --git a/docs/examples/templating/templates/request_instance.html.jinja2 b/docs/examples/templating/templates/request_instance.html.jinja2 new file mode 100644 index 0000000000..1fb35d5566 --- /dev/null +++ b/docs/examples/templating/templates/request_instance.html.jinja2 @@ -0,0 +1,7 @@ + + +
+ My state value: {{request.app.state.some_key}} +
+ + diff --git a/docs/examples/templating/templates/request_instance.html.mako b/docs/examples/templating/templates/request_instance.html.mako new file mode 100644 index 0000000000..3947a89801 --- /dev/null +++ b/docs/examples/templating/templates/request_instance.html.mako @@ -0,0 +1,7 @@ + + +
+ My state value: ${request.app.state.some_key} +
+ + diff --git a/docs/examples/templating/templates/request_instance.html.minijinja b/docs/examples/templating/templates/request_instance.html.minijinja new file mode 100644 index 0000000000..1fb35d5566 --- /dev/null +++ b/docs/examples/templating/templates/request_instance.html.minijinja @@ -0,0 +1,7 @@ + + +
+ My state value: {{request.app.state.some_key}} +
+ + diff --git a/docs/examples/testing/test_app_1.py b/docs/examples/testing/test_app_1.py new file mode 100644 index 0000000000..65cb4ee9de --- /dev/null +++ b/docs/examples/testing/test_app_1.py @@ -0,0 +1,11 @@ +from my_app.main import health_check + +from litestar.status_codes import HTTP_200_OK +from litestar.testing import create_test_client + + +def test_health_check(): + with create_test_client(route_handlers=[health_check]) as client: + response = client.get("/health-check") + assert response.status_code == HTTP_200_OK + assert response.text == "healthy" diff --git a/docs/examples/testing/test_app_2.py b/docs/examples/testing/test_app_2.py new file mode 100644 index 0000000000..bcd1db139f --- /dev/null +++ b/docs/examples/testing/test_app_2.py @@ -0,0 +1,11 @@ +from my_app.main import health_check + +from litestar.status_codes import HTTP_200_OK +from litestar.testing import create_test_client + + +def test_health_check(): + with create_test_client(route_handlers=health_check) as client: + response = client.get("/health-check") + assert response.status_code == HTTP_200_OK + assert response.text == "healthy" diff --git a/docs/examples/testing/test_client_async.py b/docs/examples/testing/test_client_async.py new file mode 100644 index 0000000000..af41a3c2b2 --- /dev/null +++ b/docs/examples/testing/test_client_async.py @@ -0,0 +1,11 @@ +from my_app.main import app + +from litestar.status_codes import HTTP_200_OK +from litestar.testing import AsyncTestClient + + +async def test_health_check(): + async with AsyncTestClient(app=app) as client: + response = await client.get("/health-check") + assert response.status_code == HTTP_200_OK + assert response.text == "healthy" diff --git a/docs/examples/testing/test_client_base.py b/docs/examples/testing/test_client_base.py new file mode 100644 index 0000000000..8d08cab0bb --- /dev/null +++ b/docs/examples/testing/test_client_base.py @@ -0,0 +1,9 @@ +from litestar import Litestar, MediaType, get + + +@get(path="/health-check", media_type=MediaType.TEXT) +def health_check() -> str: + return "healthy" + + +app = Litestar(route_handlers=[health_check]) diff --git a/docs/examples/testing/test_client_conf_async.py b/docs/examples/testing/test_client_conf_async.py new file mode 100644 index 0000000000..d9e5f714d0 --- /dev/null +++ b/docs/examples/testing/test_client_conf_async.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING, AsyncIterator + +import pytest +from my_app.main import app + +from litestar.testing import AsyncTestClient + +if TYPE_CHECKING: + from litestar import Litestar + + +@pytest.fixture(scope="function") +async def test_client() -> AsyncIterator[AsyncTestClient[Litestar]]: + async with AsyncTestClient(app=app) as client: + yield client diff --git a/docs/examples/testing/test_client_conf_sync.py b/docs/examples/testing/test_client_conf_sync.py new file mode 100644 index 0000000000..688a80e8f7 --- /dev/null +++ b/docs/examples/testing/test_client_conf_sync.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING, Iterator + +import pytest +from my_app.main import app + +from litestar.testing import TestClient + +if TYPE_CHECKING: + from litestar import Litestar + + +@pytest.fixture(scope="function") +def test_client() -> Iterator[TestClient[Litestar]]: + with TestClient(app=app) as client: + yield client diff --git a/docs/examples/testing/test_client_sync.py b/docs/examples/testing/test_client_sync.py new file mode 100644 index 0000000000..1d34f341fd --- /dev/null +++ b/docs/examples/testing/test_client_sync.py @@ -0,0 +1,11 @@ +from my_app.main import app + +from litestar.status_codes import HTTP_200_OK +from litestar.testing import TestClient + + +def test_health_check(): + with TestClient(app=app) as client: + response = client.get("/health-check") + assert response.status_code == HTTP_200_OK + assert response.text == "healthy" diff --git a/docs/examples/testing/test_polyfactory_1.py b/docs/examples/testing/test_polyfactory_1.py new file mode 100644 index 0000000000..4211b281b4 --- /dev/null +++ b/docs/examples/testing/test_polyfactory_1.py @@ -0,0 +1,19 @@ +from typing import Protocol, runtime_checkable + +from polyfactory.factories.pydantic import BaseModel + +from litestar import get + + +class Item(BaseModel): + name: str + + +@runtime_checkable +class Service(Protocol): + def get(self) -> Item: ... + + +@get(path="/item") +def get_item(service: Service) -> Item: + return service.get() diff --git a/docs/examples/testing/test_polyfactory_2.py b/docs/examples/testing/test_polyfactory_2.py new file mode 100644 index 0000000000..34cfdeec78 --- /dev/null +++ b/docs/examples/testing/test_polyfactory_2.py @@ -0,0 +1,22 @@ +import pytest +from my_app.main import Item, Service, get_item + +from litestar.di import Provide +from litestar.status_codes import HTTP_200_OK +from litestar.testing import create_test_client + + +@pytest.fixture() +def item(): + return Item(name="Chair") + + +def test_get_item(item: Item): + class MyService(Service): + def get_one(self) -> Item: + return item + + with create_test_client(route_handlers=get_item, dependencies={"service": Provide(lambda: MyService())}) as client: + response = client.get("/item") + assert response.status_code == HTTP_200_OK + assert response.json() == item.dict() diff --git a/docs/examples/testing/test_polyfactory_3.py b/docs/examples/testing/test_polyfactory_3.py new file mode 100644 index 0000000000..4dbca86440 --- /dev/null +++ b/docs/examples/testing/test_polyfactory_3.py @@ -0,0 +1,44 @@ +from typing import Protocol, runtime_checkable + +import pytest +from polyfactory.factories.pydantic_factory import ModelFactory +from pydantic import BaseModel + +from litestar import get +from litestar.di import Provide +from litestar.status_codes import HTTP_200_OK +from litestar.testing import create_test_client + + +class Item(BaseModel): + name: str + + +@runtime_checkable +class Service(Protocol): + def get_one(self) -> Item: ... + + +@get(path="/item") +def get_item(service: Service) -> Item: + return service.get_one() + + +class ItemFactory(ModelFactory[Item]): + model = Item + + +@pytest.fixture() +def item(): + return ItemFactory.build() + + +def test_get_item(item: Item): + class MyService(Service): + def get_one(self) -> Item: + return item + + with create_test_client(route_handlers=get_item, dependencies={"service": Provide(lambda: MyService())}) as client: + response = client.get("/item") + assert response.status_code == HTTP_200_OK + assert response.json() == item.dict() diff --git a/docs/examples/testing/test_request_factory_1.py b/docs/examples/testing/test_request_factory_1.py new file mode 100644 index 0000000000..f76bba1567 --- /dev/null +++ b/docs/examples/testing/test_request_factory_1.py @@ -0,0 +1,8 @@ +from litestar import Request +from litestar.exceptions import NotAuthorizedException +from litestar.handlers.base import BaseRouteHandler + + +def secret_token_guard(request: Request, route_handler: BaseRouteHandler) -> None: + if route_handler.opt.get("secret") and not request.headers.get("Secret-Header", "") == route_handler.opt["secret"]: + raise NotAuthorizedException() diff --git a/docs/examples/testing/test_request_factory_2.py b/docs/examples/testing/test_request_factory_2.py new file mode 100644 index 0000000000..a9ebd9e2af --- /dev/null +++ b/docs/examples/testing/test_request_factory_2.py @@ -0,0 +1,9 @@ +from os import environ + +from my_app.guards import secret_token_guard + +from litestar import get + + +@get(path="/secret", guards=[secret_token_guard], opt={"secret": environ.get("SECRET")}) +def secret_endpoint() -> None: ... diff --git a/docs/examples/testing/test_request_factory_3.py b/docs/examples/testing/test_request_factory_3.py new file mode 100644 index 0000000000..68801ef21a --- /dev/null +++ b/docs/examples/testing/test_request_factory_3.py @@ -0,0 +1,21 @@ +import pytest +from my_app.guards import secret_token_guard +from my_app.secret import secret_endpoint + +from litestar.exceptions import NotAuthorizedException +from litestar.testing import RequestFactory + +request = RequestFactory().get("/") + + +def test_secret_token_guard_failure_scenario(): + copied_endpoint_handler = secret_endpoint.copy() + copied_endpoint_handler.opt["secret"] = None + with pytest.raises(NotAuthorizedException): + secret_token_guard(request=request, route_handler=copied_endpoint_handler) + + +def test_secret_token_guard_success_scenario(): + copied_endpoint_handler = secret_endpoint.copy() + copied_endpoint_handler.opt["secret"] = "super-secret" + secret_token_guard(request=request, route_handler=copied_endpoint_handler) diff --git a/docs/examples/todo_app/create/dynamic_route.py b/docs/examples/todo_app/create/dynamic_route.py new file mode 100644 index 0000000000..27e4675c5c --- /dev/null +++ b/docs/examples/todo_app/create/dynamic_route.py @@ -0,0 +1,6 @@ +from litestar import get + + +@get("/{name:str}") +async def greeter(name: str) -> str: + return "Hello, " + name diff --git a/docs/examples/todo_app/hello_world_2.py b/docs/examples/todo_app/hello_world_2.py new file mode 100644 index 0000000000..e4f82540ac --- /dev/null +++ b/docs/examples/todo_app/hello_world_2.py @@ -0,0 +1,8 @@ +from litestar import get + + +async def hello_world() -> str: + return "Hello, world!" + + +hello_world = get("/")(hello_world) diff --git a/docs/examples/websockets/receiving_json_and_sending_it_back_as_messagepack.py b/docs/examples/websockets/receiving_json_and_sending_it_back_as_messagepack.py new file mode 100644 index 0000000000..5a0c980322 --- /dev/null +++ b/docs/examples/websockets/receiving_json_and_sending_it_back_as_messagepack.py @@ -0,0 +1,8 @@ +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) diff --git a/docs/examples/websockets/websocket_base.py b/docs/examples/websockets/websocket_base.py new file mode 100644 index 0000000000..dae747d1d0 --- /dev/null +++ b/docs/examples/websockets/websocket_base.py @@ -0,0 +1,10 @@ +from litestar import Litestar +from litestar.handlers.websocket_handlers import websocket_listener + + +@websocket_listener("/") +async def handler(data: str) -> str: + return data + + +app = Litestar([handler]) diff --git a/docs/examples/whats_new/__init__.py b/docs/examples/whats_new/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/whats_new/attrs_v1.py b/docs/examples/whats_new/attrs_v1.py new file mode 100644 index 0000000000..8ab6b9bce4 --- /dev/null +++ b/docs/examples/whats_new/attrs_v1.py @@ -0,0 +1,6 @@ +from litestar import get +from litestar.params import Parameter + + +@get("/") +def index(param: int = Parameter(gt=5)) -> dict[str, int]: ... diff --git a/docs/examples/whats_new/attrs_v2.py b/docs/examples/whats_new/attrs_v2.py new file mode 100644 index 0000000000..6e88beeb01 --- /dev/null +++ b/docs/examples/whats_new/attrs_v2.py @@ -0,0 +1,8 @@ +from typing import Annotated + +from litestar import get +from litestar.params import Parameter + + +@get("/") +def index(param: Annotated[int, Parameter(gt=5)]) -> dict[str, int]: ... diff --git a/docs/examples/whats_new/before_send_v1.py b/docs/examples/whats_new/before_send_v1.py new file mode 100644 index 0000000000..17493fe829 --- /dev/null +++ b/docs/examples/whats_new/before_send_v1.py @@ -0,0 +1,5 @@ +from litestar.datastructures import State +from litestar.types import Message + + +async def before_send(message: Message, state: State) -> None: ... diff --git a/docs/examples/whats_new/before_send_v2.py b/docs/examples/whats_new/before_send_v2.py new file mode 100644 index 0000000000..992877c94d --- /dev/null +++ b/docs/examples/whats_new/before_send_v2.py @@ -0,0 +1,5 @@ +from litestar.datastructures import State +from litestar.types import Message, Scope + + +async def before_send(message: Message, state: State, scope: Scope) -> None: ... diff --git a/docs/examples/whats_new/dependencies_without_provide_v1.py b/docs/examples/whats_new/dependencies_without_provide_v1.py new file mode 100644 index 0000000000..b6e2468cb8 --- /dev/null +++ b/docs/examples/whats_new/dependencies_without_provide_v1.py @@ -0,0 +1,8 @@ +from litestar import Litestar +from litestar.di import Provide + + +async def some_dependency() -> str: ... + + +app = Litestar(dependencies={"some": Provide(some_dependency)}) diff --git a/docs/examples/whats_new/dependencies_without_provide_v2.py b/docs/examples/whats_new/dependencies_without_provide_v2.py new file mode 100644 index 0000000000..966d3eed2e --- /dev/null +++ b/docs/examples/whats_new/dependencies_without_provide_v2.py @@ -0,0 +1,7 @@ +from litestar import Litestar + + +async def some_dependency() -> str: ... + + +app = Litestar(dependencies={"some": some_dependency}) diff --git a/docs/examples/whats_new/initial_state_v1.py b/docs/examples/whats_new/initial_state_v1.py new file mode 100644 index 0000000000..e6c379af27 --- /dev/null +++ b/docs/examples/whats_new/initial_state_v1.py @@ -0,0 +1,3 @@ +from starlite import Starlite + +app = Starlite(..., initial_state={"some": "key"}) diff --git a/docs/examples/whats_new/initial_state_v2.py b/docs/examples/whats_new/initial_state_v2.py new file mode 100644 index 0000000000..b3e8d842a5 --- /dev/null +++ b/docs/examples/whats_new/initial_state_v2.py @@ -0,0 +1,4 @@ +from litestar import Litestar +from litestar.datastructures import State + +app = Litestar(..., state=State({"some": "key"})) diff --git a/docs/examples/whats_new/lifespan_hook_v1.py b/docs/examples/whats_new/lifespan_hook_v1.py new file mode 100644 index 0000000000..aae6b0e627 --- /dev/null +++ b/docs/examples/whats_new/lifespan_hook_v1.py @@ -0,0 +1,5 @@ +from litestar.datastructures import State + + +def on_startup(state: State) -> None: + print(state.something) diff --git a/docs/examples/whats_new/lifespan_hook_v2.py b/docs/examples/whats_new/lifespan_hook_v2.py new file mode 100644 index 0000000000..0c8dbe40e3 --- /dev/null +++ b/docs/examples/whats_new/lifespan_hook_v2.py @@ -0,0 +1,5 @@ +from litestar import Litestar + + +def on_startup(app: Litestar) -> None: + print(app.state.something) diff --git a/docs/examples/whats_new/response_cookies_v1.py b/docs/examples/whats_new/response_cookies_v1.py new file mode 100644 index 0000000000..2c5e713ac7 --- /dev/null +++ b/docs/examples/whats_new/response_cookies_v1.py @@ -0,0 +1,6 @@ +from litestar import get +from litestar.datastructures import Cookie + + +@get("/", response_cookies=[Cookie(key="foo", value="bar")]) +async def handler() -> None: ... diff --git a/docs/examples/whats_new/response_cookies_v2.py b/docs/examples/whats_new/response_cookies_v2.py new file mode 100644 index 0000000000..c945611cfb --- /dev/null +++ b/docs/examples/whats_new/response_cookies_v2.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get("/", response_cookies={"foo": "bar"}) +async def handler() -> None: ... diff --git a/docs/examples/whats_new/response_headers_v1.py b/docs/examples/whats_new/response_headers_v1.py new file mode 100644 index 0000000000..e4dcc4d590 --- /dev/null +++ b/docs/examples/whats_new/response_headers_v1.py @@ -0,0 +1,5 @@ +from starlite import ResponseHeader, get + + +@get(response_headers={"my-header": ResponseHeader(value="header-value")}) +async def handler() -> str: ... diff --git a/docs/examples/whats_new/response_headers_v2.py b/docs/examples/whats_new/response_headers_v2.py new file mode 100644 index 0000000000..596528dd7d --- /dev/null +++ b/docs/examples/whats_new/response_headers_v2.py @@ -0,0 +1,12 @@ +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_headers() -> str: ... diff --git a/docs/examples/whats_new/sync_to_thread_v1.py b/docs/examples/whats_new/sync_to_thread_v1.py new file mode 100644 index 0000000000..201a7c8318 --- /dev/null +++ b/docs/examples/whats_new/sync_to_thread_v1.py @@ -0,0 +1,5 @@ +from litestar import get + + +@get() +def handler() -> None: ... diff --git a/docs/examples/whats_new/sync_to_thread_v2.py b/docs/examples/whats_new/sync_to_thread_v2.py new file mode 100644 index 0000000000..911158123e --- /dev/null +++ b/docs/examples/whats_new/sync_to_thread_v2.py @@ -0,0 +1,12 @@ +from litestar import get + + +@get(sync_to_thread=False) +def handler() -> None: ... + + +# or + + +@get(sync_to_thread=True) +def handler_sync() -> None: ... diff --git a/docs/index.rst b/docs/index.rst index 3fb006fa79..7bd9babd56 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -85,22 +85,9 @@ At a minimum, make sure you have installed ``litestar[standard]``, which include First, create a file named ``app.py`` with the following contents: -.. code-block:: python +.. literalinclude:: /examples/index/minimal_example.py + :language: python - 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: @@ -178,87 +165,29 @@ 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 - - +.. literalinclude:: /examples/index/expanded_example_1.py + :language: python 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 +.. literalinclude:: /examples/index/expanded_example_2.py + :language: python - 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: ... +.. literalinclude:: /examples/index/expanded_example_3.py + :language: python 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 +.. literalinclude:: /examples/index/expanded_example_4.py + :language: python - app = Litestar(route_handlers=[UserController]) To **run your application**, use an ASGI server such as `uvicorn `_ : diff --git a/docs/migration/fastapi.rst b/docs/migration/fastapi.rst index 8dda93bbbb..f53938ac60 100644 --- a/docs/migration/fastapi.rst +++ b/docs/migration/fastapi.rst @@ -13,48 +13,22 @@ controller methods. The handler can then be registered on an application or rout .. tab-item:: FastAPI :sync: fastapi - .. code-block:: python + .. literalinclude:: /examples/migrations/fastapi/routing_fastapi.py + :language: python - from fastapi import FastAPI - - - app = FastAPI() - - - @app.get("/") - async def index() -> dict[str, str]: ... .. tab-item:: Starlette :sync: starlette + .. literalinclude:: /examples/migrations/fastapi/routing_starlette.py + :language: python - .. code-block:: python - - from starlette.applications import Starlette - from starlette.routing import Route - - - async def index(request): ... - - - routes = [Route("/", endpoint=index)] - - app = Starlette(routes=routes) .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get - - - @get("/") - async def index() -> dict[str, str]: ... - - - app = Litestar([index]) - + .. literalinclude:: /examples/migrations/fastapi/routing_litestar.py + :language: python .. seealso:: @@ -99,72 +73,15 @@ and to easily access dependencies from higher levels. .. tab-item:: FastAPI :sync: fastapi - .. code-block:: python - - from fastapi import FastAPI, Depends, APIRouter - - - async def route_dependency() -> bool: ... - - - async def nested_dependency() -> str: ... - - - async def router_dependency() -> int: ... - - - async def app_dependency(data: str = Depends(nested_dependency)) -> int: ... - - - router = APIRouter(dependencies=[Depends(router_dependency)]) - app = FastAPI(dependencies=[Depends(nested_dependency)]) - app.include_router(router) - - - @app.get("/") - async def handler( - val_route: bool = Depends(route_dependency), - val_router: int = Depends(router_dependency), - val_nested: str = Depends(nested_dependency), - val_app: int = Depends(app_dependency), - ) -> None: ... - + .. literalinclude:: /examples/migrations/fastapi/di_fastapi.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, Provide, get, Router - - - async def route_dependency() -> bool: ... - - - async def nested_dependency() -> str: ... - - - async def router_dependency() -> int: ... - - - async def app_dependency(nested: str) -> int: ... - - - @get("/", dependencies={"val_route": Provide(route_dependency)}) - async def handler( - val_route: bool, val_router: int, val_nested: str, val_app: int - ) -> None: ... - - - router = Router(dependencies={"val_router": Provide(router_dependency)}) - app = Litestar( - route_handlers=[handler], - dependencies={ - "val_app": Provide(app_dependency), - "val_nested": Provide(nested_dependency), - }, - ) + .. literalinclude:: /examples/migrations/fastapi/di_litestar.py + :language: python .. seealso:: @@ -184,36 +101,15 @@ preferred way of handling this is extending :doc:`/usage/security/abstract-authe .. tab-item:: FastAPI :sync: fastapi - .. code-block:: python - - from fastapi import FastAPI, Depends, Request - - - async def authenticate(request: Request) -> None: ... - - - app = FastAPI() - - - @app.get("/", dependencies=[Depends(authenticate)]) - async def index() -> dict[str, str]: ... + .. literalinclude:: /examples/migrations/fastapi/authentication_fastapi.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get, ASGIConnection, BaseRouteHandler - - - async def authenticate( - connection: ASGIConnection, route_handler: BaseRouteHandler - ) -> None: ... - - - @get("/", guards=[authenticate]) - async def index() -> dict[str, str]: ... + .. literalinclude:: /examples/migrations/fastapi/authentication_litestar.py + :language: python .. seealso:: diff --git a/docs/migration/flask.rst b/docs/migration/flask.rst index 583138fa1b..00ace4e487 100644 --- a/docs/migration/flask.rst +++ b/docs/migration/flask.rst @@ -28,43 +28,15 @@ Routing .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask - - - app = Flask(__name__) - - - @app.route("/") - def index(): - return "Index Page" - - - @app.route("/hello") - def hello(): - return "Hello, World" + .. literalinclude:: /examples/migrations/flask/routing_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get - - - @get("/") - def index() -> str: - return "Index Page" - - - @get("/hello") - def hello() -> str: - return "Hello, World" - - - app = Litestar([index, hello]) + .. literalinclude:: /examples/migrations/flask/routing_litestar.py + :language: python Path parameters @@ -74,55 +46,15 @@ Path parameters .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask - - - app = Flask(__name__) - - - @app.route("/user/") - def show_user_profile(username): - return f"User {username}" - - - @app.route("/post/") - def show_post(post_id): - return f"Post {post_id}" - - - @app.route("/path/") - def show_subpath(subpath): - return f"Subpath {subpath}" - + .. literalinclude:: /examples/migrations/flask/path_parameters_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get - from pathlib import Path - - - @get("/user/{username:str}") - def show_user_profile(username: str) -> str: - return f"User {username}" - - - @get("/post/{post_id:int}") - def show_post(post_id: int) -> str: - return f"Post {post_id}" - - - @get("/path/{subpath:path}") - def show_subpath(subpath: Path) -> str: - return f"Subpath {subpath}" - - - app = Litestar([show_user_profile, show_post, show_subpath]) + .. literalinclude:: /examples/migrations/flask/path_parameters_litestar.py + :language: python .. seealso:: @@ -143,31 +75,15 @@ the request can be accessed through an optional parameter in the handler functio .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask, request - - - app = Flask(__name__) - - - @app.get("/") - def index(): - print(request.method) - + .. literalinclude:: /examples/migrations/flask/request_object_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get, Request - - - @get("/") - def index(request: Request) -> None: - print(request.method) + .. literalinclude:: /examples/migrations/flask/request_object_litestar.py + :language: python Request methods @@ -273,14 +189,9 @@ Like Flask, Litestar also has capabilities for serving static files, but while F will automatically serve files from a ``static`` folder, this has to be configured explicitly in Litestar. -.. code-block:: python - - from litestar import Litestar - from litestar.static_files import StaticFilesConfig +.. literalinclude:: /examples/migrations/flask/static_files.py + :language: python - app = Litestar( - [], static_files_config=[StaticFilesConfig(path="/static", directories=["static"])] - ) .. seealso:: @@ -300,40 +211,15 @@ In addition to Jinja, Litestar supports `Mako `_ .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask, render_template - - - app = Flask(__name__) - - - @app.route("/hello/") - def hello(name): - return render_template("hello.html", name=name) - + .. literalinclude:: /examples/migrations/flask/templates_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get - from litestar.contrib.jinja import JinjaTemplateEngine - from litestar.response import Template - from litestar.template.config import TemplateConfig - - - @get("/hello/{name:str}") - def hello(name: str) -> Template: - return Template(response_name="hello.html", context={"name": name}) - - - app = Litestar( - [hello], - template_config=TemplateConfig(directory="templates", engine=JinjaTemplateEngine), - ) + .. literalinclude:: /examples/migrations/flask/templates_litestar.py + :language: python .. seealso:: @@ -349,50 +235,15 @@ Setting cookies and headers .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask, make_response - - - app = Flask(__name__) - - - @app.get("/") - def index(): - response = make_response("hello") - response.set_cookie("my-cookie", "cookie-value") - response.headers["my-header"] = "header-value" - return response - + .. literalinclude:: /examples/migrations/flask/cookies_headers_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get, Response - from litestar.datastructures import ResponseHeader, Cookie - - - @get( - "/static", - response_headers={"my-header": ResponseHeader(value="header-value")}, - response_cookies=[Cookie("my-cookie", "cookie-value")], - ) - def static() -> str: - # you can set headers and cookies when defining handlers - ... - - - @get("/dynamic") - def dynamic() -> Response[str]: - # or dynamically, by returning an instance of Response - return Response( - "hello", - headers={"my-header": "header-value"}, - cookies=[Cookie("my-cookie", "cookie-value")], - ) + .. literalinclude:: /examples/migrations/flask/cookies_headers_litestar.py + :language: python .. seealso:: @@ -412,45 +263,15 @@ For redirects, instead of ``redirect`` use ``Redirect``: .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask, redirect, url_for - - - app = Flask(__name__) - - - @app.get("/") - def index(): - return "hello" - - - @app.get("/hello") - def hello(): - return redirect(url_for("index")) - + .. literalinclude:: /examples/migrations/flask/redirects_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get - from litestar.response import Redirect - - - @get("/") - def index() -> str: - return "hello" - - - @get("/hello") - def hello() -> Redirect: - return Redirect(path="index") - - - app = Litestar([index, hello]) + .. literalinclude:: /examples/migrations/flask/redirects_litestar.py + :language: python Raising HTTP errors @@ -463,35 +284,15 @@ Instead of using the ``abort`` function, raise an ``HTTPException``: .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask, abort - - - app = Flask(__name__) - - - @app.get("/") - def index(): - abort(400, "this did not work") - + .. literalinclude:: /examples/migrations/flask/errors_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get - from litestar.exceptions import HTTPException - - - @get("/") - def index() -> None: - raise HTTPException(status_code=400, detail="this did not work") - - - app = Litestar([index]) + .. literalinclude:: /examples/migrations/flask/errors_litestar.py + :language: python .. seealso:: @@ -507,39 +308,15 @@ Setting status codes .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask - - - app = Flask(__name__) - - - @app.get("/") - def index(): - return "not found", 404 - + .. literalinclude:: /examples/migrations/flask/status_codes_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get, Response - - - @get("/static", status_code=404) - def static_status() -> str: - return "not found" - - - @get("/dynamic") - def dynamic_status() -> Response[str]: - return Response("not found", status_code=404) - - - app = Litestar([static_status, dynamic_status]) + .. literalinclude:: /examples/migrations/flask/status_codes_litestar.py + :language: python Serialization @@ -554,54 +331,15 @@ the data returned is intended to be serialized into JSON and will do so unless t .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask, Response - - - app = Flask(__name__) - - - @app.get("/json") - def get_json(): - return {"hello": "world"} - - - @app.get("/text") - def get_text(): - return "hello, world!" - - - @app.get("/html") - def get_html(): - return Response("hello, world", mimetype="text/html") - + .. literalinclude:: /examples/migrations/flask/serialization_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, get, MediaType - - - @get("/json") - def get_json() -> dict[str, str]: - return {"hello": "world"} - - - @get("/text", media_type=MediaType.TEXT) - def get_text() -> str: - return "hello, world" - - - @get("/html", media_type=MediaType.HTML) - def get_html() -> str: - return "hello, world" - - - app = Litestar([get_json, get_text, get_html]) + .. literalinclude:: /examples/migrations/flask/serialization_litestar.py + :language: python Error handling @@ -612,33 +350,15 @@ Error handling .. tab-item:: Flask :sync: flask - .. code-block:: python - - from flask import Flask - from werkzeug.exceptions import HTTPException - - - app = Flask(__name__) - - - @app.errorhandler(HTTPException) - def handle_exception(e): ... - + .. literalinclude:: /examples/migrations/flask/error_handling_flask.py + :language: python .. tab-item:: Litestar :sync: litestar - .. code-block:: python - - from litestar import Litestar, Request, Response - from litestar.exceptions import HTTPException - - - def handle_exception(request: Request, exception: Exception) -> Response: ... - - - app = Litestar([], exception_handlers={HTTPException: handle_exception}) + .. literalinclude:: /examples/migrations/flask/error_handling_litestar.py + :language: python .. seealso:: diff --git a/docs/release-notes/whats-new-2.rst b/docs/release-notes/whats-new-2.rst index 6e6415e74e..35a36e4a83 100644 --- a/docs/release-notes/whats-new-2.rst +++ b/docs/release-notes/whats-new-2.rst @@ -240,31 +240,14 @@ plain :class:`Mapping[str, str] `. The typing of :class:`ResponseHeader <.datastructures.response_header.ResponseHeader>` was also changed to be more strict and now only allows string values. -.. code-block:: python +.. literalinclude:: /examples/whats_new/response_headers_v1.py :caption: 1.51 - - from starlite import ResponseHeader, get - - - @get(response_headers={"my-header": ResponseHeader(value="header-value")}) - async def handler() -> str: ... + :language: python -.. code-block:: python +.. literalinclude:: /examples/whats_new/response_headers_v2.py :caption: 2.x - - 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: ... + :language: python Response cookies @@ -273,17 +256,16 @@ Response cookies Response cookies might now also be set using a :class:`Mapping[str, str] `, analogous to `Response headers`_. -.. code-block:: python +.. literalinclude:: /examples/whats_new/response_cookies_v2.py + :caption: 1.51 + :language: python - @get("/", response_cookies=[Cookie(key="foo", value="bar")]) - async def handler() -> None: ... is equivalent to -.. code-block:: python - - @get("/", response_cookies={"foo": "bar"}) - async def handler() -> None: ... +.. literalinclude:: /examples/whats_new/response_cookies_v2.py + :caption: 2.x + :language: python SQLAlchemy Plugin @@ -336,17 +318,14 @@ The 2 argument for of ``before_send`` hook handlers has been removed. Existing h should be changed to include an additional ``scope`` parameter. -.. code-block:: python +.. literalinclude:: /examples/whats_new/before_send_v1.py :caption: 1.51 - - async def before_send(message: Message, state: State) -> None: ... + :language: python -.. code-block:: python +.. literalinclude:: /examples/whats_new/before_send_v2.py :caption: 2.x - - async def before_send(message: Message, state: State, scope: Scope) -> None: ... - + :language: python .. seealso:: @@ -363,17 +342,16 @@ with a ``state`` keyword argument, accepting an optional Existing code using this keyword argument will need to be changed from -.. code-block:: python +.. literalinclude:: /examples/whats_new/initial_state_v1.py :caption: 1.51 + :language: python - app = Starlite(..., initial_state={"some": "key"}) to -.. code-block:: python +.. literalinclude:: /examples/whats_new/initial_state_v2.py :caption: 2.x - - app = Litestar(..., state=State({"some": "key"})) + :language: python Stores @@ -444,28 +422,8 @@ and can be used to define DTOs: For example, to define a DTO from a dataclass: -.. code-block:: python - - 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) - +.. literalinclude:: /examples/data_transfer_objects/define_dto_from_dataclass.py + :language: python .. literalinclude:: /examples/data_transfer_objects/the_return_dto_parameter.py :language: python @@ -503,19 +461,15 @@ their first parameter. If your ``on_startup`` and ``on_shutdown`` hooks made use application state, they will now have to access it through the provided application instance. -.. code-block:: python +.. literalinclude:: /examples/whats_new/lifespan_hook_v1.py + :language: python :caption: 1.51 - def on_startup(state: State) -> None: - print(state.something) - -.. code-block:: python +.. literalinclude:: /examples/whats_new/lifespan_hook_v2.py + :language: python :caption: 2.x - def on_startup(app: Litestar) -> None: - print(app.state.something) - Dependencies without ``Provide`` -------------------------------- @@ -524,21 +478,16 @@ Dependencies may now be declared without :class:`~litestar.di.Provide`, by passi callable directly. This can be advantageous in places where the configuration options of :class:`~litestar.di.Provide` are not needed. -.. code-block:: python - - async def some_dependency() -> str: ... - +.. literalinclude:: /examples/whats_new/dependencies_without_provide_v1.py + :language: python + :caption: 1.51 - app = Litestar(dependencies={"some": Provide(some_dependency)}) is equivalent to -.. code-block:: python - - async def some_dependency() -> str: ... - - - app = Litestar(dependencies={"some": some_dependency}) +.. literalinclude:: /examples/whats_new/dependencies_without_provide_v2.py + :language: python + :caption: 2.x ``sync_to_thread`` @@ -555,27 +504,15 @@ a thread pool, passing ``sync_to_thread=False`` will also silence the warning. ``LITESTAR_WARN_IMPLICIT_SYNC_TO_THREAD=0`` -.. code-block:: python +.. literalinclude:: /examples/whats_new/sync_to_thread_v1.py + :language: python :caption: 1.51 - @get() - def handler() -> None: ... - -.. code-block:: python - :caption: 2.x - - @get(sync_to_thread=False) - def handler() -> None: ... - -or - -.. code-block:: python +.. literalinclude:: /examples/whats_new/sync_to_thread_v2.py + :language: python :caption: 2.x - @get(sync_to_thread=True) - def handler() -> None: ... - .. seealso:: The :doc:`/topics/sync-vs-async` topic guide @@ -626,17 +563,9 @@ handlers, OOP based event dispatching, data iterators and more. .. literalinclude:: /examples/websockets/with_dto.py :language: python -.. code-block:: python +.. literalinclude:: /examples/websockets/receiving_json_and_sending_it_back_as_messagepack.py :caption: Receiving JSON and sending it back as MessagePack - - 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) + :language: python .. seealso:: @@ -659,15 +588,14 @@ TBD :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]: ... +.. literalinclude:: /examples/whats_new/attrs_v1.py + :language: python + :caption: 1.51 -.. code-block:: python - @get("/") - def index(param: Annotated[int, Parameter(gt=5)]) -> dict[str, int]: ... +.. literalinclude:: /examples/whats_new/attrs_v2.py + :language: python + :caption: 2.x Channels diff --git a/docs/topics/deployment/docker.rst b/docs/topics/deployment/docker.rst index 6d3412dffc..c50e339f0b 100644 --- a/docs/topics/deployment/docker.rst +++ b/docs/topics/deployment/docker.rst @@ -41,31 +41,10 @@ files in your project directory: litestar[standard]>=2.4.0,<3.0.0 -.. code-block:: python +.. literalinclude:: /examples/deployment/docker.py :caption: app.py + :language: python - """Minimal Litestar application.""" - - from asyncio import sleep - from typing import Any, Dict - - from litestar import Litestar, get - - - @get("/") - async def async_hello_world() -> Dict[str, Any]: - """Route Handler that outputs hello world.""" - await sleep(0.1) - return {"hello": "world"} - - - @get("/sync", sync_to_thread=False) - def sync_hello_world() -> Dict[str, Any]: - """Route Handler that outputs hello world.""" - return {"hello": "world"} - - - app = Litestar(route_handlers=[sync_hello_world, async_hello_world]) Dockerfile ---------- diff --git a/docs/tutorials/todo-app/0-application-basics.rst b/docs/tutorials/todo-app/0-application-basics.rst index 0cad6f6167..d9dc15f920 100644 --- a/docs/tutorials/todo-app/0-application-basics.rst +++ b/docs/tutorials/todo-app/0-application-basics.rst @@ -74,13 +74,9 @@ Litestar that you only want to use this function when a ``GET`` request is being and replaces it with the return value of the decorator function. Without the decorator, the example would look like this: - .. code-block:: python + .. literalinclude:: /examples/todo_app/hello_world_2.py + :language: python - async def hello_world() -> str: - return "Hello, world!" - - - hello_world = get("/")(hello_world) For an in-depth explanation of decorators, you can read this excellent Real Python article: `Primer on Python Decorators `_ diff --git a/docs/tutorials/todo-app/2-interacting-with-the-list.rst b/docs/tutorials/todo-app/2-interacting-with-the-list.rst index cdd01f7b6d..2eaedd2092 100644 --- a/docs/tutorials/todo-app/2-interacting-with-the-list.rst +++ b/docs/tutorials/todo-app/2-interacting-with-the-list.rst @@ -79,11 +79,8 @@ specific item on the list is needed. This could be done using query parameters, there's an easier, and more semantically coherent way of expressing this: path parameters. -.. code-block:: python - - @get("/{name:str}") - async def greeter(name: str) -> str: - return "Hello, " + name +.. literalinclude:: /examples/todo_app/create/dynamic_route.py + :language: python So far all the paths in your application are static, meaning they are expressed by a diff --git a/docs/usage/applications.rst b/docs/usage/applications.rst index c0ff1e367c..3400956761 100644 --- a/docs/usage/applications.rst +++ b/docs/usage/applications.rst @@ -16,6 +16,7 @@ or :class:`Route handlers <.handlers.BaseRouteHandler>`: .. literalinclude:: /examples/hello_world.py :caption: A simple Hello World Litestar app + :language: python The app instance is the root level of the app - it has the base path of ``/`` and all root level :class:`Controllers <.controller.Controller>`, :class:`Routers <.router.Router>`, @@ -50,6 +51,7 @@ establish the connection, and another to close it, and then pass them to the :cl .. literalinclude:: /examples/startup_and_shutdown.py :caption: Startup and Shutdown + :language: python .. _lifespan-context-managers: @@ -62,6 +64,7 @@ keep a certain context object, such as a connection, around. .. literalinclude:: /examples/application_hooks/lifespan_manager.py :caption: Handling a database connection + :language: python Order of execution ------------------ @@ -73,10 +76,10 @@ shutdown hooks are invoked. Consider the case where there are two lifespan context managers ``ctx_a`` and ``ctx_b`` as well as two shutdown hooks ``hook_a`` and ``hook_b`` as shown in the following code: -.. code-block:: python +.. literalinclude:: /examples/execution_order.py :caption: Example of multiple :term:`context managers ` and shutdown hooks + :language: python - app = Litestar(lifespan=[ctx_a, ctx_b], on_shutdown=[hook_a, hook_b]) During shutdown, they are executed in the following order: @@ -117,6 +120,7 @@ of the application, as seen below: .. literalinclude:: /examples/application_state/using_application_state.py :caption: Using Application State + :language: python .. _Initializing Application State: @@ -128,6 +132,7 @@ To seed application state, you can pass a :class:`~.datastructures.state.State` .. literalinclude:: /examples/application_state/passing_initial_state.py :caption: Using Application State + :language: python .. note:: :class:`~.datastructures.state.State` can be initialized with a :class:`dictionary `, an instance of :class:`~.datastructures.state.ImmutableState` or :class:`~.datastructures.state.State`, @@ -145,15 +150,10 @@ Injecting Application State into Route Handlers and Dependencies As seen in the above example, Litestar offers an easy way to inject state into route handlers and dependencies - simply by specifying ``state`` as a kwarg to the handler or dependency function. For example: -.. code-block:: python +.. literalinclude:: /examples/application_state/injecting_application_state.py :caption: Accessing application :class:`~.datastructures.state.State` in a handler function + :language: python - from litestar import get - from litestar.datastructures import State - - - @get("/") - def handler(state: State) -> None: ... When using this pattern you can specify the class to use for the state object. This type is not merely for type checkers, rather Litestar will instantiate a new ``state`` instance based on the type you set there. @@ -167,6 +167,8 @@ You can use this class to type state and ensure that no mutation of state is all .. literalinclude:: /examples/application_state/using_immutable_state.py :caption: Using Custom State to ensure immutability + :language: python + Application Hooks ----------------- @@ -188,6 +190,7 @@ the ``exception`` that occurred and the ASGI ``scope`` of the request or websock .. literalinclude:: /examples/application_hooks/after_exception_hook.py :caption: After Exception Hook + :language: python .. attention:: This hook is not meant to handle exceptions - it just receives them to allow for side effects. To handle exceptions you should define :ref:`exception handlers `. @@ -201,6 +204,7 @@ sent. The hook receives the message instance and the ASGI ``scope``. .. literalinclude:: /examples/application_hooks/before_send_hook.py :caption: Before Send Hook + :language: python Initialization ^^^^^^^^^^^^^^ @@ -219,6 +223,7 @@ develop third-party application configuration systems. .. literalinclude:: /examples/application_hooks/on_app_init.py :caption: Example usage of the ``on_app_init`` hook to modify the application configuration. + :language: python .. _layered-architecture: diff --git a/docs/usage/caching.rst b/docs/usage/caching.rst index 7e3924b5ae..ca6d097e32 100644 --- a/docs/usage/caching.rst +++ b/docs/usage/caching.rst @@ -7,13 +7,10 @@ Caching responses Sometimes it's desirable to cache some responses, especially if these involve expensive calculations, or when polling is expected. Litestar comes with a simple mechanism for caching: -.. code-block:: python +.. literalinclude:: /examples/caching/caching_response.py + :caption: Caching response + :language: python - from litestar import get - - - @get("/cached-path", cache=True) - def my_cached_handler() -> str: ... By setting ``cache=True`` in the route handler, caching for the route handler will be enabled for the :attr:`ResponseCacheConfig.default_expiration <.config.response_cache.ResponseCacheConfig.default_expiration>`. @@ -25,26 +22,17 @@ By setting ``cache=True`` in the route handler, caching for the route handler wi Alternatively you can specify the number of seconds to cache the responses from the given handler like so: -.. code-block:: python - - from litestar import get - - - @get("/cached-path", cache=120) # seconds - def my_cached_handler() -> str: ... +.. literalinclude:: /examples/caching/caching_duration.py + :caption: Caching with specific duration + :language: python If you want the response to be cached indefinitely, you can pass the :class:`.config.response_cache.CACHE_FOREVER` sentinel instead: -.. code-block:: python - - from litestar import get - from litestar.config.response_cache import CACHE_FOREVER - - - @get("/cached-path", cache=CACHE_FOREVER) # seconds - def my_cached_handler() -> str: ... +.. literalinclude:: /examples/caching/caching_forever.py + :caption: Caching forever + :language: python Configuration @@ -60,14 +48,9 @@ Changing where data is stored By default, caching will use the :class:`MemoryStore <.stores.memory.MemoryStore>`, but it can be configured with any :class:`Store <.stores.base.Store>`, for example :class:`RedisStore <.stores.redis.RedisStore>`: -.. code-block:: python - - from litestar.config.cache import ResponseCacheConfig - from litestar.stores.redis import RedisStore - - redis_store = RedisStore(url="redis://localhost/", port=6379, db=0) - - cache_config = ResponseCacheConfig(store=redis_store) +.. literalinclude:: /examples/caching/caching_storage_redis.py + :caption: Caching with redis + :language: python Specifying a cache key builder @@ -76,27 +59,11 @@ Specifying a cache key builder Litestar uses the request's path + sorted query parameters as the cache key. This can be adjusted by providing a "key builder" function, either at application or route handler level. -.. code-block:: python - - from litestar import Litestar, Request - from litestar.config.cache import ResponseCacheConfig - - - def key_builder(request: Request) -> str: - return request.url.path + request.headers.get("my-header", "") - - - app = Litestar([], cache_config=ResponseCacheConfig(key_builder=key_builder)) - - -.. code-block:: python - - from litestar import Litestar, Request, get - - - def key_builder(request: Request) -> str: - return request.url.path + request.headers.get("my-header", "") +.. literalinclude:: /examples/caching/caching_key_builder.py + :caption: Caching key builder + :language: python - @get("/cached-path", cache=True, cache_key_builder=key_builder) - def cached_handler() -> str: ... +.. literalinclude:: /examples/caching/caching_key_builder_specific_route.py + :caption: Caching key builder for a specific route + :language: python diff --git a/docs/usage/channels.rst b/docs/usage/channels.rst index 0f6e70ee36..1c4a98e6dc 100644 --- a/docs/usage/channels.rst +++ b/docs/usage/channels.rst @@ -121,20 +121,14 @@ The channels managed by the plugin can be either defined upfront, passing them t channel) by setting ``arbitrary_channels_allowed=True``. -.. code-block:: python +.. literalinclude:: /examples/channels/passing_channels_explicitly.py :caption: Passing channels explicitly - - from litestar.channels import ChannelsPlugin - - channels_plugin = ChannelsPlugin(..., channels=["foo", "bar"]) + :language: python -.. code-block:: python +.. literalinclude:: /examples/channels/allowing_arbitrary_channels.py :caption: Allowing arbitrary channels - - from litestar.channels import ChannelsPlugin - - channels_plugin = ChannelsPlugin(..., arbitrary_channels_allowed=True) + :language: python If ``arbitrary_channels_allowed`` is not ``True``, trying to publish or subscribe to a @@ -148,9 +142,9 @@ Publishing data One of the core aspects of the plugin is publishing data, which is done through its :meth:`publish ` method: -.. code-block:: python - - channels.publish({"message": "Hello"}, "general") +.. literalinclude:: /examples/channels/publish_data.py + :caption: Publish data + :language: python The above example will publish the data to the channel ``general``, subsequently putting @@ -194,43 +188,29 @@ unsubscribed. Using the ``subscriber`` and ``unsubscribe`` methods directly shou be done when a context manager cannot be used, e.g. when the subscription would span different contexts. - -.. code-block:: python +.. literalinclude:: /examples/channels/subscribe_method_manually.py :caption: Calling the subscription methods manually - - subscriber = await channels.subscribe(["foo", "bar"]) - try: - ... # do some stuff here - finally: - await channels.unsubscribe(subscriber) - + :language: python -.. code-block:: python +.. literalinclude:: /examples/channels/subscribe_method_manually.py :caption: Using the context manager - - async with channels.start_subscription(["foo", "bar"]) as subscriber: - ... # do some stuff here + :language: python It is also possible to unsubscribe from individual channels, which may be desirable if subscriptions need to be managed dynamically. -.. code-block:: python - - subscriber = await channels.subscribe(["foo", "bar"]) - ... # do some stuff here - await channels.unsubscribe(subscriber, ["foo"]) +.. literalinclude:: /examples/channels/unsubscribe_method_manually.py + :caption: Calling the unsubscription methods manually + :language: python Or, using the context manager -.. code-block:: python - - async with channels.start_subscription(["foo", "bar"]) as subscriber: - ... # do some stuff here - await channels.unsubscribe(subscriber, ["foo"]) - +.. literalinclude:: /examples/channels/subscribe_method_context_manager.py + :caption: Using the context manager + :language: python Managing history @@ -362,30 +342,14 @@ The channels plugin provides two different strategies for managing this backpres added while the backlog is full -.. code-block:: python +.. literalinclude:: /examples/channels/backoff_strategy.py :caption: Backoff strategy - - from litestar.channels import ChannelsPlugin - from litestar.channels.memory import MemoryChannelsBackend - - channels = ChannelsPlugin( - backend=MemoryChannelsBackend(), - max_backlog=1000, - backlog_strategy="backoff", - ) + :language: python -.. code-block:: python +.. literalinclude:: /examples/channels/eviction_strategy.py :caption: Eviction strategy - - from litestar.channels import ChannelsPlugin - from litestar.channels.memory import MemoryChannelsBackend - - channels = ChannelsPlugin( - backend=MemoryChannelsBackend(), - max_backlog=1000, - backlog_strategy="dropleft", - ) + :language: python Backends diff --git a/docs/usage/cli.rst b/docs/usage/cli.rst index c1de49b3fe..d915abdbd0 100644 --- a/docs/usage/cli.rst +++ b/docs/usage/cli.rst @@ -87,9 +87,8 @@ run The ``run`` command executes a Litestar application using `uvicorn `_. -.. code-block:: shell - - litestar run +.. literalinclude:: /examples/cli/run.sh + :language: shell .. caution:: @@ -143,15 +142,15 @@ Options The ``--reload-dir`` flag allows you to specify directories to watch for changes. If you specify this flag, the ``--reload`` flag is implied. You can specify multiple directories by passing the flag multiple times: -.. code-block:: shell +.. literalinclude:: /examples/cli/reload_dir.sh + :language: shell - litestar run --reload-dir=. --reload-dir=../other-library/src To set multiple directories via an environment variable, use a comma-separated list: -.. code-block:: shell +.. literalinclude:: /examples/cli/reload_dir_multiple_directories.sh + :language: shell - LITESTAR_RELOAD_DIRS=.,../other-library/src --reload-include ++++++++++++++++ @@ -160,15 +159,14 @@ The ``--reload-include`` flag allows you to specify glob patterns to include whe You can specify multiple glob patterns by passing the flag multiple times: -.. code-block:: shell - - litestar run --reload-include="*.rst" --reload-include="*.yml" +.. literalinclude:: /examples/cli/reload_include.sh + :language: shell To set multiple directories via an environment variable, use a comma-separated list: -.. code-block:: shell +.. literalinclude:: /examples/cli/reload_include_multiple_directories.sh + :language: shell - LITESTAR_RELOAD_INCLUDES=*.rst,*.yml --reload-exclude ++++++++++++++++ @@ -177,31 +175,30 @@ The ``--reload-exclude`` flag allows you to specify glob patterns to exclude whe You can specify multiple glob patterns by passing the flag multiple times: -.. code-block:: shell - - litestar run --reload-exclude="*.py" --reload-exclude="*.yml" +.. literalinclude:: /examples/cli/reload_exclude.sh + :language: shell To set multiple directories via an environment variable, use a comma-separated list: -.. code-block:: shell +.. literalinclude:: /examples/cli/reload_exclude_multiple_directories.sh + :language: shell - LITESTAR_RELOAD_EXCLUDES=*.py,*.yml SSL +++ You can pass paths to an SSL certificate and it's private key to run the server using the HTTPS protocol: -.. code-block:: shell +.. literalinclude:: /examples/cli/ssl.sh + :language: shell - litestar run --ssl-certfile=certs/cert.pem --ssl-keyfile=certs/key.pem Both flags must be provided and both files must exist. These are then passed to ``uvicorn``. You can also use the ``--create-self-signed-cert`` flag: -.. code-block:: shell +.. literalinclude:: /examples/cli/ssl_self_signed.sh + :language: shell - litestar run --ssl-certfile=certs/cert.pem --ssl-keyfile=certs/key.pem --create-self-signed-cert This way, if the given files don't exist, a self-signed certificate and a passwordless key will be generated. If the files are found, they will be reused. @@ -211,9 +208,8 @@ info The ``info`` command displays useful information about the selected application and its configuration. -.. code-block:: shell - - litestar info +.. literalinclude:: /examples/cli/info.sh + :language: shell .. image:: /images/cli/litestar_info.png @@ -225,9 +221,9 @@ routes The ``routes`` command displays a tree view of the routing table. -.. code-block:: shell +.. literalinclude:: /examples/cli/routes.sh + :language: shell - litestar routes Options ~~~~~~~ @@ -257,18 +253,18 @@ delete The ``delete`` subcommand deletes a specific session from the backend. -.. code-block:: shell +.. literalinclude:: /examples/cli/sessions_delete.sh + :language: shell - litestar sessions delete cc3debc7-1ab6-4dc8-a220-91934a473717 clear ~~~~~ The `clear` subcommand is used to remove all sessions from the backend. -.. code-block:: shell +.. literalinclude:: /examples/cli/sessions_clear.sh + :language: shell - litestar sessions clear openapi ^^^^^^^ @@ -282,9 +278,9 @@ The `schema` subcommand generates OpenAPI specifications from the Litestar appli JSON or YAML. The serialization format depends on the filename, which is by default `openapi_schema.json`. You can specify a different filename using the `--output` flag. For example: -.. code-block:: shell +.. literalinclude:: /examples/cli/openapi_schema.sh + :language: shell - litestar schema openapi --output my-specs.yml typescript ~~~~~~~~~~ @@ -292,37 +288,35 @@ typescript The `typescript` subcommand generates TypeScript definitions from the Litestar application's OpenAPI specifications. For example: -.. code-block:: shell - litestar schema typescript +.. literalinclude:: /examples/cli/typescript_schema.sh + :language: shell + By default, this command outputs a file called `api-specs.ts`. You can change this using the `--output` option: -.. code-block:: shell - litestar schema typescript --output my-types.ts +.. literalinclude:: /examples/cli/typescript_schema_path.sh + :language: shell + You can also specify the top-level TypeScript namespace that will be created, which is `API` by default: -.. code-block:: typescript +.. literalinclude:: /examples/cli/typescript_schema.ts + :language: typescript - export namespace API { - // ... - } To do this, use the `--namespace` option: -.. code-block:: shell +.. literalinclude:: /examples/cli/typescript_schema_namespace.sh + :language: shell - litestar schema typescript --namespace MyNamespace This will result in: -.. code-block:: typescript +.. literalinclude:: /examples/cli/typescript_schema_namespace.ts + :language: typescript - export namespace MyNamespace { - // ... - } Extending the CLI ----------------- @@ -343,40 +337,24 @@ entries should point to a :class:`click.Command` or :class:`click.Group`: .. tab-item:: setup.py - .. code-block:: python - - from setuptools import setup + .. literalinclude:: /examples/cli/entry_points.py + :language: typescript - setup( - name="my-litestar-plugin", - ..., - entry_points={ - "litestar.commands": ["my_command=my_litestar_plugin.cli:main"], - }, - ) .. tab-item:: pdm - .. code-block:: toml + .. literalinclude:: /examples/cli/pdm.toml :caption: Using `PDM `_ + :language: toml - [project.scripts] - my_command = "my_litestar_plugin.cli:main" - - # Or, as an entrypoint: - - [project.entry-points."litestar.commands"] - my_command = "my_litestar_plugin.cli:main" .. tab-item:: Poetry - .. code-block:: toml + .. literalinclude:: /examples/cli/poetry.toml :caption: Using `Poetry `_ + :language: toml - [tool.poetry.plugins."litestar.commands"] - my_command = "my_litestar_plugin.cli:main" - Using a plugin ^^^^^^^^^^^^^^ @@ -386,21 +364,8 @@ A plugin extending the CLI can be created using the initialization of the CLI, and receive the root :class:`click.Group` as its first argument, which can then be used to add or override commands: -.. code-block:: python - - from litestar import Litestar - from litestar.plugins import CLIPluginProtocol - from click import Group - - - class CLIPlugin(CLIPluginProtocol): - def on_cli_init(self, cli: Group) -> None: - @cli.command() - def is_debug_mode(app: Litestar): - print(app.debug) - - - app = Litestar(plugins=[CLIPlugin()]) +.. literalinclude:: /examples/cli/plugin.py + :language: python Accessing the app instance @@ -410,14 +375,9 @@ When extending the Litestar CLI, you will most likely need access to the loaded You can achieve this by adding the special ``app`` parameter to your CLI functions. This will cause the ``Litestar`` instance to be injected into the function whenever it is called from a click-context. -.. code-block:: python - - import click - from litestar import Litestar - +.. literalinclude:: /examples/cli/app_instance.py + :language: python - @click.command() - def my_command(app: Litestar) -> None: ... CLI Reference ------------- diff --git a/docs/usage/debugging.rst b/docs/usage/debugging.rst index 50536e306a..121907a267 100644 --- a/docs/usage/debugging.rst +++ b/docs/usage/debugging.rst @@ -8,9 +8,8 @@ You can configure Litestar to drop into the :doc:`Python Debugger bool: ... - - - async def dict_fn() -> dict: ... - - - async def list_fn() -> list: ... - - - async def int_fn() -> int: ... - - - class MyController(Controller): - path = "/controller" - # on the controller - dependencies = {"controller_dependency": Provide(list_fn)} - - # on the route handler - @get(path="/handler", dependencies={"local_dependency": Provide(int_fn)}) - def my_route_handler( - self, - app_dependency: bool, - router_dependency: dict, - controller_dependency: list, - local_dependency: int, - ) -> None: ... - - # on the router - - - my_router = Router( - path="/router", - dependencies={"router_dependency": Provide(dict_fn)}, - route_handlers=[MyController], - ) +.. literalinclude:: /examples/dependency_injection/dependency_base.py + :caption: Dependency base example + :language: python - # on the app - app = Litestar( - route_handlers=[my_router], dependencies={"app_dependency": Provide(bool_fn)} - ) The above example illustrates how dependencies are declared on the different layers of the application. @@ -99,21 +57,17 @@ A basic example ~~~~~~~~~~~~~~~ .. literalinclude:: /examples/dependency_injection/dependency_yield_simple.py - :caption: dependencies.py + :caption: Dependency yield simple :language: python If you run the code you'll see that ``CONNECTION`` has been reset after the handler function returned: -.. code-block:: python - - from litestar.testing import TestClient - from dependencies import app, CONNECTION +.. literalinclude:: /examples/dependency_injection/dependency_connection.py + :caption: Dependency connection + :language: python - with TestClient(app=app) as client: - print(client.get("/").json()) # {"open": True} - print(CONNECTION) # {"open": False} Handling exceptions ~~~~~~~~~~~~~~~~~~~ @@ -124,23 +78,13 @@ of the dependency based on exceptions, for example rolling back a database sessi and committing otherwise. .. literalinclude:: /examples/dependency_injection/dependency_yield_exceptions.py - :caption: dependencies.py + :caption: Dependency yield exceptions :language: python -.. code-block:: python - - from litestar.testing import TestClient - from dependencies import STATE, app - - with TestClient(app=app) as client: - response = client.get("/John") - print(response.json()) # {"John": "hello"} - print(STATE) # {"result": "OK", "connection": "closed"} - - response = client.get("/Peter") - print(response.status_code) # 500 - print(STATE) # {"result": "error", "connection": "closed"} +.. literalinclude:: /examples/dependency_injection/dependency_yield_exceptions_state.py + :caption: Dependency yield exceptions states + :language: python .. admonition:: Best Practice @@ -150,13 +94,9 @@ and committing otherwise. want to handle exceptions, to ensure that the cleanup code is run even when exceptions occurred: - .. code-block:: python - - def generator_dependency(): - try: - yield - finally: - ... # cleanup code + .. literalinclude:: /examples/dependency_injection/dependency_yield_exceptions_trap.py + :caption: Dependency with try/finally + :language: python .. attention:: @@ -176,27 +116,10 @@ injected into them. In fact, you can inject the same data that you can :ref:`inject into route handlers `. -.. code-block:: python - - from litestar import Controller, patch - from litestar.di import Provide - from pydantic import BaseModel, UUID4 - - - class User(BaseModel): - id: UUID4 - name: str - - - async def retrieve_db_user(user_id: UUID4) -> User: ... - - - class UserController(Controller): - path = "/user" - dependencies = {"user": Provide(retrieve_db_user)} +.. literalinclude:: /examples/dependency_injection/dependency_keyword_arguments.py + :caption: Dependency keyword arguments + :language: python - @patch(path="/{user_id:uuid}") - async def get_user(self, user: User) -> User: ... In the above example we have a ``User`` model that we are persisting into a db. The model is fetched using the helper method ``retrieve_db_user`` which receives a ``user_id`` kwarg and retrieves the corresponding ``User`` instance. @@ -212,29 +135,10 @@ Dependency overrides Because dependencies are declared at each level of the app using a string keyed dictionary, overriding dependencies is very simple: -.. code-block:: python - - from litestar import Controller, get - from litestar.di import Provide - - - def bool_fn() -> bool: ... - - - def dict_fn() -> dict: ... - - - class MyController(Controller): - path = "/controller" - # on the controller - dependencies = {"some_dependency": Provide(dict_fn)} +.. literalinclude:: /examples/dependency_injection/dependency_overrides.py + :caption: Dependency overrides + :language: python - # on the route handler - @get(path="/handler", dependencies={"some_dependency": Provide(bool_fn)}) - def my_route_handler( - self, - some_dependency: bool, - ) -> None: ... The lower scoped route handler function declares a dependency with the same key as the one declared on the higher scoped controller. The lower scoped dependency therefore overrides the higher scoped one. @@ -246,26 +150,9 @@ The ``Provide`` class The :class:`Provide <.di.Provide>` class is a wrapper used for dependency injection. To inject a callable you must wrap it in ``Provide``: -.. code-block:: python - - from random import randint - from litestar import get - from litestar.di import Provide - - - def my_dependency() -> int: - return randint(1, 10) - - - @get( - "/some-path", - dependencies={ - "my_dep": Provide( - my_dependency, - ) - }, - ) - def my_handler(my_dep: int) -> None: ... +.. literalinclude:: /examples/dependency_injection/dependency_provide.py + :caption: Dependency with Provide + :language: python .. attention:: @@ -282,33 +169,10 @@ Dependencies within dependencies You can inject dependencies into other dependencies - exactly like you would into regular functions. -.. code-block:: python - - from litestar import Litestar, get - from litestar.di import Provide - from random import randint - - - def first_dependency() -> int: - return randint(1, 10) - - - def second_dependency(injected_integer: int) -> bool: - return injected_integer % 2 == 0 - - - @get("/true-or-false") - def true_or_false_handler(injected_bool: bool) -> str: - return "its true!" if injected_bool else "nope, its false..." - +.. literalinclude:: /examples/dependency_injection/dependency_within_dependency.py + :caption: Dependencies within dependencies + :language: python - app = Litestar( - route_handlers=[true_or_false_handler], - dependencies={ - "injected_integer": Provide(first_dependency), - "injected_bool": Provide(second_dependency), - }, - ) .. note:: diff --git a/docs/usage/dto/0-basic-use.rst b/docs/usage/dto/0-basic-use.rst index 46693489e6..24bbbc855e 100644 --- a/docs/usage/dto/0-basic-use.rst +++ b/docs/usage/dto/0-basic-use.rst @@ -99,45 +99,21 @@ Enabling the backend You can enable this backend globally for all DTOs by passing the appropriate feature flag to your Litestar application: -.. code-block:: python - - from litestar import Litestar - from litestar.config.app import ExperimentalFeatures - - app = Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN]) +.. literalinclude:: /examples/data_transfer_objects/enabling_backend.py + :language: python or selectively for individual DTOs: -.. code-block:: python - - from dataclasses import dataclass - from litestar.dto import DTOConfig, DataclassDTO - - - @dataclass - class Foo: - name: str +.. literalinclude:: /examples/data_transfer_objects/individual_dto.py + :language: python - class FooDTO(DataclassDTO[Foo]): - config = DTOConfig(experimental_codegen_backend=True) The same flag can be used to disable the backend selectively: -.. code-block:: python - - from dataclasses import dataclass - from litestar.dto import DTOConfig, DataclassDTO - - - @dataclass - class Foo: - name: str - - - class FooDTO(DataclassDTO[Foo]): - config = DTOConfig(experimental_codegen_backend=False) +.. literalinclude:: /examples/data_transfer_objects/disable_backend_selectively.py + :language: python Performance improvements diff --git a/docs/usage/dto/1-abstract-dto.rst b/docs/usage/dto/1-abstract-dto.rst index 346141a573..eb71e5c4d4 100644 --- a/docs/usage/dto/1-abstract-dto.rst +++ b/docs/usage/dto/1-abstract-dto.rst @@ -73,17 +73,10 @@ nested models. Here, the config is created with the exclude parameter, which is a set of strings. Each string represents the path to a field in the ``User`` object that should be excluded from the output DTO. -.. code-block:: python - - config = DTOConfig( - exclude={ - "id", - "address.id", - "address.street", - "pets.0.id", - "pets.0.user_id", - } - ) +.. literalinclude:: /examples/data_transfer_objects/factory/excluding_fields_2.py + :caption: Excluding fields + :language: python + In this example, ``"id"`` represents the id field of the ``User`` object, ``"address.id"`` and ``"address.street"`` represent fields of the ``Address`` object nested inside the ``User`` object, and ``"pets.0.id"`` and @@ -288,53 +281,22 @@ attributes you might need. In this example, we have a ``WithCount`` dataclass wh The wrapper must be a python generic type with one or more type parameters, and at least one of those type parameters should describe an instance attribute that will be populated with the data. -.. code-block:: python - - from dataclasses import dataclass - from typing import Generic, TypeVar - - T = TypeVar("T") - - - @dataclass - class WithCount(Generic[T]): - count: int - data: List[T] +.. literalinclude:: /examples/data_transfer_objects/factory/enveloping_return_data_1.py + :language: python Now, create a DTO for your data object and configure it using ``DTOConfig``. In this example, we're excluding ``password`` and ``created_at`` from the final output. -.. code-block:: python - - from advanced_alchemy.dto import SQLAlchemyDTO - from litestar.dto import DTOConfig - +.. literalinclude:: /examples/data_transfer_objects/factory/enveloping_return_data_2.py + :language: python - class UserDTO(SQLAlchemyDTO[User]): - config = DTOConfig(exclude={"password", "created_at"}) Then, set up your route handler. This example sets up a ``/users`` endpoint, where a list of ``User`` objects is returned, wrapped in the ``WithCount`` dataclass. -.. code-block:: python - - from litestar import get - - - @get("/users", dto=UserDTO, sync_to_thread=False) - def get_users() -> WithCount[User]: - return WithCount( - count=1, - data=[ - User( - id=1, - name="Litestar User", - password="xyz", - created_at=datetime.now(), - ), - ], - ) +.. literalinclude:: /examples/data_transfer_objects/factory/enveloping_return_data_3.py + :language: python This setup allows the DTO to manage the rendering of ``User`` objects into the response. The DTO Factory type will find @@ -359,39 +321,16 @@ Litestar offers paginated response wrapper types, and DTO Factory types can hand The DTO is defined and configured, in our example, we're excluding ``password`` and ``created_at`` fields from the final representation of our users. -.. code-block:: python - - from advanced_alchemy.dto import SQLAlchemyDTO - from litestar.dto import DTOConfig - +.. literalinclude:: /examples/data_transfer_objects/factory/paginated_return_data_1.py + :language: python - class UserDTO(SQLAlchemyDTO[User]): - config = DTOConfig(exclude={"password", "created_at"}) The example sets up a ``/users`` endpoint, where a paginated list of ``User`` objects is returned, wrapped in :class:`ClassicPagination <.pagination.ClassicPagination>`. -.. code-block:: python - - from litestar import get - from litestar.pagination import ClassicPagination - +.. literalinclude:: /examples/data_transfer_objects/factory/paginated_return_data_2.py + :language: python - @get("/users", dto=UserDTO, sync_to_thread=False) - def get_users() -> ClassicPagination[User]: - return ClassicPagination( - page_size=10, - total_pages=1, - current_page=1, - items=[ - User( - id=1, - name="Litestar User", - password="xyz", - created_at=datetime.now(), - ), - ], - ) The :class:`ClassicPagination <.pagination.ClassicPagination>` class contains ``page_size`` (number of items per page), ``total_pages`` (total number of pages), ``current_page`` (current page number), and ``items`` (items for the current @@ -413,34 +352,15 @@ Litestar's DTO (Data Transfer Object) Factory Types can handle data wrapped in a We create a DTO for the ``User`` type and configure it using ``DTOConfig`` to exclude ``password`` and ``created_at`` from the serialized output. -.. code-block:: python - - from advanced_alchemy.dto import SQLAlchemyDTO - from litestar.dto import DTOConfig - - - class UserDTO(SQLAlchemyDTO[User]): - config = DTOConfig(exclude={"password", "created_at"}) +.. literalinclude:: /examples/data_transfer_objects/factory/response_return_data_1.py + :language: python The example sets up a ``/users`` endpoint where a ``User`` object is returned wrapped in a ``Response`` type. -.. code-block:: python - - from litestar import get, Response - +.. literalinclude:: /examples/data_transfer_objects/factory/response_return_data_2.py + :language: python - @get("/users", dto=UserDTO, sync_to_thread=False) - def get_users() -> Response[User]: - return Response( - content=User( - id=1, - name="Litestar User", - password="xyz", - created_at=datetime.now(), - ), - headers={"X-Total-Count": "1"}, - ) The ``Response`` object encapsulates the ``User`` object in its ``content`` attribute and allows us to configure the response received by the client. In this case, we add a custom header. diff --git a/docs/usage/events.rst b/docs/usage/events.rst index bfcbe96631..eba5603a08 100644 --- a/docs/usage/events.rst +++ b/docs/usage/events.rst @@ -3,46 +3,8 @@ Events Litestar supports a simple implementation of the event emitter / listener pattern: -.. code-block:: python - - from dataclasses import dataclass - - from litestar import Request, post - from litestar.events import listener - from litestar import Litestar - - from db import user_repository - from utils.email import send_welcome_mail - - - @listener("user_created") - async def send_welcome_email_handler(email: str) -> None: - # do something here to send an email - await send_welcome_mail(email) - - - @dataclass - class CreateUserDTO: - first_name: str - last_name: str - email: str - - - @post("/users") - async def create_user_handler(data: UserDTO, request: Request) -> None: - # do something here to create a new user - # e.g. insert the user into a database - await user_repository.insert(data) - - # assuming we have now inserted a user, we want to send a welcome email. - # To do this in a none-blocking fashion, we will emit an event to a listener, which will send the email, - # using a different async block than the one where we are returning a response. - request.app.emit("user_created", email=data.email) - - - app = Litestar( - route_handlers=[create_user_handler], listeners=[send_welcome_email_handler] - ) +.. literalinclude:: /examples/events/event_base.py + :language: python The above example illustrates the power of this pattern - it allows us to perform async operations without blocking, @@ -53,18 +15,9 @@ Listening to Multiple Events Event listeners can listen to multiple events: -.. code-block:: python - - from litestar.events import listener - - - @listener("user_created", "password_changed") - async def send_email_handler(email: str, message: str) -> None: - # do something here to send an email - - await send_email(email, message) - - +.. literalinclude:: /examples/events/event_base.py + :caption: Multiple events + :language: python Using Multiple Listeners @@ -72,42 +25,9 @@ Using Multiple Listeners You can also listen to the same events using multiple listeners: -.. code-block:: python - - from uuid import UUID - from dataclasses import dataclass - - from litestar import Request, post - from litestar.events import listener - - from db import user_repository - from utils.client import client - from utils.email import send_farewell_email - - - @listener("user_deleted") - async def send_farewell_email_handler(email: str, **kwargs) -> None: - # do something here to send an email - await send_farewell_email(email) - - - @listener("user_deleted") - async def notify_customer_support(reason: str, **kwargs) -> None: - # do something here to send an email - await client.post("some-url", reason) - - - @dataclass - class DeleteUserDTO: - email: str - reason: str - - - @post("/users") - async def delete_user_handler(data: UserDTO, request: Request) -> None: - await user_repository.delete({"email": email}) - request.app.emit("user_deleted", email=data.email, reason="deleted") - +.. literalinclude:: /examples/events/multiple_listeners.py + :caption: Multiple listeners + :language: python In the above example we are performing two side effect for the same event, one sends the user an email, and the other @@ -118,10 +38,9 @@ Passing Arguments to Listeners The method :meth:`emit ` has the following signature: -.. code-block:: python - - def emit(self, event_id: str, *args: Any, **kwargs: Any) -> None: ... - +.. literalinclude:: /examples/events/argument_to_listener.py + :caption: Passing arguments to listeners + :language: python This means that it expects a string for ``event_id`` following by any number of positional and keyword arguments. While @@ -130,46 +49,16 @@ and kwargs. For example, the following would raise an exception in python: -.. code-block:: python - - @listener("user_deleted") - async def send_farewell_email_handler(email: str) -> None: - await send_farewell_email(email) - - - @listener("user_deleted") - async def notify_customer_support(reason: str) -> None: - # do something here to send an email - await client.post("some-url", reason) - - - @dataclass - class DeleteUserDTO: - email: str - reason: str - - - @post("/users") - async def delete_user_handler(data: UserDTO, request: Request) -> None: - await user_repository.delete({"email": email}) - request.app.emit("user_deleted", email=data.email, reason="deleted") - +.. literalinclude:: /examples/events/listener_exception.py + :language: python The reason for this is that both listeners will receive two kwargs - ``email`` and ``reason``. To avoid this, the previous example had ``**kwargs`` in both: -.. code-block:: python - - @listener("user_deleted") - async def send_farewell_email_handler(email: str, **kwargs) -> None: - await send_farewell_email(email) - - - @listener("user_deleted") - async def notify_customer_support(reason: str, **kwargs) -> None: - await client.post("some-url", reason) +.. literalinclude:: /examples/events/listener_no_exception.py + :language: python Creating Event Emitters diff --git a/docs/usage/htmx.rst b/docs/usage/htmx.rst index 966b166d64..3f2b99512c 100644 --- a/docs/usage/htmx.rst +++ b/docs/usage/htmx.rst @@ -9,39 +9,9 @@ HTMXRequest A special :class:`~litestar.connection.Request` class, providing interaction with the HTMX client. -.. code-block:: python +.. literalinclude:: /examples/htmx/htmx_request.py + :language: python - from litestar.contrib.htmx.request import HTMXRequest - from litestar.contrib.htmx.response import HTMXTemplate - from litestar import get, Litestar - from litestar.response import Template - - from litestar.contrib.jinja import JinjaTemplateEngine - from litestar.template.config import TemplateConfig - - from pathlib import Path - - - @get(path="/form") - def get_form(request: HTMXRequest) -> Template: - htmx = request.htmx # if true will return HTMXDetails class object - if htmx: - print(htmx.current_url) - # OR - if request.htmx: - print(request.htmx.current_url) - return HTMXTemplate(template_name="partial.html", context=context, push_url="/form") - - - app = Litestar( - route_handlers=[get_form], - debug=True, - request_class=HTMXRequest, - template_config=TemplateConfig( - directory=Path("litestar_htmx/templates"), - engine=JinjaTemplateEngine, - ), - ) See :class:`HTMXDetails ` for a full list of available properties. @@ -57,29 +27,9 @@ HTMXTemplate Response Classes The most common use-case for `htmx` to render an html page or html snippet. Litestar makes this easy by providing an :class:`HTMXTemplate ` response: -.. code-block:: python - - from litestar.contrib.htmx.response import HTMXTemplate - from litestar.response import Template - - - @get(path="/form") - def get_form( - request: HTMXRequest, - ) -> Template: # Return type is Template and not HTMXTemplate. - ... - return HTMXTemplate( - template_name="partial.html", - context=context, - # Optional parameters - push_url="/form", # update browser history - re_swap="outerHTML", # change swapping method - re_target="#new-target", # change target element - trigger_event="showMessage", # trigger event name - params={"alert": "Confirm your Choice."}, # parameter to pass to the event - after="receive", # when to trigger event, - # possible values 'receive', 'settle', and 'swap' - ) +.. literalinclude:: /examples/htmx/htmx_response.py + :language: python + .. note:: - Return type is litestar's ``Template`` and not ``HTMXTemplate``. @@ -94,30 +44,20 @@ Litestar supports both of these: Use :class:`HXStopPolling ` to stop polling for a response. -.. code-block:: python +.. literalinclude:: /examples/htmx/htmx_response_no_dom_change.py + :language: python - @get("/") - def handler() -> HXStopPolling: - ... - return HXStopPolling() Use :class:`ClientRedirect ` to redirect with a page reload. -.. code-block:: python - - @get("/") - def handler() -> ClientRedirect: - ... - return ClientRedirect(redirect_to="/contact-us") +.. literalinclude:: /examples/htmx/htmx_client_redirect.py + :language: python Use :class:`ClientRefresh ` to force a full page refresh. -.. code-block:: python +.. literalinclude:: /examples/htmx/htmx_client_refresh.py + :language: python - @get("/") - def handler() -> ClientRefresh: - ... - return ClientRefresh() 2 - Responses that may change DOM. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -126,71 +66,38 @@ Use :class:`HXLocation ` to redirect - Note: this class provides the ability to change ``target``, ``swapping`` method, the sent ``values``, and the ``headers``.) -.. code-block:: python - - @get("/about") - def handler() -> HXLocation: - ... - return HXLocation( - redirect_to="/contact-us", - # Optional parameters - source, # the source element of the request. - event, # an event that "triggered" the request. - target="#target", # element id to target to. - swap="outerHTML", # swapping method to use. - hx_headers={"attr": "val"}, # headers to pass to htmx. - values={"val": "one"}, - ) # values to submit with response. +.. literalinclude:: /examples/htmx/htmx_response_change_dom.py + :language: python + Use :class:`PushUrl ` to carry a response and push a url to the browser, optionally updating the `history` stack. - Note: If the value for ``push_url`` is set to ``False`` it will prevent updating browser history. -.. code-block:: python +.. literalinclude:: /examples/htmx/htmx_push_url.py + :language: python - @get("/about") - def handler() -> PushUrl: - ... - return PushUrl(content="Success!", push_url="/about") Use :class:`ReplaceUrl ` to carry a response and replace the url in the browser's ``location`` bar. - Note: If the value to ``replace_url`` is set to ``False`` it will prevent it updating the browser location bar. -.. code-block:: python +.. literalinclude:: /examples/htmx/htmx_replace_url.py + :language: python - @get("/contact-us") - def handler() -> ReplaceUrl: - ... - return ReplaceUrl(content="Success!", replace_url="/contact-us") Use :class:`Reswap ` to carry a response perhaps a swap -.. code-block:: python +.. literalinclude:: /examples/htmx/htmx_reswap.py + :language: python - @get("/contact-us") - def handler() -> Reswap: - ... - return Reswap(content="Success!", method="beforebegin") Use :class:`Retarget ` to carry a response and change the target element. -.. code-block:: python +.. literalinclude:: /examples/htmx/htmx_retarget.py + :language: python - @get("/contact-us") - def handler() -> Retarget: - ... - return Retarget(content="Success!", target="#new-target") Use :class:`TriggerEvent ` to carry a response and trigger an event. -.. code-block:: python - - @get("/contact-us") - def handler() -> TriggerEvent: - ... - return TriggerEvent( - content="Success!", - name="showMessage", - params={"attr": "value"}, - after="receive", # possible values 'receive', 'settle', and 'swap' - ) +.. literalinclude:: /examples/htmx/htmx_trigger_event.py + :language: python diff --git a/docs/usage/logging.rst b/docs/usage/logging.rst index c39861aea4..f5cc476f85 100644 --- a/docs/usage/logging.rst +++ b/docs/usage/logging.rst @@ -3,28 +3,10 @@ Logging Application and request level loggers can be configured using the :class:`~litestar.logging.config.LoggingConfig`: -.. code-block:: python +.. literalinclude:: /examples/logging/logging_base.py + :caption: Base logging example + :language: python - import logging - - from litestar import Litestar, Request, get - from litestar.logging import LoggingConfig - - - @get("/") - def my_router_handler(request: Request) -> None: - request.logger.info("inside a request") - return None - - - logging_config = LoggingConfig( - root={"level": logging.getLevelName(logging.INFO), "handlers": ["console"]}, - formatters={ - "standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"} - }, - ) - - app = Litestar(route_handlers=[my_router_handler], logging_config=logging_config) .. attention:: @@ -39,67 +21,16 @@ Standard Library Logging (Manual Configuration) `logging `_ is Python's builtin standard logging library and can be integrated with `LoggingConfig` as the `root` logging. By using `logging_config()()` you can build a `logger` to be used around your project. -.. code-block:: python - - import logging +.. literalinclude:: /examples/logging/logging_standard_library.py + :caption: Standard Library Logging (manual) + :language: python - from litestar import Litestar, Request, get - from litestar.logging import LoggingConfig - - logging_config = LoggingConfig( - root={"level": logging.getLevelName(logging.INFO), "handlers": ["console"]}, - formatters={ - "standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"} - }, - ) - - logger = logging_config.configure()() - - - @get("/") - def my_router_handler(request: Request) -> None: - request.logger.info("inside a request") - logger.info("here too") - - - app = Litestar( - route_handlers=[my_router_handler], - logging_config=logging_config, - ) The above example is the same as using logging without the litestar LoggingConfig. -.. code-block:: python - - import logging - - from litestar import Litestar, Request, get - from litestar.logging.config import LoggingConfig - - - def get_logger(mod_name: str) -> logging.Logger: - """Return logger object.""" - format = "%(asctime)s: %(name)s: %(levelname)s: %(message)s" - logger = logging.getLogger(mod_name) - # Writes to stdout - ch = logging.StreamHandler() - ch.setLevel(logging.INFO) - ch.setFormatter(logging.Formatter(format)) - logger.addHandler(ch) - return logger - - - logger = get_logger(__name__) - - - @get("/") - def my_router_handler(request: Request) -> None: - logger.info("logger inside a request") - - - app = Litestar( - route_handlers=[my_router_handler], - ) +.. literalinclude:: /examples/logging/logging_litestar_logging_config.py + :caption: Standard Library Logging (LoggingCondig) + :language: python Using Picologging @@ -115,21 +46,10 @@ Using StructLog `StructLog `_ is a powerful structured-logging library. Litestar ships with a dedicated logging plugin and config for using it: -.. code-block:: python - - from litestar import Litestar, Request, get - from litestar.plugins.structlog import StructlogPlugin - - - @get("/") - def my_router_handler(request: Request) -> None: - request.logger.info("inside a request") - return None - - - structlog_plugin = StructlogPlugin() +.. literalinclude:: /examples/logging/logging_structlog.py + :caption: Logging with structlog + :language: python - app = Litestar(route_handlers=[my_router_handler], plugins=[StructlogPlugin()]) Subclass Logging Configs ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/usage/metrics/open-telemetry.rst b/docs/usage/metrics/open-telemetry.rst index 27b69677ae..eaaf041eca 100644 --- a/docs/usage/metrics/open-telemetry.rst +++ b/docs/usage/metrics/open-telemetry.rst @@ -19,14 +19,9 @@ Once these requirements are satisfied, you can instrument your Litestar applicat of :class:`OpenTelemetryConfig ` and passing the middleware it creates to the Litestar constructor: -.. code-block:: python +.. literalinclude:: /examples/metrics/opentelemetry.py + :language: python - from litestar import Litestar - from litestar.contrib.opentelemetry import OpenTelemetryConfig - - open_telemetry_config = OpenTelemetryConfig() - - app = Litestar(middleware=[open_telemetry_config.middleware]) The above example will work out of the box if you configure a global ``tracer_provider`` and/or ``metric_provider`` and an exporter to use these (see the diff --git a/docs/usage/middleware/builtin-middleware.rst b/docs/usage/middleware/builtin-middleware.rst index e95102efcf..353bb9b945 100644 --- a/docs/usage/middleware/builtin-middleware.rst +++ b/docs/usage/middleware/builtin-middleware.rst @@ -8,14 +8,8 @@ CORS mechanism that is often implemented using middleware. To enable CORS in a litestar application simply pass an instance of :class:`CORSConfig <.config.cors.CORSConfig>` to :class:`Litestar <.app.Litestar>`: -.. code-block:: python - - from litestar import Litestar - from litestar.config.cors import CORSConfig - - cors_config = CORSConfig(allow_origins=["https://www.example.com"]) - - app = Litestar(route_handlers=[...], cors_config=cors_config) +.. literalinclude:: /examples/middleware/builtin/cors.py + :language: python CSRF @@ -47,32 +41,14 @@ This middleware prevents CSRF attacks by doing the following: To enable CSRF protection in a Litestar application simply pass an instance of :class:`CSRFConfig <.config.csrf.CSRFConfig>` to the Litestar constructor: -.. code-block:: python - - from litestar import Litestar, get, post - from litestar.config.csrf import CSRFConfig - - - @get() - async def get_resource() -> str: - # GET is one of the safe methods - return "some_resource" - - @post("{id:int}") - async def create_resource(id: int) -> bool: - # POST is one of the unsafe methods - return True - - csrf_config = CSRFConfig(secret="my-secret") - - app = Litestar([get_resource, create_resource], csrf_config=csrf_config) +.. literalinclude:: /examples/middleware/builtin/csrf.py + :language: python The following snippet demonstrates how to change the cookie name to "some-cookie-name" and header name to "some-header-name". -.. code-block:: python - - csrf_config = CSRFConfig(secret="my-secret", cookie_name='some-cookie-name', header_name='some-header-name') +.. literalinclude:: /examples/middleware/builtin/csrf_cookies.py + :language: python A CSRF protected route can be accessed by any client that can make a request with either the header or form-data key. @@ -86,38 +62,15 @@ In Python, any client such as `requests `_ or ` The usage of clients or sessions is recommended due to the cookie persistence it offers across requests. The following is an example using ``httpx.Client``. -.. code-block:: python - - import httpx - - - with httpx.Client() as client: - get_response = client.get("http://localhost:8000/") - - # "csrftoken" is the default cookie name - csrf = get_response.cookies["csrftoken"] - - # "x-csrftoken" is the default header name - post_response_using_header = client.post("http://localhost:8000/", headers={"x-csrftoken": csrf}) - assert post_response_using_header.status_code == 201 - - # "_csrf_token" is the default *non* configurable form-data key - post_response_using_form_data = client.post("http://localhost:8000/1", data={"_csrf_token": csrf}) - assert post_response_using_form_data.status_code == 201 +.. literalinclude:: /examples/middleware/builtin/csrf_httpx.py + :language: python - # despite the header being passed, this request will fail as it does not have a cookie in its session - # note the usage of ``httpx.post`` instead of ``client.post`` - post_response_with_no_persisted_cookie = httpx.post("http://localhost:8000/1", headers={"x-csrftoken": csrf}) - assert post_response_with_no_persisted_cookie.status_code == 403 - assert "CSRF token verification failed" in post_response_with_no_persisted_cookie.text Routes can be marked as being exempt from the protection offered by this middleware via :ref:`handler opts ` -.. code-block:: python - - @post("/post", exclude_from_csrf=True) - def handler() -> None: ... +.. literalinclude:: /examples/middleware/builtin/csrf_exclude_route.py + :language: python If you need to exempt many routes at once you might want to consider using the @@ -141,17 +94,9 @@ Litestar includes an :class:`AllowedHostsMiddleware <.middleware.allowed_hosts.A easily enabled by either passing an instance of :class:`AllowedHostsConfig <.config.allowed_hosts.AllowedHostsConfig>` or a list of domains to :class:`Litestar `: -.. code-block:: python - - from litestar import Litestar - from litestar.config.allowed_hosts import AllowedHostsConfig +.. literalinclude:: /examples/middleware/builtin/allowed_hosts.py + :language: python - app = Litestar( - route_handlers=[...], - allowed_hosts=AllowedHostsConfig( - allowed_hosts=["*.example.com", "www.wikipedia.org"] - ), - ) .. note:: @@ -186,15 +131,9 @@ You can configure the following additional gzip-specific values: * ``gzip_compress_level``: a range between 0-9, see the `official python docs `_. Defaults to ``9`` , which is the maximum value. -.. code-block:: python - - from litestar import Litestar - from litestar.config.compression import CompressionConfig +.. literalinclude:: /examples/middleware/builtin/gzip.py + :language: python - app = Litestar( - route_handlers=[...], - compression_config=CompressionConfig(backend="gzip", gzip_compress_level=9), - ) Brotli ^^^^^^ @@ -219,15 +158,9 @@ You can configure the following additional brotli-specific values: be set based on the quality. Defaults to 0. * ``brotli_gzip_fallback``: a boolean to indicate if gzip should be used if brotli is not supported. -.. code-block:: python - - from litestar import Litestar - from litestar.config.compression import CompressionConfig +.. literalinclude:: /examples/middleware/builtin/brotli.py + :language: python - app = Litestar( - route_handlers=[...], - compression_config=CompressionConfig(backend="brotli", brotli_gzip_fallback=True), - ) Rate-Limit Middleware --------------------- @@ -264,16 +197,9 @@ Obfuscating Logging Output Sometimes certain data, e.g. request or response headers, needs to be obfuscated. This is supported by the middleware configuration: -.. code-block:: python - - from litestar.middleware.logging import LoggingMiddlewareConfig +.. literalinclude:: /examples/middleware/builtin/loggin_middleware_obfuscating_output.py + :language: python - logging_middleware_config = LoggingMiddlewareConfig( - request_cookies_to_obfuscate={"my-custom-session-key"}, - response_cookies_to_obfuscate={"my-custom-session-key"}, - request_headers_to_obfuscate={"my-custom-header"}, - response_headers_to_obfuscate={"my-custom-header"}, - ) The middleware will obfuscate the headers ``Authorization`` and ``X-API-KEY`` , and the cookie ``session`` by default. diff --git a/docs/usage/middleware/creating-middleware.rst b/docs/usage/middleware/creating-middleware.rst index f9824b27c6..a9cd8b285a 100644 --- a/docs/usage/middleware/creating-middleware.rst +++ b/docs/usage/middleware/creating-middleware.rst @@ -8,18 +8,8 @@ is **any callable** that takes a kwarg called ``app``, which is the next ASGI ha The example previously given was using a factory function, i.e.: -.. code-block:: python - - from litestar.types import ASGIApp, Scope, Receive, Send - - - def middleware_factory(app: ASGIApp) -> ASGIApp: - async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: - # do something here - ... - await app(scope, receive, send) - - return my_middleware +.. literalinclude:: /examples/middleware/creation/create_basic_middleware.py + :language: python While using functions is a perfectly viable approach, you can also use classes to do the same. See the next sections on two base classes you can use for this purpose - the :class:`MiddlewareProtocol <.middleware.base.MiddlewareProtocol>` , @@ -33,16 +23,9 @@ The :class:`MiddlewareProtocol ` cl `PEP 544 Protocol `_ that specifies the minimal implementation of a middleware as follows: -.. code-block:: python - - from typing import Protocol, Any - from litestar.types import ASGIApp, Scope, Receive, Send - - - class MiddlewareProtocol(Protocol): - def __init__(self, app: ASGIApp, **kwargs: Any) -> None: ... +.. literalinclude:: /examples/middleware/creation/create_middleware_using_middleware_protocol_1.py + :language: python - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ... The ``__init__`` method receives and sets "app". *It's important to understand* that app is not an instance of Litestar in this case, but rather the next middleware in the stack, which is also an ASGI app. @@ -55,27 +38,9 @@ server (e.g. *uvicorn*\ ) used to run Litestar. To use this protocol as a basis, simply subclass it - as you would any other class, and implement the two methods it specifies: -.. code-block:: python - - import logging - - from litestar.types import ASGIApp, Receive, Scope, Send - from litestar import Request - from litestar.middleware.base import MiddlewareProtocol - - logger = logging.getLogger(__name__) - - - class MyRequestLoggingMiddleware(MiddlewareProtocol): - def __init__(self, app: ASGIApp) -> None: - super().__init__(app) - self.app = app +.. literalinclude:: /examples/middleware/creation/create_middleware_using_middleware_protocol_2.py + :language: python - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - if scope["type"] == "http": - request = Request(scope) - logger.info("%s - %s" % request.method, request.url) - await self.app(scope, receive, send) .. important:: @@ -92,26 +57,9 @@ Once a middleware finishes doing whatever its doing, it should pass ``scope``, ` and await it. This is what's happening in the above example with: ``await self.app(scope, receive, send)``. Let's explore another example - redirecting the request to a different url from a middleware: -.. code-block:: python - - from litestar.types import ASGIApp, Receive, Scope, Send - - from litestar.response.redirect import ASGIRedirectResponse - from litestar import Request - from litestar.middleware.base import MiddlewareProtocol - - - class RedirectMiddleware(MiddlewareProtocol): - def __init__(self, app: ASGIApp) -> None: - super().__init__(app) - self.app = app +.. literalinclude:: /examples/middleware/creation/responding_using_middleware_protocol.py + :language: python - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - if Request(scope).session is None: - response = ASGIRedirectResponse(path="/login") - await response(scope, receive, send) - else: - await self.app(scope, receive, send) As you can see in the above, given some condition (``request.session`` being None) we create a :class:`ASGIRedirectResponse ` and then await it. Otherwise, we await ``self.app`` @@ -133,35 +81,9 @@ functions. To demonstrate this, lets say we want to append a header with a timestamp to all outgoing responses. We could achieve this by doing the following: -.. code-block:: python - - import time - - from litestar.datastructures import MutableScopeHeaders - from litestar.types import Message, Receive, Scope, Send - from litestar.middleware.base import MiddlewareProtocol - from litestar.types import ASGIApp - - - class ProcessTimeHeader(MiddlewareProtocol): - def __init__(self, app: ASGIApp) -> None: - super().__init__(app) - self.app = app - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - if scope["type"] == "http": - start_time = time.time() - - async def send_wrapper(message: Message) -> None: - if message["type"] == "http.response.start": - process_time = time.time() - start_time - headers = MutableScopeHeaders.from_message(message=message) - headers["X-Process-Time"] = str(process_time) - await send(message) +.. literalinclude:: /examples/middleware/creation/responding_using_middleware_protocol_asgi.py + :language: python - await self.app(scope, receive, send_wrapper) - else: - await self.app(scope, receive, send) Inheriting AbstractMiddleware ----------------------------- @@ -169,36 +91,9 @@ Inheriting AbstractMiddleware Litestar offers an :class:`AbstractMiddleware <.middleware.base.AbstractMiddleware>` class that can be extended to create middleware: -.. code-block:: python - - from typing import TYPE_CHECKING - from time import time - - from litestar.enums import ScopeType - from litestar.middleware import AbstractMiddleware - from litestar.datastructures import MutableScopeHeaders - - - if TYPE_CHECKING: - from litestar.types import Message, Receive, Scope, Send - - - class MyMiddleware(AbstractMiddleware): - scopes = {ScopeType.HTTP} - exclude = ["first_path", "second_path"] - exclude_opt_key = "exclude_from_middleware" - - async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: - start_time = time() - - async def send_wrapper(message: "Message") -> None: - if message["type"] == "http.response.start": - process_time = time() - start_time - headers = MutableScopeHeaders.from_message(message=message) - headers["X-Process-Time"] = str(process_time) - await send(message) +.. literalinclude:: /examples/middleware/creation/inheriting_abstract_middleware.py + :language: python - await self.app(scope, receive, send_wrapper) The three class variables defined in the above example ``scopes``, ``exclude``, and ``exclude_opt_key`` can be used to fine-tune for which routes and request types the middleware is called: @@ -214,7 +109,6 @@ Thus, in the following example, the middleware will only run against the route h :language: python - Using DefineMiddleware to pass arguments ---------------------------------------- @@ -223,26 +117,9 @@ using the :class:`DefineMiddleware ` the factory function used in the examples above to take some args and kwargs and then use ``DefineMiddleware`` to pass these values to our middleware: -.. code-block:: python - - from litestar.types import ASGIApp, Scope, Receive, Send - from litestar import Litestar - from litestar.middleware import DefineMiddleware - - - def middleware_factory(my_arg: int, *, app: ASGIApp, my_kwarg: str) -> ASGIApp: - async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: - # here we can use my_arg and my_kwarg for some purpose - ... - await app(scope, receive, send) - - return my_middleware - +.. literalinclude:: /examples/middleware/creation/using_define_middleware.py + :language: python - app = Litestar( - route_handlers=[...], - middleware=[DefineMiddleware(middleware_factory, 1, my_kwarg="abc")], - ) The ``DefineMiddleware`` is a simple container - it takes a middleware callable as a first parameter, and then any positional arguments, followed by key word arguments. The middleware callable will be called with these values as well diff --git a/docs/usage/middleware/using-middleware.rst b/docs/usage/middleware/using-middleware.rst index 2190676fc5..54a6a2a792 100644 --- a/docs/usage/middleware/using-middleware.rst +++ b/docs/usage/middleware/using-middleware.rst @@ -9,38 +9,16 @@ the websocket connection. For example, the following function can be used as a middleware because it receives the ``app`` kwarg and returns an ``ASGIApp``: -.. code-block:: python - - from litestar.types import ASGIApp, Scope, Receive, Send - - - def middleware_factory(app: ASGIApp) -> ASGIApp: - async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: - # do something here - ... - await app(scope, receive, send) +.. literalinclude:: /examples/middleware/using_middleware_1.py + :language: python - return my_middleware We can then pass this middleware to the :class:`Litestar <.app.Litestar>` instance, where it will be called on every request: -.. code-block:: python - - from litestar.types import ASGIApp, Scope, Receive, Send - from litestar import Litestar - - - def middleware_factory(app: ASGIApp) -> ASGIApp: - async def my_middleware(scope: Scope, receive: Receive, send: Send) -> None: - # do something here - ... - await app(scope, receive, send) - - return my_middleware - +.. literalinclude:: /examples/middleware/using_middleware_2.py + :language: python - app = Litestar(route_handlers=[...], middleware=[middleware_factory]) In the above example, Litestar will call the ``middleware_factory`` function and pass to it ``app``. It's important to understand that this kwarg does not designate the Litestar application but rather the next ``ASGIApp`` in the stack. It diff --git a/docs/usage/openapi/schema_generation.rst b/docs/usage/openapi/schema_generation.rst index 35edc63227..c7d58e8f13 100644 --- a/docs/usage/openapi/schema_generation.rst +++ b/docs/usage/openapi/schema_generation.rst @@ -5,15 +5,8 @@ OpenAPI schema generation is enabled by default. To configure it you can pass an :class:`OpenAPIConfig <.openapi.OpenAPIConfig>` to the :class:`Litestar ` class using the ``openapi_config`` kwarg: -.. code-block:: python - - from litestar import Litestar - from litestar.openapi import OpenAPIConfig - - app = Litestar( - route_handlers=[...], openapi_config=OpenAPIConfig(title="My API", version="1.0.0") - ) - +.. literalinclude:: /examples/openapi/schema_generation.py + :language: python Disabling schema generation @@ -22,12 +15,8 @@ Disabling schema generation If you wish to disable schema generation and not include the schema endpoints in your API, simply pass ``None`` as the value for ``openapi_config``: -.. code-block:: python - - from litestar import Litestar - - app = Litestar(route_handlers=[...], openapi_config=None) - +.. literalinclude:: /examples/openapi/disable_schema_generation.py + :language: python Configuring schema generation on a route handler @@ -36,14 +25,10 @@ Configuring schema generation on a route handler By default, an `operation `_ schema is generated for all route handlers. You can omit a route handler from the schema by setting ``include_in_schema=False``: -.. code-block:: python - - from litestar import get +.. literalinclude:: /examples/openapi/configure_schema_generation_on_route_1.py + :language: python - @get(path="/some-path", include_in_schema=False) - def my_route_handler() -> None: ... - You can also modify the generated schema for the route handler using the following kwargs: @@ -94,78 +79,15 @@ You can also modify the generated schema for the route handler using the followi `operation_id` will be prefixed with the method name when function is decorated with `HTTPRouteHandler` and multiple `http_method`. Will also be prefixed with path strings used in `Routers` and `Controllers` to make sure id is unique. -.. code-block:: python - - from datetime import datetime - from typing import Optional - - from pydantic import BaseModel - - from litestar import get - from litestar.openapi.datastructures import ResponseSpec - - - class Item(BaseModel): ... - +.. literalinclude:: /examples/openapi/configure_schema_generation_on_route_2.py + :language: python - class ItemNotFound(BaseModel): - was_removed: bool - removed_at: Optional[datetime] - - - @get( - path="/items/{pk:int}", - responses={ - 404: ResponseSpec( - data_container=ItemNotFound, description="Item was removed or not found" - ) - }, - ) - def retrieve_item(pk: int) -> Item: ... You can also specify ``security`` and ``tags`` on higher level of the application, e.g. on a controller, router, or the app instance itself. For example: -.. code-block:: python - - from litestar import Litestar, get - from litestar.openapi import OpenAPIConfig - from litestar.openapi.spec import Components, SecurityScheme, Tag - - - @get( - "/public", - tags=["public"], - security=[{}], # this endpoint is marked as having optional security - ) - def public_path_handler() -> dict[str, str]: - return {"hello": "world"} - - - @get("/other", tags=["internal"], security=[{"apiKey": []}]) - def internal_path_handler() -> None: ... - - - app = Litestar( - route_handlers=[public_path_handler, internal_path_handler], - openapi_config=OpenAPIConfig( - title="my api", - version="1.0.0", - tags=[ - Tag(name="public", description="This endpoint is for external users"), - Tag(name="internal", description="This endpoint is for internal users"), - ], - security=[{"BearerToken": []}], - components=Components( - security_schemes={ - "BearerToken": SecurityScheme( - type="http", - scheme="bearer", - ) - }, - ), - ), - ) +.. literalinclude:: /examples/openapi/configure_schema_generation_on_route_3.py + :language: python Accessing the OpenAPI schema in code @@ -175,15 +97,8 @@ The OpenAPI schema is generated during the :class:`Litestar dict: - schema = request.app.openapi_schema - return schema.dict() +.. literalinclude:: /examples/openapi/accessing_schema_in_code.py + :language: python Customizing Pydantic model schemas diff --git a/docs/usage/responses.rst b/docs/usage/responses.rst index 6b9780cfc6..17a97a5106 100644 --- a/docs/usage/responses.rst +++ b/docs/usage/responses.rst @@ -5,20 +5,9 @@ Litestar allows for several ways in which HTTP responses can be specified and ha case. The base pattern though is straightforward - simply return a value from a route handler function and let Litestar take care of the rest: -.. code-block:: python - - from pydantic import BaseModel - from litestar import get - - - class Resource(BaseModel): - id: int - name: str - +.. literalinclude:: /examples/responses/response_simple.py + :language: python - @get("/resources") - def retrieve_resource() -> Resource: - return Resource(id=1, name="my resource") In the example above, the route handler function returns an instance of the ``Resource`` pydantic class. This value will then be used by Litestar to construct an instance of the :class:`Response ` @@ -34,14 +23,9 @@ You do not have to specify the ``media_type`` kwarg in the route handler functio if you wish to return a response other than JSON, you should specify this value. You can use the :class:`MediaType ` enum for this purpose: -.. code-block:: python - - from litestar import MediaType, get - +.. literalinclude:: /examples/responses/response_media_type.py + :language: python - @get("/resources", media_type=MediaType.TEXT) - def retrieve_resource() -> str: - return "The rumbling rabbit ran around the rock" The value of the ``media_type`` kwarg affects both the serialization of response data and the generation of OpenAPI docs. The above example will cause Litestar to serialize the response as a simple bytes string with a ``Content-Type`` header @@ -97,51 +81,27 @@ format which can be a time and space efficient alternative to JSON. It supports all the same types as JSON serialization. To send a ``MessagePack`` response, simply specify the media type as ``MediaType.MESSAGEPACK``\ : -.. code-block:: python - - from typing import Dict - from litestar import get, MediaType - +.. literalinclude:: /examples/responses/response_messagepack.py + :language: python - @get(path="/health-check", media_type=MediaType.MESSAGEPACK) - def health_check() -> Dict[str, str]: - return {"hello": "world"} Plaintext responses +++++++++++++++++++ For ``MediaType.TEXT``, route handlers should return a :class:`str` or :class:`bytes` value: -.. code-block:: python - - from litestar import get, MediaType - +.. literalinclude:: /examples/responses/response_plaintext.py + :language: python - @get(path="/health-check", media_type=MediaType.TEXT) - def health_check() -> str: - return "healthy" HTML responses ++++++++++++++ For ``MediaType.HTML``, route handlers should return a :class:`str` or :class:`bytes` value that contains HTML: -.. code-block:: python - - from litestar import get, MediaType - +.. literalinclude:: /examples/responses/response_html.py + :language: python - @get(path="/page", media_type=MediaType.HTML) - def health_check() -> str: - return """ - - -
- Hello World! -
- - - """ .. tip:: @@ -167,21 +127,9 @@ Status Codes You can control the response ``status_code`` by setting the corresponding kwarg to the desired value: -.. code-block:: python - - from pydantic import BaseModel - from litestar import get - from litestar.status_codes import HTTP_202_ACCEPTED - - - class Resource(BaseModel): - id: int - name: str - +.. literalinclude:: /examples/responses/response_status_codes.py + :language: python - @get("/resources", status_code=HTTP_202_ACCEPTED) - def retrieve_resource() -> Resource: - return Resource(id=1, name="my resource") If ``status_code`` is not set by the user, the following defaults are used: @@ -251,17 +199,9 @@ Returning ASGI Applications Litestar also supports returning ASGI applications directly, as you would responses. For example: -.. code-block:: python - - from litestar import get - from litestar.types import ASGIApp, Receive, Scope, Send - - - @get("/") - def handler() -> ASGIApp: - async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: ... +.. literalinclude:: /examples/responses/returning_asgi_applications.py + :language: python - return my_asgi_app What is an ASGI Application? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -275,42 +215,23 @@ For example, all the following examples are ASGI applications: Function ASGI Application ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: python - - from litestar.types import Receive, Scope, Send - +.. literalinclude:: /examples/responses/function_asgi_application.py + :language: python - async def my_asgi_app_function(scope: Scope, receive: Receive, send: Send) -> None: - # do something here - ... Method ASGI Application ~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: python - - from litestar.types import Receive, Scope, Send - +.. literalinclude:: /examples/responses/method_asgi_application.py + :language: python - class MyClass: - async def my_asgi_app_method( - self, scope: Scope, receive: Receive, send: Send - ) -> None: - # do something here - ... Class ASGI Application ~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: python - - from litestar.types import Receive, Scope, Send - +.. literalinclude:: /examples/responses/class_asgi_application.py + :language: python - class ASGIApp: - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - # do something here - ... Returning responses from third party libraries ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -318,17 +239,9 @@ Returning responses from third party libraries Because you can return any ASGI Application from a route handler, you can also use any ASGI application from other libraries. For example, you can return the response classes from Starlette or FastAPI directly from route handlers: -.. code-block:: python - - from starlette.responses import JSONResponse - - from litestar import get - from litestar.types import ASGIApp - +.. literalinclude:: /examples/responses/returning_responses_from_third_party.py + :language: python - @get("/") - def handler() -> ASGIApp: - return JSONResponse(content={"hello": "world"}) # type: ignore .. attention:: @@ -371,11 +284,8 @@ The respective descriptions will be used for the OpenAPI documentation. If you don't need those, you can optionally define `response_headers` using a mapping - such as a dictionary - as well: - .. code-block:: python - - @get(response_headers={"my-header": "header-value"}) - async def handler() -> str: ... - + .. literalinclude:: /examples/responses/response_headers_attributes.py + :language: python Setting Headers Dynamically @@ -517,10 +427,8 @@ Of the two declarations of ``my-cookie`` only the route handler one will be used If all you need for your cookies are key and value, you can supply them using a :class:`Mapping[str, str] ` - like a :class:`dict` - instead: - .. code-block:: python - - @get(response_cookies={"my-cookie": "cookie-value"}) - async def handler() -> str: ... + .. literalinclude:: /examples/responses/response_cookies_dict.py + :language: python .. seealso:: @@ -581,19 +489,9 @@ status code in the 30x range. In Litestar, a redirect response looks like this: -.. code-block:: python - - from litestar.status_codes import HTTP_302_FOUND - from litestar import get - from litestar.response import Redirect - +.. literalinclude:: /examples/responses/redirect_response.py + :language: python - @get(path="/some-path", status_code=HTTP_302_FOUND) - def redirect() -> Redirect: - # do some stuff here - # ... - # finally return redirect - return Redirect(path="/other-path") To return a redirect response you should do the following: @@ -606,19 +504,9 @@ File Responses File responses send a file: -.. code-block:: python - - from pathlib import Path - from litestar import get - from litestar.response import File - +.. literalinclude:: /examples/responses/file_response.py + :language: python - @get(path="/file-download") - def handle_file_download() -> File: - return File( - path=Path(Path(__file__).resolve().parent, "report").with_suffix(".pdf"), - filename="report.pdf", - ) The :class:`File <.response.File>` class expects two kwargs: @@ -638,19 +526,8 @@ The :class:`File <.response.File>` class expects two kwargs: For example: -.. code-block:: python - - from pathlib import Path - from litestar import get - from litestar.response import File - - - @get(path="/file-download", media_type="application/pdf") - def handle_file_download() -> File: - return File( - path=Path(Path(__file__).resolve().parent, "report").with_suffix(".pdf"), - filename="report.pdf", - ) +.. literalinclude:: /examples/responses/file_response_2.py + :language: python Streaming Responses @@ -704,15 +581,9 @@ Template responses are used to render templates into HTML. To use a template res :ref:`register a template engine ` on the application level. Once an engine is in place, you can use a template response like so: -.. code-block:: python - - from litestar import Request, get - from litestar.response import Template - +.. literalinclude:: /examples/responses/template_responses.py + :language: python - @get(path="/info") - def info(request: Request) -> Template: - return Template(template_name="info.html", context={"user": request.user}) In the above example, :class:`Template <.response.Template>` is passed the template name, which is a path like value, and a context dictionary that maps string keys into values that will be rendered in the template. diff --git a/docs/usage/routing/handlers.rst b/docs/usage/routing/handlers.rst index 78fc9f6e7d..a86e64faf8 100644 --- a/docs/usage/routing/handlers.rst +++ b/docs/usage/routing/handlers.rst @@ -6,15 +6,10 @@ handler :term:`decorators ` exported from Litestar. For example: -.. code-block:: python +.. literalinclude:: /examples/routing/handler.py :caption: Defining a route handler by decorating a function with the :class:`@get() <.handlers.get>` :term:`decorator` + :language: python - from litestar import get - - - @get("/") - def greet() -> str: - return "hello world" In the above example, the :term:`decorator` includes all the information required to define the endpoint operation for the combination of the path ``"/"`` and the HTTP verb ``GET``. In this case it will be a HTTP response with a @@ -29,51 +24,33 @@ All route handler :term:`decorators ` accept an optional path :term:` This :term:`argument` can be declared as a :term:`kwarg ` using the :paramref:`~.handlers.base.BaseRouteHandler.path` parameter: -.. code-block:: python +.. literalinclude:: /examples/routing/declaring_path.py :caption: Defining a route handler by passing the path as a keyword argument - - from litestar import get + :language: python - @get(path="/some-path") - async def my_route_handler() -> None: ... - It can also be passed as an :term:`argument` without the keyword: -.. code-block:: python +.. literalinclude:: /examples/routing/declaring_path_argument.py :caption: Defining a route handler but not using the keyword argument - - from litestar import get + :language: python - @get("/some-path") - async def my_route_handler() -> None: ... - And the value for this :term:`argument` can be either a string path, as in the above examples, or a :class:`list` of :class:`string ` paths: -.. code-block:: python +.. literalinclude:: /examples/routing/declaring_multiple_paths.py :caption: Defining a route handler with multiple paths + :language: python - from litestar import get - - - @get(["/some-path", "/some-other-path"]) - async def my_route_handler() -> None: ... This is particularly useful when you want to have optional :ref:`path parameters `: -.. code-block:: python +.. literalinclude:: /examples/routing/declaring_path_optional_parameter.py :caption: Defining a route handler with a path that has an optional path parameter + :language: python - from litestar import get - - - @get( - ["/some-path", "/some-path/{some_id:int}"], - ) - async def my_route_handler(some_id: int = 1) -> None: ... .. _handler-function-kwargs: @@ -107,22 +84,10 @@ Note that if your parameters collide with any of the reserved :term:`keyword arg For example: -.. code-block:: python +.. literalinclude:: /examples/routing/reserved_keyword_argument.py :caption: Providing an alternative name for a reserved keyword argument + :language: python - from typing import Any, Dict - from litestar import Request, get - from litestar.datastructures import Headers, State - - - @get(path="/") - async def my_request_handler( - state: State, - request: Request, - headers: Dict[str, str], - query: Dict[str, Any], - cookies: Dict[str, Any], - ) -> None: ... .. tip:: You can define a custom typing for your application state and then use it as a type instead of just using the :class:`~.datastructures.state.State` class from Litestar @@ -150,29 +115,17 @@ The most commonly used route handlers are those that handle HTTP requests and re These route handlers all inherit from the :class:`~.handlers.HTTPRouteHandler` class, which is aliased as the :term:`decorator` called :func:`~.handlers.route`: -.. code-block:: python +.. literalinclude:: /examples/routing/route_handler_http_1.py :caption: Defining a route handler by decorating a function with the :class:`@route() <.handlers.route>` - :term:`decorator` - - from litestar import HttpMethod, route + :language: python - @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: -.. code-block:: python +.. literalinclude:: /examples/routing/route_handler_http_2.py :caption: Defining a route handler by decorating a function with the - :class:`HTTPRouteHandler <.handlers.HTTPRouteHandler>` class - - 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: ... + :language: python Semantic handler :term:`decorators ` @@ -194,51 +147,10 @@ These are used exactly like :func:`@route() <.handlers.route>` with the sole exc .. dropdown:: Click to see the predefined route handlers - .. code-block:: python + .. literalinclude:: /examples/routing/handler_decorator.py :caption: Predefined :term:`decorators ` for HTTP route handlers + :language: python - from litestar import delete, get, patch, post, put, head - from litestar.dto import DTOConfig, DTOData - from litestar.contrib.pydantic import PydanticDTO - - from pydantic import BaseModel - - - class Resource(BaseModel): ... - - - class PartialResourceDTO(PydanticDTO[Resource]): - config = DTOConfig(partial=True) - - - @get(path="/resources") - async def list_resources() -> list[Resource]: ... - - - @post(path="/resources") - async def create_resource(data: Resource) -> Resource: ... - - - @get(path="/resources/{pk:int}") - async def retrieve_resource(pk: int) -> Resource: ... - - - @head(path="/resources/{pk:int}") - async def retrieve_resource_head(pk: int) -> None: ... - - - @put(path="/resources/{pk:int}") - async def update_resource(data: Resource, pk: int) -> Resource: ... - - - @patch(path="/resources/{pk:int}", dto=PartialResourceDTO) - async def partially_update_resource( - data: DTOData[PartialResourceDTO], pk: int - ) -> Resource: ... - - - @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>`, @@ -265,33 +177,18 @@ A WebSocket connection can be handled with a :func:`@websocket() <.handlers.Webs For a more high level approach to handling WebSockets, see :doc:`/usage/websockets` -.. code-block:: python +.. literalinclude:: /examples/routing/handler_websocket_1.py :caption: Using the :func:`@websocket() <.handlers.WebsocketRouteHandler>` route handler :term:`decorator` + :language: python - from litestar import WebSocket, websocket - - - @websocket(path="/socket") - async def my_websocket_handler(socket: WebSocket) -> None: - await socket.accept() - 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: -.. code-block:: python +.. literalinclude:: /examples/routing/handler_websocket_2.py :caption: Using the :class:`~.handlers.WebsocketRouteHandler` class directly + :language: python - 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() In difference to HTTP routes handlers, websocket handlers have the following requirements: @@ -312,50 +209,19 @@ ASGI route handlers If you need to write your own ASGI application, you can do so using the :func:`@asgi() <.handlers.asgi>` :term:`decorator`: -.. code-block:: python +.. literalinclude:: /examples/routing/handler_asgi_1.py :caption: Using the :func:`@asgi() <.handlers.asgi>` route handler :term:`decorator` + :language: python - from litestar.types import Scope, Receive, Send - from litestar.status_codes import HTTP_400_BAD_REQUEST - from litestar import Response, asgi - - - @asgi(path="/my-asgi-app") - async def my_asgi_app(scope: Scope, receive: Receive, send: Send) -> None: - if scope["type"] == "http": - if scope["method"] == "GET": - response = Response({"hello": "world"}) - await response(scope=scope, receive=receive, send=send) - return - response = Response( - {"detail": "unsupported request"}, status_code=HTTP_400_BAD_REQUEST - ) - 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: -.. code-block:: python +.. literalinclude:: /examples/routing/handler_asgi_2.py :caption: Using the :class:`~.handlers.ASGIRouteHandler` class directly - - from litestar import Response - from litestar.handlers.asgi_handlers import ASGIRouteHandler - from litestar.status_codes import HTTP_400_BAD_REQUEST - from litestar.types import Scope, Receive, Send + :language: python - @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": - response = Response({"hello": "world"}) - await response(scope=scope, receive=receive, send=send) - return - response = Response( - {"detail": "unsupported request"}, status_code=HTTP_400_BAD_REQUEST - ) - await response(scope=scope, receive=receive, send=send) - Limitations of ASGI route handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -390,50 +256,11 @@ The default value for :paramref:`~.handlers.base.BaseRouteHandler.name` is value :paramref:`~.handlers.base.BaseRouteHandler.name` can be used to dynamically retrieve (i.e. during runtime) a mapping containing the route handler instance and paths, also it can be used to build a URL path for that handler: -.. code-block:: python +.. literalinclude:: /examples/routing/handler_indexing_1.py :caption: Using the :paramref:`~.handlers.base.BaseRouteHandler.name` :term:`kwarg ` to retrieve a route handler instance and paths + :language: python - from litestar import Litestar, Request, get - from litestar.exceptions import NotFoundException - from litestar.response import Redirect - - - @get("/abc", name="one") - def handler_one() -> None: - pass - - - @get("/xyz", name="two") - def handler_two() -> None: - pass - - - @get("/def/{param:int}", name="three") - def handler_three(param: int) -> None: - pass - - - @get("/{handler_name:str}", name="four") - def handler_four(request: Request, name: str) -> Redirect: - handler_index = request.app.get_handler_index_by_name(name) - if not handler_index: - raise NotFoundException(f"no handler matching the name {name} was found") - - # handler_index == { "paths": ["/"], "handler": ..., "qualname": ... } - # do something with the handler index below, e.g. send a redirect response to the handler, or access - # handler.opt and some values stored there etc. - - return Redirect(path=handler_index[0]) - - - @get("/redirect/{param_value:int}", name="five") - def handler_five(request: Request, param_value: int) -> Redirect: - path = request.app.route_reverse("three", param=param_value) - return Redirect(path=path) - - - app = Litestar(route_handlers=[handler_one, handler_two, handler_three]) :meth:`~.app.Litestar.route_reverse` will raise :exc:`~.exceptions.NoRouteMatchFoundException` if route with given name was not found or if any of path :term:`parameters ` is missing or if any of passed path @@ -446,31 +273,10 @@ parameters, so you can apply custom formatting and pass the result to :meth:`~.a If handler has multiple paths attached to it :meth:`~.app.Litestar.route_reverse` will return the path that consumes the most number of :term:`keyword arguments ` passed to the function. -.. code-block:: python +.. literalinclude:: /examples/routing/handler_indexing_2.py :caption: Using the :meth:`~.app.Litestar.route_reverse` method to build a URL path for a route handler + :language: python - from litestar import get, Request - - - @get( - ["/some-path", "/some-path/{id:int}", "/some-path/{id:int}/{val:str}"], - name="handler_name", - ) - def handler(id: int = 1, val: str = "default") -> None: ... - - - @get("/path-info") - def path_info(request: Request) -> str: - path_optional = request.app.route_reverse("handler_name") - # /some-path` - - path_partial = request.app.route_reverse("handler_name", id=100) - # /some-path/100 - - path_full = request.app.route_reverse("handler_name", id=100, val="value") - # /some-path/100/value` - - return f"{path_optional} {path_partial} {path_full}" When a handler is associated with multiple routes having identical path :term:`parameters ` (e.g., an indexed handler registered across multiple routers), the output of :meth:`~.app.Litestar.route_reverse` is @@ -489,14 +295,10 @@ Adding arbitrary metadata to handlers All route handler :term:`decorators ` accept a key called ``opt`` which accepts a :term:`dictionary ` of arbitrary values, e.g., -.. code-block:: python +.. literalinclude:: /examples/routing/handler_metadata_1.py :caption: Adding arbitrary metadata to a route handler through the ``opt`` :term:`kwarg ` + :language: python - from litestar import get - - - @get("/", opt={"my_key": "some-value"}) - def handler() -> None: ... This dictionary can be accessed by a :doc:`route guard `, or by accessing the :attr:`~.connection.ASGIConnection.route_handler` property on a :class:`~.connection.request.Request` object, @@ -505,18 +307,11 @@ or using the :class:`ASGI scope ` object directly. Building on ``opt``, you can pass any arbitrary :term:`kwarg ` to the route handler :term:`decorator`, and it will be automatically set as a key in the ``opt`` dictionary: -.. code-block:: python +.. literalinclude:: /examples/routing/handler_metadata_2.py :caption: Adding arbitrary metadata to a route handler through the ``opt`` :term:`kwarg ` - - from litestar import get - - - @get("/", my_key="some-value") - def handler() -> None: ... + :language: python - assert handler.opt["my_key"] == "some-value" - You can specify the ``opt`` :term:`dictionary ` at all layers of your application. On specific route handlers, on a controller, a router, and even on the app instance itself as described in :ref:`layered architecture ` @@ -538,39 +333,19 @@ or ``flake8-type-checking`` will actively monitor, and suggest against. For example, the name ``Model`` is *not* available at runtime in the following snippet: -.. code-block:: python +.. literalinclude:: /examples/signature_namespace/handler_signature_1.py :caption: A route handler with a type that is not available at runtime - - from __future__ import annotations - - from typing import TYPE_CHECKING - - from litestar import Controller, post - - if TYPE_CHECKING: - from domain import Model + :language: python - class MyController(Controller): - @post() - def create_item(data: Model) -> Model: - return data - In this example, Litestar will be unable to generate the signature model because the type ``Model`` does not exist in the module scope at runtime. We can address this on a case-by-case basis by silencing our linters, for example: -.. code-block:: python +.. literalinclude:: /examples/signature_namespace/handler_signature_2.py :no-upgrade: - :caption: Silencing linters for a type that is not available at runtime - - from __future__ import annotations - - from typing import TYPE_CHECKING - - from litestar import Controller, post + :caption: A Silencing linters for a type that is not available at runtime + :language: python - # Choose the appropriate noqa directive according to your linter - from domain import Model # noqa: TCH002 However, this approach can get tedious; as an alternative, Litestar accepts a ``signature_types`` sequence at every :ref:`layer ` of the application, as demonstrated in the following example: diff --git a/docs/usage/routing/overview.rst b/docs/usage/routing/overview.rst index a2494b6aea..7785dfee37 100644 --- a/docs/usage/routing/overview.rst +++ b/docs/usage/routing/overview.rst @@ -10,38 +10,20 @@ on which the root level :class:`controllers <.controller.Controller>`, :class:`r and :class:`route handler <.handlers.BaseRouteHandler>` functions are registered using the :paramref:`~litestar.config.app.AppConfig.route_handlers` :term:`kwarg `: -.. code-block:: python +.. literalinclude:: /examples/routing/registering_route_1.py :caption: Registering route handlers + :language: python - from litestar import Litestar, get - - - @get("/sub-path") - def sub_path_handler() -> None: ... - - - @get() - def root_handler() -> None: ... - - - app = Litestar(route_handlers=[root_handler, sub_path_handler]) Components registered on the app are appended to the root path. Thus, the ``root_handler`` function will be called for the path ``"/"``, whereas the ``sub_path_handler`` will be called for ``"/sub-path"``. You can also declare a function to handle multiple paths, e.g.: -.. code-block:: python +.. literalinclude:: /examples/routing/registering_route_2.py :caption: Registering a route handler for multiple paths + :language: python - from litestar import get, Litestar - - - @get(["/", "/sub-path"]) - def handler() -> None: ... - - - app = Litestar(route_handlers=[handler]) To handle more complex path schemas you should use :class:`controllers <.controller.Controller>` and :class:`routers <.router.Router>` @@ -52,46 +34,20 @@ Registering routes dynamically Occasionally there is a need for dynamic route registration. Litestar supports this via the :paramref:`~.app.Litestar.register` method exposed by the Litestar app instance: -.. code-block:: python +.. literalinclude:: /examples/routing/registering_route_3.py :caption: Registering a route handler dynamically with the :paramref:`~.app.Litestar.register` method + :language: python - from litestar import Litestar, get - - - @get() - def root_handler() -> None: ... - - - app = Litestar(route_handlers=[root_handler]) - - - @get("/sub-path") - def sub_path_handler() -> None: ... - - - app.register(sub_path_handler) Since the app instance is attached to all instances of :class:`~.connection.base.ASGIConnection`, :class:`~.connection.request.Request`, and :class:`~.connection.websocket.WebSocket` objects, you can in effect call the :meth:`~.router.Router.register` method inside route handler functions, middlewares, and even injected dependencies. For example: -.. code-block:: python +.. literalinclude:: /examples/routing/registering_route_4.py :caption: Call the :meth:`~.router.Router.register` method from inside a route handler function + :language: python - from typing import Any - from litestar import Litestar, Request, get - - - @get("/some-path") - def route_handler(request: Request[Any, Any]) -> None: - @get("/sub-path") - def sub_path_handler() -> None: ... - - request.app.register(sub_path_handler) - - - app = Litestar(route_handlers=[route_handler]) In the above we dynamically created the ``sub_path_handler`` and registered it inside the ``route_handler`` function. @@ -108,20 +64,11 @@ class which is the base class for the :class:`Litestar app <.app.Litestar>` itse A :class:`~.router.Router` can register :class:`Controllers <.controller.Controller>`, :class:`route handler <.handlers.BaseRouteHandler>` functions, and other routers, similarly to the Litestar constructor: -.. code-block:: python +.. literalinclude:: /examples/routing/registering_route_5.py :caption: Registering a :class:`~.router.Router` - - from litestar import Litestar, Router, get + :language: python - @get("/{order_id:int}") - def order_handler(order_id: int) -> None: ... - - - order_router = Router(path="/orders", route_handlers=[order_handler]) - base_router = Router(path="/base", route_handlers=[order_router]) - app = Litestar(route_handlers=[base_router]) - Once ``order_router`` is registered on ``base_router``, the handler function registered on ``order_router`` will become available on ``/base/orders/{order_id}``. @@ -134,41 +81,10 @@ Their purpose is to allow users to utilize Python OOP for better code organizati .. dropdown:: Click to see an example of registering a controller - .. code-block:: python + .. literalinclude:: /examples/routing/registering_controller.py :caption: Registering a :class:`~.controller.Controller` + :language: python - from litestar.contrib.pydantic import PydanticDTO - from litestar.controller import Controller - from litestar.dto import DTOConfig, DTOData - from litestar.handlers import get, post, patch, delete - from pydantic import BaseModel, UUID4 - - - class UserOrder(BaseModel): - user_id: int - order: str - - - class PartialUserOrderDTO(PydanticDTO[UserOrder]): - config = DTOConfig(partial=True) - - - class UserOrderController(Controller): - path = "/user-order" - - @post() - async def create_user_order(self, data: UserOrder) -> UserOrder: ... - - @get(path="/{order_id:uuid}") - async def retrieve_user_order(self, order_id: UUID4) -> UserOrder: ... - - @patch(path="/{order_id:uuid}", dto=PartialUserOrderDTO) - async def update_user_order( - self, order_id: UUID4, data: DTOData[PartialUserOrderDTO] - ) -> UserOrder: ... - - @delete(path="/{order_id:uuid}") - async def delete_user_order(self, order_id: UUID4) -> None: ... The above is a simple example of a "CRUD" controller for a model called ``UserOrder``. You can place as many :doc:`route handler methods ` on a controller, @@ -189,22 +105,10 @@ You can register both standalone route handler functions and controllers multipl Controllers ^^^^^^^^^^^ -.. code-block:: python - :caption: Registering a controller multiple times - - from litestar import Router, Controller, get - - - class MyController(Controller): - path = "/controller" - - @get() - def handler(self) -> None: ... - +.. literalinclude:: /examples/routing/registering_controller_1.py + :caption: Registering a controller multiple times + :language: python - internal_router = Router(path="/internal", route_handlers=[MyController]) - partner_router = Router(path="/partner", route_handlers=[MyController]) - consumer_router = Router(path="/consumer", route_handlers=[MyController]) In the above, the same ``MyController`` class has been registered on three different routers. This is possible because what is passed to the :class:`router <.router.Router>` is not a class instance but rather the class itself. @@ -219,21 +123,10 @@ Route handlers You can also register standalone route handlers multiple times: -.. code-block:: python - :caption: Registering a route handler multiple times - - from litestar import Litestar, Router, get - - - @get(path="/handler") - def my_route_handler() -> None: ... - - - internal_router = Router(path="/internal", route_handlers=[my_route_handler]) - partner_router = Router(path="/partner", route_handlers=[my_route_handler]) - consumer_router = Router(path="/consumer", route_handlers=[my_route_handler]) +.. literalinclude:: /examples/routing/registering_route_handlers_multiple_times.py + :caption: Registering a route handler multiple times + :language: python - Litestar(route_handlers=[internal_router, partner_router, consumer_router]) When the handler function is registered, it's actually copied. Thus, each router has its own unique instance of the route handler. Path behaviour is identical to that of controllers above, namely, the route handler @@ -252,6 +145,7 @@ requests addressed to a given path. .. literalinclude:: /examples/routing/mount_custom_app.py :caption: Mounting an ASGI App + :language: python The handler function will receive all requests with an url that begins with ``/some/sub-path``, e.g, ``/some/sub-path``, ``/some/sub-path/abc``, ``/some/sub-path/123/another/sub-path``, etc. diff --git a/docs/usage/security/abstract-authentication-middleware.rst b/docs/usage/security/abstract-authentication-middleware.rst index 735c384d01..494a288c80 100644 --- a/docs/usage/security/abstract-authentication-middleware.rst +++ b/docs/usage/security/abstract-authentication-middleware.rst @@ -6,21 +6,9 @@ which is an Abstract Base Class (ABC) that implements the :class:`MiddlewareProt To add authentication to your app using this class as a basis, subclass it and implement the abstract method :meth:`authenticate_request <.middleware.authentication.AbstractAuthenticationMiddleware.authenticate_request>`: -.. code-block:: python +.. literalinclude:: /examples/security/middleware/auth_middleware_1.py + :language: python - from litestar.middleware import ( - AbstractAuthenticationMiddleware, - AuthenticationResult, - ) - from litestar.connection import ASGIConnection - - - class MyAuthenticationMiddleware(AbstractAuthenticationMiddleware): - async def authenticate_request( - self, connection: ASGIConnection - ) -> AuthenticationResult: - # do something here. - ... As you can see, ``authenticate_request`` is an async function that receives a connection instance and is supposed to return an :class:`AuthenticationResult <.middleware.authentication.AuthenticationResult>` instance, which is a pydantic model @@ -43,232 +31,54 @@ Since the above is quite hard to grasp in the abstract, lets see an example. We start off by creating a user model. It can be implemented using pydantic, and ODM, ORM, etc. For the sake of the example here lets say it's a SQLAlchemy model: -.. code-block:: python +.. literalinclude:: /examples/security/middleware/auth_middleware_model.py :caption: my_app/db/models.py - - import uuid - - from sqlalchemy import Column - from sqlalchemy.dialects.postgresql import UUID - from sqlalchemy.orm import declarative_base - - Base = declarative_base() - - - class User(Base): - id: uuid.UUID | None = Column( - UUID(as_uuid=True), default=uuid.uuid4, primary_key=True - ) - # ... other fields follow, but we only require id for this example - + :language: python We will also need some utility methods to encode and decode tokens. To this end we will use the `python-jose `_ library. We will also create a pydantic model representing a JWT Token: - .. code-block:: python - :caption: my_app/security/jwt.py - - from datetime import datetime, timedelta - from uuid import UUID - - from jose import JWTError, jwt - from pydantic import BaseModel, UUID4 - from litestar.exceptions import NotAuthorizedException - - from app.config import settings - - DEFAULT_TIME_DELTA = timedelta(days=1) - ALGORITHM = "HS256" - +.. literalinclude:: /examples/security/middleware/auth_middleware_jwt.py + :caption: my_app/security/jwt.py + :language: python - class Token(BaseModel): - exp: datetime - iat: datetime - sub: UUID4 - - - def decode_jwt_token(encoded_token: str) -> Token: - """ - Helper function that decodes a jwt token and returns the value stored under the ``sub`` key - - If the token is invalid or expired (i.e. the value stored under the exp key is in the past) an exception is raised - """ - try: - payload = jwt.decode( - token=encoded_token, key=settings.JWT_SECRET, algorithms=[ALGORITHM] - ) - return Token(**payload) - except JWTError as e: - raise NotAuthorizedException("Invalid token") from e - - - def encode_jwt_token(user_id: UUID, expiration: timedelta = DEFAULT_TIME_DELTA) -> str: - """Helper function that encodes a JWT token with expiration and a given user_id""" - token = Token( - exp=datetime.now() + expiration, - iat=datetime.now(), - sub=user_id, - ) - return jwt.encode(token.dict(), settings.JWT_SECRET, algorithm=ALGORITHM) We can now create our authentication middleware: -.. code-block:: python - :caption: my_app/security/authentication_middleware.py - - from typing import cast, TYPE_CHECKING - - from sqlalchemy import select - from sqlalchemy.ext.asyncio import AsyncSession - from litestar.middleware import ( - AbstractAuthenticationMiddleware, - AuthenticationResult, - ) - from litestar.exceptions import NotAuthorizedException - from litestar.connection import ASGIConnection - - from app.db.models import User - from app.security.jwt import decode_jwt_token - - if TYPE_CHECKING: - from sqlalchemy.ext.asyncio import AsyncEngine - - API_KEY_HEADER = "X-API-KEY" - - - class JWTAuthenticationMiddleware(AbstractAuthenticationMiddleware): - async def authenticate_request( - self, connection: ASGIConnection - ) -> AuthenticationResult: - """ - Given a request, parse the request api key stored in the header and retrieve the user correlating to the token from the DB - """ - - # retrieve the auth header - auth_header = connection.headers.get(API_KEY_HEADER) - if not auth_header: - raise NotAuthorizedException() - - # decode the token, the result is a ``Token`` model instance - token = decode_jwt_token(encoded_token=auth_header) - - engine = cast("AsyncEngine", connection.app.state.postgres_connection) - async with AsyncSession(engine) as async_session: - async with async_session.begin(): - user = await async_session.execute( - select(User).where(User.id == token.sub) - ) - if not user: - raise NotAuthorizedException() - return AuthenticationResult(user=user, auth=token) +.. literalinclude:: /examples/security/middleware/auth_middleware_creation.py + :caption: my_app/security/authentication_middleware_cr.py + :language: python Finally, we need to pass our middleware to the Litestar constructor: -.. code-block:: python +.. literalinclude:: /examples/security/middleware/auth_middleware_to_app.py :caption: my_app/main.py + :language: python - from litestar import Litestar - from litestar.middleware.base import DefineMiddleware - - from my_app.security.authentication_middleware import JWTAuthenticationMiddleware - - # you can optionally exclude certain paths from authentication. - # the following excludes all routes mounted at or under `/schema*` - auth_mw = DefineMiddleware(JWTAuthenticationMiddleware, exclude="schema") - - app = Litestar(route_handlers=[...], middleware=[auth_mw]) That's it. The ``JWTAuthenticationMiddleware`` will now run for every request, and we would be able to access these in a http route handler in the following way: -.. code-block:: python - - from litestar import Request, get - from litestar.datastructures import State +.. literalinclude:: /examples/security/middleware/auth_middleware_route.py + :language: python - from my_app.db.models import User - from my_app.security.jwt import Token - - - @get("/") - def my_route_handler(request: Request[User, Token, State]) -> None: - user = request.user # correctly typed as User - auth = request.auth # correctly typed as Token - assert isinstance(user, User) - assert isinstance(auth, Token) Or for a websocket route: -.. code-block:: python - - from litestar import WebSocket, websocket - from litestar.datastructures import State +.. literalinclude:: /examples/security/middleware/auth_middleware_websocket.py + :language: python - from my_app.db.models import User - from my_app.security.jwt import Token - - - @websocket("/") - async def my_route_handler(socket: WebSocket[User, Token, State]) -> None: - user = socket.user # correctly typed as User - auth = socket.auth # correctly typed as Token - assert isinstance(user, User) - assert isinstance(auth, Token) And if you'd like to exclude individual routes outside those configured: -.. code-block:: python - - import anyio - from litestar import Litestar, MediaType, Response, get - from litestar.exceptions import NotFoundException - from litestar.middleware.base import DefineMiddleware +.. literalinclude:: /examples/security/middleware/auth_middleware_exclude_route.py + :language: python - from my_app.security.authentication_middleware import JWTAuthenticationMiddleware - - # you can optionally exclude certain paths from authentication. - # the following excludes all routes mounted at or under `/schema*` - # additionally, - # you can modify the default exclude key of "exclude_from_auth", by overriding the `exclude_from_auth_key` parameter on the Authentication Middleware - auth_mw = DefineMiddleware(JWTAuthenticationMiddleware, exclude="schema") - - - @get(path="/", exclude_from_auth=True) - async def site_index() -> Response: - """Site index""" - exists = await anyio.Path("index.html").exists() - if exists: - async with await anyio.open_file(anyio.Path("index.html")) as file: - content = await file.read() - return Response(content=content, status_code=200, media_type=MediaType.HTML) - raise NotFoundException("Site index was not found") - - - app = Litestar(route_handlers=[site_index], middleware=[auth_mw]) And of course use the same kind of mechanism for dependencies: -.. code-block:: python - - from typing import Any - - from litestar import Request, Provide, Router - from litestar.datastructures import State - - from my_app.db.models import User - from my_app.security.jwt import Token - - - async def my_dependency(request: Request[User, Token, State]) -> Any: - user = request.user # correctly typed as User - auth = request.auth # correctly typed as Token - assert isinstance(user, User) - assert isinstance(auth, Token) - - - my_router = Router( - path="sub-path/", dependencies={"some_dependency": Provide(my_dependency)} - ) +.. literalinclude:: /examples/security/middleware/auth_middleware_dependencies.py + :language: python diff --git a/docs/usage/security/excluding-and-including-endpoints.rst b/docs/usage/security/excluding-and-including-endpoints.rst index c63142bbd1..ca6c776b8c 100644 --- a/docs/usage/security/excluding-and-including-endpoints.rst +++ b/docs/usage/security/excluding-and-including-endpoints.rst @@ -12,75 +12,28 @@ The ``exclude`` argument takes a :class:`string ` or :class:`list` of :clas This also means that passing ``/`` will disable authentication for all routes. -.. code-block:: python - - session_auth = SessionAuth[User, ServerSideSessionBackend]( - retrieve_user_handler=retrieve_user_handler, - # we must pass a config for a session backend. - # all session backends are supported - session_backend_config=ServerSideSessionConfig(), - # exclude any URLs that should not have authentication. - # We exclude the documentation URLs, signup and login. - exclude=["/login", "/signup", "/schema"], - ) - ... +.. literalinclude:: /examples/security/excluding_routes.py + :language: python + Including routes ---------------- Since the exclusion rules are evaluated as regex, it is possible to pass a rule that inverts exclusion - meaning, no path but the one specified in the pattern will be protected by authentication. In the example below, only endpoints under the ``/secured`` route will require authentication - all other routes do not. -.. code-block:: python +.. literalinclude:: /examples/security/including_routes.py + :language: python - ... - session_auth = SessionAuth[User, ServerSideSessionBackend]( - retrieve_user_handler=retrieve_user_handler, - # we must pass a config for a session backend. - # all session backends are supported - session_backend_config=ServerSideSessionConfig(), - # exclude any URLs that should not have authentication. - # We exclude the documentation URLs, signup and login. - exclude=[r"^(?!.*\/secured$).*$"], - ) - ... Exclude from auth -------------------- Sometimes, you might want to apply authentication to all endpoints under a route but a few selected. In this case, you can pass ``exclude_from_auth=True`` to the route handler as shown below. -.. code-block:: python - - ... - @get("/secured") - def secured_route() -> Any: - ... +.. literalinclude:: /examples/security/exclude_from_auth.py + :language: python - @get("/unsecured", exclude_from_auth=True) - def unsecured_route() -> Any: - ... - ... You can set an alternative option key in the security configuration, e.g., you can use ``no_auth`` instead of ``exclude_from_auth``. -.. code-block:: python - - ... - @get("/secured") - def secured_route() -> Any: - ... - - @get("/unsecured", no_auth=True) - def unsecured_route() -> Any: - ... - - session_auth = SessionAuth[User, ServerSideSessionBackend]( - retrieve_user_handler=retrieve_user_handler, - # we must pass a config for a session backend. - # all session backends are supported - session_backend_config=ServerSideSessionConfig(), - # exclude any URLs that should not have authentication. - # We exclude the documentation URLs, signup and login. - exclude=["/login", "/signup", "/schema"], - exclude_opt_key="no_auth" # default value is `exclude_from_auth` - ) - ... +.. literalinclude:: /examples/security/exclude_from_auth_with_key.py + :language: python diff --git a/docs/usage/security/guards.rst b/docs/usage/security/guards.rst index f32ab35de1..c8de301e7e 100644 --- a/docs/usage/security/guards.rst +++ b/docs/usage/security/guards.rst @@ -14,73 +14,22 @@ specifying it in the example. We begin by creating an :class:`Enum ` with two roles - ``consumer`` and ``admin``\ : -.. code-block:: python +.. literalinclude:: /examples/guards/enum.py + :language: python - from enum import Enum - - - class UserRole(str, Enum): - CONSUMER = "consumer" - ADMIN = "admin" Our ``User`` model will now look like this: -.. code-block:: python - - from pydantic import BaseModel, UUID4 - from enum import Enum - +.. literalinclude:: /examples/guards/model.py + :language: python - class UserRole(str, Enum): - CONSUMER = "consumer" - ADMIN = "admin" - - - class User(BaseModel): - id: UUID4 - role: UserRole - - @property - def is_admin(self) -> bool: - """Determines whether the user is an admin user""" - return self.role == UserRole.ADMIN Given that the User model has a "role" property we can use it to authorize a request. Let's create a guard that only allows admin users to access certain route handlers and then add it to a route handler function: -.. code-block:: python - - from enum import Enum - - from pydantic import BaseModel, UUID4 - from litestar import post - from litestar.connection import ASGIConnection - from litestar.exceptions import NotAuthorizedException - from litestar.handlers.base import BaseRouteHandler - - - class UserRole(str, Enum): - CONSUMER = "consumer" - ADMIN = "admin" - - - class User(BaseModel): - id: UUID4 - role: UserRole - - @property - def is_admin(self) -> bool: - """Determines whether the user is an admin user""" - return self.role == UserRole.ADMIN - +.. literalinclude:: /examples/guards/guard.py + :language: python - def admin_user_guard(connection: ASGIConnection, _: BaseRouteHandler) -> None: - if not connection.user.is_admin: - raise NotAuthorizedException() - - - @post(path="/user", guards=[admin_user_guard]) - def create_user(data: User) -> User: ... Thus, only an admin user would be able to send a post request to the ``create_user`` handler. @@ -90,30 +39,10 @@ Guard scopes Guards can be declared on all levels of the app - the Litestar instance, routers, controllers, and individual route handlers: -.. code-block:: python - - from litestar import Controller, Router, Litestar - from litestar.connection import ASGIConnection - from litestar.handlers.base import BaseRouteHandler - - - def my_guard(connection: ASGIConnection, handler: BaseRouteHandler) -> None: ... - - - # controller - class UserController(Controller): - path = "/user" - guards = [my_guard] - - ... +.. literalinclude:: /examples/guards/guard_scope.py + :language: python - # router - admin_router = Router(path="admin", route_handlers=[UserController], guards=[my_guard]) - - # app - app = Litestar(route_handlers=[admin_router], guards=[my_guard]) - The deciding factor on where to place a guard is on the kind of access restriction that are required: do only specific route handlers need to be restricted? An entire controller? All the paths under a specific router? Or the entire app? @@ -136,25 +65,5 @@ other flag. This can be achieved with :ref:`the opts kwarg ` of ro To illustrate this lets say we want to have an endpoint that is guarded by a "secret" token, to which end we create the following guard: -.. code-block:: python - - from litestar import get - from litestar.exceptions import NotAuthorizedException - from litestar.connection import ASGIConnection - from litestar.handlers.base import BaseRouteHandler - from os import environ - - - def secret_token_guard( - connection: ASGIConnection, route_handler: BaseRouteHandler - ) -> None: - if ( - route_handler.opt.get("secret") - and not connection.headers.get("Secret-Header", "") - == route_handler.opt["secret"] - ): - raise NotAuthorizedException() - - - @get(path="/secret", guards=[secret_token_guard], opt={"secret": environ.get("SECRET")}) - def secret_endpoint() -> None: ... +.. literalinclude:: /examples/guards/route_handler.py + :language: python diff --git a/docs/usage/templating.rst b/docs/usage/templating.rst index c43a62235b..a57296dc34 100644 --- a/docs/usage/templating.rst +++ b/docs/usage/templating.rst @@ -76,21 +76,10 @@ Registering a Custom Template Engine The above example will create a jinja Environment instance, but you can also pass in your own instance. -.. code-block:: python +.. literalinclude:: /examples/templating/registering_new_template.py + :language: python - from litestar import Litestar - from litestar.contrib.jinja import JinjaTemplateEngine - from litestar.template import TemplateConfig - from jinja2 import Environment, DictLoader - - my_custom_env = Environment(loader=DictLoader({"index.html": "Hello {{name}}!"})) - app = Litestar( - template_config=TemplateConfig( - instance=JinjaTemplateEngine.from_environment(my_custom_env) - ) - ) - .. note:: The ``instance`` parameter passed to :class:`TemplateConfig ` @@ -103,23 +92,9 @@ If you wish to use another templating engine, you can easily do so by implementi :class:`TemplateEngineProtocol `. This class accepts a generic argument which should be the template class, and it specifies two methods: -.. code-block:: python - - from typing import Protocol, Union, List - from pydantic import DirectoryPath - - # the template class of the respective library - from some_lib import SomeTemplate - - - class TemplateEngineProtocol(Protocol[SomeTemplate]): - def __init__(self, directory: Union[DirectoryPath, List[DirectoryPath]]) -> None: - """Builds a template engine.""" - ... +.. literalinclude:: /examples/templating/engine_custom.py + :language: python - def get_template(self, template_name: str) -> SomeTemplate: - """Loads the template with template_name and returns it.""" - ... Once you have your custom engine you can register it as you would the built-in engines. @@ -192,22 +167,17 @@ for small templates or :doc:`HTMX ` responses for example. .. tab-item:: File name - .. code-block:: python + .. literalinclude:: /examples/templating/template_file.py :caption: Template via file + :language: python - @get() - async def example() -> Template: - return Template(template_name="test.html", context={"hello": "world"}) .. tab-item:: String - .. code-block:: python + .. literalinclude:: /examples/templating/template_string.py :caption: Template via string + :language: python - @get() - async def example() -> Template: - template_string = "{{ hello }}" - return Template(template_str=template_string, context={"hello": "world"}) Template context ---------------- @@ -229,44 +199,22 @@ Accessing ``app.state.key`` for example would look like this: .. tab-item:: Jinja :sync: jinja - .. code-block:: html - - - -
- My state value: {{request.app.state.some_key}} -
- - + .. literalinclude:: /examples/templating/templates/request_instance.html.jinja2 + :language: html .. tab-item:: Mako :sync: mako - .. code-block:: html - - html - - -
- My state value: ${request.app.state.some_key} -
- - + .. literalinclude:: /examples/templating/templates/request_instance.html.mako + :language: html .. tab-item:: MiniJinja :sync: minijinja - .. code-block:: html - - - -
- My state value: {{request.app.state.some_key}} -
- - + .. literalinclude:: /examples/templating/templates/request_instance.html.minijinja + :language: html Adding CSRF inputs @@ -282,59 +230,22 @@ With that in place, you can now insert the CSRF input field inside an HTML form: .. tab-item:: Jinja :sync: jinja - .. code-block:: html - - - -
-
- {{ csrf_input | safe }} -
- -
- -
-
- - + .. literalinclude:: /examples/templating/templates/csrf_inputs.html.jinja2 + :language: html + .. tab-item:: Mako :sync: mako - .. code-block:: html - - - -
-
- ${csrf_input | n} -
- -
- -
-
- - + .. literalinclude:: /examples/templating/templates/csrf_inputs.html.mako + :language: html + .. tab-item:: MiniJinja :sync: minijinja - .. code-block:: html - - - -
-
- {{ csrf_input | safe}} -
- -
- -
-
- - + .. literalinclude:: /examples/templating/templates/csrf_inputs.html.minijinja + :language: html The input holds a CSRF token as its value and is hidden so users cannot see or interact with it. The token is sent @@ -350,15 +261,8 @@ Passing template context Passing context to the template is very simple - its one of the kwargs expected by the :class:`Template ` container, so simply pass a string keyed dictionary of values: -.. code-block:: python - - from litestar import get - from litestar.response import Template - - - @get(path="/info") - def info() -> Template: - return Template(template_name="info.html", context={"numbers": "1234567890"}) +.. literalinclude:: /examples/templating/passing_template_context.py + :language: python Template callables diff --git a/docs/usage/testing.rst b/docs/usage/testing.rst index 90102f640d..e91f492660 100644 --- a/docs/usage/testing.rst +++ b/docs/usage/testing.rst @@ -13,18 +13,9 @@ instance of Litestar as the ``app`` kwarg. Let's say we have a very simple app with a health check endpoint: -.. code-block:: python +.. literalinclude:: /examples/testing/test_client_base.py :caption: my_app/main.py - - from litestar import Litestar, MediaType, get - - - @get(path="/health-check", media_type=MediaType.TEXT) - def health_check() -> str: - return "healthy" - - - app = Litestar(route_handlers=[health_check]) + :language: python We would then test it using the test client like so: @@ -34,38 +25,17 @@ We would then test it using the test client like so: .. tab-item:: Sync :sync: sync - .. code-block:: python + .. literalinclude:: /examples/testing/test_client_sync.py :caption: tests/test_health_check.py + :language: python - from litestar.status_codes import HTTP_200_OK - from litestar.testing import TestClient - - from my_app.main import app - - - def test_health_check(): - with TestClient(app=app) as client: - response = client.get("/health-check") - assert response.status_code == HTTP_200_OK - assert response.text == "healthy" .. tab-item:: Async :sync: async - .. code-block:: python + .. literalinclude:: /examples/testing/test_client_async.py :caption: tests/test_health_check.py - - from litestar.status_codes import HTTP_200_OK - from litestar.testing import AsyncTestClient - - from my_app.main import app - - - async def test_health_check(): - async with AsyncTestClient(app=app) as client: - response = await client.get("/health-check") - assert response.status_code == HTTP_200_OK - assert response.text == "healthy" + :language: python Since we would probably need to use the client in multiple places, it's better to make it into a pytest fixture: @@ -76,49 +46,18 @@ Since we would probably need to use the client in multiple places, it's better t .. tab-item:: Sync :sync: sync - .. code-block:: python + .. literalinclude:: /examples/testing/test_client_conf_sync.py :caption: tests/conftest.py - - from typing import TYPE_CHECKING, Iterator - - import pytest - - from litestar.testing import TestClient - - from my_app.main import app - - if TYPE_CHECKING: - from litestar import Litestar - - - @pytest.fixture(scope="function") - def test_client() -> Iterator[TestClient[Litestar]]: - with TestClient(app=app) as client: - yield client + :language: python .. tab-item:: Async :sync: async - .. code-block:: python + .. literalinclude:: /examples/testing/test_client_conf_async.py :caption: tests/conftest.py + :language: python - from typing import TYPE_CHECKING, AsyncIterator - - import pytest - - from litestar.testing import AsyncTestClient - - from my_app.main import app - - if TYPE_CHECKING: - from litestar import Litestar - - - @pytest.fixture(scope="function") - async def test_client() -> AsyncIterator[AsyncTestClient[Litestar]]: - async with AsyncTestClient(app=app) as client: - yield client We would then be able to rewrite our test like so: @@ -246,37 +185,16 @@ expects ``route_handlers`` to be a list, here you can also pass individual value For example, you can do this: -.. code-block:: python - :caption: my_app/tests/test_health_check.py - - from litestar.status_codes import HTTP_200_OK - from litestar.testing import create_test_client - - from my_app.main import health_check - +.. literalinclude:: /examples/testing/test_app_1.py + :caption: my_app/tests/test_health_check.py + :language: python - def test_health_check(): - with create_test_client(route_handlers=[health_check]) as client: - response = client.get("/health-check") - assert response.status_code == HTTP_200_OK - assert response.text == "healthy" But also this: -.. code-block:: python - :caption: my_app/tests/test_health_check.py - - from litestar.status_codes import HTTP_200_OK - from litestar.testing import create_test_client - - from my_app.main import health_check - - - def test_health_check(): - with create_test_client(route_handlers=health_check) as client: - response = client.get("/health-check") - assert response.status_code == HTTP_200_OK - assert response.text == "healthy" +.. literalinclude:: /examples/testing/test_app_2.py + :caption: my_app/tests/test_health_check.py + :language: python RequestFactory @@ -290,63 +208,23 @@ For example, lets say we wanted to unit test a *guard* function in isolation, to from the :doc:`route guards
` documentation: -.. code-block:: python - :caption: my_app/guards.py - - from litestar import Request - from litestar.exceptions import NotAuthorizedException - from litestar.handlers.base import BaseRouteHandler - +.. literalinclude:: /examples/testing/test_request_factory_1.py + :caption: my_app/guards.py + :language: python - def secret_token_guard(request: Request, route_handler: BaseRouteHandler) -> None: - if ( - route_handler.opt.get("secret") - and not request.headers.get("Secret-Header", "") == route_handler.opt["secret"] - ): - raise NotAuthorizedException() We already have our route handler in place: -.. code-block:: python - :caption: my_app/secret.py - - from os import environ - - from litestar import get - - from my_app.guards import secret_token_guard - +.. literalinclude:: /examples/testing/test_request_factory_2.py + :caption: my_app/secret.py + :language: python - @get(path="/secret", guards=[secret_token_guard], opt={"secret": environ.get("SECRET")}) - def secret_endpoint() -> None: ... We could thus test the guard function like so: -.. code-block:: python - :caption: tests/guards/test_secret_token_guard.py - - import pytest - - from litestar.exceptions import NotAuthorizedException - from litestar.testing import RequestFactory - - from my_app.guards import secret_token_guard - from my_app.secret import secret_endpoint - - request = RequestFactory().get("/") - - - def test_secret_token_guard_failure_scenario(): - copied_endpoint_handler = secret_endpoint.copy() - copied_endpoint_handler.opt["secret"] = None - with pytest.raises(NotAuthorizedException): - secret_token_guard(request=request, route_handler=copied_endpoint_handler) - - - def test_secret_token_guard_success_scenario(): - copied_endpoint_handler = secret_endpoint.copy() - copied_endpoint_handler.opt["secret"] = "super-secret" - secret_token_guard(request=request, route_handler=copied_endpoint_handler) +.. literalinclude:: /examples/testing/test_request_factory_3.py + :caption: tests/guards/test_secret_token_guard.py + :language: python Using polyfactory @@ -357,110 +235,22 @@ and powerful way to generate mock data from pydantic models and dataclasses. Let's say we have an API that talks to an external service and retrieves some data: -.. code-block:: python - :caption: main.py - - from typing import Protocol, runtime_checkable - - from polyfactory.factories.pydantic import BaseModel - from litestar import get - - - class Item(BaseModel): - name: str - - - @runtime_checkable - class Service(Protocol): - def get(self) -> Item: ... - - - @get(path="/item") - def get_item(service: Service) -> Item: - return service.get() +.. literalinclude:: /examples/testing/test_polyfactory_1.py + :caption: main.py + :language: python We could test the ``/item`` route like so: -.. code-block:: python - :caption: tests/conftest.py - - import pytest - - from litestar.di import Provide - from litestar.status_codes import HTTP_200_OK - from litestar.testing import create_test_client - - from my_app.main import Service, Item, get_item - - - @pytest.fixture() - def item(): - return Item(name="Chair") - - - def test_get_item(item: Item): - class MyService(Service): - def get_one(self) -> Item: - return item +.. literalinclude:: /examples/testing/test_polyfactory_2.py + :caption: tests/conftest.py + :language: python - with create_test_client( - route_handlers=get_item, dependencies={"service": Provide(lambda: MyService())} - ) as client: - response = client.get("/item") - assert response.status_code == HTTP_200_OK - assert response.json() == item.dict() While we can define the test data manually, as is done in the above, this can be quite cumbersome. That's where `polyfactory `_ library comes in. It generates mock data for pydantic models and dataclasses based on type annotations. With it, we could rewrite the above example like so: - -.. code-block:: python - :caption: main.py - - from typing import Protocol, runtime_checkable - - import pytest - from pydantic import BaseModel - from polyfactory.factories.pydantic_factory import ModelFactory - from litestar.status_codes import HTTP_200_OK - from litestar import get - from litestar.di import Provide - from litestar.testing import create_test_client - - - class Item(BaseModel): - name: str - - - @runtime_checkable - class Service(Protocol): - def get_one(self) -> Item: ... - - - @get(path="/item") - def get_item(service: Service) -> Item: - return service.get_one() - - - class ItemFactory(ModelFactory[Item]): - model = Item - - - @pytest.fixture() - def item(): - return ItemFactory.build() - - - def test_get_item(item: Item): - class MyService(Service): - def get_one(self) -> Item: - return item - - with create_test_client( - route_handlers=get_item, dependencies={"service": Provide(lambda: MyService())} - ) as client: - response = client.get("/item") - assert response.status_code == HTTP_200_OK - assert response.json() == item.dict() +.. literalinclude:: /examples/testing/test_polyfactory_3.py + :caption: main.py + :language: python diff --git a/docs/usage/websockets.rst b/docs/usage/websockets.rst index dad724587b..ed4835d80c 100644 --- a/docs/usage/websockets.rst +++ b/docs/usage/websockets.rst @@ -17,18 +17,8 @@ in incoming data in an already pre-processed form and returns data to be seriali sent over the connection. The low level details will be handled behind the curtains. -.. code-block:: python - - from litestar import Litestar - from litestar.handlers.websocket_handlers import websocket_listener - - - @websocket_listener("/") - async def handler(data: str) -> str: - return data - - - app = Litestar([handler]) +.. literalinclude:: /examples/websockets/websocket_base.py + :language: python This handler will accept connections on ``/``, and wait to receive data. Once a message