From 1b47eec8ebd46d613e8736ba6d71900653a3ca1d Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Sun, 1 Oct 2023 04:39:15 +1000 Subject: [PATCH 1/4] fix: add `create_engine()` method to SQLAlchemy configs. (#2382) * fix: add `create_engine()` method to SQLAlchemy configs. This PR fixes a break in backward compatibility where the `create_engine()` method was removed from the SQLAlchemy config types in a minor version increment. * fixes doc references. --- docs/conf.py | 1 + .../plugins/sqlalchemy_init_plugin.rst | 9 ++++---- .../sqlalchemy/plugins/init/config/asyncio.py | 11 ++++++++- .../sqlalchemy/plugins/init/config/compat.py | 23 +++++++++++++++++++ .../sqlalchemy/plugins/init/config/sync.py | 11 ++++++++- .../test_init_plugin/test_config/__init__.py | 0 .../test_config/test_asyncio.py | 20 ++++++++++++++++ .../test_init_plugin/test_config/test_sync.py | 20 ++++++++++++++++ 8 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 litestar/contrib/sqlalchemy/plugins/init/config/compat.py create mode 100644 tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/__init__.py create mode 100644 tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/test_asyncio.py create mode 100644 tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/test_sync.py diff --git a/docs/conf.py b/docs/conf.py index fd3ea919cb..e6f1b9617c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -115,6 +115,7 @@ (PY_CLASS, "NoneType"), (PY_CLASS, "litestar._openapi.schema_generation.schema.SchemaCreator"), (PY_CLASS, "litestar._signature.model.SignatureModel"), + (PY_CLASS, "litestar.contrib.sqlalchemy.plugins.init.config.compat._CreateEngineMixin"), (PY_CLASS, "litestar.utils.signature.ParsedSignature"), (PY_CLASS, "litestar.utils.sync.AsyncCallable"), # types in changelog that no longer exist diff --git a/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst b/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst index 158a147265..2ebd069f6a 100644 --- a/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst +++ b/docs/usage/databases/sqlalchemy/plugins/sqlalchemy_init_plugin.rst @@ -39,7 +39,8 @@ Renaming the dependencies ######################### You can change the name that the engine and session are bound to by setting the -:attr:`engine_dependency_key` and :attr:`session_dependency_key` +:attr:`engine_dependency_key ` +and :attr:`session_dependency_key ` attributes on the plugin configuration. Configuring the before send handler @@ -49,9 +50,9 @@ The plugin configures a ``before_send`` handler that is called before sending a session and removes it from the connection scope. You can change the handler by setting the -:attr:`before_send_handler` attribute -on the configuration object. For example, an alternate handler is available that will also commit the session on success -and rollback upon failure. +:attr:`before_send_handler ` +attribute on the configuration object. For example, an alternate handler is available that will also commit the session +on success and rollback upon failure. .. tab-set:: diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py b/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py index 528b0e3c91..4f50e2bb71 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py +++ b/litestar/contrib/sqlalchemy/plugins/init/config/asyncio.py @@ -2,10 +2,15 @@ from advanced_alchemy.config.asyncio import AlembicAsyncConfig, AsyncSessionConfig from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( - SQLAlchemyAsyncConfig, + SQLAlchemyAsyncConfig as _SQLAlchemyAsyncConfig, +) +from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import ( autocommit_before_send_handler, default_before_send_handler, ) +from sqlalchemy.ext.asyncio import AsyncEngine + +from litestar.contrib.sqlalchemy.plugins.init.config.compat import _CreateEngineMixin __all__ = ( "SQLAlchemyAsyncConfig", @@ -14,3 +19,7 @@ "default_before_send_handler", "autocommit_before_send_handler", ) + + +class SQLAlchemyAsyncConfig(_SQLAlchemyAsyncConfig, _CreateEngineMixin[AsyncEngine]): + ... diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/compat.py b/litestar/contrib/sqlalchemy/plugins/init/config/compat.py new file mode 100644 index 0000000000..af6c92bd2f --- /dev/null +++ b/litestar/contrib/sqlalchemy/plugins/init/config/compat.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Generic, Protocol, TypeVar + +from litestar.utils.deprecation import deprecated + +if TYPE_CHECKING: + from sqlalchemy import Engine + from sqlalchemy.ext.asyncio import AsyncEngine + + +EngineT_co = TypeVar("EngineT_co", bound="Engine | AsyncEngine", covariant=True) + + +class HasGetEngine(Protocol[EngineT_co]): + def get_engine(self) -> EngineT_co: + ... + + +class _CreateEngineMixin(Generic[EngineT_co]): + @deprecated(version="2.1.1", removal_in="3.0.0", alternative="get_engine()") + def create_engine(self: HasGetEngine[EngineT_co]) -> EngineT_co: + return self.get_engine() diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/sync.py b/litestar/contrib/sqlalchemy/plugins/init/config/sync.py index f033638817..a7839fb62c 100644 --- a/litestar/contrib/sqlalchemy/plugins/init/config/sync.py +++ b/litestar/contrib/sqlalchemy/plugins/init/config/sync.py @@ -2,10 +2,15 @@ from advanced_alchemy.config.sync import AlembicSyncConfig, SyncSessionConfig from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( - SQLAlchemySyncConfig, + SQLAlchemySyncConfig as _SQLAlchemySyncConfig, +) +from advanced_alchemy.extensions.litestar.plugins.init.config.sync import ( autocommit_before_send_handler, default_before_send_handler, ) +from sqlalchemy import Engine + +from litestar.contrib.sqlalchemy.plugins.init.config.compat import _CreateEngineMixin __all__ = ( "SQLAlchemySyncConfig", @@ -14,3 +19,7 @@ "default_before_send_handler", "autocommit_before_send_handler", ) + + +class SQLAlchemySyncConfig(_SQLAlchemySyncConfig, _CreateEngineMixin[Engine]): + ... diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/__init__.py b/tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/test_asyncio.py b/tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/test_asyncio.py new file mode 100644 index 0000000000..e02879d25c --- /dev/null +++ b/tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/test_asyncio.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import pytest +from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine + +from litestar.contrib.sqlalchemy.plugins.init.config.asyncio import SQLAlchemyAsyncConfig + + +def test_create_engine_with_engine_instance() -> None: + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + config = SQLAlchemyAsyncConfig(engine_instance=engine) + with pytest.deprecated_call(): + assert engine is config.create_engine() + + +def test_create_engine_with_connection_string() -> None: + config = SQLAlchemyAsyncConfig(connection_string="sqlite+aiosqlite:///:memory:") + with pytest.deprecated_call(): + engine = config.create_engine() + assert isinstance(engine, AsyncEngine) diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/test_sync.py b/tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/test_sync.py new file mode 100644 index 0000000000..6d58cb5240 --- /dev/null +++ b/tests/unit/test_contrib/test_sqlalchemy/test_init_plugin/test_config/test_sync.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import pytest +from sqlalchemy import Engine, create_engine + +from litestar.contrib.sqlalchemy.plugins.init.config.sync import SQLAlchemySyncConfig + + +def test_create_engine_with_engine_instance() -> None: + engine = create_engine("sqlite:///:memory:") + config = SQLAlchemySyncConfig(engine_instance=engine) + with pytest.deprecated_call(): + assert engine is config.create_engine() + + +def test_create_engine_with_connection_string() -> None: + config = SQLAlchemySyncConfig(connection_string="sqlite:///:memory:") + with pytest.deprecated_call(): + engine = config.create_engine() + assert isinstance(engine, Engine) From 61b71d442438819fa3eb43a4d77094d814265528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= Date: Sun, 1 Oct 2023 18:47:17 +0200 Subject: [PATCH 2/4] fix: #1301 - Apply compression before caching (#2393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: #1301 - Apply compression before caching Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * fix typing Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> --------- Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> --- litestar/_asgi/routing_trie/mapping.py | 6 ++ litestar/constants.py | 1 + litestar/middleware/compression.py | 11 ++- litestar/middleware/response_cache.py | 48 +++++++++++++ litestar/routes/http.py | 48 ++++--------- tests/e2e/test_response_caching.py | 70 ++++++++++++++++++- .../test_compression_middleware.py | 23 ++++++ 7 files changed, 171 insertions(+), 36 deletions(-) create mode 100644 litestar/middleware/response_cache.py diff --git a/litestar/_asgi/routing_trie/mapping.py b/litestar/_asgi/routing_trie/mapping.py index ffa58dda80..d0f0fe5b7f 100644 --- a/litestar/_asgi/routing_trie/mapping.py +++ b/litestar/_asgi/routing_trie/mapping.py @@ -186,6 +186,8 @@ def build_route_middleware_stack( from litestar.middleware.allowed_hosts import AllowedHostsMiddleware from litestar.middleware.compression import CompressionMiddleware from litestar.middleware.csrf import CSRFMiddleware + from litestar.middleware.response_cache import ResponseCacheMiddleware + from litestar.routes import HTTPRoute # we wrap the route.handle method in the ExceptionHandlerMiddleware asgi_handler = wrap_in_exception_handler( @@ -197,6 +199,10 @@ def build_route_middleware_stack( if app.compression_config: asgi_handler = CompressionMiddleware(app=asgi_handler, config=app.compression_config) + + if isinstance(route, HTTPRoute) and any(r.cache for r in route.route_handlers): + asgi_handler = ResponseCacheMiddleware(app=asgi_handler, config=app.response_cache_config) + if app.allowed_hosts: asgi_handler = AllowedHostsMiddleware(app=asgi_handler, config=app.allowed_hosts) diff --git a/litestar/constants.py b/litestar/constants.py index 9db278b4a2..b3bb2c5b2c 100644 --- a/litestar/constants.py +++ b/litestar/constants.py @@ -20,6 +20,7 @@ SCOPE_STATE_DEPENDENCY_CACHE: Final = "dependency_cache" SCOPE_STATE_NAMESPACE: Final = "__litestar__" SCOPE_STATE_RESPONSE_COMPRESSED: Final = "response_compressed" +SCOPE_STATE_IS_CACHED: Final = "is_cached" SKIP_VALIDATION_NAMES: Final = {"request", "socket", "scope", "receive", "send"} UNDEFINED_SENTINELS: Final = {Signature.empty, Empty, Ellipsis, MISSING, UnsetType} WEBSOCKET_CLOSE: Final = "websocket.close" diff --git a/litestar/middleware/compression.py b/litestar/middleware/compression.py index 4648087010..e6443f05b2 100644 --- a/litestar/middleware/compression.py +++ b/litestar/middleware/compression.py @@ -4,12 +4,12 @@ from io import BytesIO from typing import TYPE_CHECKING, Any, Literal, Optional -from litestar.constants import SCOPE_STATE_RESPONSE_COMPRESSED +from litestar.constants import SCOPE_STATE_IS_CACHED, SCOPE_STATE_RESPONSE_COMPRESSED from litestar.datastructures import Headers, MutableScopeHeaders from litestar.enums import CompressionEncoding, ScopeType from litestar.exceptions import MissingDependencyException from litestar.middleware.base import AbstractMiddleware -from litestar.utils import Ref, set_litestar_scope_state +from litestar.utils import Ref, get_litestar_scope_state, set_litestar_scope_state __all__ = ("CompressionFacade", "CompressionMiddleware") @@ -176,6 +176,8 @@ def create_compression_send_wrapper( initial_message = Ref[Optional["HTTPResponseStartEvent"]](None) started = Ref[bool](False) + _own_encoding = compression_encoding.encode("latin-1") + async def send_wrapper(message: Message) -> None: """Handle and compresses the HTTP Message with brotli. @@ -187,6 +189,11 @@ async def send_wrapper(message: Message) -> None: initial_message.value = message return + if initial_message.value and get_litestar_scope_state(scope, SCOPE_STATE_IS_CACHED): + await send(initial_message.value) + await send(message) + return + if initial_message.value and message["type"] == "http.response.body": body = message["body"] more_body = message.get("more_body") diff --git a/litestar/middleware/response_cache.py b/litestar/middleware/response_cache.py new file mode 100644 index 0000000000..905a90f040 --- /dev/null +++ b/litestar/middleware/response_cache.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from msgspec.msgpack import encode as encode_msgpack + +from litestar.enums import ScopeType +from litestar.utils import get_litestar_scope_state + +from .base import AbstractMiddleware + +__all__ = ["ResponseCacheMiddleware"] + +from typing import TYPE_CHECKING, cast + +from litestar import Request +from litestar.constants import SCOPE_STATE_IS_CACHED + +if TYPE_CHECKING: + from litestar.config.response_cache import ResponseCacheConfig + from litestar.handlers import HTTPRouteHandler + from litestar.types import ASGIApp, Message, Receive, Scope, Send + + +class ResponseCacheMiddleware(AbstractMiddleware): + def __init__(self, app: ASGIApp, config: ResponseCacheConfig) -> None: + self.config = config + super().__init__(app=app, scopes={ScopeType.HTTP}) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + route_handler = cast("HTTPRouteHandler", scope["route_handler"]) + store = self.config.get_store_from_app(scope["app"]) + + expires_in: int | None = None + if route_handler.cache is True: + expires_in = self.config.default_expiration + elif route_handler.cache is not False and isinstance(route_handler.cache, int): + expires_in = route_handler.cache + + messages = [] + + async def wrapped_send(message: Message) -> None: + if not get_litestar_scope_state(scope, SCOPE_STATE_IS_CACHED): + messages.append(message) + if message["type"] == "http.response.body" and not message["more_body"]: + key = (route_handler.cache_key_builder or self.config.key_builder)(Request(scope)) + await store.set(key, encode_msgpack(messages), expires_in=expires_in) + await send(message) + + await self.app(scope, receive, wrapped_send) diff --git a/litestar/routes/http.py b/litestar/routes/http.py index 1df06aa1f4..2582439f8c 100644 --- a/litestar/routes/http.py +++ b/litestar/routes/http.py @@ -1,10 +1,11 @@ from __future__ import annotations -import pickle from itertools import chain from typing import TYPE_CHECKING, Any, cast -from litestar.constants import DEFAULT_ALLOWED_CORS_HEADERS +from msgspec.msgpack import decode as _decode_msgpack_plain + +from litestar.constants import DEFAULT_ALLOWED_CORS_HEADERS, SCOPE_STATE_IS_CACHED from litestar.datastructures.headers import Headers from litestar.datastructures.upload_file import UploadFile from litestar.enums import HttpMethod, MediaType, ScopeType @@ -13,6 +14,7 @@ from litestar.response import Response from litestar.routes.base import BaseRoute from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST +from litestar.utils import set_litestar_scope_state if TYPE_CHECKING: from litestar._kwargs import KwargsModel @@ -128,19 +130,10 @@ async def _get_response_for_request( ): return response - response = await self._call_handler_function( + return await self._call_handler_function( scope=scope, request=request, parameter_model=parameter_model, route_handler=route_handler ) - if route_handler.cache: - await self._set_cached_response( - response=response, - request=request, - route_handler=route_handler, - ) - - return response - async def _call_handler_function( self, scope: Scope, request: Request, parameter_model: KwargsModel, route_handler: HTTPRouteHandler ) -> ASGIApp: @@ -225,30 +218,19 @@ async def _get_cached_response(request: Request, route_handler: HTTPRouteHandler cache_key = (route_handler.cache_key_builder or cache_config.key_builder)(request) store = cache_config.get_store_from_app(request.app) - cached_response = await store.get(key=cache_key) - - if cached_response: - return cast("ASGIApp", pickle.loads(cached_response)) # noqa: S301 + if not (cached_response_data := await store.get(key=cache_key)): + return None - return None + # we use the regular msgspec.msgpack.decode here since we don't need any of + # the added decoders + messages = _decode_msgpack_plain(cached_response_data) - @staticmethod - async def _set_cached_response( - response: Response | ASGIApp, request: Request, route_handler: HTTPRouteHandler - ) -> None: - """Pickles and caches a response object.""" - cache_config = request.app.response_cache_config - cache_key = (route_handler.cache_key_builder or cache_config.key_builder)(request) - - expires_in: int | None = None - if route_handler.cache is True: - expires_in = cache_config.default_expiration - elif route_handler.cache is not False and isinstance(route_handler.cache, int): - expires_in = route_handler.cache - - store = cache_config.get_store_from_app(request.app) + async def cached_response(scope: Scope, receive: Receive, send: Send) -> None: + set_litestar_scope_state(scope, SCOPE_STATE_IS_CACHED, True) + for message in messages: + await send(message) - await store.set(key=cache_key, value=pickle.dumps(response, pickle.HIGHEST_PROTOCOL), expires_in=expires_in) + return cached_response def create_options_handler(self, path: str) -> HTTPRouteHandler: """Args: diff --git a/tests/e2e/test_response_caching.py b/tests/e2e/test_response_caching.py index a8bf7fedc7..8a33e6ca37 100644 --- a/tests/e2e/test_response_caching.py +++ b/tests/e2e/test_response_caching.py @@ -1,13 +1,18 @@ +import gzip import random from datetime import timedelta -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Type, Union from unittest.mock import MagicMock from uuid import uuid4 +import msgspec import pytest from litestar import Litestar, Request, get +from litestar.config.compression import CompressionConfig from litestar.config.response_cache import CACHE_FOREVER, ResponseCacheConfig +from litestar.enums import CompressionEncoding +from litestar.middleware.response_cache import ResponseCacheMiddleware from litestar.stores.base import Store from litestar.stores.memory import MemoryStore from litestar.testing import TestClient, create_test_client @@ -180,3 +185,66 @@ def handler() -> str: assert response_two.text == mock.return_value assert mock.call_count == 1 + + +def test_does_not_apply_to_non_cached_routes(mock: MagicMock) -> None: + @get("/") + def handler() -> str: + return mock() # type: ignore[no-any-return] + + with create_test_client([handler]) as client: + first_response = client.get("/") + second_response = client.get("/") + + assert first_response.status_code == 200 + assert second_response.status_code == 200 + assert mock.call_count == 2 + + +@pytest.mark.parametrize( + "cache,expect_applied", + [ + (True, True), + (False, False), + (1, True), + (CACHE_FOREVER, True), + ], +) +def test_middleware_not_applied_to_non_cached_routes( + cache: Union[bool, int, Type[CACHE_FOREVER]], expect_applied: bool +) -> None: + @get(path="/", cache=cache) + def handler() -> None: + ... + + client = create_test_client(route_handlers=[handler]) + unpacked_middleware = [] + cur = client.app.asgi_router.root_route_map_node.children["/"].asgi_handlers["GET"][0] + while hasattr(cur, "app"): + unpacked_middleware.append(cur) + cur = cur.app + unpacked_middleware.append(cur) + + assert len([m for m in unpacked_middleware if isinstance(m, ResponseCacheMiddleware)]) == int(expect_applied) + + +async def test_compression_applies_before_cache() -> None: + return_value = "_litestar_" * 4000 + mock = MagicMock(return_value=return_value) + + @get(path="/", cache=True) + def handler_fn() -> str: + return mock() # type: ignore[no-any-return] + + app = Litestar( + route_handlers=[handler_fn], + compression_config=CompressionConfig(backend="gzip"), + ) + + with TestClient(app) as client: + client.get("/", headers={"Accept-Encoding": str(CompressionEncoding.GZIP.value)}) + + stored_value = await app.response_cache_config.get_store_from_app(app).get("/") + assert stored_value + stored_messages = msgspec.msgpack.decode(stored_value) + assert gzip.decompress(stored_messages[1]["body"]).decode() == return_value diff --git a/tests/unit/test_middleware/test_compression_middleware.py b/tests/unit/test_middleware/test_compression_middleware.py index 2694e3ab6b..e1c3ec08bb 100644 --- a/tests/unit/test_middleware/test_compression_middleware.py +++ b/tests/unit/test_middleware/test_compression_middleware.py @@ -193,3 +193,26 @@ async def fake_send(message: Message) -> None: # second body message with more_body=True will be empty if zlib buffers output and is not flushed await wrapped_send(HTTPResponseBodyEvent(type="http.response.body", body=b"abc", more_body=True)) assert mock.mock_calls[-1].args[0]["body"] + + +@pytest.mark.parametrize( + "backend, compression_encoding", (("brotli", CompressionEncoding.BROTLI), ("gzip", CompressionEncoding.GZIP)) +) +def test_dont_recompress_cached(backend: Literal["gzip", "brotli"], compression_encoding: CompressionEncoding) -> None: + mock = MagicMock(return_value="_litestar_" * 4000) + + @get(path="/", media_type=MediaType.TEXT, cache=True) + def handler_fn() -> str: + return mock() # type: ignore[no-any-return] + + with create_test_client( + route_handlers=[handler_fn], compression_config=CompressionConfig(backend=backend) + ) as client: + client.get("/", headers={"Accept-Encoding": str(compression_encoding.value)}) + response = client.get("/", headers={"Accept-Encoding": str(compression_encoding.value)}) + + assert mock.call_count == 1 + assert response.status_code == HTTP_200_OK + assert response.text == "_litestar_" * 4000 + assert response.headers["Content-Encoding"] == compression_encoding + assert int(response.headers["Content-Length"]) < 40000 From 3d950e97efca4c2b4615a020490a954f4f08cff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= Date: Mon, 2 Oct 2023 16:51:54 +0200 Subject: [PATCH 3/4] fix: don't implicitly parse URL encoded form data as JSON (#2394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: don't implicitly parse form data as JSON Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> --- litestar/_parsers.py | 24 +++++++++++++++++------- poetry.lock | 8 ++++---- pyproject.toml | 7 ++++--- tests/unit/test_parsers.py | 8 ++++---- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/litestar/_parsers.py b/litestar/_parsers.py index 5d66636d73..49b795514d 100644 --- a/litestar/_parsers.py +++ b/litestar/_parsers.py @@ -1,18 +1,25 @@ from __future__ import annotations +from collections import defaultdict from functools import lru_cache from http.cookies import _unquote as unquote_cookie -from typing import Any, Iterable +from typing import Iterable from urllib.parse import unquote -from fast_query_parsers import parse_query_string as fast_parse_query_string -from fast_query_parsers import parse_url_encoded_dict +try: + from fast_query_parsers import parse_query_string as parse_qsl +except ImportError: + from urllib.parse import parse_qsl as _parse_qsl + + def parse_qsl(qs: bytes, separator: str) -> list[tuple[str, str]]: + return _parse_qsl(qs.decode("latin-1"), keep_blank_values=True, separator=separator) + __all__ = ("parse_cookie_string", "parse_headers", "parse_query_string", "parse_url_encoded_form_data") @lru_cache(1024) -def parse_url_encoded_form_data(encoded_data: bytes) -> dict[str, Any]: +def parse_url_encoded_form_data(encoded_data: bytes) -> dict[str, str | list[str]]: """Parse an url encoded form data dict. Args: @@ -21,11 +28,14 @@ def parse_url_encoded_form_data(encoded_data: bytes) -> dict[str, Any]: Returns: A parsed dict. """ - return parse_url_encoded_dict(qs=encoded_data, parse_numbers=False) + decoded_dict: defaultdict[str, list[str]] = defaultdict(list) + for k, v in parse_qsl(encoded_data, separator="&"): + decoded_dict[k].append(v) + return {k: v if len(v) > 1 else v[0] for k, v in decoded_dict.items()} @lru_cache(1024) -def parse_query_string(query_string: bytes) -> tuple[tuple[str, Any], ...]: +def parse_query_string(query_string: bytes) -> tuple[tuple[str, str], ...]: """Parse a query string into a tuple of key value pairs. Args: @@ -34,7 +44,7 @@ def parse_query_string(query_string: bytes) -> tuple[tuple[str, Any], ...]: Returns: A tuple of key value pairs. """ - return tuple(fast_parse_query_string(query_string, "&")) + return tuple(parse_qsl(query_string, separator="&")) @lru_cache(1024) diff --git a/poetry.lock b/poetry.lock index edb008e6ad..b179618e72 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1104,7 +1104,7 @@ typing-extensions = {version = ">=3.10.0.1", markers = "python_version <= \"3.8\ name = "fast-query-parsers" version = "1.0.3" description = "Ultra-fast query string and url-encoded form-data parsers" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "fast_query_parsers-1.0.3-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:afbf71c1b4398dacfb9d84755eb026f8e759f68a066f1f3cc19e471fc342e74f"}, @@ -4293,7 +4293,7 @@ attrs = ["attrs"] brotli = ["brotli"] cli = ["jsbeautifier", "uvicorn"] cryptography = ["cryptography"] -full = ["advanced-alchemy", "annotated-types", "attrs", "brotli", "cryptography", "jinja2", "jsbeautifier", "mako", "minijinja", "opentelemetry-instrumentation-asgi", "piccolo", "picologging", "prometheus-client", "pydantic", "pydantic-extra-types", "python-jose", "redis", "structlog", "uvicorn"] +full = ["advanced-alchemy", "annotated-types", "attrs", "brotli", "cryptography", "fast-query-parsers", "jinja2", "jsbeautifier", "mako", "minijinja", "opentelemetry-instrumentation-asgi", "piccolo", "picologging", "prometheus-client", "pydantic", "pydantic-extra-types", "python-jose", "redis", "structlog", "uvicorn"] jinja = ["jinja2"] jsbeautifier = ["jsbeautifier"] jwt = ["cryptography", "python-jose"] @@ -4306,10 +4306,10 @@ prometheus = ["prometheus-client"] pydantic = ["pydantic", "pydantic-extra-types"] redis = ["redis"] sqlalchemy = ["advanced-alchemy"] -standard = ["jinja2", "jsbeautifier", "uvicorn"] +standard = ["fast-query-parsers", "jinja2", "jsbeautifier", "uvicorn"] structlog = ["structlog"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "ea7b1c0ac6f00e05451ed86ad33c3d47bb46418d728beb69780f42c61f850f3f" +content-hash = "162ff5be14a8e045a386af07cecc80464c936d38bd59f04d98f4bdb2d45338bd" diff --git a/pyproject.toml b/pyproject.toml index a16f1d7f2a..6e4a6e4c84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ attrs = { version = "*", optional = true } brotli = { version = "*", optional = true } click = "*" cryptography = { version = "*", optional = true } -fast-query-parsers = ">=1.0.2" +fast-query-parsers = {version = ">=1.0.2", optional = true } httpx = ">=0.22" importlib-metadata = { version = "*", python = "<3.10" } importlib-resources = { version = ">=5.12.0", python = "<3.9" } @@ -180,7 +180,7 @@ prometheus = ["prometheus-client"] pydantic = ["pydantic", "pydantic-extra-types"] redis = ["redis"] sqlalchemy = ["advanced-alchemy"] -standard = ["jinja2", "jsbeautifier", "uvicorn"] +standard = ["jinja2", "jsbeautifier", "uvicorn", "fast-query-parsers"] structlog = ["structlog"] full = [ @@ -202,7 +202,8 @@ full = [ "redis", "structlog", "uvicorn", - "advanced-alchemy" + "advanced-alchemy", + "fast-query-parsers", ] [tool.poetry.scripts] diff --git a/tests/unit/test_parsers.py b/tests/unit/test_parsers.py index 6c89bacbeb..14c8e90a66 100644 --- a/tests/unit/test_parsers.py +++ b/tests/unit/test_parsers.py @@ -31,11 +31,11 @@ def test_parse_form_data() -> None: ) assert result == { "value": ["10", "12"], - "veggies": ["tomato", "potato", "aubergine"], - "nested": {"some_key": "some_value"}, + "veggies": '["tomato", "potato", "aubergine"]', + "nested": '{"some_key": "some_value"}', "calories": "122.53", - "healthy": True, - "polluting": False, + "healthy": "true", + "polluting": "false", } From df33c4f3a155d688e9e147f9cc5fb139cab229d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 21:37:16 -0500 Subject: [PATCH 4/4] chore(deps-dev): bump urllib3 from 2.0.5 to 2.0.6 (#2395) Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.5 to 2.0.6. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/v2.0.5...2.0.6) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index b179618e72..c089eeab44 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1300,6 +1300,7 @@ files = [ {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -1308,6 +1309,7 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -1337,6 +1339,7 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -1345,6 +1348,7 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -1946,6 +1950,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -3099,6 +3113,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -3106,8 +3121,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -3124,6 +3146,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -3131,6 +3154,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -3970,13 +3994,13 @@ files = [ [[package]] name = "urllib3" -version = "2.0.5" +version = "2.0.6" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, - {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, + {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, + {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, ] [package.extras]