diff --git a/.gitignore b/.gitignore index 5bf2443ccb..0c3c190c57 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .scannerwork/ .unasyncd_cache/ .venv/ +.venv* .vscode/ __pycache__/ build/ diff --git a/docs/reference/channels/backends/asyncpg.rst b/docs/reference/channels/backends/asyncpg.rst new file mode 100644 index 0000000000..91d44ecdf1 --- /dev/null +++ b/docs/reference/channels/backends/asyncpg.rst @@ -0,0 +1,5 @@ +asyncpg +======= + +.. automodule:: litestar.channels.backends.asyncpg + :members: diff --git a/docs/reference/channels/backends/index.rst b/docs/reference/channels/backends/index.rst index 010ae7e509..02deff518a 100644 --- a/docs/reference/channels/backends/index.rst +++ b/docs/reference/channels/backends/index.rst @@ -6,3 +6,5 @@ backends base memory redis + psycopg + asyncpg diff --git a/docs/reference/channels/backends/psycopg.rst b/docs/reference/channels/backends/psycopg.rst new file mode 100644 index 0000000000..4a8163db60 --- /dev/null +++ b/docs/reference/channels/backends/psycopg.rst @@ -0,0 +1,5 @@ +psycopg +======= + +.. automodule:: litestar.channels.backends.psycopg + :members: diff --git a/docs/usage/channels.rst b/docs/usage/channels.rst index cbf0ef2721..6e2cd13c02 100644 --- a/docs/usage/channels.rst +++ b/docs/usage/channels.rst @@ -399,7 +399,7 @@ implemented are: A basic in-memory backend, mostly useful for testing and local development, but still fully capable. Since it stores all data in-process, it can achieve the highest performance of all the backends, but at the same time is not suitable for - applications running on multiple processes. + applications running on multiple processes :class:`RedisChannelsPubSubBackend <.redis.RedisChannelsPubSubBackend>` A Redis based backend, using `Pub/Sub `_ to @@ -413,6 +413,17 @@ implemented are: when history is needed +:class:`AsyncPgChannelsBackend <.asyncpg.AsyncPgChannelsBackend>` + A postgres backend using the + `asyncpg `_ driver + + +:class:`PsycoPgChannelsBackend <.psycopg.AsyncPgChannelsBackend>` + A postgres backend using the `psycopg3 `_ + async driver + + + Integrating with websocket handlers ----------------------------------- diff --git a/litestar/channels/backends/asyncpg.py b/litestar/channels/backends/asyncpg.py new file mode 100644 index 0000000000..4b3948d179 --- /dev/null +++ b/litestar/channels/backends/asyncpg.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import asyncio +from contextlib import AsyncExitStack +from functools import partial +from typing import AsyncGenerator, Awaitable, Callable, Iterable, overload + +import asyncpg + +from litestar.channels import ChannelsBackend +from litestar.exceptions import ImproperlyConfiguredException + + +class AsyncPgChannelsBackend(ChannelsBackend): + _listener_conn: asyncpg.Connection + _queue: asyncio.Queue[tuple[str, bytes]] + + @overload + def __init__(self, dsn: str) -> None: + ... + + @overload + def __init__( + self, + *, + make_connection: Callable[[], Awaitable[asyncpg.Connection]], + ) -> None: + ... + + def __init__( + self, + dsn: str | None = None, + *, + make_connection: Callable[[], Awaitable[asyncpg.Connection]] | None = None, + ) -> None: + if not (dsn or make_connection): + raise ImproperlyConfiguredException("Need to specify dsn or make_connection") + + self._subscribed_channels: set[str] = set() + self._exit_stack = AsyncExitStack() + self._connect = make_connection or partial(asyncpg.connect, dsn=dsn) + + async def on_startup(self) -> None: + self._queue = asyncio.Queue() + self._listener_conn = await self._connect() + + async def on_shutdown(self) -> None: + await self._listener_conn.close() + del self._queue + + async def publish(self, data: bytes, channels: Iterable[str]) -> None: + dec_data = data.decode("utf-8") + + conn = await self._connect() + try: + for channel in channels: + await conn.execute("SELECT pg_notify($1, $2);", channel, dec_data) + finally: + await conn.close() + + async def subscribe(self, channels: Iterable[str]) -> None: + for channel in set(channels) - self._subscribed_channels: + await self._listener_conn.add_listener(channel, self._listener) # type: ignore[arg-type] + self._subscribed_channels.add(channel) + + async def unsubscribe(self, channels: Iterable[str]) -> None: + for channel in channels: + await self._listener_conn.remove_listener(channel, self._listener) # type: ignore[arg-type] + self._subscribed_channels = self._subscribed_channels - set(channels) + + async def stream_events(self) -> AsyncGenerator[tuple[str, bytes], None]: + while self._queue: + yield await self._queue.get() + self._queue.task_done() + + async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: + raise NotImplementedError() + + def _listener(self, /, connection: asyncpg.Connection, pid: int, channel: str, payload: object) -> None: + if not isinstance(payload, str): + raise RuntimeError("Invalid data received") + self._queue.put_nowait((channel, payload.encode("utf-8"))) diff --git a/litestar/channels/backends/psycopg.py b/litestar/channels/backends/psycopg.py new file mode 100644 index 0000000000..14b53bcd1a --- /dev/null +++ b/litestar/channels/backends/psycopg.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from contextlib import AsyncExitStack +from typing import AsyncGenerator, Iterable + +import psycopg + +from .base import ChannelsBackend + + +def _safe_quote(ident: str) -> str: + return '"{}"'.format(ident.replace('"', '""')) # sourcery skip + + +class PsycoPgChannelsBackend(ChannelsBackend): + _listener_conn: psycopg.AsyncConnection + + def __init__(self, pg_dsn: str) -> None: + self._pg_dsn = pg_dsn + self._subscribed_channels: set[str] = set() + self._exit_stack = AsyncExitStack() + + async def on_startup(self) -> None: + self._listener_conn = await psycopg.AsyncConnection.connect(self._pg_dsn, autocommit=True) + await self._exit_stack.enter_async_context(self._listener_conn) + + async def on_shutdown(self) -> None: + await self._exit_stack.aclose() + + async def publish(self, data: bytes, channels: Iterable[str]) -> None: + dec_data = data.decode("utf-8") + async with await psycopg.AsyncConnection.connect(self._pg_dsn) as conn: + for channel in channels: + await conn.execute("SELECT pg_notify(%s, %s);", (channel, dec_data)) + + async def subscribe(self, channels: Iterable[str]) -> None: + for channel in set(channels) - self._subscribed_channels: + # can't use placeholders in LISTEN + await self._listener_conn.execute(f"LISTEN {_safe_quote(channel)};") # pyright: ignore + + self._subscribed_channels.add(channel) + + async def unsubscribe(self, channels: Iterable[str]) -> None: + for channel in channels: + # can't use placeholders in UNLISTEN + await self._listener_conn.execute(f"UNLISTEN {_safe_quote(channel)};") # pyright: ignore + self._subscribed_channels = self._subscribed_channels - set(channels) + + async def stream_events(self) -> AsyncGenerator[tuple[str, bytes], None]: + async for notify in self._listener_conn.notifies(): + yield notify.channel, notify.payload.encode("utf-8") + + async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: + raise NotImplementedError() diff --git a/litestar/channels/plugin.py b/litestar/channels/plugin.py index ae7dcc78b3..985c337847 100644 --- a/litestar/channels/plugin.py +++ b/litestar/channels/plugin.py @@ -311,10 +311,10 @@ async def _sub_worker(self) -> None: subscriber.put_nowait(payload) async def _on_startup(self) -> None: + await self._backend.on_startup() self._pub_queue = Queue() self._pub_task = create_task(self._pub_worker()) self._sub_task = create_task(self._sub_worker()) - await self._backend.on_startup() if self._channels: await self._backend.subscribe(list(self._channels)) diff --git a/litestar/contrib/pydantic/pydantic_dto_factory.py b/litestar/contrib/pydantic/pydantic_dto_factory.py index d61f95d671..e7a7f901ee 100644 --- a/litestar/contrib/pydantic/pydantic_dto_factory.py +++ b/litestar/contrib/pydantic/pydantic_dto_factory.py @@ -8,7 +8,7 @@ from litestar.contrib.pydantic.utils import is_pydantic_undefined from litestar.dto.base_dto import AbstractDTO from litestar.dto.data_structures import DTOFieldDefinition -from litestar.dto.field import DTO_FIELD_META_KEY, DTOField +from litestar.dto.field import DTO_FIELD_META_KEY, extract_dto_field from litestar.exceptions import MissingDependencyException, ValidationException from litestar.types.empty import Empty @@ -81,7 +81,8 @@ def generate_field_definitions( for field_name, field_info in model_fields.items(): field_definition = model_field_definitions[field_name] - dto_field = (field_definition.extra or {}).pop(DTO_FIELD_META_KEY, DTOField()) + dto_field = extract_dto_field(field_definition, field_definition.extra) + field_definition.extra.pop(DTO_FIELD_META_KEY, None) if not is_pydantic_undefined(field_info.default): default = field_info.default diff --git a/litestar/dto/dataclass_dto.py b/litestar/dto/dataclass_dto.py index 554b0f3343..5301abac83 100644 --- a/litestar/dto/dataclass_dto.py +++ b/litestar/dto/dataclass_dto.py @@ -5,7 +5,7 @@ from litestar.dto.base_dto import AbstractDTO from litestar.dto.data_structures import DTOFieldDefinition -from litestar.dto.field import DTO_FIELD_META_KEY, DTOField +from litestar.dto.field import extract_dto_field from litestar.params import DependencyKwarg, KwargDefinition from litestar.types.empty import Empty @@ -40,7 +40,7 @@ def generate_field_definitions( DTOFieldDefinition.from_field_definition( field_definition=field_definition, default_factory=default_factory, - dto_field=dc_field.metadata.get(DTO_FIELD_META_KEY, DTOField()), + dto_field=extract_dto_field(field_definition, dc_field.metadata), model_name=model_type.__name__, ), name=key, diff --git a/litestar/dto/field.py b/litestar/dto/field.py index d5ed4b0a99..447b40a796 100644 --- a/litestar/dto/field.py +++ b/litestar/dto/field.py @@ -3,13 +3,19 @@ from dataclasses import dataclass from enum import Enum -from typing import Literal +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Literal, Mapping + + from litestar.typing import FieldDefinition __all__ = ( "DTO_FIELD_META_KEY", "DTOField", "Mark", "dto_field", + "extract_dto_field", ) DTO_FIELD_META_KEY = "__dto__" @@ -26,7 +32,7 @@ class Mark(str, Enum): """To mark a field that can neither be read or updated by clients.""" -@dataclass +@dataclass(unsafe_hash=True) class DTOField: """For configuring DTO behavior on model fields.""" @@ -47,3 +53,28 @@ def dto_field(mark: Literal["read-only", "write-only", "private"] | Mark) -> dic Marking a field automates its inclusion/exclusion from DTO field definitions, depending on the DTO's purpose. """ return {DTO_FIELD_META_KEY: DTOField(mark=Mark(mark))} + + +def extract_dto_field(field_definition: FieldDefinition, field_info_mapping: Mapping[str, Any]) -> DTOField: + """Extract ``DTOField`` instance for a model field. + + Supports ``DTOField`` to bet set via ``Annotated`` or via a field info/metadata mapping. + + E.g., ``Annotated[str, DTOField(mark="read-only")]`` or ``info=dto_field(mark="read-only")``. + + If a value is found in ``field_info_mapping``, it is prioritized over the field definition's metadata. + + Args: + field_definition: A field definition. + field_info_mapping: A field metadata/info attribute mapping, e.g., SQLAlchemy's ``info`` attribute, + or dataclasses ``metadata`` attribute. + + Returns: + DTO field info, if any. + """ + if inst := field_info_mapping.get(DTO_FIELD_META_KEY): + if not isinstance(inst, DTOField): + raise TypeError(f"DTO field info must be an instance of DTOField, got '{inst}'") + return inst + + return next((f for f in field_definition.metadata if isinstance(f, DTOField)), DTOField()) diff --git a/litestar/dto/msgspec_dto.py b/litestar/dto/msgspec_dto.py index 826a1d274f..9996319747 100644 --- a/litestar/dto/msgspec_dto.py +++ b/litestar/dto/msgspec_dto.py @@ -7,7 +7,7 @@ from litestar.dto.base_dto import AbstractDTO from litestar.dto.data_structures import DTOFieldDefinition -from litestar.dto.field import DTO_FIELD_META_KEY, DTOField +from litestar.dto.field import DTO_FIELD_META_KEY, extract_dto_field from litestar.types.empty import Empty if TYPE_CHECKING: @@ -36,7 +36,8 @@ def default_or_none(value: Any) -> Any: for key, field_definition in cls.get_model_type_hints(model_type).items(): msgspec_field = msgspec_fields[key] - dto_field = (field_definition.extra or {}).pop(DTO_FIELD_META_KEY, DTOField()) + dto_field = extract_dto_field(field_definition, field_definition.extra) + field_definition.extra.pop(DTO_FIELD_META_KEY, None) yield replace( DTOFieldDefinition.from_field_definition( diff --git a/pdm.lock b/pdm.lock index 118c916546..21574c4f6d 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "standard", "jwt", "pydantic", "cli", "picologging", "dev-contrib", "piccolo", "prometheus", "dev", "mako", "test", "brotli", "cryptography", "linting", "attrs", "opentelemetry", "docs", "redis", "sqlalchemy", "full", "annotated-types", "jinja", "structlog", "minijinja"] strategy = ["cross_platform"] lock_version = "4.4" -content_hash = "sha256:e3ef2b1bbcc753c48d8185f97fb73f909523db7b83d3c793e33c69f06ca5377c" +content_hash = "sha256:41d1a0bec4e7f740ddcc225a576790c26341ac50eee37864a7a1486d04cb3687" [[package]] name = "accessible-pygments" @@ -284,6 +284,20 @@ files = [ {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, ] +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +requires_python = ">=3.6" +summary = "Backport of the standard library zoneinfo module" +files = [ + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + [[package]] name = "beanie" version = "1.23.6" @@ -1409,7 +1423,7 @@ files = [ [[package]] name = "litestar-sphinx-theme" version = "0.2.0" -requires_python = ">=3.8,<4.0" +requires_python = "<4.0,>=3.8" git = "https://github.com/litestar-org/litestar-sphinx-theme.git" revision = "c5ce66aadc8f910c24f54bf0d172798c237a67eb" summary = "A Sphinx theme for the Litestar organization" @@ -2043,6 +2057,182 @@ files = [ {file = "prometheus_client-0.19.0.tar.gz", hash = "sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1"}, ] +[[package]] +name = "psycopg" +version = "3.1.13" +requires_python = ">=3.7" +summary = "PostgreSQL database adapter for Python" +dependencies = [ + "backports-zoneinfo>=0.2.0; python_version < \"3.9\"", + "typing-extensions>=4.1", + "tzdata; sys_platform == \"win32\"", +] +files = [ + {file = "psycopg-3.1.13-py3-none-any.whl", hash = "sha256:1253010894cfb64e2da4556d4eff5f05e45cafee641f64e02453be849c8f7687"}, + {file = "psycopg-3.1.13.tar.gz", hash = "sha256:e6d047ce16950651d6e26c7c19ca57cc42e1d4841b58729f691244baeee46e30"}, +] + +[[package]] +name = "psycopg-binary" +version = "3.1.13" +requires_python = ">=3.7" +summary = "PostgreSQL database adapter for Python -- C optimisation distribution" +files = [ + {file = "psycopg_binary-3.1.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2cebf20e3c63e9fd5bb73a644b1327fed3f9496c394aec559a49f77ac0772fe2"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:323e6b2caedcb81a57e7b563d31b7cdb2b12aa29f641c3f4a8d071b96cdfafbe"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eac64d6b11e0ea9cadaaa3eda30ac3406c46561b1c482113bbdd7e64446a96e1"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae25d20847962f1800dc1d24e8b22876f736ce3076d923db9d902522c21498a8"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ed5c7ca4a0b241b4360c90cae961156f0c2a6c2822fb61f68076b928650b523"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7104f8e508d02532d2796563ed6c49a47d24935192f1c13a5b54f3cd78f5686a"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b4e12ced8c8be2cd8d164d26c247c43713ba3e8c303a2b6334830bd081ace4b"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31d00c5ad42ec6a7f5365dc2ada0ac1597741e49781aa49a9c0db708a68b07f5"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c574e8e418fc98fcce054e24b3ea274e9ccbcf5310e47db8d5c07834e22bfa15"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8adf7af9a92d3b4cb849a79604735512d15fe51497a6b8a9accfe480b656a2a8"}, + {file = "psycopg_binary-3.1.13-cp310-cp310-win_amd64.whl", hash = "sha256:c7cc4a583e279c6aa11aad41c99067e84477debd4501bac08c74539ddcebd4e3"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:770f16be9c0b542ae31c68204b4fb06e1484398a71ca9d9826cf37e6f6aa7973"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b82e82ed025ca09449a97323f82db10edf31dc2a96b021b42fcf351cb95b56"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95f62d0140b47a71aed55ff52eaae81a134ffc047cffdf73c564aebbc3259b9e"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cccc2b2516ed00bfffd3d5eb8d29f0983ae57b1273c026c6a914125d2519be6"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbba364e29522d8e073c968f30e67e4018a596270244ebf64ed4e59b759492d6"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2aca6b55ef50911a0b05ec71b3ec749487813ac08085f260ad3caccbd7ecbb62"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:152e01605305aede07fe00eebd5f1f4792452efc13f017ae871e88b2fb8bf562"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d38b5b490e34e9b90e3734a84b546a33c39a69dbd3687d700ca2908c6389cb7f"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5a432c14c9f732345be2dcd94a1287affa562b98ffc6620863181ce15d3e5089"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:86919233d4293f01e0e00ec449746bd46803624e3bb52dd90831a4f3b2959bd8"}, + {file = "psycopg_binary-3.1.13-cp311-cp311-win_amd64.whl", hash = "sha256:dea1c83d5f77651cb88d4726c1a4a4726d2712454081e016191d566de20def99"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:31db5f96438408a8b61ae487d29a11c4a7730b2e9e0857743ce99595fda1a148"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da6e9b00c058557c5a0ba198f4fb7838863a0f88cafbf65c079c7c2e7d57d753"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18f207da55a5b2a5a828e4ea46ff3253bd641f79f45844a42ae981a94181b87c"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df292c11b66172b61ef16f6dffbf7363cbad873ff8c79a785c49fb237db4e720"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24866bd53f138d7aff9e88d2e52d6f205d974fe98788cc69d954cde60a76f2f1"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a875ec279b8cd34562963cc89f71b290cc0d65c7c1dd9f8ff53679ca52fbec2f"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:22aa5381db8e499b5a8489e1f6437c5e171eca476f975c138d6413ff15b66cfa"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ed05b5831146d648e43860670b2bc200eaa1bc0a8f744faeb8dd39d4de648bc5"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:51e4f98a48ddf41a0634a202e89b08448979b9cc88bd1a74207301b05db4c572"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6bf92d298a5fff2477f8dd030fdaad84c8420e03fe76f175675ba3ac44e647f0"}, + {file = "psycopg_binary-3.1.13-cp312-cp312-win_amd64.whl", hash = "sha256:eb49604d27dfe7ea8560f5fdbde667049f961273b64ec1d6c09df4b2c3e83256"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4d6f3bf6a6ac319c8660c44b22bc63e5bba9085ec7e04b171ceec9bb047ed20f"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44cb86654e0759de040dde495cd6bfe6b5f98b4f94c441f5fcbbdda371c62073"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace2606984c0489a18e9dc455acceb8af1eed64c4afc7db4e7a46f4f77734db2"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c24927f5442eeb8ab42090c2882ec9208762bb8d7ae999c4916d55d36a68ee00"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d01ad4594d05ffb374b56be0331bc3bd8525db07e4e18f70ed8a73e45e4a13"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a996e64c4cb61b49432ee81e7486ec94aa1fe369629b88e0dddbe03b76cdc7fb"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:af37704f0086da8ba3ab724ae770902fed09f25efc94979d38fc73857dfd2ea3"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:00e7c7bfa1d4bc9a31ed5da59bc402f29d14cc3156c08a5549998ef74fc54128"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52fa599635789d5a093525d7ac268f00910bae93bb6d896b5d9ad36e87c1b60e"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:da972ac7d62ee758e8da626d4b7626a13488ed9c0165574e745a02868793f636"}, + {file = "psycopg_binary-3.1.13-cp38-cp38-win_amd64.whl", hash = "sha256:420314039fb004e3459d02430024673984bc098e9fccc17d4c9597cfb9b5ea84"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c8cfc2ec46a732912acd9402666d52adcf97205dd7fda984602d46fa51b7199"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:535f6c77ef1a4f309fafd13fc910f2138befa0855a1e12979cd838ae54e27dda"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f500db8566f69ff81275640132ae96b7c4654c623de4ee29a0d0762b1d68086f"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5056e5025f6b2733f0ae913029743f40625bda354cd0b0bd00d4e5b04489bbf1"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a19b69b71e0dadf0a1ee3701eb580e01dd6bfc67c5f017fefb4b039035cd666e"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29bbf26240eaf51bc950f3746c7924e9e0d181368c4967602aea75a5a091f8a6"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:722512f354453134c29df781ed717a79a953a1f2742e6388aa0cd0ba18e1c796"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0ab0b9118f5ba650d6aefd0d4dcbfbd36dfb415f0022a5f413d69b934a31a8a8"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ad4e691483fb7b88dde237c7c7e9691322e7ceccd35f23f4b27e6214b1ef22ae"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6a9545c5c9ccbb6ba0c45deb28f4626aade88a8ace36260324b7673965e7df64"}, + {file = "psycopg_binary-3.1.13-cp39-cp39-win_amd64.whl", hash = "sha256:cd9550cfaf47db9eb44207278b9d418de0076df87cf3a2ea99bc123bd8d379c7"}, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.0" +requires_python = ">=3.8" +summary = "Connection Pool for Psycopg" +dependencies = [ + "typing-extensions>=3.10", +] +files = [ + {file = "psycopg-pool-3.2.0.tar.gz", hash = "sha256:2e857bb6c120d012dba240e30e5dff839d2d69daf3e962127ce6b8e40594170e"}, + {file = "psycopg_pool-3.2.0-py3-none-any.whl", hash = "sha256:73371d4e795d9363c7b496cbb2dfce94ee8fbf2dcdc384d0a937d1d9d8bdd08d"}, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +requires_python = ">=3.7" +summary = "psycopg2 - Python-PostgreSQL Database Adapter" +files = [ + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, +] + +[[package]] +name = "psycopg" +version = "3.1.13" +extras = ["binary", "pool"] +requires_python = ">=3.7" +summary = "PostgreSQL database adapter for Python" +dependencies = [ + "psycopg-binary==3.1.13", + "psycopg-pool", + "psycopg==3.1.13", +] +files = [ + {file = "psycopg-3.1.13-py3-none-any.whl", hash = "sha256:1253010894cfb64e2da4556d4eff5f05e45cafee641f64e02453be849c8f7687"}, + {file = "psycopg-3.1.13.tar.gz", hash = "sha256:e6d047ce16950651d6e26c7c19ca57cc42e1d4841b58729f691244baeee46e30"}, +] + [[package]] name = "pyasn1" version = "0.5.0" @@ -3353,6 +3543,16 @@ files = [ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] +[[package]] +name = "tzdata" +version = "2023.3" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + [[package]] name = "urllib3" version = "2.0.7" diff --git a/pyproject.toml b/pyproject.toml index 528aa8948b..dbfb684a7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,9 @@ dev = [ "trio", "aiosqlite", "exceptiongroup; python_version < \"3.11\"", + "asyncpg>=0.29.0", + "psycopg[pool,binary]>=3.1.10", + "psycopg2-binary", ] dev-contrib = ["opentelemetry-sdk", "httpx-sse"] docs = [ diff --git a/tests/docker_service_fixtures.py b/tests/docker_service_fixtures.py index 536a75c025..ee70f10761 100644 --- a/tests/docker_service_fixtures.py +++ b/tests/docker_service_fixtures.py @@ -139,3 +139,8 @@ async def postgres_responsive(host: str) -> bool: return (await conn.fetchrow("SELECT 1"))[0] == 1 # type: ignore finally: await conn.close() + + +@pytest.fixture() +async def postgres_service(docker_services: DockerServiceRegistry) -> None: + await docker_services.start("postgres", check=postgres_responsive) diff --git a/tests/unit/test_channels/conftest.py b/tests/unit/test_channels/conftest.py index c95799143d..1f041ae901 100644 --- a/tests/unit/test_channels/conftest.py +++ b/tests/unit/test_channels/conftest.py @@ -3,7 +3,9 @@ import pytest from redis.asyncio import Redis as AsyncRedis +from litestar.channels.backends.asyncpg import AsyncPgChannelsBackend from litestar.channels.backends.memory import MemoryChannelsBackend +from litestar.channels.backends.psycopg import PsycoPgChannelsBackend from litestar.channels.backends.redis import RedisChannelsPubSubBackend, RedisChannelsStreamBackend @@ -37,3 +39,13 @@ def redis_pub_sub_backend(redis_client: AsyncRedis) -> RedisChannelsPubSubBacken @pytest.fixture() def memory_backend() -> MemoryChannelsBackend: return MemoryChannelsBackend(history=10) + + +@pytest.fixture() +def postgres_asyncpg_backend(postgres_service: None, docker_ip: str) -> AsyncPgChannelsBackend: + return AsyncPgChannelsBackend(f"postgres://postgres:super-secret@{docker_ip}:5423") + + +@pytest.fixture() +def postgres_psycopg_backend(postgres_service: None, docker_ip: str) -> PsycoPgChannelsBackend: + return PsycoPgChannelsBackend(f"postgres://postgres:super-secret@{docker_ip}:5423") diff --git a/tests/unit/test_channels/test_backends.py b/tests/unit/test_channels/test_backends.py index 17eb0baf8f..871d355131 100644 --- a/tests/unit/test_channels/test_backends.py +++ b/tests/unit/test_channels/test_backends.py @@ -3,14 +3,18 @@ import asyncio from datetime import timedelta from typing import AsyncGenerator, cast +from unittest.mock import AsyncMock, MagicMock import pytest from _pytest.fixtures import FixtureRequest from redis.asyncio.client import Redis from litestar.channels import ChannelsBackend +from litestar.channels.backends.asyncpg import AsyncPgChannelsBackend from litestar.channels.backends.memory import MemoryChannelsBackend +from litestar.channels.backends.psycopg import PsycoPgChannelsBackend from litestar.channels.backends.redis import RedisChannelsPubSubBackend, RedisChannelsStreamBackend +from litestar.exceptions import ImproperlyConfiguredException from litestar.utils.compat import async_next @@ -18,6 +22,8 @@ params=[ pytest.param("redis_pub_sub_backend", id="redis:pubsub", marks=pytest.mark.xdist_group("redis")), pytest.param("redis_stream_backend", id="redis:stream", marks=pytest.mark.xdist_group("redis")), + pytest.param("postgres_asyncpg_backend", id="postgres:asyncpg", marks=pytest.mark.xdist_group("postgres")), + pytest.param("postgres_psycopg_backend", id="postgres:psycopg", marks=pytest.mark.xdist_group("postgres")), pytest.param("memory_backend", id="memory"), ] ) @@ -82,7 +88,7 @@ async def test_unsubscribe_without_subscription(channels_backend: ChannelsBacken async def test_get_history( channels_backend: ChannelsBackend, history_limit: int | None, expected_history_length: int ) -> None: - if isinstance(channels_backend, RedisChannelsPubSubBackend): + if isinstance(channels_backend, (RedisChannelsPubSubBackend, AsyncPgChannelsBackend, PsycoPgChannelsBackend)): pytest.skip("Redis pub/sub backend does not support history") messages = [str(i).encode() for i in range(100)] @@ -97,7 +103,7 @@ async def test_get_history( async def test_discards_history_entries(channels_backend: ChannelsBackend) -> None: - if isinstance(channels_backend, RedisChannelsPubSubBackend): + if isinstance(channels_backend, (RedisChannelsPubSubBackend, AsyncPgChannelsBackend, PsycoPgChannelsBackend)): pytest.skip("Redis pub/sub backend does not support history") for _ in range(20): @@ -133,3 +139,35 @@ async def test_memory_publish_not_initialized_raises() -> None: with pytest.raises(RuntimeError): await backend.publish(b"foo", ["something"]) + + +@pytest.mark.xdist_group("postgres") +async def test_asyncpg_get_history(postgres_asyncpg_backend: AsyncPgChannelsBackend) -> None: + with pytest.raises(NotImplementedError): + await postgres_asyncpg_backend.get_history("something") + + +@pytest.mark.xdist_group("postgres") +async def test_psycopg_get_history(postgres_psycopg_backend: PsycoPgChannelsBackend) -> None: + with pytest.raises(NotImplementedError): + await postgres_psycopg_backend.get_history("something") + + +async def test_asyncpg_make_connection() -> None: + make_connection = AsyncMock() + + backend = AsyncPgChannelsBackend(make_connection=make_connection) + await backend.on_startup() + + make_connection.assert_awaited_once() + + +async def test_asyncpg_no_make_conn_or_dsn_passed_raises() -> None: + with pytest.raises(ImproperlyConfiguredException): + AsyncPgChannelsBackend() # type: ignore[call-overload] + + +def test_asyncpg_listener_raises_on_non_string_payload() -> None: + backend = AsyncPgChannelsBackend(make_connection=AsyncMock()) + with pytest.raises(RuntimeError): + backend._listener(connection=MagicMock(), pid=1, payload=b"abc", channel="foo") diff --git a/tests/unit/test_channels/test_plugin.py b/tests/unit/test_channels/test_plugin.py index c6743e6102..856c8bc9a8 100644 --- a/tests/unit/test_channels/test_plugin.py +++ b/tests/unit/test_channels/test_plugin.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import time from secrets import token_hex from typing import cast from unittest.mock import AsyncMock, MagicMock @@ -24,6 +25,8 @@ params=[ pytest.param("redis_pub_sub_backend", id="redis:pubsub", marks=pytest.mark.xdist_group("redis")), pytest.param("redis_stream_backend", id="redis:stream", marks=pytest.mark.xdist_group("redis")), + pytest.param("postgres_asyncpg_backend", id="postgres:asyncpg", marks=pytest.mark.xdist_group("postgres")), + pytest.param("postgres_psycopg_backend", id="postgres:psycopg", marks=pytest.mark.xdist_group("postgres")), pytest.param("memory_backend", id="memory"), ] ) @@ -119,7 +122,7 @@ def test_create_ws_route_handlers( @pytest.mark.flaky(reruns=5) -def test_ws_route_handlers_receive_arbitrary_message(channels_backend: ChannelsBackend) -> None: +async def test_ws_route_handlers_receive_arbitrary_message(channels_backend: ChannelsBackend) -> None: """The websocket handlers await `WebSocket.receive()` to detect disconnection and stop the subscription. This test ensures that the subscription is only stopped in the case of receiving a `websocket.disconnect` message. @@ -140,7 +143,7 @@ def test_ws_route_handlers_receive_arbitrary_message(channels_backend: ChannelsB @pytest.mark.flaky(reruns=5) -async def test_create_ws_route_handlers_arbitrary_channels_allowed(channels_backend: ChannelsBackend) -> None: +def test_create_ws_route_handlers_arbitrary_channels_allowed(channels_backend: ChannelsBackend) -> None: channels_plugin = ChannelsPlugin( backend=channels_backend, arbitrary_channels_allowed=True, @@ -155,6 +158,8 @@ async def test_create_ws_route_handlers_arbitrary_channels_allowed(channels_back channels_plugin.publish("something", "foo") assert ws.receive_text(timeout=2) == "something" + time.sleep(0.1) + with client.websocket_connect("/ws/bar") as ws: channels_plugin.publish("something else", "bar") assert ws.receive_text(timeout=2) == "something else" diff --git a/tests/unit/test_contrib/test_msgspec.py b/tests/unit/test_contrib/test_msgspec.py index 783f78f80f..9c28ed4ba9 100644 --- a/tests/unit/test_contrib/test_msgspec.py +++ b/tests/unit/test_contrib/test_msgspec.py @@ -5,7 +5,7 @@ from msgspec import Meta, Struct, field from typing_extensions import Annotated -from litestar.dto import MsgspecDTO, dto_field +from litestar.dto import DTOField, MsgspecDTO, dto_field from litestar.dto.data_structures import DTOFieldDefinition from litestar.typing import FieldDefinition @@ -38,3 +38,17 @@ class NotStruct: assert MsgspecDTO.detect_nested_field(FieldDefinition.from_annotation(TestStruct)) is True assert MsgspecDTO.detect_nested_field(FieldDefinition.from_annotation(NotStruct)) is False + + +ReadOnlyInt = Annotated[int, DTOField("read-only")] + + +def test_msgspec_dto_annotated_dto_field() -> None: + class Model(Struct): + a: Annotated[int, DTOField("read-only")] + b: ReadOnlyInt + + dto_type = MsgspecDTO[Model] + fields = list(dto_type.generate_field_definitions(Model)) + assert fields[0].dto_field == DTOField("read-only") + assert fields[1].dto_field == DTOField("read-only") diff --git a/tests/unit/test_contrib/test_pydantic/test_pydantic_dto_factory.py b/tests/unit/test_contrib/test_pydantic/test_pydantic_dto_factory.py index 562537f909..afe898eff9 100644 --- a/tests/unit/test_contrib/test_pydantic/test_pydantic_dto_factory.py +++ b/tests/unit/test_contrib/test_pydantic/test_pydantic_dto_factory.py @@ -7,7 +7,7 @@ from typing_extensions import Annotated from litestar.contrib.pydantic import PydanticDTO -from litestar.dto import dto_field +from litestar.dto import DTOField, dto_field from litestar.dto.data_structures import DTOFieldDefinition from litestar.typing import FieldDefinition @@ -58,3 +58,17 @@ class NotModel: assert PydanticDTO.detect_nested_field(FieldDefinition.from_annotation(TestModel)) is True assert PydanticDTO.detect_nested_field(FieldDefinition.from_annotation(NotModel)) is False + + +ReadOnlyInt = Annotated[int, DTOField("read-only")] + + +def test_pydantic_dto_annotated_dto_field() -> None: + class Model(BaseModel): + a: Annotated[int, DTOField("read-only")] + b: ReadOnlyInt + + dto_type = PydanticDTO[Model] + fields = list(dto_type.generate_field_definitions(Model)) + assert fields[0].dto_field == DTOField("read-only") + assert fields[1].dto_field == DTOField("read-only") diff --git a/tests/unit/test_dto/test_factory/test_dataclass_dto.py b/tests/unit/test_dto/test_factory/test_dataclass_dto.py index 42aea8cc91..7a334a2c39 100644 --- a/tests/unit/test_dto/test_factory/test_dataclass_dto.py +++ b/tests/unit/test_dto/test_factory/test_dataclass_dto.py @@ -6,6 +6,7 @@ from unittest.mock import ANY import pytest +from typing_extensions import Annotated from litestar.dto import DataclassDTO, DTOField from litestar.dto.data_structures import DTOFieldDefinition @@ -121,3 +122,20 @@ def test_dataclass_field_definitions(dto_type: type[DataclassDTO[Model]]) -> Non def test_dataclass_detect_nested(dto_type: type[DataclassDTO[Model]]) -> None: assert dto_type.detect_nested_field(FieldDefinition.from_annotation(Model)) is True assert dto_type.detect_nested_field(FieldDefinition.from_annotation(int)) is False + + +ReadOnlyInt = Annotated[int, DTOField("read-only")] + + +def test_dataclass_dto_annotated_dto_field() -> None: + Annotated[int, DTOField("read-only")] + + @dataclass + class Model: + a: Annotated[int, DTOField("read-only")] + b: ReadOnlyInt + + dto_type = DataclassDTO[Model] + fields = list(dto_type.generate_field_definitions(Model)) + assert fields[0].dto_field == DTOField("read-only") + assert fields[1].dto_field == DTOField("read-only")