diff --git a/.all-contributorsrc b/.all-contributorsrc index 169f07269b..855bc358f6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -910,7 +910,9 @@ "avatar_url": "https://avatars.githubusercontent.com/u/35638715?v=4", "profile": "https://github.com/kedod", "contributions": [ - "doc" + "doc", + "code", + "test" ] }, { @@ -1602,6 +1604,71 @@ "contributions": [ "bug" ] + }, + { + "login": "betaprior", + "name": "Leo Alekseyev", + "avatar_url": "https://avatars.githubusercontent.com/u/338250?v=4", + "profile": "http://dnquark.com", + "contributions": [ + "code" + ] + }, + { + "login": "aranvir", + "name": "aranvir", + "avatar_url": "https://avatars.githubusercontent.com/u/75439739?v=4", + "profile": "https://github.com/aranvir", + "contributions": [ + "doc" + ] + }, + { + "login": "bunny-therapist", + "name": "bunny-therapist", + "avatar_url": "https://avatars.githubusercontent.com/u/87039365?v=4", + "profile": "https://github.com/bunny-therapist", + "contributions": [ + "code" + ] + }, + { + "login": "benluo", + "name": "Ben Luo", + "avatar_url": "https://avatars.githubusercontent.com/u/70398?v=4", + "profile": "http://www.benluo.cc", + "contributions": [ + "doc" + ] + }, + { + "login": "hugovk", + "name": "Hugo van Kemenade", + "avatar_url": "https://avatars.githubusercontent.com/u/1324225?v=4", + "profile": "https://github.com/hugovk", + "contributions": [ + "doc" + ] + }, + { + "login": "error418", + "name": "Michael Gerbig", + "avatar_url": "https://avatars.githubusercontent.com/u/7716544?v=4", + "profile": "https://error418.github.io", + "contributions": [ + "doc" + ] + }, + { + "login": "crisog", + "name": "CrisOG", + "avatar_url": "https://avatars.githubusercontent.com/u/40803711?v=4", + "profile": "https://github.com/crisog", + "contributions": [ + "bug", + "code", + "test" + ] } ], "contributorsPerLine": 7, diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8015de1af6..f9bc02a381 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,8 +4,15 @@ By submitting this pull request, you agree to: - follow [Litestar's contribution guidelines](https://github.com/litestar-org/.github/blob/main/CONTRIBUTING.md) - follow the [PSFs's Code of Conduct](https://www.python.org/psf/conduct/) --> +## Description + +- +## Closes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab587d4192..bbf0986b56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,41 @@ jobs: coverage: ${{ (matrix.python-version == '3.12' || matrix.python-version == '3.8') }} python-version: ${{ matrix.python-version }} + test_integration: + name: Test server integration + runs-on: ubuntu-latest + strategy: + matrix: + uvicorn-version: ["uvicorn<0.27.0", "uvicorn>=0.27.0"] + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - uses: pdm-project/setup-pdm@v4 + name: Set up PDM + with: + python-version: 3.11 + allow-python-prereleases: false + cache: true + cache-dependency-path: | + ./pdm.lock + + - name: Install dependencies + run: | + pdm install -G:all + pip install -U "${{ matrix.uvicorn-version }}" + + - name: Set PYTHONPATH + run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV + + - name: Test + run: pdm run pytest tests -m server_integration + upload-test-coverage: runs-on: ubuntu-latest needs: test @@ -112,16 +147,17 @@ jobs: python -Im coverage combine python -Im coverage xml -i - - name: Fix coverage file for sonarcloud + - name: Fix coverage file name run: sed -i "s/home\/runner\/work\/litestar\/litestar/github\/workspace/g" coverage.xml - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} test-platform-compat: - if: github.event_name == 'push' + if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'test platform compat') strategy: fail-fast: false matrix: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 123ee9a173..e564aa83d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: unasyncd additional_dependencies: ["ruff"] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.1.14" + rev: "v0.2.1" hooks: - id: ruff args: ["--fix"] @@ -35,15 +35,6 @@ repos: exclude: "tests/openapi/typescript_converter/test_converter|README.md" additional_dependencies: - tomli - - repo: https://github.com/asottile/blacken-docs - rev: 1.16.0 - hooks: - - id: blacken-docs - - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v4.0.0-alpha.8" - hooks: - - id: prettier - exclude: "_templates|.git|.all-contributorsrc" - repo: https://github.com/python-formate/flake8-dunder-all rev: v0.3.1 hooks: @@ -51,7 +42,7 @@ repos: exclude: "test*|examples*|tools" args: ["--use-tuple"] - repo: https://github.com/ariebovenberg/slotscheck - rev: v0.17.1 + rev: v0.17.3 hooks: - id: slotscheck exclude: "test_*|docs|.github" diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1999ea504e..0042054d62 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -44,7 +44,7 @@ Workflow 5. (Optional) Run ``pre-commit run --all-files`` to run linters and formatters. This step is optional and will be executed automatically by git before you make a commit, but you may want to run it manually in order to apply fixes 6. Commit your changes to git. Note - we follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), - which are enforced using a `pre-commit` hook. + which are enforced using a ``pre-commit`` hook. 7. Push the changes to your fork 8. Open a `pull request `_. Give the pull request a descriptive title indicating what it changes. The style of the PR title should also follow @@ -56,7 +56,7 @@ Guidelines for writing code - Code should be `Pythonic and zen `_ - All code should be fully `typed `_. This is enforced via - `mypy `_ and `pyright `_ + `mypy `_ and `Pyright `_ * When requiring complex types, use a `type alias `_. Check ``litestar/types`` if a type alias for your use case already exists @@ -76,7 +76,7 @@ Guidelines for writing code across a function or method that doesn't conform to this standard, please update it as you go - When adding a new public interface, it has to be included in the reference documentation located in ``docs/reference``. If applicable, add or modify examples in the docs related to the new functionality implemented, - following the guidelines established in `Adding examples`_ + following the guidelines established in `Adding examples`_. Writing and running tests @@ -115,16 +115,16 @@ Our type checkers are run on Python 3.8 in CI, so you should make sure to run th Project documentation --------------------- -The documentation is located in the ``/docs`` directory and is `ReST `_ and +The documentation is located in the ``/docs`` directory and is `reST `_ and `Sphinx `_. If you're unfamiliar with any of those, -`ReStructuredText primer `_ and +`reStructuredText primer `_ and `Sphinx quickstart `_ are recommended reads. Docs theme and appearance +++++++++++++++++++++++++ We welcome contributions that enhance / improve the appearance and usability of the docs. We use the excellent -`Furo `_ theme, which comes with a lot of options out of the box. If you wish to +`PyData Sphinx Theme `_ theme, which comes with a lot of options out of the box. If you wish to contribute to the docs style / setup, or static site generation, you should consult the theme docs as a first step. Running the docs locally @@ -142,13 +142,13 @@ Writing and editing docs We welcome contributions that enhance / improve the content of the docs. Feel free to add examples, clarify text, restructure the docs, etc., but make sure to follow these guidelines: -- Write text in idiomatic english, using simple language +- Write text in idiomatic English, using simple language - Opt for `Oxford commas `_ when listing a series of terms - Keep examples simple and self contained - Provide links where applicable - Use `intersphinx `_ wherever possible when referencing external libraries -- Provide diagrams using `mermaidjs `_ where applicable and possible +- Provide diagrams using `Mermaid `_ where applicable and possible Adding examples ~~~~~~~~~~~~~~~ @@ -182,7 +182,7 @@ will be launched, and the requests specified in the comments will be run against comments will be stripped from the result, and the output of the ``curl`` invocation inserted after the example code-block. -The ``# run:`` syntax is nothing special; Everything after the colon will be passed to +The ``# run:`` syntax is nothing special; everything after the colon will be passed to the ``curl`` command that's being invoked. The URL is built automatically, so the specified path can just be a path relative to the app. @@ -237,7 +237,7 @@ Creating a new release 1. Increment the version in ``pyproject.toml`` according to the `versioning scheme `_ .. note:: - The version should follow `semantic versioning `_ and `PEP 440 `_. + The version should follow `semantic versioning `_ and `PEP 440 `_. 2. Commit and push. 2. `Draft a new release `_ on GitHub diff --git a/README.md b/README.md index dc34ae7622..09b86afd3f 100644 --- a/README.md +++ b/README.md @@ -446,7 +446,7 @@ see [the contribution guide](CONTRIBUTING.rst). Tomas Jonsson
Tomas Jonsson

⚠️ 💻 Khiem Doan
Khiem Doan

📖 - kedod
kedod

📖 + kedod
kedod

📖 💻 ⚠️ sonpro1296
sonpro1296

💻 ⚠️ 🚇 📖 Patrick Armengol
Patrick Armengol

📖 Sander
Sander

📖 @@ -540,6 +540,15 @@ see [the contribution guide](CONTRIBUTING.rst). Mike Korneev
Mike Korneev

📖 Patrick Neise
Patrick Neise

💻 Jean Arhancet
Jean Arhancet

🐛 + Leo Alekseyev
Leo Alekseyev

💻 + + + aranvir
aranvir

📖 + bunny-therapist
bunny-therapist

💻 + Ben Luo
Ben Luo

📖 + Hugo van Kemenade
Hugo van Kemenade

📖 + Michael Gerbig
Michael Gerbig

📖 + CrisOG
CrisOG

🐛 💻 ⚠️ diff --git a/docs/conf.py b/docs/conf.py index e9a00e4fb0..2bcb2c679e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -178,6 +178,7 @@ ("py:exc", "InternalServerError"), ("py:exc", "HTTPExceptions"), (PY_CLASS, "litestar.template.Template"), + (PY_CLASS, "litestar.middleware.compression.gzip_facade.GzipCompression"), ] nitpick_ignore_regex = [ diff --git a/docs/examples/contrib/piccolo/app.py b/docs/examples/contrib/piccolo/app.py index c6e38d5443..3260be2200 100644 --- a/docs/examples/contrib/piccolo/app.py +++ b/docs/examples/contrib/piccolo/app.py @@ -77,7 +77,4 @@ async def on_startup(): await create_db_tables(Task, if_not_exists=True) -app = Litestar( - route_handlers=[tasks, create_task, delete_task, update_task], - on_startup=[on_startup], -) +app = Litestar(route_handlers=[tasks, create_task, delete_task, update_task], on_startup=[on_startup], debug=True) diff --git a/docs/examples/plugins/di_plugin.py b/docs/examples/plugins/di_plugin.py new file mode 100644 index 0000000000..35625d4163 --- /dev/null +++ b/docs/examples/plugins/di_plugin.py @@ -0,0 +1,31 @@ +from inspect import Parameter, Signature +from typing import Any, Dict, Tuple + +from litestar import Litestar, get +from litestar.di import Provide +from litestar.plugins import DIPlugin + + +class MyBaseType: + def __init__(self, param): + self.param = param + + +class MyDIPlugin(DIPlugin): + def has_typed_init(self, type_: Any) -> bool: + return issubclass(type_, MyBaseType) + + def get_typed_init(self, type_: Any) -> Tuple[Signature, Dict[str, Any]]: + signature = Signature([Parameter(name="param", kind=Parameter.POSITIONAL_OR_KEYWORD)]) + annotations = {"param": str} + return signature, annotations + + +@get("/", dependencies={"injected": Provide(MyBaseType, sync_to_thread=False)}) +async def handler(injected: MyBaseType) -> str: + return injected.param + + +app = Litestar(route_handlers=[handler], plugins=[MyDIPlugin()]) + +# run: /?param=hello diff --git a/docs/examples/security/jwt/using_jwt_cookie_auth.py b/docs/examples/security/jwt/using_jwt_cookie_auth.py index 909a38a733..c1e86f9ad5 100644 --- a/docs/examples/security/jwt/using_jwt_cookie_auth.py +++ b/docs/examples/security/jwt/using_jwt_cookie_auth.py @@ -40,7 +40,7 @@ async def retrieve_user_handler(token: "Token", connection: "ASGIConnection[Any, # and our openAPI docs. exclude=["/login", "/schema"], # Tip: We can optionally supply cookie options to the configuration. Here is an example of enabling the secure cookie option - # auth_cookie_options=CookieOptions(secure=True), + # secure=True, ) diff --git a/docs/examples/static_files/__init__.py b/docs/examples/static_files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/static_files/custom_router.py b/docs/examples/static_files/custom_router.py new file mode 100644 index 0000000000..cce97ac9c1 --- /dev/null +++ b/docs/examples/static_files/custom_router.py @@ -0,0 +1,18 @@ +from litestar import Litestar +from litestar.router import Router +from litestar.static_files import create_static_files_router + + +class MyRouter(Router): + pass + + +app = Litestar( + route_handlers=[ + create_static_files_router( + path="/static", + directories=["assets"], + router_class=MyRouter, + ) + ] +) diff --git a/docs/examples/static_files/file_system.py b/docs/examples/static_files/file_system.py new file mode 100644 index 0000000000..e98c1f459a --- /dev/null +++ b/docs/examples/static_files/file_system.py @@ -0,0 +1,14 @@ +from fsspec.implementations.ftp import FTPFileSystem + +from litestar import Litestar +from litestar.static_files import create_static_files_router + +app = Litestar( + route_handlers=[ + create_static_files_router( + path="/static", + directories=["assets"], + file_system=FTPFileSystem(host="127.0.0.1"), + ), + ] +) diff --git a/docs/examples/static_files/full_example.py b/docs/examples/static_files/full_example.py new file mode 100644 index 0000000000..f2fb443f6a --- /dev/null +++ b/docs/examples/static_files/full_example.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from litestar import Litestar +from litestar.static_files import create_static_files_router + +ASSETS_DIR = Path("assets") + + +def on_startup(): + ASSETS_DIR.mkdir(exist_ok=True) + ASSETS_DIR.joinpath("hello.txt").write_text("Hello, world!") + + +app = Litestar( + route_handlers=[ + create_static_files_router(path="/static", directories=["assets"]), + ], + on_startup=[on_startup], +) + + +# run: /static/hello.txt diff --git a/docs/examples/static_files/html_mode.py b/docs/examples/static_files/html_mode.py new file mode 100644 index 0000000000..6b6ed95b65 --- /dev/null +++ b/docs/examples/static_files/html_mode.py @@ -0,0 +1,29 @@ +from pathlib import Path + +from litestar import Litestar +from litestar.static_files import create_static_files_router + +HTML_DIR = Path("html") + + +def on_startup() -> None: + HTML_DIR.mkdir(exist_ok=True) + HTML_DIR.joinpath("index.html").write_text("Hello, world!") + HTML_DIR.joinpath("404.html").write_text("

Not found

") + + +app = Litestar( + route_handlers=[ + create_static_files_router( + path="/", + directories=["html"], + html_mode=True, + ) + ], + on_startup=[on_startup], +) + + +# run: / +# run: /index.html +# run: /something diff --git a/docs/examples/static_files/passing_options.py b/docs/examples/static_files/passing_options.py new file mode 100644 index 0000000000..63f4449c17 --- /dev/null +++ b/docs/examples/static_files/passing_options.py @@ -0,0 +1,14 @@ +from litestar import Litestar +from litestar.static_files import create_static_files_router + +app = Litestar( + route_handlers=[ + create_static_files_router( + path="/", + directories=["assets"], + opt={"some": True}, + include_in_schema=False, + tags=["static"], + ) + ] +) diff --git a/docs/examples/static_files/route_reverse.py b/docs/examples/static_files/route_reverse.py new file mode 100644 index 0000000000..0276c51974 --- /dev/null +++ b/docs/examples/static_files/route_reverse.py @@ -0,0 +1,11 @@ +from litestar import Litestar +from litestar.static_files import create_static_files_router + +app = Litestar( + route_handlers=[ + create_static_files_router(path="/static", directories=["assets"]), + ] +) + + +print(app.route_reverse(name="static", file_path="/some_file.txt")) # /static/some_file.txt diff --git a/docs/examples/static_files/send_as_attachment.py b/docs/examples/static_files/send_as_attachment.py new file mode 100644 index 0000000000..e13ae69c05 --- /dev/null +++ b/docs/examples/static_files/send_as_attachment.py @@ -0,0 +1,12 @@ +from litestar import Litestar +from litestar.static_files import create_static_files_router + +app = Litestar( + route_handlers=[ + create_static_files_router( + path="/static", + directories=["assets"], + send_as_attachment=True, + ) + ] +) diff --git a/docs/examples/static_files/upgrade_from_static_1.py b/docs/examples/static_files/upgrade_from_static_1.py new file mode 100644 index 0000000000..ad0b8aa61d --- /dev/null +++ b/docs/examples/static_files/upgrade_from_static_1.py @@ -0,0 +1,8 @@ +from litestar import Litestar +from litestar.static_files.config import StaticFilesConfig + +app = Litestar( + static_files_config=[ + StaticFilesConfig(directories=["assets"], path="/static"), + ], +) diff --git a/docs/examples/static_files/upgrade_from_static_2.py b/docs/examples/static_files/upgrade_from_static_2.py new file mode 100644 index 0000000000..af4578f333 --- /dev/null +++ b/docs/examples/static_files/upgrade_from_static_2.py @@ -0,0 +1,8 @@ +from litestar import Litestar +from litestar.static_files import create_static_files_router + +app = Litestar( + route_handlers=[ + create_static_files_router(directories=["assets"], path="/static"), + ], +) diff --git a/docs/index.rst b/docs/index.rst index 8c3eb02557..be01d21e10 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -157,7 +157,7 @@ A huge thank you to our current sponsors:
- Stok + Stok

Stok

diff --git a/docs/release-notes/changelog.rst b/docs/release-notes/changelog.rst index 296e595437..b2d82d05b3 100644 --- a/docs/release-notes/changelog.rst +++ b/docs/release-notes/changelog.rst @@ -3,6 +3,342 @@ 2.x Changelog ============= +.. changelog:: 2.6.3 + :date: 2024-03-04 + + .. change:: Pydantic V1 schema generation for PrivateAttr in GenericModel + :type: bugfix + :pr: 3161 + :issue: 3150 + + Fixes a bug that caused a ``NameError`` when a Pydantic V1 ``GenericModel`` has a private attribute of which the type annotation cannot be resolved at the time of schema generation. + + +.. changelog:: 2.6.2 + :date: 2024/03/02 + + .. change:: DTO msgspec meta constraints not being included in transfer model + :type: bugfix + :pr: 3113 + :issue: 3026 + + Fix an issue where msgspec constraints set in ``msgspec.Meta`` would not be + honoured by the DTO. + + In the given example, the ``min_length=3`` constraint would be ignored by the + model generated by ``MsgspecDTO``. + + .. code-block:: python + + from typing import Annotated + + import msgspec + from litestar import post, Litestar + from litestar.dto import MsgspecDTO + + class Request(msgspec.Struct): + foo: Annotated[str, msgspec.Meta(min_length=3)] + + @post("/example/", dto=MsgspecDTO[Request]) + async def example(data: Request) -> Request: + return data + + Constraints like these are now transferred. + + Two things to note are: + + - For DTOs with ``DTOConfig(partial=True)`` we cannot transfer the length + constraints as they are only supported on fields that as subtypes of ``str``, + ``bytes`` or a collection type, but ``partial=True`` sets all fields as + ``T | UNSET`` + - For the ``PiccoloDTO``, fields which are not required will also drop the + length constraints. A warning about this will be raised here. + + .. change:: Missing control header for static files + :type: bugfix + :pr: 3131 + :issue: 3129 + + Fix an issue where a ``cache_control`` that is set on a router created by + ``create_static_files_router`` wasn't passed to the generated handler + + .. change:: Fix OpenAPI schema generation for Pydantic v2 constrained ``Secret`` types + :type: bugfix + :pr: 3149 + :issue: 3148 + + Fix schema generation for ``pydantic.SecretStr`` and ``pydantic.SecretBytes`` + which, when constrained, would not be recognised as such with Pydantic V2 since + they're not subtypes of their respective bases anymore. + + .. change:: Fix OpenAPI schema generation for Pydantic private attributes + :type: bugfix + :pr: 3151 + :issue: 3150 + + Fix a bug that caused a :exc:`NameError` when trying to resolve forward + references in Pydantic private fields. + + Although private fields were respected excluded from the schema, it was still + attempted to extract their type annotation. This was fixed by not relying on + ``typing.get_type_hints`` to get the type information, but instead using + Pydantic's own APIs, allowing us to only extract information about the types of + relevant fields. + + .. change:: OpenAPI description not set for UUID based path parameters in OpenAPI + :type: bugfix + :pr: 3118 + :issue: 2967 + + Resolved a bug where the description was not set for UUID-based path + parameters in OpenAPI due to the reason mentioned in the issue. + + .. change:: Fix ``RedisStore`` client created with ``with_client`` unclosed + :type: bugfix + :pr: 3111 + :issue: 3083 + + Fix a bug where, when a :class:`~litestar.stores.redis.RedisStore` was created + with the :meth:`~litestar.stores.redis.RedisStore.with_client` method, that + client wasn't closed explicitly + + +.. changelog:: 2.6.1 + :date: 2024/02/14 + + .. change:: SQLAlchemy: Use `IntegrityError` instead of deprecated `ConflictError` + :type: bugfix + :pr: 3094 + + Updated the repository to return ``IntegrityError`` instead of the now + deprecated ``ConflictError`` + + .. change:: Remove usage of deprecated `static_files` property + :type: bugfix + :pr: 3087 + + Remove the usage of the deprecated ``Litestar.static_files_config`` in + ``Litestar.__init__``. + + .. change:: Sessions: Fix cookie naming for short cookies + :type: bugfix + :pr: 3095 + :issue: 3090 + + Previously, cookie names always had a suffix of the form ``"-{i}"`` appended to + them. With this change, the suffix is omitted if the cookie is short enough + (< 4 KB) to not be split into multiple chunks. + + .. change:: Static files: Fix path resolution for windows + :type: bugfix + :pr: 3102 + + Fix an issue with the path resolution on Windows introduced in + https://github.com/litestar-org/litestar/pull/2960 that would lead to 404s + + .. change:: Fix logging middleware with structlog causes application to return a ``500`` when request body is malformed + :type: bugfix + :pr: 3109 + :issue: 3063 + + Gracefully handle malformed request bodies during parsing when using structlog; + Instead of erroring out and returning a ``500``, the raw body is now being used + when an error occurs during parsing + + .. change:: OpenAPI: Generate correct response schema for ``ResponseSpec(None)`` + :type: bugfix + :pr: 3098 + :issue: 3069 + + Explicitly declaring ``responses={...: ResponseSpec(None)}`` used to generate + OpenAPI a ``content`` property, when it should be omitted. + + .. change:: Prevent exception handlers from extracting details from non-Litestar exceptions + :type: bugfix + :pr: 3106 + :issue: 3082 + + Fix a bug where exception classes that had a ``status_code`` attribute would be + treated as Litestar exceptions and details from them would be extracted and + added to the exception response. + +.. changelog:: 2.6.0 + :date: 2024/02/06 + + .. change:: Enable disabling configuring ``root`` logger within ``LoggingConfig`` + :type: feature + :pr: 2969 + + The option :attr:`~litestar.logging.config.LoggingConfig.configure_root_logger` was + added to :class:`~litestar.logging.config.LoggingConfig` attribute. It is enabled by + default to not implement a breaking change. + + When set to ``False`` the ``root`` logger will not be modified for ``logging`` + or ``picologging`` loggers. + + .. change:: Simplified static file handling and enhancements + :type: feature + :pr: 2960 + :issue: 2629 + + Static file serving has been implemented with regular route handlers instead of + a specialised ASGI app. At the moment, this is complementary to the usage of + :class:`~litestar.static_files.StaticFilesConfig` to maintain backwards + compatibility. + + This achieves a few things: + + - Fixes https://github.com/litestar-org/litestar/issues/2629 + - Circumvents special casing needed in the routing logic for the static files app + - Removes the need for a ``static_files_config`` attribute on the app + - Removes the need for a special :meth:`~litestar.app.Litestar.url_for_static_asset` + method on the app since `route_reverse` can be used instead + + Additionally: + + - Most router options can now be passed to the + :func:`~litestar.static_files.create_static_files_router`, allowing further + customisation + - A new ``resolve_symlinks`` flag has been added, defaulting to ``True`` to keep + backwards compatibility + + **Usage** + + Instead of + + .. code-block:: python + + app = Litestar( + static_files_config=[StaticFilesConfig(path="/static", directories=["some_dir"])] + ) + + + You can now simply use + + .. code-block:: python + + app = Litestar( + route_handlers=[ + create_static_files_router(path="/static", directories=["some_dir"]) + ] + ) + + .. seealso:: + :doc:`/usage/static-files` + + + .. change:: Exclude Piccolo ORM columns with ``secret=True`` from ``PydanticDTO`` output + :type: feature + :pr: 3030 + + For Piccolo columns with ``secret=True`` set, corresponding ``PydanticDTO`` + attributes will be marked as ``WRITE_ONLY`` to prevent the column being included + in ``return_dto`` + + + .. change:: Allow discovering registered plugins by their fully qualified name + :type: feature + :pr: 3027 + + `PluginRegistryPluginRegistry`` now supports retrieving a plugin by its fully + qualified name. + + + .. change:: Support externally typed classes as dependency providers + :type: feature + :pr: 3066 + :issue: 2979 + + - Implement a new :class:`~litestar.plugins.DIPlugin` class that allows the + generation of signatures for arbitrary types where their signature cannot be + extracted from the type's ``__init__`` method + - Implement ``DIPlugin``\ s for Pydantic and Msgspec to allow using their + respective modelled types as dependency providers. These plugins will be + registered by default + + .. change:: Add structlog plugin + :type: feature + :pr: 2943 + + A Structlog plugin to make it easier to configure structlog in a single place. + + The plugin: + + - Detects if a logger has ``setLevel`` before calling + - Set even message name to be init-cap + - Add ``set_level`` interface to config + - Allows structlog printer to detect if console is TTY enabled. If so, a + Struglog color formatter with Rich traceback printer is used + - Auto-configures stdlib logger to use the structlog logger + + .. change:: Add reload-include and reload-exclude to CLI run command + :type: feature + :pr: 2973 + :issue: 2875 + + The options ``reload-exclude`` and ``reload-include`` were added to the CLI + ``run`` command to explicitly in-/exclude specific paths from the reloading + watcher. + + +.. changelog:: 2.5.5 + :date: 2024/02/04 + + .. change:: Fix scope ``state`` key handling + :type: bugfix + :pr: 3070 + + Fix a regression introduced in #2751 that would wrongfully assume the ``state`` + key is always present within the ASGI Scope. This is *only* the case when the + Litestar root application is invoked first, since we enforce such a key there, + but the presence of that key is not actually guaranteed by the ASGI spec and + some servers, such as hypercorn, do not provide it. + + +.. changelog:: 2.5.4 + :date: 2024/01/31 + + .. change:: Handle ``KeyError`` when `root_path` is not present in ASGI scope + :type: bugfix + :pr: 3051 + + Nginx Unit ASGI server does not set "root_path" in the ASGI scope, which is + expected as part of the changes done in #3039. This PR fixes the assumption that + the key is always present and instead tries to optionally retrieve it. + + .. code-block:: + + KeyError on GET / + 'root_path' + + .. change:: ServerSentEvent typing error + :type: bugfix + :pr: 3048 + + fixes small typing error: + + .. code-block:: + + error: Argument 1 to "ServerSentEvent" has incompatible type "AsyncIterable[ServerSentEventMessage]"; expected "str | bytes | Iterable[str | bytes] | Iterator[str | bytes] | AsyncIterable[str | bytes] | AsyncIterator[str | bytes]" [arg-type] + + inside ``test_sse`` there was a ``Any`` I changed to trigger the test then solved it. + + +.. changelog:: 2.5.3 + :date: 2024/01/29 + + .. change:: Handle diverging ASGI ``root_path`` behaviour + :type: bugfix + :pr: 3039 + :issue: 3041 + + Uvicorn `0.26.0 `_ + introduced a breaking change in its handling of the ASGI ``root_path`` behaviour, + which, while adhering to the spec, diverges from the interpretation of other + ASGI servers of this aspect of the spec (e.g. hypercorn and daphne do not follow + uvicorn's interpretation as of today). A fix was introduced that ensures + consistent behaviour of applications in any case. + .. changelog:: 2.5.2 :date: 2024/01/27 diff --git a/docs/tutorials/todo-app/1-accessing-the-list.rst b/docs/tutorials/todo-app/1-accessing-the-list.rst index 5be553d96c..6cc6334cbc 100644 --- a/docs/tutorials/todo-app/1-accessing-the-list.rst +++ b/docs/tutorials/todo-app/1-accessing-the-list.rst @@ -5,7 +5,7 @@ Intro ----- The first thing you'll be setting up for our app is a route handler that returns a -single TODO-list. A TODO-list in this case will be a list of dictionaries representing +single TODO list. A TODO list in this case will be a list of dictionaries representing the items on that TODO list. .. literalinclude:: /examples/todo_app/get_list/dict.py @@ -64,7 +64,7 @@ Currently ``get_list`` will always return all items on the list, but what if you are interested in only those items with a specific status, for example all items that are not yet marked as *done*? -For this you can employ query parameters; To define a query parameter, all that's needed +For this you can employ query parameters; to define a query parameter, all that's needed is to add an otherwise unused parameter to the function. Litestar will recognize this and infer that it's going to be used as a query parameter. When a request is being made, the query parameter will be extracted from the URL, and passed to the function parameter @@ -152,7 +152,7 @@ Converting and validating query parameters ++++++++++++++++++++++++++++++++++++++++++ As mentioned earlier, type annotations can be used for more than static type checking -in Litestar; They can also define and configure behaviour. In this case, you can get +in Litestar; they can also define and configure behaviour. In this case, you can get Litestar to convert the query parameter to a boolean value, matching the values of the ``TodoItem.done`` attribute, and in the same step validate it, returning error responses for you should the supplied value not be a valid boolean. @@ -185,7 +185,7 @@ instead. .. tip:: It is important to note that this conversion is not the result of calling - :class:`bool` on the raw value. ``bool("john")`` would be ``True``, since Python + :class:`bool` on the raw value. ``bool("john")`` would be :obj:`True`, since Python considers all non-empty strings to be truthy. Litestar however supports customary boolean representation commonly used in the HTTP 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 7c2dc85465..cdd01f7b6d 100644 --- a/docs/tutorials/todo-app/2-interacting-with-the-list.rst +++ b/docs/tutorials/todo-app/2-interacting-with-the-list.rst @@ -54,7 +54,7 @@ As in the previous chapter, this too can be improved by using This is not only easier on the eyes and adds more structure to the code, but also gives -better interactive documentation; It will now present us with the field names and +better interactive documentation; it will now present us with the field names and default values for the dataclass we have defined: .. figure:: images/swagger-dict-vs-dataclass.png @@ -62,7 +62,7 @@ default values for the dataclass we have defined: Documentation for the ``add_item`` route with ``data`` typed as a ``dict`` vs ``dataclass`` -Using a dataclass also gives you better validation: Omitting a key such as ``title`` +Using a dataclass also gives you better validation: omitting a key such as ``title`` will result in a useful error response: @@ -76,7 +76,7 @@ Create dynamic routes using path parameters The next task on the list is updating an item's status. For this, a way to refer to a specific item on the list is needed. This could be done using query parameters, but -there's an easier, and more semantically coherent way of expressing this: Path +there's an easier, and more semantically coherent way of expressing this: path parameters. .. code-block:: python @@ -90,10 +90,10 @@ So far all the paths in your application are static, meaning they are expressed constant string which does not change. In fact, the only path used so far is ``/``. Path parameters allow you to construct dynamic paths and later refer to the dynamically -captured parts. This may sound complex at first, but it's actually quite simple; You can +captured parts. This may sound complex at first, but it's actually quite simple; you can think of it as a regular expression that's being used on the requested path. -Path parameters consist of two parts: An expression inside the path, describing the +Path parameters consist of two parts: an expression inside the path, describing the parameter, and a corresponding function parameter of the same name in the route handler function, which will receive the path parameter's value. @@ -114,7 +114,7 @@ as ``greeter(name="john")``, similar to how query parameters are injected. By using this pattern and combining it with those from the earlier section about receiving data you can now set up a route handler that takes in the title of a -TODO-item, an updated item in form of a dataclass instance, and updates the item in the +TODO item, an updated item in form of a dataclass instance, and updates the item in the list. diff --git a/docs/tutorials/todo-app/3-assembling-the-app.rst b/docs/tutorials/todo-app/3-assembling-the-app.rst index b2f3328533..7e5a18f822 100644 --- a/docs/tutorials/todo-app/3-assembling-the-app.rst +++ b/docs/tutorials/todo-app/3-assembling-the-app.rst @@ -26,7 +26,7 @@ Recap A route handler set up with ``get("/")`` responds to ``GET`` requests and returns a list -of all items on our TODO-list. The optional query parameter ``done`` allows filtering +of all items on our TODO list. The optional query parameter ``done`` allows filtering the items by status. The type annotation of ``bool`` converts the query parameter into a :class:`bool`, and wrapping it in :class:`Optional ` makes it optional. @@ -39,7 +39,7 @@ a :class:`bool`, and wrapping it in :class:`Optional ` makes it A route handler set up with ``post("/")`` responds to ``POST`` requests and adds an item -to the TODO-list. The data for the new item is received via the request data, which the +to the TODO list. The data for the new item is received via the request data, which the route handler accesses by specifying the ``data`` parameter. The type annotation of ``TodoItem`` means the request data will parsed as JSON, which is then used to create an instance of the ``TodoItem`` dataclass, which - finally - gets passed into the function. diff --git a/docs/usage/applications.rst b/docs/usage/applications.rst index 624d374887..2a5b4c2412 100644 --- a/docs/usage/applications.rst +++ b/docs/usage/applications.rst @@ -25,7 +25,6 @@ and Route Handlers should be registered on it. * :ref:`Routing - Registering Routes ` - Startup and Shutdown -------------------- @@ -93,8 +92,9 @@ On the other hand, the shutdown hooks are invoked in their specified order. Using Application State ----------------------- -As seen in the examples for the `on_startup <#before-after-startup>`_ / `on_shutdown <#before-after-shutdown>`_ , -callables passed to these hooks can receive an optional kwarg called ``state``, which is the application's state object. +As seen in the examples for the `on_startup / on_shutdown `, +callables passed to these hooks can receive an optional kwarg called ``app``, through which the application's state object +and other properties can be accessed. The advantage of using application ``state``, is that it can be accessed during multiple stages of the connection, and it can be injected into dependencies and route handlers. @@ -186,8 +186,7 @@ After Exception ^^^^^^^^^^^^^^^ The ``after_exception`` hook takes a :class:`sync or async callable ` that is called with -three arguments: the ``exception`` that occurred, the ASGI ``scope`` of the request or websocket connection, and the -application ``state``. +two arguments: the ``exception`` that occurred and the ASGI ``scope`` of the request or websocket connection. .. literalinclude:: /examples/application_hooks/after_exception_hook.py :caption: After Exception Hook @@ -203,7 +202,7 @@ Before Send ^^^^^^^^^^^ The ``before_send`` hook takes a :class:`sync or async callable ` that is called when -an ASGI message is sent. The hook receives the message instance and the application state. +an ASGI message is sent. The hook receives the message instance and the ASGI ``scope``. .. literalinclude:: /examples/application_hooks/before_send_hook.py :caption: Before Send Hook diff --git a/docs/usage/cli.rst b/docs/usage/cli.rst index 2360962691..b52463d315 100644 --- a/docs/usage/cli.rst +++ b/docs/usage/cli.rst @@ -107,6 +107,10 @@ Options +-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+ | ``-R``\ , ``--reload-dir`` | ``LITESTAR_RELOAD_DIRS`` | Specify directories to watch for reload. | +-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+ +| ``-I``\ , ``--reload-include`` | ``LITESTAR_RELOAD_INCLUDES`` | Specify glob patterns for files to include when watching for reload. | ++-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+ +| ``-E``\ , ``--reload-exclude`` | ``LITESTAR_RELOAD_EXCLUDES`` | Specify glob patterns for files to exclude when watching for reload. | ++-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+ | ``-p``\ , ``--port`` | ``LITESTAR_PORT`` | Bind the server to this port [default: 8000] | +-------------------------------------------+----------------------------------------------+----------------------------------------------------------------------------------+ | ``--wc``\ , ``--web-concurrency`` | ``WEB_CONCURRENCY`` | The number of concurrent web workers to start [default: 1] | @@ -143,6 +147,40 @@ To set multiple directories via an environment variable, use a comma-separated l LITESTAR_RELOAD_DIRS=.,../other-library/src +--reload-include +++++++++++++++++ + +The ``--reload-include`` flag allows you to specify glob patterns to include when watching for file changes. If you specify this flag, the ``--reload`` flag is implied. Furthermore, ``.py`` files are included implicitly by default. + +You can specify multiple glob patterns by passing the flag multiple times: + +.. code-block:: shell + + litestar run --reload-include="*.rst" --reload-include="*.yml" + +To set multiple directories via an environment variable, use a comma-separated list: + +.. code-block:: shell + + LITESTAR_RELOAD_INCLUDES=*.rst,*.yml + +--reload-exclude +++++++++++++++++ + +The ``--reload-exclude`` flag allows you to specify glob patterns to exclude when watching for file changes. If you specify this flag, the ``--reload`` flag is implied. + +You can specify multiple glob patterns by passing the flag multiple times: + +.. code-block:: shell + + litestar run --reload-exclude="*.py" --reload-exclude="*.yml" + +To set multiple directories via an environment variable, use a comma-separated list: + +.. code-block:: shell + + LITESTAR_RELOAD_EXCLUDES=*.py,*.yml + SSL +++ diff --git a/docs/usage/databases/sqlalchemy/models_and_repository.rst b/docs/usage/databases/sqlalchemy/models_and_repository.rst index e2fc6dda6f..d5dc0f1b77 100644 --- a/docs/usage/databases/sqlalchemy/models_and_repository.rst +++ b/docs/usage/databases/sqlalchemy/models_and_repository.rst @@ -36,7 +36,7 @@ implementations: * :class:`UUIDAuditBase ` Both include a ``UUID`` based primary key -and ``UUIDAuditBase`` includes an ``updated`` and ``created`` timestamp column. +and ``UUIDAuditBase`` includes an ``updated_at`` and ``created_at`` timestamp column. The ``UUID`` will be a native ``UUID``/``GUID`` type on databases that support it such as Postgres. For other engines without a native UUID data type, the UUID is stored as a 16-byte ``BYTES`` or ``RAW`` field. @@ -45,7 +45,7 @@ a native UUID data type, the UUID is stored as a 16-byte ``BYTES`` or ``RAW`` fi * :class:`BigIntAuditBase ` Both include a ``BigInteger`` based primary key -and ``BigIntAuditBase`` includes an ``updated`` and ``created`` timestamp column. +and ``BigIntAuditBase`` includes an ``updated_at`` and ``created_at`` timestamp column. Models using these bases also include the following enhancements: diff --git a/docs/usage/logging.rst b/docs/usage/logging.rst index ca35ea782d..c39861aea4 100644 --- a/docs/usage/logging.rst +++ b/docs/usage/logging.rst @@ -113,12 +113,12 @@ Using StructLog ^^^^^^^^^^^^^^^ `StructLog `_ is a powerful structured-logging library. Litestar ships with a dedicated -logging config for using it: +logging plugin and config for using it: .. code-block:: python from litestar import Litestar, Request, get - from litestar.logging import StructLoggingConfig + from litestar.plugins.structlog import StructlogPlugin @get("/") @@ -127,9 +127,9 @@ logging config for using it: return None - logging_config = StructLoggingConfig() + structlog_plugin = StructlogPlugin() - app = Litestar(route_handlers=[my_router_handler], logging_config=logging_config) + app = Litestar(route_handlers=[my_router_handler], plugins=[StructlogPlugin()]) Subclass Logging Configs ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/usage/plugins.rst b/docs/usage/plugins.rst index 8c4b64ce0a..4911b8da23 100644 --- a/docs/usage/plugins.rst +++ b/docs/usage/plugins.rst @@ -19,7 +19,7 @@ that can interact with the data that is used to instantiate the application inst the contract for plugins that extend serialization functionality of the application. InitPluginProtocol -~~~~~~~~~~~~~~~~~~ +------------------ ``InitPluginProtocol`` defines an interface that allows for customization of the application's initialization process. Init plugins can define dependencies, add route handlers, configure middleware, and much more! @@ -37,7 +37,7 @@ they are provided in the ``plugins`` argument of the :class:`app ` instance is then returned. SerializationPluginProtocol -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------- The SerializationPluginProtocol defines a contract for plugins that provide serialization functionality for data types that are otherwise unsupported by the framework. @@ -79,7 +79,7 @@ the plugin, and doesn't otherwise have a ``dto`` or ``return_dto`` defined, the that annotation. Example -------- ++++++++ The following example shows the actual implementation of the ``SerializationPluginProtocol`` for `SQLAlchemy `_ models that is is provided in ``advanced_alchemy``. @@ -106,3 +106,20 @@ subtypes are not created for the same model. If the annotation is not in the ``_type_dto_map`` dictionary, the method creates a new DTO type for the annotation, adds it to the ``_type_dto_map`` dictionary, and returns it. + + +DIPlugin +-------- + +:class:`~litestar.plugins.DIPlugin` can be used to extend Litestar's dependency +injection by providing information about injectable types. + +Its main purpose it to facilitate the injection of callables with unknown signatures, +for example Pydantic's ``BaseModel`` classes; These are not supported natively since, +while they are callables, their type information is not contained within their callable +signature (their :func:`__init__` method). + + +.. literalinclude:: /examples/plugins/di_plugin.py + :language: python + :caption: Dynamically generating signature information for a custom type diff --git a/docs/usage/responses.rst b/docs/usage/responses.rst index 1adc52d8a9..7f9f82af03 100644 --- a/docs/usage/responses.rst +++ b/docs/usage/responses.rst @@ -675,6 +675,16 @@ which is used in for sending pings, and ``retry_duration``, which dictates the d You can use different kinds of values for the iterator. It can be a callable returning a sync or async generator, a generator itself, a sync or async iterator class, or an instance of a sync or async iterator class. +In your iterator function you can yield integers, strings or bytes, the message sent in that case will have ``message`` +as the ``event_type`` if the ServerSentEvent has no ``event_type`` set, otherwise it will use the ``event_type`` +specified, and the data will be the yielded value. + +If you want to send a different event type, you can use a dictionary with the keys ``event_type`` and ``data`` or the :class:`ServerSentMessage <.response.ServerSentEventMessage>` class. + +.. note:: + + You can further customize all the sse parameters, add comments, and set the retry duration by using the :class:`ServerSentEvent <.response.ServerSentEvent>` class directly or by using the :class:`ServerSentEventMessage <.response.ServerSentEventMessage>` or dictionaries with the appropriate keys. + Template Responses ------------------ @@ -711,11 +721,11 @@ instances. .. admonition:: Layered architecture :class: seealso - Response classes are part of Litestar's layered architecture, which means you can - set a response class on every layer of the application. If you have set a response - class on multiple layers, the layer closest to the route handler will take precedence. + Response classes are part of Litestar's layered architecture, which means you can + set a response class on every layer of the application. If you have set a response + class on multiple layers, the layer closest to the route handler will take precedence. - You can read more about this here: :ref:`usage/applications:layered architecture` + You can read more about this here: :ref:`usage/applications:layered architecture` Background Tasks ---------------- diff --git a/docs/usage/static-files.rst b/docs/usage/static-files.rst index fb1b8f2d47..8216a37818 100644 --- a/docs/usage/static-files.rst +++ b/docs/usage/static-files.rst @@ -1,89 +1,116 @@ -Static Files +Static files ============ -Static files are served by the app from predefined locations. To configure static file serving, either pass an -instance of :class:`StaticFilesConfig <.static_files.config.StaticFilesConfig>` or a list -thereof to :class:`Litestar <.app.Litestar>` using the ``static_files_config`` kwarg. +To serve static files (i.e. serve arbitrary files from a given directory), the +:func:`~litestar.static_files.create_static_files_router` can be used to create a +:class:`Router ` to handle this task. -For example, lets say our Litestar app is going to serve **regular files** from the ``my_app/static`` folder and **html -documents** from the ``my_app/html`` folder, and we would like to serve the **static files** on the ``/files`` path, -and the **html files** on the ``/html`` path: +.. literalinclude:: /examples/static_files/full_example.py + :language: python -.. code-block:: python +In this example, files from the directory ``assets`` will be served on the path +``/static``. A file ``assets/hello.txt`` would now be available on ``/static/hello.txt`` - from litestar import Litestar - from litestar.static_files.config import StaticFilesConfig +.. attention:: + Directories are interpreted as relative to the working directory from which the + application is started - app = Litestar( - route_handlers=[...], - static_files_config=[ - StaticFilesConfig(directories=["static"], path="/files"), - StaticFilesConfig(directories=["html"], path="/html", html_mode=True), - ], - ) -Matching is done based on filename, for example, assume we have a request that is trying to retrieve the path -``/files/file.txt``\ , the **directory for the base path** ``/files`` **will be searched** for the file ``file.txt``. If it is -found, the file will be sent, otherwise a **404 response** will be sent. +Sending files as attachments +---------------------------- -If ``html_mode`` is enabled and no specific file is requested, the application will fall back to serving ``index.html``. If -no file is found the application will look for a ``404.html`` file in order to render a response, otherwise a 404 -:class:`NotFoundException <.exceptions.http_exceptions.NotFoundException>` will be returned. +By default, files are sent "inline", meaning they will have a +``Content-Disposition: inline`` header. Setting ``send_as_attachment=True`` flag will +send them with a ``Content-Disposition: attachment`` instead: -You can provide a ``name`` parameter to ``StaticFilesConfig`` to identify the given config and generate links to files in -folders belonging to that config. ``name`` should be a unique string across all static configs and -:doc:`/usage/routing/handlers`. +.. literalinclude:: /examples/static_files/send_as_attachment.py + :language: python -.. code-block:: python - from litestar import Litestar - from litestar.static_files.config import StaticFilesConfig +HTML mode +--------- - app = Litestar( - route_handlers=[...], - static_files_config=[ - StaticFilesConfig( - directories=["static"], path="/some_folder/static/path", name="static" - ), - ], - ) +"HTML mode" can be enabled by setting ``html_mode=True``. This will: - url_path = app.url_for_static_asset("static", "file.pdf") - # /some_folder/static/path/file.pdf +- Serve and ``/index.html`` when the path ``/`` is requested +- Attempt to serve ``/404.html`` when a requested file is not found + + +.. literalinclude:: /examples/static_files/html_mode.py + :language: python + + +Passing options to the generated router +--------------------------------------- + +Options available on :class:`~litestar.router.Router` can be passed to directly +:func:`~litestar.static_files.create_static_files_router`: + +.. literalinclude:: /examples/static_files/passing_options.py + :language: python + + +Using a custom router class +--------------------------- + +The router class used can be customized with the ``router_class`` parameter: + +.. literalinclude:: /examples/static_files/custom_router.py + :language: python + + + +Retrieving paths to static files +-------------------------------- + +:meth:`~litestar.app.Litestar.route_reverse` and +:meth:`~litestar.connection.ASGIConnection.url_for` can be used to retrieve the path +under which a specific file will be available: + +.. literalinclude:: /examples/static_files/route_reverse.py + :language: python + +.. tip:: + + The ``name`` parameter has to match the ``name`` parameter passed to + :func:`create_static_files_router`, which defaults to ``static``. + + +(Remote) file systems +--------------------- + +To customize how Litestar interacts with the file system, a class implementing the +:class:`~litestar.types.FileSystemProtocol` can be passed to ``file_system``. An example +of this are the file systems provided by +`fsspec `_, which includes support +for FTP, SFTP, Hadoop, SMB, GitHub and +`many more `_, +with support for popular cloud providers available via 3rd party implementations such as + +- S3 via `S3FS `_ +- Google Cloud Storage via `GCFS `_ +- Azure Blob Storage via `adlfs `_ + + +.. literalinclude:: /examples/static_files/file_system.py + :language: python -Sending files as attachments ----------------------------- -By default, files are sent "inline", meaning they will have a ``Content-Disposition: inline`` header. -To send them as attachments, use the ``send_as_attachment=True`` flag, which will add a -``Content-Disposition: attachment`` header: +Upgrading from legacy StaticFilesConfig +--------------------------------------- -.. code-block:: python +.. important:: Info + :class:`StaticFilesConfig` is deprecated and will be removed in Litestar 3.0 - from litestar import Litestar - from litestar.static_files.config import StaticFilesConfig - app = Litestar( - route_handlers=[...], - static_files_config=[ - StaticFilesConfig( - directories=["static"], - path="/some_folder/static/path", - name="static", - send_as_attachment=True, - ), - ], - ) +Existing code can be upgraded to :func:`create_static_files_router` by replacing +:class:`StaticFilesConfig` instances with this function call and passing the result to +``route_handlers`` instead of ``static_files_config``: -File System support and Cloud Files ------------------------------------ -The :class:`StaticFilesConfig <.static_files.StaticFilesConfig>` class accepts a value called ``file_system``, -which can be any class adhering to the Litestar :class:`FileSystemProtocol `. +.. literalinclude:: /examples/static_files/upgrade_from_static_1.py + :language: python -This protocol is similar to the file systems defined by `fsspec `_, -which cover all major cloud providers and a wide range of other use cases (e.g. HTTP based file service, ``ftp``, etc.). -In order to use any file system, simply use `fsspec `_ or one of -the libraries based upon it, or provide a custom implementation adhering to the -:class:`FileSystemProtocol `. +.. literalinclude:: /examples/static_files/upgrade_from_static_2.py + :language: python diff --git a/docs/usage/stores.rst b/docs/usage/stores.rst index 52118f9332..fb580f75f7 100644 --- a/docs/usage/stores.rst +++ b/docs/usage/stores.rst @@ -259,3 +259,11 @@ with minimal boilerplate: Without any extra configuration, every call to ``app.stores.get`` with a unique name will return a namespace for this name only, while re-using the underlying Redis instance. + + +Store lifetime +++++++++++++++ + +Stores may not be automatically closed when the application is shut down. +This is the case in particular for the RedisStore if you are not using the class method :meth:`RedisStore.with_client <.redis.RedisStore.with_client>` and passing in your own Redis instance. +In this case you're responsible to close the Redis instance yourself. diff --git a/litestar/_asgi/asgi_router.py b/litestar/_asgi/asgi_router.py index f5ba96f996..ebafaf049f 100644 --- a/litestar/_asgi/asgi_router.py +++ b/litestar/_asgi/asgi_router.py @@ -77,7 +77,12 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: The main entry point to the Router class. """ scope.setdefault("path_params", {}) - normalized_path = normalize_path(scope["path"]) + + path = scope["path"] + if root_path := scope.get("root_path", ""): + path = path.split(root_path, maxsplit=1)[-1] + normalized_path = normalize_path(path) + asgi_app, scope["route_handler"], scope["path"], scope["path_params"] = self.handle_routing( path=normalized_path, method=scope.get("method") ) diff --git a/litestar/_openapi/responses.py b/litestar/_openapi/responses.py index 4d9e42b7af..ba1bfa6aac 100644 --- a/litestar/_openapi/responses.py +++ b/litestar/_openapi/responses.py @@ -240,14 +240,21 @@ def create_additional_responses(self) -> Iterator[tuple[str, OpenAPIResponse]]: prefer_alias=False, generate_examples=additional_response.generate_examples, ) - schema = schema_creator.for_field_definition( - FieldDefinition.from_annotation(additional_response.data_container) - ) + + content: dict[str, OpenAPIMediaType] | None + if additional_response.data_container is not None: + schema = schema_creator.for_field_definition( + FieldDefinition.from_annotation(additional_response.data_container) + ) + content = {additional_response.media_type: OpenAPIMediaType(schema=schema)} + else: + content = None + yield ( str(status_code), OpenAPIResponse( description=additional_response.description, - content={additional_response.media_type: OpenAPIMediaType(schema=schema)}, + content=content, ), ) diff --git a/litestar/_openapi/schema_generation/schema.py b/litestar/_openapi/schema_generation/schema.py index faba560043..891ace9c43 100644 --- a/litestar/_openapi/schema_generation/schema.py +++ b/litestar/_openapi/schema_generation/schema.py @@ -117,7 +117,7 @@ Sequence: Schema(type=OpenAPIType.ARRAY), Set: Schema(type=OpenAPIType.ARRAY), Tuple: Schema(type=OpenAPIType.ARRAY), - UUID: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.UUID, description="Any UUID string"), + UUID: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.UUID), bool: Schema(type=OpenAPIType.BOOLEAN), bytearray: Schema(type=OpenAPIType.STRING), bytes: Schema(type=OpenAPIType.STRING), diff --git a/litestar/app.py b/litestar/app.py index af5fc57c35..4bdb2648c3 100644 --- a/litestar/app.py +++ b/litestar/app.py @@ -59,12 +59,13 @@ from litestar.config.compression import CompressionConfig from litestar.config.cors import CORSConfig from litestar.config.csrf import CSRFConfig - from litestar.datastructures import CacheControlHeader, ETag, ResponseHeader + from litestar.datastructures import CacheControlHeader, ETag from litestar.dto import AbstractDTO from litestar.events.listener import EventListener from litestar.logging.config import BaseLoggingConfig from litestar.openapi.spec import SecurityRequirement from litestar.openapi.spec.open_api import OpenAPI + from litestar.response import Response from litestar.static_files.config import StaticFilesConfig from litestar.stores.base import Store from litestar.types import ( @@ -91,7 +92,7 @@ ParametersMap, Receive, ResponseCookies, - ResponseType, + ResponseHeaders, RouteHandlerType, Scope, Send, @@ -137,6 +138,7 @@ class Litestar(Router): "_server_lifespan_managers", "_debug", "_openapi_schema", + "_static_files_config", "plugins", "after_exception", "allowed_hosts", @@ -160,7 +162,6 @@ class Litestar(Router): "route_map", "signature_namespace", "state", - "static_files_config", "stores", "template_engine", "websocket_class", @@ -203,9 +204,9 @@ def __init__( plugins: Sequence[PluginProtocol] | None = None, request_class: type[Request] | None = None, response_cache_config: ResponseCacheConfig | None = None, - response_class: ResponseType | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, - response_headers: Sequence[ResponseHeader] | None = None, + response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, security: Sequence[SecurityRequirement] | None = None, signature_namespace: Mapping[str, Any] | None = None, @@ -387,7 +388,12 @@ def __init__( self._openapi_schema: OpenAPI | None = None self._debug: bool = True + self.stores: StoreRegistry = ( + config.stores if isinstance(config.stores, StoreRegistry) else StoreRegistry(config.stores) + ) self._lifespan_managers = config.lifespan + for store in self.stores._stores.values(): + self._lifespan_managers.append(store) self._server_lifespan_managers = [p.server_lifespan for p in config.plugins or [] if isinstance(p, CLIPlugin)] self.experimental_features = frozenset(config.experimental_features or []) self.get_logger: GetLogger = get_logger_placeholder @@ -410,7 +416,7 @@ def __init__( self.request_class = config.request_class or Request self.response_cache_config = config.response_cache_config self.state = config.state - self.static_files_config = config.static_files_config + self._static_files_config = config.static_files_config self.template_engine = config.template_config.engine_instance if config.template_config else None self.websocket_class = config.websocket_class or WebSocket self.debug = config.debug @@ -420,6 +426,15 @@ def __init__( if self.pdb_on_exception: warn_pdb_on_exception() + try: + from starlette.exceptions import HTTPException as StarletteHTTPException + + from litestar.middleware.exceptions.middleware import _starlette_exception_handler + + config.exception_handlers.setdefault(StarletteHTTPException, _starlette_exception_handler) + except ImportError: + pass + super().__init__( after_request=config.after_request, after_response=config.after_response, @@ -456,14 +471,15 @@ def __init__( self.get_logger = self.logging_config.configure() self.logger = self.get_logger("litestar") - for static_config in self.static_files_config: + for static_config in self._static_files_config: self.register(static_config.to_static_files_app()) self.asgi_handler = self._create_asgi_handler() - self.stores: StoreRegistry = ( - config.stores if isinstance(config.stores, StoreRegistry) else StoreRegistry(config.stores) - ) + @property + @deprecated(version="2.6.0", kind="property", info="Use create_static_files router instead") + def static_files_config(self) -> list[StaticFilesConfig]: + return self._static_files_config @property @deprecated(version="2.0", alternative="Litestar.plugins.cli", kind="property") @@ -482,18 +498,30 @@ def serialization_plugins(self) -> list[SerializationPluginProtocol]: @staticmethod def _get_default_plugins(plugins: list[PluginProtocol]) -> list[PluginProtocol]: + from litestar.plugins.core import MsgspecDIPlugin + + plugins.append(MsgspecDIPlugin()) + with suppress(MissingDependencyException): - from litestar.contrib.pydantic import PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin + from litestar.contrib.pydantic import ( + PydanticDIPlugin, + PydanticInitPlugin, + PydanticPlugin, + PydanticSchemaPlugin, + ) pydantic_plugin_found = any(isinstance(plugin, PydanticPlugin) for plugin in plugins) pydantic_init_plugin_found = any(isinstance(plugin, PydanticInitPlugin) for plugin in plugins) pydantic_schema_plugin_found = any(isinstance(plugin, PydanticSchemaPlugin) for plugin in plugins) + pydantic_serialization_plugin_found = any(isinstance(plugin, PydanticDIPlugin) for plugin in plugins) if not pydantic_plugin_found and not pydantic_init_plugin_found and not pydantic_schema_plugin_found: plugins.append(PydanticPlugin()) elif not pydantic_plugin_found and pydantic_init_plugin_found and not pydantic_schema_plugin_found: plugins.append(PydanticSchemaPlugin()) elif not pydantic_plugin_found and not pydantic_init_plugin_found: plugins.append(PydanticInitPlugin()) + if not pydantic_plugin_found and not pydantic_serialization_plugin_found: + plugins.append(PydanticDIPlugin()) with suppress(MissingDependencyException): from litestar.contrib.attrs import AttrsSchemaPlugin @@ -508,8 +536,14 @@ def debug(self) -> bool: @debug.setter def debug(self, value: bool) -> None: - if self.logger: - self.logger.setLevel(logging.DEBUG if value else logging.INFO) + """Sets the debug logging level for the application. + + When possible, it calls the `self.logging_config.set_level` method. This allows for implementation specific code and APIs to be called. + """ + if self.logger and self.logging_config: + self.logging_config.set_level(self.logger, logging.DEBUG if value else logging.INFO) + elif self.logger and hasattr(self.logger, "setLevel"): # pragma: no cover + self.logger.setLevel(logging.DEBUG if value else logging.INFO) # pragma: no cover if isinstance(self.logging_config, LoggingConfig): self.logging_config.loggers["litestar"]["level"] = "DEBUG" if value else "INFO" self._debug = value @@ -537,7 +571,7 @@ async def __call__( return scope["app"] = self - scope["state"] = {} + scope.setdefault("state", {}) await self.asgi_handler(scope, receive, self._wrap_send(send=send, scope=scope)) # type: ignore[arg-type] async def _call_lifespan_hook(self, hook: LifespanHook) -> None: @@ -733,6 +767,9 @@ def get_membership_details(group_id: int, user_id: int) -> None: return join_paths(output) + @deprecated( + "2.6.0", info="Use create_static_files router instead of StaticFilesConfig, which works with route_reverse" + ) def url_for_static_asset(self, name: str, file_path: str) -> str: """Receives a static files handler name, an asset file path and returns resolved url path to the asset. diff --git a/litestar/cli/__init__.py b/litestar/cli/__init__.py index 72e6bd55c8..b5ffa38d9a 100644 --- a/litestar/cli/__init__.py +++ b/litestar/cli/__init__.py @@ -1,6 +1,28 @@ """Litestar CLI.""" from __future__ import annotations +from importlib.util import find_spec + +# Ensure `rich_click` patching occurs before we do any imports from `click`. +if find_spec("rich_click") is not None: # pragma: no cover + import rich_click as click + from rich_click.cli import patch as rich_click_patch + + rich_click_patch() + click.rich_click.USE_RICH_MARKUP = True + click.rich_click.USE_MARKDOWN = False + click.rich_click.SHOW_ARGUMENTS = True + click.rich_click.GROUP_ARGUMENTS_OPTIONS = True + click.rich_click.SHOW_ARGUMENTS = True + click.rich_click.GROUP_ARGUMENTS_OPTIONS = True + click.rich_click.STYLE_ERRORS_SUGGESTION = "magenta italic" + click.rich_click.ERRORS_SUGGESTION = "" + click.rich_click.ERRORS_EPILOGUE = "" + click.rich_click.MAX_WIDTH = 80 + click.rich_click.SHOW_METAVARS_COLUMN = True + click.rich_click.APPEND_METAVARS_HELP = True + + from .main import litestar_group __all__ = ["litestar_group"] diff --git a/litestar/cli/_utils.py b/litestar/cli/_utils.py index 63b9c3684e..fa482482d4 100644 --- a/litestar/cli/_utils.py +++ b/litestar/cli/_utils.py @@ -36,13 +36,11 @@ from litestar.types import AnyCallable -RICH_CLICK_INSTALLED = find_spec("rich-click") is not None UVICORN_INSTALLED = find_spec("uvicorn") is not None JSBEAUTIFIER_INSTALLED = find_spec("jsbeautifier") is not None __all__ = ( - "RICH_CLICK_INSTALLED", "UVICORN_INSTALLED", "JSBEAUTIFIER_INSTALLED", "LoadedApp", @@ -85,6 +83,8 @@ class LitestarEnv: uds: str | None = None reload: bool | None = None reload_dirs: tuple[str, ...] | None = None + reload_include: tuple[str, ...] | None = None + reload_exclude: tuple[str, ...] | None = None web_concurrency: int | None = None is_app_factory: bool = False certfile_path: str | None = None @@ -120,6 +120,8 @@ def from_env(cls, app_path: str | None, app_dir: Path | None = None) -> Litestar uds = getenv("LITESTAR_UNIX_DOMAIN_SOCKET") fd = getenv("LITESTAR_FILE_DESCRIPTOR") reload_dirs = tuple(s.strip() for s in getenv("LITESTAR_RELOAD_DIRS", "").split(",") if s) or None + reload_include = tuple(s.strip() for s in getenv("LITESTAR_RELOAD_INCLUDES", "").split(",") if s) or None + reload_exclude = tuple(s.strip() for s in getenv("LITESTAR_RELOAD_EXCLUDES", "").split(",") if s) or None return cls( app_path=loaded_app.app_path, @@ -131,6 +133,8 @@ def from_env(cls, app_path: str | None, app_dir: Path | None = None) -> Litestar fd=int(fd) if fd else None, reload=_bool_from_env("LITESTAR_RELOAD"), reload_dirs=reload_dirs, + reload_include=reload_include, + reload_exclude=reload_exclude, web_concurrency=int(web_concurrency) if web_concurrency else None, is_app_factory=loaded_app.is_factory, cwd=cwd, diff --git a/litestar/cli/commands/core.py b/litestar/cli/commands/core.py index a77abe053b..5b552533b1 100644 --- a/litestar/cli/commands/core.py +++ b/litestar/cli/commands/core.py @@ -69,6 +69,8 @@ def _run_uvicorn_in_subprocess( workers: int | None, reload: bool, reload_dirs: tuple[str, ...] | None, + reload_include: tuple[str, ...] | None, + reload_exclude: tuple[str, ...] | None, fd: int | None, uds: str | None, certfile_path: str | None, @@ -87,6 +89,10 @@ def _run_uvicorn_in_subprocess( process_args["uds"] = uds if reload_dirs: process_args["reload-dir"] = reload_dirs + if reload_include: + process_args["reload-include"] = reload_include + if reload_exclude: + process_args["reload-exclude"] = reload_exclude if certfile_path is not None: process_args["ssl-certfile"] = certfile_path if keyfile_path is not None: @@ -116,6 +122,12 @@ def info_command(app: Litestar) -> None: @command(name="run") @option("-r", "--reload", help="Reload server on changes", default=False, is_flag=True) @option("-R", "--reload-dir", help="Directories to watch for file changes", multiple=True) +@option( + "-I", "--reload-include", help="Glob patterns for files to include when watching for file changes", multiple=True +) +@option( + "-E", "--reload-exclude", help="Glob patterns for files to exclude when watching for file changes", multiple=True +) @option("-p", "--port", help="Serve under this port", type=int, default=8000, show_default=True) @option( "-W", @@ -155,6 +167,8 @@ def run_command( uds: str | None, debug: bool, reload_dir: tuple[str, ...], + reload_include: tuple[str, ...], + reload_exclude: tuple[str, ...], pdb: bool, ssl_certfile: str | None, ssl_keyfile: str | None, @@ -194,12 +208,14 @@ def run_command( app = env.app reload_dirs = env.reload_dirs or reload_dir + reload_include = env.reload_include or reload_include + reload_exclude = env.reload_exclude or reload_exclude host = env.host or host port = env.port if env.port is not None else port fd = env.fd if env.fd is not None else fd uds = env.uds or uds - reload = env.reload or reload or bool(reload_dirs) + reload = env.reload or reload or bool(reload_dirs) or bool(reload_include) or bool(reload_exclude) workers = env.web_concurrency or wc ssl_certfile = ssl_certfile or env.certfile_path @@ -248,6 +264,8 @@ def run_command( workers=workers, reload=reload, reload_dirs=reload_dirs, + reload_include=reload_include, + reload_exclude=reload_exclude, fd=fd, uds=uds, certfile_path=certfile_path, diff --git a/litestar/cli/main.py b/litestar/cli/main.py index 8fd4ef676c..32505f62d8 100644 --- a/litestar/cli/main.py +++ b/litestar/cli/main.py @@ -1,35 +1,12 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING -from ._utils import RICH_CLICK_INSTALLED, LitestarEnv, LitestarExtensionGroup -from .commands import core, schema, sessions - -if TYPE_CHECKING or not RICH_CLICK_INSTALLED: # pragma: no cover - import click - from click import Context, group, option, pass_context - from click import Path as ClickPath -else: # pragma: no cover - import rich_click as click - from rich_click import Context, group, option, pass_context - from rich_click import Path as ClickPath - from rich_click.cli import patch as rich_click_patch - - rich_click_patch() - click.rich_click.USE_RICH_MARKUP = True - click.rich_click.USE_MARKDOWN = False - click.rich_click.SHOW_ARGUMENTS = True - click.rich_click.GROUP_ARGUMENTS_OPTIONS = True - click.rich_click.SHOW_ARGUMENTS = True - click.rich_click.GROUP_ARGUMENTS_OPTIONS = True - click.rich_click.STYLE_ERRORS_SUGGESTION = "magenta italic" - click.rich_click.ERRORS_SUGGESTION = "" - click.rich_click.ERRORS_EPILOGUE = "" - click.rich_click.MAX_WIDTH = 80 - click.rich_click.SHOW_METAVARS_COLUMN = True - click.rich_click.APPEND_METAVARS_HELP = True +from click import Context, group, option, pass_context +from click import Path as ClickPath +from ._utils import LitestarEnv, LitestarExtensionGroup +from .commands import core, schema, sessions __all__ = ("litestar_group",) diff --git a/litestar/config/app.py b/litestar/config/app.py index ff12a05d97..859999bfd6 100644 --- a/litestar/config/app.py +++ b/litestar/config/app.py @@ -2,7 +2,7 @@ import enum from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Callable, Sequence +from typing import TYPE_CHECKING, Any, Callable from litestar.config.allowed_hosts import AllowedHostsConfig from litestar.config.response_cache import ResponseCacheConfig @@ -13,12 +13,12 @@ if TYPE_CHECKING: from contextlib import AbstractAsyncContextManager - from litestar import Litestar + from litestar import Litestar, Response from litestar.config.compression import CompressionConfig from litestar.config.cors import CORSConfig from litestar.config.csrf import CSRFConfig from litestar.connection import Request, WebSocket - from litestar.datastructures import CacheControlHeader, ETag, ResponseHeader + from litestar.datastructures import CacheControlHeader, ETag from litestar.di import Provide from litestar.dto import AbstractDTO from litestar.events.emitter import BaseEventEmitterBackend @@ -43,7 +43,7 @@ Middleware, ParametersMap, ResponseCookies, - ResponseType, + ResponseHeaders, TypeEncodersMap, ) from litestar.types.callable_types import LifespanHook @@ -157,11 +157,11 @@ class AppConfig: """List of :class:`SerializationPluginProtocol <.plugins.SerializationPluginProtocol>`.""" request_class: type[Request] | None = field(default=None) """An optional subclass of :class:`Request <.connection.Request>` to use for http connections.""" - response_class: ResponseType | None = field(default=None) + response_class: type[Response] | None = field(default=None) """A custom subclass of :class:`Response <.response.Response>` to be used as the app's default response.""" response_cookies: ResponseCookies = field(default_factory=list) """A list of :class:`Cookie <.datastructures.Cookie>`.""" - response_headers: Sequence[ResponseHeader] = field(default_factory=list) + response_headers: ResponseHeaders = field(default_factory=list) """A string keyed dictionary mapping :class:`ResponseHeader <.datastructures.ResponseHeader>`.""" response_cache_config: ResponseCacheConfig = field(default_factory=ResponseCacheConfig) """Configures caching behavior of the application.""" diff --git a/litestar/config/compression.py b/litestar/config/compression.py index 2d6ecf1491..c339329144 100644 --- a/litestar/config/compression.py +++ b/litestar/config/compression.py @@ -1,10 +1,14 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Literal +from typing import TYPE_CHECKING, Any, Literal from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware.compression import CompressionMiddleware +from litestar.middleware.compression.gzip_facade import GzipCompression + +if TYPE_CHECKING: + from litestar.middleware.compression.facade import CompressionFacade __all__ = ("CompressionConfig",) @@ -17,8 +21,11 @@ class CompressionConfig: using the ``compression_config`` key. """ - backend: Literal["gzip", "brotli"] - """Literal of "gzip" or "brotli".""" + backend: Literal["gzip", "brotli"] | str + """The backend to use. + + If the value given is `gzip` or `brotli`, then the builtin gzip and brotli compression is used. + """ minimum_size: int = field(default=500) """Minimum response size (bytes) to enable compression, affects all backends.""" gzip_compress_level: int = field(default=9) @@ -48,16 +55,29 @@ class CompressionConfig: """A pattern or list of patterns to skip in the compression middleware.""" exclude_opt_key: str | None = None """An identifier to use on routes to disable compression for a particular route.""" + compression_facade: type[CompressionFacade] = GzipCompression + """The compression facade to use for the actual compression.""" + backend_config: Any = None + """Configuration specific to the backend.""" + gzip_fallback: bool = True + """Use GZIP as a fallback if the provided backend is not supported by the client.""" def __post_init__(self) -> None: if self.minimum_size <= 0: raise ImproperlyConfiguredException("minimum_size must be greater than 0") - if self.gzip_compress_level < 0 or self.gzip_compress_level > 9: - raise ImproperlyConfiguredException("gzip_compress_level must be a value between 0 and 9") + if self.backend == "gzip": + if self.gzip_compress_level < 0 or self.gzip_compress_level > 9: + raise ImproperlyConfiguredException("gzip_compress_level must be a value between 0 and 9") + elif self.backend == "brotli": + # Brotli is not guaranteed to be installed. + from litestar.middleware.compression.brotli_facade import BrotliCompression + + if self.brotli_quality < 0 or self.brotli_quality > 11: + raise ImproperlyConfiguredException("brotli_quality must be a value between 0 and 11") - if self.brotli_quality < 0 or self.brotli_quality > 11: - raise ImproperlyConfiguredException("brotli_quality must be a value between 0 and 11") + if self.brotli_lgwin < 10 or self.brotli_lgwin > 24: + raise ImproperlyConfiguredException("brotli_lgwin must be a value between 10 and 24") - if self.brotli_lgwin < 10 or self.brotli_lgwin > 24: - raise ImproperlyConfiguredException("brotli_lgwin must be a value between 10 and 24") + self.gzip_fallback = self.brotli_gzip_fallback + self.compression_facade = BrotliCompression diff --git a/litestar/connection/base.py b/litestar/connection/base.py index ada24f1588..7fb7098101 100644 --- a/litestar/connection/base.py +++ b/litestar/connection/base.py @@ -117,7 +117,7 @@ def state(self) -> StateT: Returns: A State instance constructed from the scope["state"] value. """ - return cast("StateT", State(self.scope["state"])) + return cast("StateT", State(self.scope.get("state"))) @property def url(self) -> URL: diff --git a/litestar/contrib/mako.py b/litestar/contrib/mako.py index 859a814723..9cb4c476b1 100644 --- a/litestar/contrib/mako.py +++ b/litestar/contrib/mako.py @@ -130,7 +130,7 @@ def render_string(self, template_string: str, context: Mapping[str, Any]) -> str Returns: The rendered template as a string. """ - template = _MakoTemplate(template_string) + template = _MakoTemplate(template_string) # noqa: S702 return template.render(**context) # type: ignore[no-any-return] @classmethod diff --git a/litestar/contrib/piccolo.py b/litestar/contrib/piccolo.py index 314eb328e2..217db8445d 100644 --- a/litestar/contrib/piccolo.py +++ b/litestar/contrib/piccolo.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from dataclasses import replace from decimal import Decimal from typing import Any, Generator, Generic, List, Optional, TypeVar @@ -9,7 +10,7 @@ from litestar.dto import AbstractDTO, DTOField, Mark from litestar.dto.data_structures import DTOFieldDefinition -from litestar.exceptions import MissingDependencyException +from litestar.exceptions import LitestarWarning, MissingDependencyException from litestar.types import Empty from litestar.typing import FieldDefinition from litestar.utils import warn_deprecation @@ -38,12 +39,22 @@ def __getattr__(name: str) -> Any: def _parse_piccolo_type(column: Column, extra: dict[str, Any]) -> FieldDefinition: + is_optional = not column._meta.required + if isinstance(column, (column_types.Decimal, column_types.Numeric)): column_type: Any = Decimal meta = Meta(extra=extra) elif isinstance(column, (column_types.Email, column_types.Varchar)): column_type = str - meta = Meta(max_length=column.length, extra=extra) + if is_optional: + meta = Meta(extra=extra) + warnings.warn( + f"Dropping max_length constraint for column {column!r} because the " "column is optional", + category=LitestarWarning, + stacklevel=2, + ) + else: + meta = Meta(max_length=column.length, extra=extra) elif isinstance(column, column_types.Array): column_type = List[column.base_column.value_type] # type: ignore meta = Meta(extra=extra) @@ -57,7 +68,7 @@ def _parse_piccolo_type(column: Column, extra: dict[str, Any]) -> FieldDefinitio column_type = column.value_type meta = Meta(extra=extra) - if not column._meta.required: + if is_optional: column_type = Optional[column_type] return FieldDefinition.from_annotation(Annotated[column_type, meta]) @@ -79,10 +90,11 @@ class PiccoloDTO(AbstractDTO[T], Generic[T]): @classmethod def generate_field_definitions(cls, model_type: type[Table]) -> Generator[DTOFieldDefinition, None, None]: for column in model_type._meta.columns: + mark = Mark.WRITE_ONLY if column._meta.secret else Mark.READ_ONLY if column._meta.primary_key else None yield replace( DTOFieldDefinition.from_field_definition( field_definition=_parse_piccolo_type(column, _create_column_extra(column)), - dto_field=DTOField(mark=Mark.READ_ONLY if column._meta.primary_key else None), + dto_field=DTOField(mark=mark), model_name=model_type.__name__, default_factory=None, ), diff --git a/litestar/contrib/pydantic/__init__.py b/litestar/contrib/pydantic/__init__.py index 122a710539..9bab707c31 100644 --- a/litestar/contrib/pydantic/__init__.py +++ b/litestar/contrib/pydantic/__init__.py @@ -4,6 +4,7 @@ from litestar.plugins import InitPluginProtocol +from .pydantic_di_plugin import PydanticDIPlugin from .pydantic_dto_factory import PydanticDTO from .pydantic_init_plugin import PydanticInitPlugin from .pydantic_schema_plugin import PydanticSchemaPlugin @@ -14,7 +15,13 @@ from litestar.config.app import AppConfig -__all__ = ("PydanticDTO", "PydanticInitPlugin", "PydanticSchemaPlugin", "PydanticPlugin") +__all__ = ( + "PydanticDTO", + "PydanticInitPlugin", + "PydanticSchemaPlugin", + "PydanticPlugin", + "PydanticDIPlugin", +) def _model_dump(model: BaseModel | BaseModelV1, *, by_alias: bool = False) -> dict[str, Any]: @@ -53,6 +60,10 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig: app_config: The :class:`AppConfig <.config.app.AppConfig>` instance. """ app_config.plugins.extend( - [PydanticInitPlugin(prefer_alias=self.prefer_alias), PydanticSchemaPlugin(prefer_alias=self.prefer_alias)] + [ + PydanticInitPlugin(prefer_alias=self.prefer_alias), + PydanticSchemaPlugin(prefer_alias=self.prefer_alias), + PydanticDIPlugin(), + ] ) return app_config diff --git a/litestar/contrib/pydantic/pydantic_di_plugin.py b/litestar/contrib/pydantic/pydantic_di_plugin.py new file mode 100644 index 0000000000..2096fd4ab6 --- /dev/null +++ b/litestar/contrib/pydantic/pydantic_di_plugin.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import inspect +from inspect import Signature +from typing import Any + +from litestar.contrib.pydantic.utils import is_pydantic_model_class +from litestar.plugins import DIPlugin + + +class PydanticDIPlugin(DIPlugin): + def has_typed_init(self, type_: Any) -> bool: + return is_pydantic_model_class(type_) + + def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: + try: + model_fields = dict(type_.model_fields) + except AttributeError: + model_fields = {k: model_field.field_info for k, model_field in type_.__fields__.items()} + + parameters = [ + inspect.Parameter(name=field_name, kind=inspect.Parameter.KEYWORD_ONLY, annotation=Any) + for field_name in model_fields + ] + type_hints = {field_name: Any for field_name in model_fields} + return Signature(parameters), type_hints diff --git a/litestar/contrib/pydantic/pydantic_schema_plugin.py b/litestar/contrib/pydantic/pydantic_schema_plugin.py index 7a074931f3..af719a49af 100644 --- a/litestar/contrib/pydantic/pydantic_schema_plugin.py +++ b/litestar/contrib/pydantic/pydantic_schema_plugin.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Optional from typing_extensions import Annotated @@ -11,14 +11,15 @@ is_pydantic_constrained_field, is_pydantic_model_class, is_pydantic_undefined, - pydantic_get_unwrapped_annotation_and_type_hints, + pydantic_get_type_hints_with_generics_resolved, + pydantic_unwrap_and_get_origin, ) from litestar.exceptions import MissingDependencyException from litestar.openapi.spec import Example, OpenAPIFormat, OpenAPIType, Schema from litestar.plugins import OpenAPISchemaPlugin from litestar.types import Empty from litestar.typing import FieldDefinition -from litestar.utils import is_class_and_subclass +from litestar.utils import is_class_and_subclass, is_generic try: # check if we have pydantic v2 installed, and try to import both versions @@ -142,6 +143,8 @@ if pydantic_v2 is not None: # pragma: no cover PYDANTIC_TYPE_MAP.update( { + pydantic_v2.SecretStr: Schema(type=OpenAPIType.STRING), + pydantic_v2.SecretBytes: Schema(type=OpenAPIType.STRING), pydantic_v2.ByteSize: Schema(type=OpenAPIType.INTEGER), pydantic_v2.EmailStr: Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.EMAIL), pydantic_v2.IPvAnyAddress: Schema( @@ -247,17 +250,22 @@ def for_pydantic_model(cls, field_definition: FieldDefinition, schema_creator: S """ annotation = field_definition.annotation - unwrapped_annotation, annotation_hints = pydantic_get_unwrapped_annotation_and_type_hints(annotation) + if is_generic(annotation): + is_generic_model = True + model = pydantic_unwrap_and_get_origin(annotation) or annotation + else: + is_generic_model = False + model = annotation - if is_pydantic_2_model(annotation): - model_config = annotation.model_config - model_field_info = unwrapped_annotation.model_fields + if is_pydantic_2_model(model): + model_config = model.model_config + model_field_info = model.model_fields title = model_config.get("title") example = model_config.get("example") is_v2_model = True else: model_config = annotation.__config__ - model_field_info = unwrapped_annotation.__fields__ + model_field_info = model.__fields__ title = getattr(model_config, "title", None) example = getattr(model_config, "example", None) is_v2_model = False @@ -266,15 +274,34 @@ def for_pydantic_model(cls, field_definition: FieldDefinition, schema_creator: S k: getattr(f, "field_info", f) for k, f in model_field_info.items() } + if is_v2_model: + # extract the annotations from the FieldInfo. This allows us to skip fields + # which have been marked as private + model_annotations = {k: field_info.annotation for k, field_info in model_fields.items()} # type: ignore[union-attr] + + else: + # pydantic v1 requires some workarounds here + model_annotations = { + k: f.outer_type_ if f.required else Optional[f.outer_type_] for k, f in model.__fields__.items() + } + + if is_generic_model: + # if the model is generic, resolve the type variables. We pass in the + # already extracted annotations, to keep the logic of respecting private + # fields consistent with the above + model_annotations = pydantic_get_type_hints_with_generics_resolved( + annotation, model_annotations=model_annotations, include_extras=True + ) + property_fields = { - f.alias if f.alias and schema_creator.prefer_alias else k: FieldDefinition.from_kwarg( - annotation=Annotated[annotation_hints[k], f, f.metadata] # type: ignore[union-attr] + field_info.alias if field_info.alias and schema_creator.prefer_alias else k: FieldDefinition.from_kwarg( + annotation=Annotated[model_annotations[k], field_info, field_info.metadata] # type: ignore[union-attr] if is_v2_model - else Annotated[annotation_hints[k], f], # pyright: ignore - name=f.alias if f.alias and schema_creator.prefer_alias else k, - default=Empty if schema_creator.is_undefined(f.default) else f.default, + else Annotated[model_annotations[k], field_info], # pyright: ignore + name=field_info.alias if field_info.alias and schema_creator.prefer_alias else k, + default=Empty if schema_creator.is_undefined(field_info.default) else field_info.default, ) - for k, f in model_fields.items() + for k, field_info in model_fields.items() } computed_field_definitions = create_field_definitions_for_computed_fields( diff --git a/litestar/contrib/pydantic/utils.py b/litestar/contrib/pydantic/utils.py index f4f8d0f98a..6aee3225da 100644 --- a/litestar/contrib/pydantic/utils.py +++ b/litestar/contrib/pydantic/utils.py @@ -8,7 +8,7 @@ from litestar.params import KwargDefinition from litestar.types import Empty from litestar.typing import FieldDefinition -from litestar.utils import is_class_and_subclass +from litestar.utils import deprecated, is_class_and_subclass from litestar.utils.predicates import is_generic from litestar.utils.typing import ( _substitute_typevars, @@ -129,24 +129,32 @@ def pydantic_get_type_hints_with_generics_resolved( globalns: dict[str, Any] | None = None, localns: dict[str, Any] | None = None, include_extras: bool = False, + model_annotations: dict[str, Any] | None = None, ) -> dict[str, Any]: if pydantic_v2 is Empty or (pydantic_v1 is not Empty and is_class_and_subclass(annotation, pydantic_v1.BaseModel)): - return get_type_hints_with_generics_resolved(annotation) + return get_type_hints_with_generics_resolved(annotation, type_hints=model_annotations) origin = pydantic_unwrap_and_get_origin(annotation) if origin is None: - type_hints = get_type_hints(annotation, globalns=globalns, localns=localns, include_extras=include_extras) + if model_annotations is None: # pragma: no cover + model_annotations = get_type_hints( + annotation, globalns=globalns, localns=localns, include_extras=include_extras + ) typevar_map = {p: p for p in annotation.__pydantic_generic_metadata__["parameters"]} else: - type_hints = get_type_hints(origin, globalns=globalns, localns=localns, include_extras=include_extras) + if model_annotations is None: + model_annotations = get_type_hints( + origin, globalns=globalns, localns=localns, include_extras=include_extras + ) args = annotation.__pydantic_generic_metadata__["args"] parameters = origin.__pydantic_generic_metadata__["parameters"] typevar_map = dict(zip(parameters, args)) - return {n: _substitute_typevars(type_, typevar_map) for n, type_ in type_hints.items()} + return {n: _substitute_typevars(type_, typevar_map) for n, type_ in model_annotations.items()} -def pydantic_get_unwrapped_annotation_and_type_hints(annotation: Any) -> tuple[Any, dict[str, Any]]: +@deprecated(version="2.6.2") +def pydantic_get_unwrapped_annotation_and_type_hints(annotation: Any) -> tuple[Any, dict[str, Any]]: # pragma: pver """Get the unwrapped annotation and the type hints after resolving generics. Args: diff --git a/litestar/data_extractors.py b/litestar/data_extractors.py index 6d4b182133..5a6f6607f0 100644 --- a/litestar/data_extractors.py +++ b/litestar/data_extractors.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Coroutine, Literal, TypedDict, cast +import inspect +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Iterable, Literal, TypedDict, cast from litestar._parsers import parse_cookie_string from litestar.connection.request import Request @@ -70,6 +71,7 @@ class ConnectionDataExtractor: "parse_query", "obfuscate_headers", "obfuscate_cookies", + "skip_parse_malformed_body", ) def __init__( @@ -88,6 +90,7 @@ def __init__( obfuscate_headers: set[str] | None = None, parse_body: bool = False, parse_query: bool = False, + skip_parse_malformed_body: bool = False, ) -> None: """Initialize ``ConnectionDataExtractor`` @@ -106,9 +109,11 @@ def __init__( obfuscate_cookies: cookie keys to obfuscate. Obfuscated values are replaced with '*****'. parse_body: Whether to parse the body value or return the raw byte string, (for requests only). parse_query: Whether to parse query parameters or return the raw byte string. + skip_parse_malformed_body: Whether to skip parsing the body if it is malformed """ self.parse_body = parse_body self.parse_query = parse_query + self.skip_parse_malformed_body = skip_parse_malformed_body self.obfuscate_headers = {h.lower() for h in (obfuscate_headers or set())} self.obfuscate_cookies = {c.lower() for c in (obfuscate_cookies or set())} self.connection_extractors: dict[str, Callable[[ASGIConnection[Any, Any, Any, Any]], Any]] = {} @@ -153,6 +158,25 @@ def __call__(self, connection: ASGIConnection[Any, Any, Any, Any]) -> ExtractedR ) return cast("ExtractedRequestData", {key: extractor(connection) for key, extractor in extractors.items()}) + async def extract( + self, connection: ASGIConnection[Any, Any, Any, Any], fields: Iterable[str] + ) -> ExtractedRequestData: + extractors = ( + {**self.connection_extractors, **self.request_extractors} # type: ignore + if isinstance(connection, Request) + else self.connection_extractors + ) + data = {} + for key, extractor in extractors.items(): + if key not in fields: + continue + if inspect.iscoroutinefunction(extractor): + value = await extractor(connection) + else: + value = extractor(connection) + data[key] = value + return cast("ExtractedRequestData", data) + @staticmethod def extract_scheme(connection: ASGIConnection[Any, Any, Any, Any]) -> str: """Extract the scheme from an ``ASGIConnection`` @@ -272,13 +296,20 @@ async def extract_body(self, request: Request[Any, Any, Any]) -> Any: return None if not self.parse_body: return await request.body() - request_encoding_type = request.content_type[0] - if request_encoding_type == RequestEncodingType.JSON: - return await request.json() - form_data = await request.form() - if request_encoding_type == RequestEncodingType.URL_ENCODED: - return dict(form_data) - return {key: repr(value) if isinstance(value, UploadFile) else value for key, value in form_data.multi_items()} + try: + request_encoding_type = request.content_type[0] + if request_encoding_type == RequestEncodingType.JSON: + return await request.json() + form_data = await request.form() + if request_encoding_type == RequestEncodingType.URL_ENCODED: + return dict(form_data) + return { + key: repr(value) if isinstance(value, UploadFile) else value for key, value in form_data.multi_items() + } + except Exception as exc: + if self.skip_parse_malformed_body: + return await request.body() + raise exc class ExtractedResponseData(TypedDict, total=False): diff --git a/litestar/datastructures/state.py b/litestar/datastructures/state.py index 10578e2124..71980e0512 100644 --- a/litestar/datastructures/state.py +++ b/litestar/datastructures/state.py @@ -188,8 +188,8 @@ def __init__( state: An object to initialize the state from. Can be a dict, an instance of 'ImmutableState', or a tuple of key value paris. deep_copy: Whether to 'deepcopy' the passed in state. - Examples: .. code-block:: python + :caption: Examples from litestar.datastructures import State diff --git a/litestar/dto/_backend.py b/litestar/dto/_backend.py index b93c457ddc..15792d103e 100644 --- a/litestar/dto/_backend.py +++ b/litestar/dto/_backend.py @@ -17,7 +17,9 @@ cast, ) +import msgspec from msgspec import UNSET, Struct, UnsetType, convert, defstruct, field +from typing_extensions import Annotated from litestar.dto._types import ( CollectionType, @@ -33,6 +35,7 @@ from litestar.dto.data_structures import DTOData, DTOFieldDefinition from litestar.dto.field import Mark from litestar.enums import RequestEncodingType +from litestar.params import KwargDefinition from litestar.serialization import decode_json, decode_msgpack from litestar.types import Empty from litestar.typing import FieldDefinition @@ -740,6 +743,24 @@ def _create_msgspec_field(field_definition: TransferDTOFieldDefinition) -> Any: return field(**kwargs) +def _create_struct_field_meta_for_field_definition(field_definition: TransferDTOFieldDefinition) -> msgspec.Meta | None: + if (kwarg_definition := field_definition.kwarg_definition) is None or not isinstance( + kwarg_definition, KwargDefinition + ): + return None + + return msgspec.Meta( + gt=kwarg_definition.gt, + ge=kwarg_definition.ge, + lt=kwarg_definition.lt, + le=kwarg_definition.le, + multiple_of=kwarg_definition.multiple_of, + min_length=kwarg_definition.min_length if not field_definition.is_partial else None, + max_length=kwarg_definition.max_length if not field_definition.is_partial else None, + pattern=kwarg_definition.pattern, + ) + + def _create_struct_for_field_definitions( model_name: str, field_definitions: tuple[TransferDTOFieldDefinition, ...], @@ -755,6 +776,9 @@ def _create_struct_for_field_definitions( if field_definition.is_partial: field_type = Union[field_type, UnsetType] + if (field_meta := _create_struct_field_meta_for_field_definition(field_definition)) is not None: + field_type = Annotated[field_type, field_meta] + struct_fields.append( ( field_definition.name, diff --git a/litestar/events/emitter.py b/litestar/events/emitter.py index 14499741e3..7c33c9e73f 100644 --- a/litestar/events/emitter.py +++ b/litestar/events/emitter.py @@ -87,7 +87,7 @@ async def _worker(self, receive_stream: MemoryObjectReceiveStream) -> None: fn, args, kwargs = item if kwargs: fn = partial(fn, **kwargs) - task_group.start_soon(fn, *args) + task_group.start_soon(fn, *args) # pyright: ignore[reportGeneralTypeIssues] async def __aenter__(self) -> SimpleEventEmitter: self._exit_stack = AsyncExitStack() diff --git a/litestar/handlers/base.py b/litestar/handlers/base.py index da2c53f3b9..aa02b56a4f 100644 --- a/litestar/handlers/base.py +++ b/litestar/handlers/base.py @@ -9,6 +9,7 @@ from litestar.di import Provide from litestar.dto import DTOData from litestar.exceptions import ImproperlyConfiguredException +from litestar.plugins import DIPlugin, PluginRegistry from litestar.serialization import default_deserializer, default_serializer from litestar.types import ( Dependencies, @@ -339,37 +340,60 @@ def resolve_guards(self) -> list[Guard]: return self._resolved_guards + def _get_plugin_registry(self) -> PluginRegistry | None: + from litestar.app import Litestar + + root_owner = self.ownership_layers[0] + if isinstance(root_owner, Litestar): + return root_owner.plugins + return None + def resolve_dependencies(self) -> dict[str, Provide]: """Return all dependencies correlating to handler function's kwargs that exist in the handler's scope.""" + plugin_registry = self._get_plugin_registry() if self._resolved_dependencies is Empty: self._resolved_dependencies = {} - for layer in self.ownership_layers: for key, provider in (layer.dependencies or {}).items(): - if not isinstance(provider, Provide): - provider = Provide(provider) - - self._validate_dependency_is_unique( - dependencies=self._resolved_dependencies, key=key, provider=provider + self._resolved_dependencies[key] = self._resolve_dependency( + key=key, provider=provider, plugin_registry=plugin_registry ) - if not getattr(provider, "parsed_signature", None): - provider.parsed_fn_signature = ParsedSignature.from_fn( - unwrap_partial(provider.dependency), self.resolve_signature_namespace() - ) - - if not getattr(provider, "signature_model", None): - provider.signature_model = SignatureModel.create( - dependency_name_set=self.dependency_name_set, - fn=provider.dependency, - parsed_signature=provider.parsed_fn_signature, - data_dto=self.resolve_data_dto(), - type_decoders=self.resolve_type_decoders(), - ) - - self._resolved_dependencies[key] = provider return self._resolved_dependencies + def _resolve_dependency( + self, key: str, provider: Provide | AnyCallable, plugin_registry: PluginRegistry | None + ) -> Provide: + if not isinstance(provider, Provide): + provider = Provide(provider) + + if self._resolved_dependencies is not Empty: # pragma: no cover + self._validate_dependency_is_unique(dependencies=self._resolved_dependencies, key=key, provider=provider) + + if not getattr(provider, "parsed_fn_signature", None): + dependency = unwrap_partial(provider.dependency) + plugin: DIPlugin | None = None + if plugin_registry: + plugin = next( + (p for p in plugin_registry.di if isinstance(p, DIPlugin) and p.has_typed_init(dependency)), + None, + ) + if plugin: + signature, init_type_hints = plugin.get_typed_init(dependency) + provider.parsed_fn_signature = ParsedSignature.from_signature(signature, init_type_hints) + else: + provider.parsed_fn_signature = ParsedSignature.from_fn(dependency, self.resolve_signature_namespace()) + + if not getattr(provider, "signature_model", None): + provider.signature_model = SignatureModel.create( + dependency_name_set=self.dependency_name_set, + fn=provider.dependency, + parsed_signature=provider.parsed_fn_signature, + data_dto=self.resolve_data_dto(), + type_decoders=self.resolve_type_decoders(), + ) + return provider + def resolve_middleware(self) -> list[Middleware]: """Build the middleware stack for the RouteHandler and return it. diff --git a/litestar/handlers/http_handlers/_utils.py b/litestar/handlers/http_handlers/_utils.py index e840383cba..2df6717be6 100644 --- a/litestar/handlers/http_handlers/_utils.py +++ b/litestar/handlers/http_handlers/_utils.py @@ -15,14 +15,7 @@ from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.connection import Request from litestar.datastructures import Cookie, ResponseHeader - from litestar.types import ( - AfterRequestHookHandler, - ASGIApp, - AsyncAnyCallable, - Method, - ResponseType, - TypeEncodersMap, - ) + from litestar.types import AfterRequestHookHandler, ASGIApp, AsyncAnyCallable, Method, TypeEncodersMap from litestar.typing import FieldDefinition __all__ = ( @@ -42,7 +35,7 @@ def create_data_handler( cookies: frozenset[Cookie], headers: frozenset[ResponseHeader], media_type: str, - response_class: ResponseType, + response_class: type[Response], status_code: int, type_encoders: TypeEncodersMap | None, ) -> AsyncAnyCallable: diff --git a/litestar/handlers/http_handlers/base.py b/litestar/handlers/http_handlers/base.py index b4c3862120..d234a24cca 100644 --- a/litestar/handlers/http_handlers/base.py +++ b/litestar/handlers/http_handlers/base.py @@ -39,7 +39,6 @@ Middleware, ResponseCookies, ResponseHeaders, - ResponseType, TypeEncodersMap, ) from litestar.utils import ensure_async_callable @@ -135,7 +134,7 @@ def __init__( middleware: Sequence[Middleware] | None = None, name: str | None = None, opt: Mapping[str, Any] | None = None, - response_class: ResponseType | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, diff --git a/litestar/handlers/http_handlers/decorators.py b/litestar/handlers/http_handlers/decorators.py index 045534fe07..f78168efef 100644 --- a/litestar/handlers/http_handlers/decorators.py +++ b/litestar/handlers/http_handlers/decorators.py @@ -13,7 +13,7 @@ from .base import HTTPRouteHandler if TYPE_CHECKING: - from typing import Any, Mapping + from typing import Any, Mapping, Sequence from litestar.background_tasks import BackgroundTask, BackgroundTasks from litestar.config.response_cache import CACHE_FOREVER @@ -21,6 +21,7 @@ from litestar.dto import AbstractDTO from litestar.openapi.datastructures import ResponseSpec from litestar.openapi.spec import SecurityRequirement + from litestar.response import Response from litestar.types import ( AfterRequestHookHandler, AfterResponseHookHandler, @@ -33,7 +34,6 @@ Middleware, ResponseCookies, ResponseHeaders, - ResponseType, TypeEncodersMap, ) from litestar.types.callable_types import OperationIDCreator @@ -52,7 +52,7 @@ class delete(HTTPRouteHandler): def __init__( self, - path: str | list[str] | None = None, + path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, @@ -65,12 +65,12 @@ def __init__( dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, - guards: list[Guard] | None = None, + guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, - middleware: list[Middleware] | None = None, + middleware: Sequence[Middleware] | None = None, name: str | None = None, - opt: dict[str, Any] | None = None, - response_class: ResponseType | None = None, + opt: Mapping[str, Any] | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, @@ -85,12 +85,12 @@ def __init__( include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, - raises: list[type[HTTPException]] | None = None, + raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, - responses: dict[int, ResponseSpec] | None = None, - security: list[SecurityRequirement] | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, - tags: list[str] | None = None, + tags: Sequence[str] | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: @@ -216,7 +216,7 @@ class get(HTTPRouteHandler): def __init__( self, - path: str | list[str] | None = None, + path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, @@ -229,12 +229,12 @@ def __init__( dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, - guards: list[Guard] | None = None, + guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, - middleware: list[Middleware] | None = None, + middleware: Sequence[Middleware] | None = None, name: str | None = None, - opt: dict[str, Any] | None = None, - response_class: ResponseType | None = None, + opt: Mapping[str, Any] | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, @@ -249,12 +249,12 @@ def __init__( include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, - raises: list[type[HTTPException]] | None = None, + raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, - responses: dict[int, ResponseSpec] | None = None, - security: list[SecurityRequirement] | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, - tags: list[str] | None = None, + tags: Sequence[str] | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: @@ -381,7 +381,7 @@ class head(HTTPRouteHandler): def __init__( self, - path: str | list[str] | None = None, + path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, @@ -394,12 +394,12 @@ def __init__( dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, - guards: list[Guard] | None = None, + guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, - middleware: list[Middleware] | None = None, + middleware: Sequence[Middleware] | None = None, name: str | None = None, - opt: dict[str, Any] | None = None, - response_class: ResponseType | None = None, + opt: Mapping[str, Any] | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, signature_namespace: Mapping[str, Any] | None = None, @@ -413,13 +413,13 @@ def __init__( include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, - raises: list[type[HTTPException]] | None = None, + raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, - responses: dict[int, ResponseSpec] | None = None, + responses: Mapping[int, ResponseSpec] | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, - security: list[SecurityRequirement] | None = None, + security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, - tags: list[str] | None = None, + tags: Sequence[str] | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: @@ -563,7 +563,7 @@ class patch(HTTPRouteHandler): def __init__( self, - path: str | list[str] | None = None, + path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, @@ -576,12 +576,12 @@ def __init__( dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, - guards: list[Guard] | None = None, + guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, - middleware: list[Middleware] | None = None, + middleware: Sequence[Middleware] | None = None, name: str | None = None, - opt: dict[str, Any] | None = None, - response_class: ResponseType | None = None, + opt: Mapping[str, Any] | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, @@ -596,12 +596,12 @@ def __init__( include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, - raises: list[type[HTTPException]] | None = None, + raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, - responses: dict[int, ResponseSpec] | None = None, - security: list[SecurityRequirement] | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, - tags: list[str] | None = None, + tags: Sequence[str] | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: @@ -727,7 +727,7 @@ class post(HTTPRouteHandler): def __init__( self, - path: str | list[str] | None = None, + path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, @@ -740,12 +740,12 @@ def __init__( dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, - guards: list[Guard] | None = None, + guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, - middleware: list[Middleware] | None = None, + middleware: Sequence[Middleware] | None = None, name: str | None = None, - opt: dict[str, Any] | None = None, - response_class: ResponseType | None = None, + opt: Mapping[str, Any] | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, @@ -760,12 +760,12 @@ def __init__( include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, - raises: list[type[HTTPException]] | None = None, + raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, - responses: dict[int, ResponseSpec] | None = None, - security: list[SecurityRequirement] | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, - tags: list[str] | None = None, + tags: Sequence[str] | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: @@ -891,7 +891,7 @@ class put(HTTPRouteHandler): def __init__( self, - path: str | list[str] | None = None, + path: str | None | Sequence[str] = None, *, after_request: AfterRequestHookHandler | None = None, after_response: AfterResponseHookHandler | None = None, @@ -904,12 +904,12 @@ def __init__( dto: type[AbstractDTO] | None | EmptyType = Empty, etag: ETag | None = None, exception_handlers: ExceptionHandlersMap | None = None, - guards: list[Guard] | None = None, + guards: Sequence[Guard] | None = None, media_type: MediaType | str | None = None, - middleware: list[Middleware] | None = None, + middleware: Sequence[Middleware] | None = None, name: str | None = None, - opt: dict[str, Any] | None = None, - response_class: ResponseType | None = None, + opt: Mapping[str, Any] | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, @@ -924,12 +924,12 @@ def __init__( include_in_schema: bool | EmptyType = Empty, operation_class: type[Operation] = Operation, operation_id: str | OperationIDCreator | None = None, - raises: list[type[HTTPException]] | None = None, + raises: Sequence[type[HTTPException]] | None = None, response_description: str | None = None, - responses: dict[int, ResponseSpec] | None = None, - security: list[SecurityRequirement] | None = None, + responses: Mapping[int, ResponseSpec] | None = None, + security: Sequence[SecurityRequirement] | None = None, summary: str | None = None, - tags: list[str] | None = None, + tags: Sequence[str] | None = None, type_encoders: TypeEncodersMap | None = None, **kwargs: Any, ) -> None: diff --git a/litestar/logging/config.py b/litestar/logging/config.py index 4b5e64d67d..d7733ac39d 100644 --- a/litestar/logging/config.py +++ b/litestar/logging/config.py @@ -9,15 +9,19 @@ from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException from litestar.serialization import encode_json +from litestar.serialization.msgspec_hooks import _msgspec_json_encoder +from litestar.utils.deprecation import deprecated __all__ = ("BaseLoggingConfig", "LoggingConfig", "StructLoggingConfig") if TYPE_CHECKING: + from collections.abc import Iterable from typing import NoReturn # these imports are duplicated on purpose so sphinx autodoc can find and link them from structlog.types import BindableLogger, Processor, WrappedLogger + from structlog.typing import EventDict from litestar.types import Logger, Scope from litestar.types.callable_types import ExceptionLoggingHandler, GetLogger @@ -100,7 +104,7 @@ def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[st if is_struct_logger: logger.exception( - "uncaught exception", + "Uncaught Exception", connection_type=scope["type"], path=scope["path"], traceback="".join(tb[-traceback_line_limit:]), @@ -135,6 +139,11 @@ def configure(self) -> GetLogger: """ raise NotImplementedError("abstract method") + @staticmethod + def set_level(logger: Any, level: int) -> None: + """Provides a consistent interface to call `setLevel` for all loggers.""" + raise NotImplementedError("abstract method") + @dataclass class LoggingConfig(BaseLoggingConfig): @@ -187,6 +196,8 @@ class LoggingConfig(BaseLoggingConfig): Processing of the configuration will be as for any logger, except that the propagate setting will not be applicable. """ + configure_root_logger: bool = field(default=True) + """Should the root logger be configured, defaults to True for ease of configuration.""" log_exceptions: Literal["always", "debug", "never"] = field(default="debug") """Should exceptions be logged, defaults to log exceptions when 'app.debug == True'""" traceback_line_limit: int = field(default=20) @@ -219,27 +230,82 @@ def configure(self) -> GetLogger: if "picologging" in str(encode_json(self.handlers)): try: - import picologging # noqa: F401 + from picologging import config, getLogger except ImportError as e: raise MissingDependencyException("picologging") from e - from picologging import config, getLogger - - values = {k: v for k, v in asdict(self).items() if v is not None and k != "incremental"} + values = { + k: v + for k, v in asdict(self).items() + if v is not None and k not in ("incremental", "configure_root_logger") + } else: from logging import config, getLogger # type: ignore[no-redef, assignment] - values = {k: v for k, v in asdict(self).items() if v is not None} - + values = {k: v for k, v in asdict(self).items() if v is not None and k not in ("configure_root_logger",)} + if not self.configure_root_logger: + values.pop("root") config.dictConfig(values) return cast("Callable[[str], Logger]", getLogger) + @staticmethod + def set_level(logger: Logger, level: int) -> None: + """Provides a consistent interface to call `setLevel` for all loggers.""" + logger.setLevel(level) + + +class StructlogEventFilter: + """Remove keys from the log event. + + Add an instance to the processor chain. + + .. code-block:: python + :caption: Examples + + structlog.configure( + ..., + processors=[ + ..., + EventFilter(["color_message"]), + ..., + ], + ) + + """ + + def __init__(self, filter_keys: Iterable[str]) -> None: + """Initialize the EventFilter. + + Args: + filter_keys: Iterable of string keys to be excluded from the log event. + """ + self.filter_keys = filter_keys + + def __call__(self, _: WrappedLogger, __: str, event_dict: EventDict) -> EventDict: + """Receive the log event, and filter keys. + + Args: + _ (): + __ (): + event_dict (): The data to be logged. + + Returns: + The log event with any key in `self.filter_keys` removed. + """ + for key in self.filter_keys: + event_dict.pop(key, None) + return event_dict + + +def default_json_serializer(value: EventDict, **_: Any) -> bytes: + return _msgspec_json_encoder.encode(value) + -def default_json_serializer(value: Any, default: Callable[[Any], Any] | None = None) -> bytes: - return encode_json(value=value, serializer=default) +def stdlib_json_serializer(value: EventDict, **_: Any) -> str: # pragma: no cover + return _msgspec_json_encoder.encode(value).decode("utf-8") -def default_structlog_processors() -> list[Processor] | None: # pyright: ignore +def default_structlog_processors(as_json: bool = True) -> list[Processor]: # pyright: ignore """Set the default processors for structlog. Returns: @@ -247,34 +313,63 @@ def default_structlog_processors() -> list[Processor] | None: # pyright: ignore """ try: import structlog - + from structlog.dev import RichTracebackFormatter + + if as_json: + return [ + structlog.contextvars.merge_contextvars, + structlog.processors.add_log_level, + structlog.processors.format_exc_info, + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.JSONRenderer(serializer=default_json_serializer), + ] return [ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, - structlog.processors.format_exc_info, structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.JSONRenderer(serializer=default_json_serializer), + structlog.dev.ConsoleRenderer( + colors=True, exception_formatter=RichTracebackFormatter(max_frames=1, show_locals=False, width=80) + ), ] + except ImportError: - return None + return [] -def default_wrapper_class() -> type[BindableLogger] | None: # pyright: ignore - """Set the default wrapper class for structlog. +def default_structlog_standard_lib_processors(as_json: bool = True) -> list[Processor]: # pyright: ignore + """Set the default processors for structlog stdlib. Returns: - An optional wrapper class. + An optional list of processors. """ - try: import structlog - - return structlog.make_filtering_bound_logger(INFO) + from structlog.dev import RichTracebackFormatter + + if as_json: + return [ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.stdlib.ExtraAdder(), + StructlogEventFilter(["color_message"]), + structlog.stdlib.ProcessorFormatter.remove_processors_meta, + structlog.processors.JSONRenderer(serializer=stdlib_json_serializer), + ] + return [ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.stdlib.ExtraAdder(), + StructlogEventFilter(["color_message"]), + structlog.stdlib.ProcessorFormatter.remove_processors_meta, + structlog.dev.ConsoleRenderer( + colors=True, exception_formatter=RichTracebackFormatter(max_frames=1, show_locals=False, width=80) + ), + ] except ImportError: - return None + return [] -def default_logger_factory() -> Callable[..., WrappedLogger] | None: +def default_logger_factory(as_json: bool = True) -> Callable[..., WrappedLogger] | None: """Set the default logger factory for structlog. Returns: @@ -283,7 +378,9 @@ def default_logger_factory() -> Callable[..., WrappedLogger] | None: try: import structlog - return structlog.BytesLoggerFactory() + if as_json: + return structlog.BytesLoggerFactory() + return structlog.WriteLoggerFactory() except ImportError: return None @@ -296,13 +393,18 @@ class StructLoggingConfig(BaseLoggingConfig): - requires ``structlog`` to be installed. """ - processors: list[Processor] | None = field(default_factory=default_structlog_processors) # pyright: ignore + processors: list[Processor] | None = field(default=None) # pyright: ignore """Iterable of structlog logging processors.""" - wrapper_class: type[BindableLogger] | None = field(default_factory=default_wrapper_class) # pyright: ignore + standard_lib_logging_config: LoggingConfig | None = field(default=None) # pyright: ignore + """Optional customized standard logging configuration. + + Use this when you need to modify the standard library outside of the Structlog pre-configured implementation. + """ + wrapper_class: type[BindableLogger] | None = field(default=None) # pyright: ignore """Structlog bindable logger.""" context_class: dict[str, Any] | None = None """Context class (a 'contextvar' context) for the logger.""" - logger_factory: Callable[..., WrappedLogger] | None = field(default_factory=default_logger_factory) + logger_factory: Callable[..., WrappedLogger] | None = field(default=None) # pyright: ignore """Logger factory to use.""" cache_logger_on_first_use: bool = field(default=True) """Whether to cache the logger configuration and reuse.""" @@ -312,12 +414,34 @@ class StructLoggingConfig(BaseLoggingConfig): """Max number of lines to print for exception traceback""" exception_logging_handler: ExceptionLoggingHandler | None = field(default=None) """Handler function for logging exceptions.""" + pretty_print_tty: bool = field(default=True) + """Pretty print log output when run from an interactive terminal.""" def __post_init__(self) -> None: + if self.processors is None: + self.processors = default_structlog_processors(not sys.stderr.isatty() and self.pretty_print_tty) + if self.logger_factory is None: + self.logger_factory = default_logger_factory(not sys.stderr.isatty() and self.pretty_print_tty) if self.log_exceptions != "never" and self.exception_logging_handler is None: self.exception_logging_handler = _default_exception_logging_handler_factory( is_struct_logger=True, traceback_line_limit=self.traceback_line_limit ) + try: + import structlog + + if self.standard_lib_logging_config is None: + self.standard_lib_logging_config = LoggingConfig( + formatters={ + "standard": { + "()": structlog.stdlib.ProcessorFormatter, + "processors": default_structlog_standard_lib_processors( + as_json=not sys.stderr.isatty() and self.pretty_print_tty + ), + } + } + ) + except ImportError: + self.standard_lib_logging_config = LoggingConfig() def configure(self) -> GetLogger: """Return logger with the given configuration. @@ -326,13 +450,11 @@ def configure(self) -> GetLogger: A 'logging.getLogger' like function. """ try: - import structlog # noqa: F401 + import structlog except ImportError as e: raise MissingDependencyException("structlog") from e - from structlog import configure, get_logger - - configure( + structlog.configure( **{ k: v for k, v in asdict(self).items() @@ -342,7 +464,30 @@ def configure(self) -> GetLogger: "log_exceptions", "traceback_line_limit", "exception_logging_handler", + "pretty_print_tty", ) } ) - return get_logger + return structlog.get_logger + + @staticmethod + def set_level(logger: Logger, level: int) -> None: + """Provides a consistent interface to call `setLevel` for all loggers.""" + + try: + import structlog + + structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(level)) + except ImportError: + """""" + return + + +@deprecated(version="2.6.0", removal_in="3.0.0", alternative="`StructLoggingConfig.set_level`") +def default_wrapper_class(log_level: int = INFO) -> type[BindableLogger] | None: # pragma: no cover # pyright: ignore + try: # pragma: no cover + import structlog + + return structlog.make_filtering_bound_logger(log_level) + except ImportError: + return None diff --git a/litestar/middleware/compression/__init__.py b/litestar/middleware/compression/__init__.py new file mode 100644 index 0000000000..0885932dd0 --- /dev/null +++ b/litestar/middleware/compression/__init__.py @@ -0,0 +1,4 @@ +from litestar.middleware.compression.facade import CompressionFacade +from litestar.middleware.compression.middleware import CompressionMiddleware + +__all__ = ("CompressionMiddleware", "CompressionFacade") diff --git a/litestar/middleware/compression/brotli_facade.py b/litestar/middleware/compression/brotli_facade.py new file mode 100644 index 0000000000..3d01950a45 --- /dev/null +++ b/litestar/middleware/compression/brotli_facade.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +from litestar.enums import CompressionEncoding +from litestar.exceptions import MissingDependencyException +from litestar.middleware.compression.facade import CompressionFacade + +try: + from brotli import MODE_FONT, MODE_GENERIC, MODE_TEXT, Compressor +except ImportError as e: + raise MissingDependencyException("brotli") from e + + +if TYPE_CHECKING: + from io import BytesIO + + from litestar.config.compression import CompressionConfig + + +class BrotliCompression(CompressionFacade): + __slots__ = ("compressor", "buffer", "compression_encoding") + + encoding = CompressionEncoding.BROTLI + + def __init__( + self, + buffer: BytesIO, + compression_encoding: Literal[CompressionEncoding.BROTLI] | str, + config: CompressionConfig, + ) -> None: + self.buffer = buffer + self.compression_encoding = compression_encoding + modes: dict[Literal["generic", "text", "font"], int] = { + "text": int(MODE_TEXT), + "font": int(MODE_FONT), + "generic": int(MODE_GENERIC), + } + self.compressor = Compressor( + quality=config.brotli_quality, + mode=modes[config.brotli_mode], + lgwin=config.brotli_lgwin, + lgblock=config.brotli_lgblock, + ) + + def write(self, body: bytes) -> None: + self.buffer.write(self.compressor.process(body)) + self.buffer.write(self.compressor.flush()) + + def close(self) -> None: + self.buffer.write(self.compressor.finish()) diff --git a/litestar/middleware/compression/facade.py b/litestar/middleware/compression/facade.py new file mode 100644 index 0000000000..0074b57419 --- /dev/null +++ b/litestar/middleware/compression/facade.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar, Protocol + +if TYPE_CHECKING: + from io import BytesIO + + from litestar.config.compression import CompressionConfig + from litestar.enums import CompressionEncoding + + +class CompressionFacade(Protocol): + """A unified facade offering a uniform interface for different compression libraries.""" + + encoding: ClassVar[str] + """The encoding of the compression.""" + + def __init__( + self, buffer: BytesIO, compression_encoding: CompressionEncoding | str, config: CompressionConfig + ) -> None: + """Initialize ``CompressionFacade``. + + Args: + buffer: A bytes IO buffer to write the compressed data into. + compression_encoding: The compression encoding used. + config: The app compression config. + """ + ... + + def write(self, body: bytes) -> None: + """Write compressed bytes. + + Args: + body: Message body to process + + Returns: + None + """ + ... + + def close(self) -> None: + """Close the compression stream. + + Returns: + None + """ + ... diff --git a/litestar/middleware/compression/gzip_facade.py b/litestar/middleware/compression/gzip_facade.py new file mode 100644 index 0000000000..b10ef73991 --- /dev/null +++ b/litestar/middleware/compression/gzip_facade.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from gzip import GzipFile +from typing import TYPE_CHECKING, Literal + +from litestar.enums import CompressionEncoding +from litestar.middleware.compression.facade import CompressionFacade + +if TYPE_CHECKING: + from io import BytesIO + + from litestar.config.compression import CompressionConfig + + +class GzipCompression(CompressionFacade): + __slots__ = ("compressor", "buffer", "compression_encoding") + + encoding = CompressionEncoding.GZIP + + def __init__( + self, buffer: BytesIO, compression_encoding: Literal[CompressionEncoding.GZIP] | str, config: CompressionConfig + ) -> None: + self.buffer = buffer + self.compression_encoding = compression_encoding + self.compressor = GzipFile(mode="wb", fileobj=buffer, compresslevel=config.gzip_compress_level) + + def write(self, body: bytes) -> None: + self.compressor.write(body) + self.compressor.flush() + + def close(self) -> None: + self.compressor.close() diff --git a/litestar/middleware/compression.py b/litestar/middleware/compression/middleware.py similarity index 65% rename from litestar/middleware/compression.py rename to litestar/middleware/compression/middleware.py index c5cd860dda..7ea7853b08 100644 --- a/litestar/middleware/compression.py +++ b/litestar/middleware/compression/middleware.py @@ -1,18 +1,18 @@ from __future__ import annotations -from gzip import GzipFile from io import BytesIO from typing import TYPE_CHECKING, Any, Literal 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.middleware.compression.gzip_facade import GzipCompression from litestar.utils.empty import value_or_default from litestar.utils.scope.state import ScopeState if TYPE_CHECKING: from litestar.config.compression import CompressionConfig + from litestar.middleware.compression.facade import CompressionFacade from litestar.types import ( ASGIApp, HTTPResponseStartEvent, @@ -27,76 +27,6 @@ except ImportError: Compressor = Any -__all__ = ("CompressionFacade", "CompressionMiddleware") - - -class CompressionFacade: - """A unified facade offering a uniform interface for different compression libraries.""" - - __slots__ = ("compressor", "buffer", "compression_encoding") - - compressor: GzipFile | Compressor # pyright: ignore - - def __init__(self, buffer: BytesIO, compression_encoding: CompressionEncoding, config: CompressionConfig) -> None: - """Initialize ``CompressionFacade``. - - Args: - buffer: A bytes IO buffer to write the compressed data into. - compression_encoding: The compression encoding used. - config: The app compression config. - """ - self.buffer = buffer - self.compression_encoding = compression_encoding - - if compression_encoding == CompressionEncoding.BROTLI: - try: - import brotli # noqa: F401 - except ImportError as e: - raise MissingDependencyException("brotli") from e - - from brotli import MODE_FONT, MODE_GENERIC, MODE_TEXT, Compressor - - modes: dict[Literal["generic", "text", "font"], int] = { - "text": int(MODE_TEXT), - "font": int(MODE_FONT), - "generic": int(MODE_GENERIC), - } - self.compressor = Compressor( - quality=config.brotli_quality, - mode=modes[config.brotli_mode], - lgwin=config.brotli_lgwin, - lgblock=config.brotli_lgblock, - ) - else: - self.compressor = GzipFile(mode="wb", fileobj=buffer, compresslevel=config.gzip_compress_level) - - def write(self, body: bytes) -> None: - """Write compressed bytes. - - Args: - body: Message body to process - - Returns: - None - """ - - if self.compression_encoding == CompressionEncoding.BROTLI: - self.buffer.write(self.compressor.process(body) + self.compressor.flush()) # type: ignore - else: - self.compressor.write(body) - self.compressor.flush() - - def close(self) -> None: - """Close the compression stream. - - Returns: - None - """ - if self.compression_encoding == CompressionEncoding.BROTLI: - self.buffer.write(self.compressor.finish()) # type: ignore - else: - self.compressor.close() - class CompressionMiddleware(AbstractMiddleware): """Compression Middleware Wrapper. @@ -128,20 +58,19 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: None """ accept_encoding = Headers.from_scope(scope).get("accept-encoding", "") + config = self.config - if CompressionEncoding.BROTLI in accept_encoding and self.config.backend == "brotli": + if config.compression_facade.encoding in accept_encoding: await self.app( scope, receive, self.create_compression_send_wrapper( - send=send, compression_encoding=CompressionEncoding.BROTLI, scope=scope + send=send, compression_encoding=config.compression_facade.encoding, scope=scope ), ) return - if CompressionEncoding.GZIP in accept_encoding and ( - self.config.backend == "gzip" or self.config.brotli_gzip_fallback - ): + if config.gzip_fallback and CompressionEncoding.GZIP in accept_encoding: await self.app( scope, receive, @@ -156,7 +85,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: def create_compression_send_wrapper( self, send: Send, - compression_encoding: Literal[CompressionEncoding.BROTLI, CompressionEncoding.GZIP], + compression_encoding: Literal[CompressionEncoding.BROTLI, CompressionEncoding.GZIP] | str, scope: Scope, ) -> Send: """Wrap ``send`` to handle brotli compression. @@ -170,13 +99,20 @@ def create_compression_send_wrapper( An ASGI send function. """ bytes_buffer = BytesIO() - facade = CompressionFacade(buffer=bytes_buffer, compression_encoding=compression_encoding, config=self.config) + + facade: CompressionFacade + # We can't use `self.config.compression_facade` directly if the compression is `gzip` since + # it may be being used as a fallback. + if compression_encoding == CompressionEncoding.GZIP: + facade = GzipCompression(buffer=bytes_buffer, compression_encoding=compression_encoding, config=self.config) + else: + facade = self.config.compression_facade( + buffer=bytes_buffer, compression_encoding=compression_encoding, config=self.config + ) initial_message: HTTPResponseStartEvent | None = None started = False - _own_encoding = compression_encoding.encode("latin-1") - connection_state = ScopeState.from_scope(scope) async def send_wrapper(message: Message) -> None: diff --git a/litestar/middleware/cors.py b/litestar/middleware/cors.py index 8d48b73af3..6c4de31f8f 100644 --- a/litestar/middleware/cors.py +++ b/litestar/middleware/cors.py @@ -70,6 +70,15 @@ async def wrapped_send(message: Message) -> None: headers["Access-Control-Allow-Origin"] = origin headers["Vary"] = "Origin" + # We don't want to overwrite this for preflight requests. + allow_headers = headers.get("Access-Control-Allow-Headers") + if not allow_headers and self.config.allow_headers: + headers["Access-Control-Allow-Headers"] = ", ".join(sorted(set(self.config.allow_headers))) + + allow_methods = headers.get("Access-Control-Allow-Methods") + if not allow_methods and self.config.allow_methods: + headers["Access-Control-Allow-Methods"] = ", ".join(sorted(set(self.config.allow_methods))) + await send(message) return wrapped_send diff --git a/litestar/middleware/exceptions/middleware.py b/litestar/middleware/exceptions/middleware.py index 4828cfd8d5..f3ff1572b6 100644 --- a/litestar/middleware/exceptions/middleware.py +++ b/litestar/middleware/exceptions/middleware.py @@ -9,7 +9,7 @@ from litestar.datastructures import Headers from litestar.enums import MediaType, ScopeType -from litestar.exceptions import WebSocketException +from litestar.exceptions import HTTPException, LitestarException, WebSocketException from litestar.middleware.cors import CORSMiddleware from litestar.middleware.exceptions._debug_response import _get_type_encoders_for_request, create_debug_response from litestar.serialization import encode_json @@ -20,6 +20,8 @@ if TYPE_CHECKING: + from starlette.exceptions import HTTPException as StarletteHTTPException + from litestar import Response from litestar.app import Litestar from litestar.connection import Request @@ -58,15 +60,16 @@ def get_exception_handler(exception_handlers: ExceptionHandlersMap, exc: Excepti if not exception_handlers: return None - status_code: int | None = getattr(exc, "status_code", None) - if status_code and (exception_handler := exception_handlers.get(status_code)): - return exception_handler + default_handler: ExceptionHandler | None = None + if isinstance(exc, HTTPException): + if exception_handler := exception_handlers.get(exc.status_code): + return exception_handler + else: + default_handler = exception_handlers.get(HTTP_500_INTERNAL_SERVER_ERROR) return next( (exception_handlers[cast("Type[Exception]", cls)] for cls in getmro(type(exc)) if cls in exception_handlers), - exception_handlers[HTTP_500_INTERNAL_SERVER_ERROR] - if not hasattr(exc, "status_code") and HTTP_500_INTERNAL_SERVER_ERROR in exception_handlers - else None, + default_handler, ) @@ -107,6 +110,17 @@ def to_response(self, request: Request | None = None) -> Response: ) +def _starlette_exception_handler(request: Request[Any, Any, Any], exc: StarletteHTTPException) -> Response: + return create_exception_response( + request=request, + exc=HTTPException( + detail=exc.detail, + status_code=exc.status_code, + headers=exc.headers, + ), + ) + + def create_exception_response(request: Request[Any, Any, Any], exc: Exception) -> Response: """Construct a response from an exception. @@ -122,11 +136,23 @@ def create_exception_response(request: Request[Any, Any, Any], exc: Exception) - Returns: Response: HTTP response constructed from exception details. """ - status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR) - if status_code == HTTP_500_INTERNAL_SERVER_ERROR: - detail = "Internal Server Error" + headers: dict[str, Any] | None + extra: dict[str, Any] | list | None + + if isinstance(exc, HTTPException): + status_code = exc.status_code + headers = exc.headers + extra = exc.extra else: - detail = getattr(exc, "detail", repr(exc)) + status_code = HTTP_500_INTERNAL_SERVER_ERROR + headers = None + extra = None + + detail = ( + exc.detail + if isinstance(exc, LitestarException) and status_code != HTTP_500_INTERNAL_SERVER_ERROR + else "Internal Server Error" + ) try: media_type = request.route_handler.media_type @@ -136,8 +162,8 @@ def create_exception_response(request: Request[Any, Any, Any], exc: Exception) - content = ExceptionResponseContent( status_code=status_code, detail=detail, - headers=getattr(exc, "headers", None), - extra=getattr(exc, "extra", None), + headers=headers, + extra=extra, media_type=media_type, ) return content.to_response(request=request) @@ -246,12 +272,13 @@ async def handle_websocket_exception(send: Send, exc: Exception) -> None: Returns: None. """ + code = 4000 + HTTP_500_INTERNAL_SERVER_ERROR + reason = "Internal Server Error" if isinstance(exc, WebSocketException): code = exc.code reason = exc.detail - else: - code = 4000 + getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR) - reason = getattr(exc, "detail", repr(exc)) + elif isinstance(exc, LitestarException): + reason = exc.detail event: WebSocketCloseEvent = {"type": "websocket.close", "code": code, "reason": reason} await send(event) @@ -266,7 +293,7 @@ def default_http_exception_handler(self, request: Request, exc: Exception) -> Re Returns: An HTTP response. """ - status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR) + status_code = exc.status_code if isinstance(exc, HTTPException) else HTTP_500_INTERNAL_SERVER_ERROR if status_code == HTTP_500_INTERNAL_SERVER_ERROR and self._get_debug_scope(request.scope): return create_debug_response(request=request, exc=exc) return create_exception_response(request=request, exc=exc) diff --git a/litestar/middleware/logging.py b/litestar/middleware/logging.py index 007692cb6a..dc827e303e 100644 --- a/litestar/middleware/logging.py +++ b/litestar/middleware/logging.py @@ -1,7 +1,6 @@ from __future__ import annotations from dataclasses import dataclass, field -from inspect import isawaitable from typing import TYPE_CHECKING, Any, Iterable from litestar.constants import ( @@ -81,6 +80,7 @@ def __init__(self, app: ASGIApp, config: LoggingMiddlewareConfig) -> None: obfuscate_headers=self.config.request_headers_to_obfuscate, parse_body=self.is_struct_logger, parse_query=self.is_struct_logger, + skip_parse_malformed_body=True, ) self.response_extractor = ResponseDataExtractor( extract_body="body" in self.config.response_log_fields, @@ -172,12 +172,11 @@ async def extract_request_data(self, request: Request) -> dict[str, Any]: data: dict[str, Any] = {"message": self.config.request_log_message} serializer = get_serializer_from_scope(request.scope) - extracted_data = self.request_extractor(connection=request) + + extracted_data = await self.request_extractor.extract(connection=request, fields=self.config.request_log_fields) + for key in self.config.request_log_fields: - value = extracted_data.get(key) - if isawaitable(value): - value = await value - data[key] = self._serialize_value(serializer, value) + data[key] = self._serialize_value(serializer, extracted_data.get(key)) return data def extract_response_data(self, scope: Scope) -> dict[str, Any]: @@ -344,10 +343,12 @@ def middleware(self) -> DefineMiddleware: logging_middleware_config = LoggingMiddlewareConfig() + @get("/") def my_handler(request: Request) -> None: ... + app = Litestar( route_handlers=[my_handler], logging_config=logging_config, diff --git a/litestar/middleware/rate_limit.py b/litestar/middleware/rate_limit.py index 4b2c2bf4ce..8517581ccd 100644 --- a/litestar/middleware/rate_limit.py +++ b/litestar/middleware/rate_limit.py @@ -257,10 +257,12 @@ def middleware(self) -> DefineMiddleware: # limit to 10 requests per minute, excluding the schema path throttle_config = RateLimitConfig(rate_limit=("minute", 10), exclude=["/schema"]) + @get("/") def my_handler(request: Request) -> None: ... + app = Litestar(route_handlers=[my_handler], middleware=[throttle_config.middleware]) Returns: diff --git a/litestar/middleware/session/base.py b/litestar/middleware/session/base.py index aed3196dde..b7e2e1586a 100644 --- a/litestar/middleware/session/base.py +++ b/litestar/middleware/session/base.py @@ -85,7 +85,8 @@ def middleware(self) -> DefineMiddleware: @get("/") - def my_handler(request: Request) -> None: ... + def my_handler(request: Request) -> None: + ... app = Litestar(route_handlers=[my_handler], middleware=[session_config.middleware]) diff --git a/litestar/middleware/session/client_side.py b/litestar/middleware/session/client_side.py index f8f230edb0..cce502f64d 100644 --- a/litestar/middleware/session/client_side.py +++ b/litestar/middleware/session/client_side.py @@ -108,7 +108,10 @@ def get_cookie_keys(self, connection: ASGIConnection) -> list[str]: return sorted(key for key in connection.cookies if self.cookie_re.fullmatch(key)) def _create_session_cookies(self, data: list[bytes], cookie_params: dict[str, Any] | None = None) -> list[Cookie]: - """Create a list of cookies containing the session data.""" + """Create a list of cookies containing the session data. + If the data is split into multiple cookies, the key will be of the format ``session-{segment number}``, + however if only one cookie is needed, the key will be ``session``. + """ if cookie_params is None: cookie_params = dict( extract_dataclass_items( @@ -117,6 +120,16 @@ def _create_session_cookies(self, data: list[bytes], cookie_params: dict[str, An include={f for f in Cookie.__dict__ if f not in ("key", "secret")}, ) ) + + if len(data) == 1: + return [ + Cookie( + value=data[0].decode("utf-8"), + key=self.config.key, + **cookie_params, + ) + ] + return [ Cookie( value=datum.decode("utf-8"), diff --git a/litestar/openapi/datastructures.py b/litestar/openapi/datastructures.py index eceec2c5ee..93d0cf4d3f 100644 --- a/litestar/openapi/datastructures.py +++ b/litestar/openapi/datastructures.py @@ -16,7 +16,7 @@ class ResponseSpec: """Container type of additional responses.""" - data_container: DataContainerType + data_container: DataContainerType | None """A model that describes the content of the response.""" generate_examples: bool = field(default=True) """Generate examples for the response content.""" diff --git a/litestar/plugins/__init__.py b/litestar/plugins/__init__.py index 6f71b78b4a..f09310436d 100644 --- a/litestar/plugins/__init__.py +++ b/litestar/plugins/__init__.py @@ -1,6 +1,7 @@ from litestar.plugins.base import ( CLIPlugin, CLIPluginProtocol, + DIPlugin, InitPluginProtocol, OpenAPISchemaPlugin, OpenAPISchemaPluginProtocol, @@ -11,6 +12,7 @@ __all__ = ( "SerializationPluginProtocol", + "DIPlugin", "CLIPlugin", "InitPluginProtocol", "OpenAPISchemaPluginProtocol", diff --git a/litestar/plugins/base.py b/litestar/plugins/base.py index 254c94ab55..afc571efe7 100644 --- a/litestar/plugins/base.py +++ b/litestar/plugins/base.py @@ -1,9 +1,12 @@ from __future__ import annotations +import abc from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Iterator, Protocol, TypeVar, Union, cast, runtime_checkable if TYPE_CHECKING: + from inspect import Signature + from click import Group from litestar._openapi.schema_generation import SchemaCreator @@ -23,6 +26,7 @@ "CLIPlugin", "CLIPluginProtocol", "PluginRegistry", + "DIPlugin", ) @@ -154,6 +158,26 @@ def create_dto_for_type(self, field_definition: FieldDefinition) -> type[Abstrac raise NotImplementedError() +class DIPlugin(abc.ABC): + """Extend dependency injection""" + + @abc.abstractmethod + def has_typed_init(self, type_: Any) -> bool: + """Return ``True`` if ``type_`` has type information available for its + :func:`__init__` method that cannot be extracted from this method's type + annotations (e.g. a Pydantic BaseModel subclass), and + :meth:`DIPlugin.get_typed_init` supports extraction of these annotations. + """ + ... + + @abc.abstractmethod + def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: + r"""Return signature and type information about the ``type_``\ s :func:`__init__` + method. + """ + ... + + @runtime_checkable class OpenAPISchemaPluginProtocol(Protocol): """Plugin protocol to extend the support of OpenAPI schema generation for non-library types.""" @@ -241,6 +265,7 @@ def is_constrained_field(field_definition: FieldDefinition) -> bool: OpenAPISchemaPluginProtocol, ReceiveRoutePlugin, SerializationPluginProtocol, + DIPlugin, ] PluginT = TypeVar("PluginT", bound=PluginProtocol) @@ -250,9 +275,10 @@ class PluginRegistry: __slots__ = { "init": "Plugins that implement the InitPluginProtocol", "openapi": "Plugins that implement the OpenAPISchemaPluginProtocol", - "receive_route": "ReceiveRoutePlugin types", + "receive_route": "ReceiveRoutePlugin instances", "serialization": "Plugins that implement the SerializationPluginProtocol", "cli": "Plugins that implement the CLIPluginProtocol", + "di": "DIPlugin instances", "_plugins_by_type": None, "_plugins": None, "_get_plugins_of_type": None, @@ -266,12 +292,25 @@ def __init__(self, plugins: list[PluginProtocol]) -> None: self.receive_route = tuple(p for p in plugins if isinstance(p, ReceiveRoutePlugin)) self.serialization = tuple(p for p in plugins if isinstance(p, SerializationPluginProtocol)) self.cli = tuple(p for p in plugins if isinstance(p, CLIPluginProtocol)) + self.di = tuple(p for p in plugins if isinstance(p, DIPlugin)) - def get(self, type_: type[PluginT]) -> PluginT: + def get(self, type_: type[PluginT] | str) -> PluginT: """Return the registered plugin of ``type_``. This should be used with subclasses of the plugin protocols. """ + if isinstance(type_, str): + for plugin in self._plugins: + _name = plugin.__class__.__name__ + _module = plugin.__class__.__module__ + _qualname = ( + f"{_module}.{plugin.__class__.__qualname__}" + if _module is not None and _module != "__builtin__" + else plugin.__class__.__qualname__ + ) + if type_ in {_name, _qualname}: + return cast(PluginT, plugin) + raise KeyError(f"No plugin of type {type_!r} registered") try: return cast(PluginT, self._plugins_by_type[type_]) # type: ignore[index] except KeyError as e: diff --git a/litestar/plugins/core.py b/litestar/plugins/core.py new file mode 100644 index 0000000000..d25d6d661b --- /dev/null +++ b/litestar/plugins/core.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import inspect +from inspect import Signature +from typing import Any + +import msgspec + +from litestar.plugins import DIPlugin + +__all__ = ("MsgspecDIPlugin",) + + +class MsgspecDIPlugin(DIPlugin): + def has_typed_init(self, type_: Any) -> bool: + return type(type_) is type(msgspec.Struct) # noqa: E721 + + def get_typed_init(self, type_: Any) -> tuple[Signature, dict[str, Any]]: + parameters = [] + type_hints = {} + for field_info in msgspec.structs.fields(type_): + type_hints[field_info.name] = field_info.type + parameters.append( + inspect.Parameter( + name=field_info.name, + kind=inspect.Parameter.KEYWORD_ONLY, + annotation=field_info.type, + default=field_info.default, + ) + ) + return inspect.Signature(parameters), type_hints diff --git a/litestar/plugins/structlog.py b/litestar/plugins/structlog.py new file mode 100644 index 0000000000..fafa3dde8f --- /dev/null +++ b/litestar/plugins/structlog.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from litestar.cli._utils import console +from litestar.logging.config import StructLoggingConfig +from litestar.middleware.logging import LoggingMiddlewareConfig +from litestar.plugins import InitPluginProtocol + +if TYPE_CHECKING: + from litestar.config.app import AppConfig + + +@dataclass +class StructlogConfig: + structlog_logging_config: StructLoggingConfig = field(default_factory=StructLoggingConfig) + """Structlog Logging configuration for Litestar. See ``litestar.logging.config.StructLoggingConfig``` for details.""" + middleware_logging_config: LoggingMiddlewareConfig = field(default_factory=LoggingMiddlewareConfig) + """Middleware logging config.""" + enable_middleware_logging: bool = True + """Enable request logging.""" + + +class StructlogPlugin(InitPluginProtocol): + """Structlog Plugin.""" + + __slots__ = ("_config",) + + def __init__(self, config: StructlogConfig | None = None) -> None: + if config is None: + config = StructlogConfig() + self._config = config + super().__init__() + + def on_app_init(self, app_config: AppConfig) -> AppConfig: + """Structlog Plugin + + Args: + app_config: The :class:`AppConfig ` instance. + + Returns: + The app config object. + """ + if app_config.logging_config is not None and isinstance(app_config.logging_config, StructLoggingConfig): + console.print( + "[red dim]* Found pre-configured `StructLoggingConfig` on the `app` instance. Skipping configuration.[/]", + ) + else: + app_config.logging_config = self._config.structlog_logging_config + app_config.logging_config.configure() + if self._config.structlog_logging_config.standard_lib_logging_config is not None: # pragma: no cover + self._config.structlog_logging_config.standard_lib_logging_config.configure() # pragma: no cover + if self._config.enable_middleware_logging: + app_config.middleware.append(self._config.middleware_logging_config.middleware) + return app_config # pragma: no cover diff --git a/litestar/repository/exceptions.py b/litestar/repository/exceptions.py index 2490aa9f3e..8dad182f24 100644 --- a/litestar/repository/exceptions.py +++ b/litestar/repository/exceptions.py @@ -1,5 +1,6 @@ try: - from advanced_alchemy.exceptions import ConflictError, NotFoundError, RepositoryError + from advanced_alchemy.exceptions import IntegrityError as ConflictError + from advanced_alchemy.exceptions import NotFoundError, RepositoryError except ImportError: # pragma: no cover from ._exceptions import ConflictError, NotFoundError, RepositoryError # type: ignore[assignment] diff --git a/litestar/response/__init__.py b/litestar/response/__init__.py index 8e49cb5d64..c655758dfe 100644 --- a/litestar/response/__init__.py +++ b/litestar/response/__init__.py @@ -1,7 +1,7 @@ from .base import Response from .file import File from .redirect import Redirect -from .sse import ServerSentEvent +from .sse import ServerSentEvent, ServerSentEventMessage from .streaming import Stream from .template import Template @@ -10,6 +10,7 @@ "Redirect", "Response", "ServerSentEvent", + "ServerSentEventMessage", "Stream", "Template", ) diff --git a/litestar/response/sse.py b/litestar/response/sse.py index 253a2fde34..48a9192b98 100644 --- a/litestar/response/sse.py +++ b/litestar/response/sse.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from litestar.background_tasks import BackgroundTask, BackgroundTasks - from litestar.types import ResponseCookies, ResponseHeaders, StreamType + from litestar.types import ResponseCookies, ResponseHeaders, SSEData, StreamType _LINE_BREAK_RE = re.compile(r"\r\n|\r|\n") DEFAULT_SEPARATOR = "\r\n" @@ -21,11 +21,11 @@ class _ServerSentEventIterator(AsyncIteratorWrapper[bytes]): __slots__ = ("content_async_iterator", "event_id", "event_type", "retry_duration", "comment_message") - content_async_iterator: AsyncIteratorWrapper[bytes | str] | AsyncIterable[str | bytes] | AsyncIterator[str | bytes] + content_async_iterator: AsyncIterable[SSEData] def __init__( self, - content: str | bytes | StreamType[str | bytes], + content: str | bytes | StreamType[SSEData], event_type: str | None = None, event_id: int | str | None = None, retry_duration: int | None = None, @@ -56,7 +56,7 @@ def __init__( if isinstance(content, (str, bytes)): self.content_async_iterator = AsyncIteratorWrapper([content]) elif isinstance(content, (Iterable, Iterator)): - self.content_async_iterator = AsyncIteratorWrapper(content) # type: ignore[arg-type] + self.content_async_iterator = AsyncIteratorWrapper(content) elif isinstance(content, (AsyncIterable, AsyncIterator, AsyncIteratorWrapper)): self.content_async_iterator = content else: @@ -85,14 +85,13 @@ async def _async_generator(self) -> AsyncGenerator[bytes, None]: yield await sync_to_thread(self._call_next) except ValueError: async for value in self.content_async_iterator: - data = self.ensure_bytes(value, DEFAULT_SEPARATOR) - yield data + yield self.ensure_bytes(value, DEFAULT_SEPARATOR) break @dataclass class ServerSentEventMessage: - data: str | int | bytes | None = None + data: str | int | bytes | None = "" event: str | None = None id: int | str | None = None retry: int | None = None @@ -131,7 +130,7 @@ def encode(self) -> bytes: class ServerSentEvent(Stream): def __init__( self, - content: str | bytes | StreamType[str | bytes], + content: str | bytes | StreamType[SSEData], *, background: BackgroundTask | BackgroundTasks | None = None, cookies: ResponseCookies | None = None, diff --git a/litestar/router.py b/litestar/router.py index 772b12a620..d65f39d47b 100644 --- a/litestar/router.py +++ b/litestar/router.py @@ -23,6 +23,7 @@ from litestar.datastructures import CacheControlHeader, ETag from litestar.dto import AbstractDTO from litestar.openapi.spec import SecurityRequirement + from litestar.response import Response from litestar.routes import BaseRoute from litestar.types import ( AfterRequestHookHandler, @@ -34,7 +35,6 @@ Middleware, ParametersMap, ResponseCookies, - ResponseType, RouteHandlerMapItem, RouteHandlerType, TypeEncodersMap, @@ -95,7 +95,7 @@ def __init__( middleware: Sequence[Middleware] | None = None, opt: Mapping[str, Any] | None = None, parameters: ParametersMap | None = None, - response_class: ResponseType | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, diff --git a/litestar/security/session_auth/auth.py b/litestar/security/session_auth/auth.py index 7a5c5422fd..3bdf78a9ba 100644 --- a/litestar/security/session_auth/auth.py +++ b/litestar/security/session_auth/auth.py @@ -86,7 +86,8 @@ async def retrieve_user_from_session(session: dict[str, Any]) -> Any: @get("/") - def my_handler(request: Request) -> None: ... + def my_handler(request: Request) -> None: + ... app = Litestar(route_handlers=[my_handler], middleware=[session_auth_config.middleware]) diff --git a/litestar/static_files/__init__.py b/litestar/static_files/__init__.py index 22bab52f8f..3cd45945f7 100644 --- a/litestar/static_files/__init__.py +++ b/litestar/static_files/__init__.py @@ -1,4 +1,4 @@ from litestar.static_files.base import StaticFiles -from litestar.static_files.config import StaticFilesConfig +from litestar.static_files.config import StaticFilesConfig, create_static_files_router -__all__ = ("StaticFiles", "StaticFilesConfig") +__all__ = ("StaticFiles", "StaticFilesConfig", "create_static_files_router") diff --git a/litestar/static_files/base.py b/litestar/static_files/base.py index 889d7020d0..9827697933 100644 --- a/litestar/static_files/base.py +++ b/litestar/static_files/base.py @@ -22,7 +22,7 @@ class StaticFiles: """ASGI App that handles file sending.""" - __slots__ = ("is_html_mode", "directories", "adapter", "send_as_attachment") + __slots__ = ("is_html_mode", "directories", "adapter", "send_as_attachment", "headers") def __init__( self, @@ -30,6 +30,8 @@ def __init__( directories: Sequence[PathType], file_system: FileSystemProtocol, send_as_attachment: bool = False, + resolve_symlinks: bool = True, + headers: dict[str, str] | None = None, ) -> None: """Initialize the Application. @@ -39,11 +41,14 @@ def __init__( file_system: The file_system spec to use for serving files. send_as_attachment: Whether to send the file with a ``content-disposition`` header of ``attachment`` or ``inline`` + resolve_symlinks: Resolve symlinks to the directories + headers: Headers that will be sent with every response. """ self.adapter = FileSystemAdapter(file_system) - self.directories = tuple(Path(p).resolve() for p in directories) + self.directories = tuple(Path(p).resolve() if resolve_symlinks else Path(p) for p in directories) self.is_html_mode = is_html_mode self.send_as_attachment = send_as_attachment + self.headers = headers async def get_fs_info( self, directories: Sequence[PathType], file_path: PathType @@ -82,7 +87,11 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != ScopeType.HTTP or scope["method"] not in {"GET", "HEAD"}: raise MethodNotAllowedException() - split_path = scope["path"].split("/") + res = await self.handle(path=scope["path"], is_head_response=scope["method"] == "HEAD") + await res(scope=scope, receive=receive, send=send) + + async def handle(self, path: str, is_head_response: bool) -> ASGIFileResponse: + split_path = path.split("/") filename = split_path[-1] joined_path = Path(*split_path) resolved_path, fs_info = await self.get_fs_info(directories=self.directories, file_path=joined_path) @@ -98,15 +107,15 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ) if fs_info and fs_info["type"] == "file": - await ASGIFileResponse( + return ASGIFileResponse( file_path=resolved_path or joined_path, file_info=fs_info, file_system=self.adapter.file_system, filename=filename, content_disposition_type=content_disposition_type, - is_head_response=scope["method"] == "HEAD", - )(scope, receive, send) - return + is_head_response=is_head_response, + headers=self.headers, + ) if self.is_html_mode: # for some reason coverage doesn't catch these two lines @@ -116,16 +125,16 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ) if fs_info and fs_info["type"] == "file": - await ASGIFileResponse( + return ASGIFileResponse( file_path=resolved_path or joined_path, file_info=fs_info, file_system=self.adapter.file_system, filename=filename, status_code=HTTP_404_NOT_FOUND, content_disposition_type=content_disposition_type, - is_head_response=scope["method"] == "HEAD", - )(scope, receive, send) - return + is_head_response=is_head_response, + headers=self.headers, + ) raise NotFoundException( f"no file or directory match the path {resolved_path or joined_path} was found" diff --git a/litestar/static_files/config.py b/litestar/static_files/config.py index 9cca30fa79..22b6620aa4 100644 --- a/litestar/static_files/config.py +++ b/litestar/static_files/config.py @@ -1,20 +1,34 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from pathlib import PurePath # noqa: TCH003 +from typing import TYPE_CHECKING, Any, Sequence from litestar.exceptions import ImproperlyConfiguredException from litestar.file_system import BaseLocalFileSystem -from litestar.handlers import asgi +from litestar.handlers import asgi, get, head +from litestar.response.file import ASGIFileResponse # noqa: TCH001 +from litestar.router import Router from litestar.static_files.base import StaticFiles -from litestar.utils import normalize_path +from litestar.types import Empty +from litestar.utils import normalize_path, warn_deprecation __all__ = ("StaticFilesConfig",) - if TYPE_CHECKING: + from litestar.datastructures import CacheControlHeader from litestar.handlers.asgi_handlers import ASGIRouteHandler - from litestar.types import ExceptionHandlersMap, Guard, PathType + from litestar.openapi.spec import SecurityRequirement + from litestar.types import ( + AfterRequestHookHandler, + AfterResponseHookHandler, + BeforeRequestHookHandler, + EmptyType, + ExceptionHandlersMap, + Guard, + Middleware, + PathType, + ) @dataclass @@ -58,21 +72,17 @@ class StaticFilesConfig: """Whether to send the file as an attachment.""" def __post_init__(self) -> None: - if not self.path: - raise ImproperlyConfiguredException("path must be a non-zero length string,") - - if not self.directories or not any(bool(d) for d in self.directories): - raise ImproperlyConfiguredException("directories must include at least one path.") - - if "{" in self.path: - raise ImproperlyConfiguredException("path parameters are not supported for static files") - - if not ( - callable(getattr(self.file_system, "info", None)) and callable(getattr(self.file_system, "open", None)) - ): - raise ImproperlyConfiguredException("file_system must adhere to the FileSystemProtocol type") - + _validate_config(path=self.path, directories=self.directories, file_system=self.file_system) self.path = normalize_path(self.path) + warn_deprecation( + "2.6.0", + kind="class", + deprecated_name="StaticFilesConfig", + removal_in="3.0", + alternative="create_static_files_router", + info='Replace static_files_config=[StaticFilesConfig(path="/static", directories=["assets"])] with ' + 'route_handlers=[..., create_static_files_router(path="/static", directories=["assets"])]', + ) def to_static_files_app(self) -> ASGIRouteHandler: """Return an ASGI app serving static files based on the config. @@ -94,3 +104,121 @@ def to_static_files_app(self) -> ASGIRouteHandler: guards=self.guards, exception_handlers=self.exception_handlers, )(static_files) + + +def create_static_files_router( + path: str, + directories: list[PathType], + file_system: Any = None, + send_as_attachment: bool = False, + html_mode: bool = False, + name: str = "static", + after_request: AfterRequestHookHandler | None = None, + after_response: AfterResponseHookHandler | None = None, + before_request: BeforeRequestHookHandler | None = None, + cache_control: CacheControlHeader | None = None, + exception_handlers: ExceptionHandlersMap | None = None, + guards: list[Guard] | None = None, + include_in_schema: bool | EmptyType = Empty, + middleware: Sequence[Middleware] | None = None, + opt: dict[str, Any] | None = None, + security: Sequence[SecurityRequirement] | None = None, + tags: Sequence[str] | None = None, + router_class: type[Router] = Router, + resolve_symlinks: bool = True, +) -> Router: + """Create a router with handlers to serve static files. + + Args: + path: Path to serve static files under + directories: Directories to serve static files from + file_system: A *file system* implementing + :class:`~litestar.types.FileSystemProtocol`. + `fsspec `_ can be passed + here as well + send_as_attachment: Whether to send the file as an attachment + html_mode: When in HTML: + - Serve an ``index.html`` file from ``/`` + - Serve ``404.html`` when a file could not be found + name: Name to pass to the generated handlers + after_request: ``after_request`` handlers passed to the router + after_response: ``after_response`` handlers passed to the router + before_request: ``before_request`` handlers passed to the router + cache_control: ``cache_control`` passed to the router + exception_handlers: Exception handlers passed to the router + guards: Guards passed to the router + include_in_schema: Include the routes / router in the OpenAPI schema + middleware: Middlewares passed to the router + opt: Opts passed to the router + security: Security options passed to the router + tags: ``tags`` passed to the router + router_class: The class used to construct a router from + resolve_symlinks: Resolve symlinks of ``directories`` + """ + + if file_system is None: + file_system = BaseLocalFileSystem() + + _validate_config(path=path, directories=directories, file_system=file_system) + path = normalize_path(path) + + headers = None + if cache_control: + headers = {cache_control.HEADER_NAME: cache_control.to_header()} + + static_files = StaticFiles( + is_html_mode=html_mode, + directories=directories, + file_system=file_system, + send_as_attachment=send_as_attachment, + resolve_symlinks=resolve_symlinks, + headers=headers, + ) + + @get("{file_path:path}", name=name) + async def get_handler(file_path: PurePath) -> ASGIFileResponse: + return await static_files.handle(path=file_path.as_posix(), is_head_response=False) + + @head("/{file_path:path}", name=f"{name}/head") + async def head_handler(file_path: PurePath) -> ASGIFileResponse: + return await static_files.handle(path=file_path.as_posix(), is_head_response=True) + + handlers = [get_handler, head_handler] + + if html_mode: + + @get("/", name=f"{name}/index") + async def index_handler() -> ASGIFileResponse: + return await static_files.handle(path="/", is_head_response=False) + + handlers.append(index_handler) + + return router_class( + after_request=after_request, + after_response=after_response, + before_request=before_request, + cache_control=cache_control, + exception_handlers=exception_handlers, + guards=guards, + include_in_schema=include_in_schema, + middleware=middleware, + opt=opt, + path=path, + route_handlers=handlers, + security=security, + tags=tags, + ) + + +def _validate_config(path: str, directories: list[PathType], file_system: Any) -> None: + if not path: + raise ImproperlyConfiguredException("path must be a non-zero length string,") + + if not directories or not any(bool(d) for d in directories): + raise ImproperlyConfiguredException("directories must include at least one path.") + + if "{" in path: + raise ImproperlyConfiguredException("path parameters are not supported for static files") + + if not (callable(getattr(file_system, "info", None)) and callable(getattr(file_system, "open", None))): + raise ImproperlyConfiguredException("file_system must adhere to the FileSystemProtocol type") diff --git a/litestar/stores/base.py b/litestar/stores/base.py index 585a2cbf5b..34aa514fca 100644 --- a/litestar/stores/base.py +++ b/litestar/stores/base.py @@ -9,6 +9,8 @@ from msgspec.msgpack import encode as msgpack_encode if TYPE_CHECKING: + from types import TracebackType + from typing_extensions import Self @@ -76,6 +78,17 @@ async def expires_in(self, key: str) -> int | None: """ raise NotImplementedError + async def __aenter__(self) -> None: # noqa: B027 + pass + + async def __aexit__( # noqa: B027 + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + pass + class NamespacedStore(Store): """A subclass of :class:`Store`, offering hierarchical namespacing. diff --git a/litestar/stores/redis.py b/litestar/stores/redis.py index f0baa2538a..6697962fab 100644 --- a/litestar/stores/redis.py +++ b/litestar/stores/redis.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import timedelta -from typing import cast +from typing import TYPE_CHECKING, cast from redis.asyncio import Redis from redis.asyncio.connection import ConnectionPool @@ -14,13 +14,18 @@ __all__ = ("RedisStore",) +if TYPE_CHECKING: + from types import TracebackType + class RedisStore(NamespacedStore): """Redis based, thread and process safe asynchronous key/value store.""" __slots__ = ("_redis",) - def __init__(self, redis: Redis, namespace: str | None | EmptyType = Empty) -> None: + def __init__( + self, redis: Redis, namespace: str | None | EmptyType = Empty, handle_client_shutdown: bool = False + ) -> None: """Initialize :class:`RedisStore` Args: @@ -28,9 +33,11 @@ def __init__(self, redis: Redis, namespace: str | None | EmptyType = Empty) -> N namespace: A key prefix to simulate a namespace in redis. If not given, defaults to ``LITESTAR``. Namespacing can be explicitly disabled by passing ``None``. This will make :meth:`.delete_all` unavailable. + handle_client_shutdown: If ``True``, handle the shutdown of the `redis` instance automatically during the store's lifespan. Should be set to `True` unless the shutdown is handled externally """ self._redis = redis self.namespace: str | None = value_or_default(namespace, "LITESTAR") + self.handle_client_shutdown = handle_client_shutdown # script to get and renew a key in one atomic step self._get_and_renew_script = self._redis.register_script( @@ -64,6 +71,18 @@ def __init__(self, redis: Redis, namespace: str | None | EmptyType = Empty) -> N """ ) + async def _shutdown(self) -> None: + if self.handle_client_shutdown: + await self._redis.aclose(close_connection_pool=True) # type: ignore[attr-defined] + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self._shutdown() + @classmethod def with_client( cls, @@ -93,14 +112,22 @@ def with_client( username=username, password=password, ) - return cls(redis=Redis(connection_pool=pool), namespace=namespace) + return cls( + redis=Redis(connection_pool=pool), + namespace=namespace, + handle_client_shutdown=True, + ) def with_namespace(self, namespace: str) -> RedisStore: """Return a new :class:`RedisStore` with a nested virtual key namespace. The current instances namespace will serve as a prefix for the namespace, so it can be considered the parent namespace. """ - return type(self)(redis=self._redis, namespace=f"{self.namespace}_{namespace}" if self.namespace else namespace) + return type(self)( + redis=self._redis, + namespace=f"{self.namespace}_{namespace}" if self.namespace else namespace, + handle_client_shutdown=self.handle_client_shutdown, + ) def _make_key(self, key: str) -> str: prefix = f"{self.namespace}:" if self.namespace else "" diff --git a/litestar/testing/client/async_client.py b/litestar/testing/client/async_client.py index 1802d9f688..b71a68ed66 100644 --- a/litestar/testing/client/async_client.py +++ b/litestar/testing/client/async_client.py @@ -73,7 +73,6 @@ def __init__( ) AsyncClient.__init__( self, - app=app, base_url=base_url, headers={"user-agent": "testclient"}, follow_redirects=True, diff --git a/litestar/testing/client/sync_client.py b/litestar/testing/client/sync_client.py index a4c4342e9f..b63017b036 100644 --- a/litestar/testing/client/sync_client.py +++ b/litestar/testing/client/sync_client.py @@ -76,7 +76,6 @@ def __init__( Client.__init__( self, - app=self.app, base_url=base_url, headers={"user-agent": "testclient"}, follow_redirects=True, diff --git a/litestar/testing/helpers.py b/litestar/testing/helpers.py index 1af5f7a5de..5ac59af7c8 100644 --- a/litestar/testing/helpers.py +++ b/litestar/testing/helpers.py @@ -12,14 +12,14 @@ if TYPE_CHECKING: from contextlib import AbstractAsyncContextManager - from litestar import Request, WebSocket + from litestar import Request, Response, WebSocket from litestar.config.allowed_hosts import AllowedHostsConfig from litestar.config.app import ExperimentalFeatures from litestar.config.compression import CompressionConfig from litestar.config.cors import CORSConfig from litestar.config.csrf import CSRFConfig from litestar.config.response_cache import ResponseCacheConfig - from litestar.datastructures import CacheControlHeader, ETag, ResponseHeader, State + from litestar.datastructures import CacheControlHeader, ETag, State from litestar.dto import AbstractDTO from litestar.events import BaseEventEmitterBackend, EventListener from litestar.logging.config import BaseLoggingConfig @@ -47,7 +47,7 @@ OnAppInitHandler, ParametersMap, ResponseCookies, - ResponseType, + ResponseHeaders, TypeEncodersMap, ) @@ -92,9 +92,9 @@ def create_test_client( pdb_on_exception: bool | None = None, request_class: type[Request] | None = None, response_cache_config: ResponseCacheConfig | None = None, - response_class: ResponseType | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, - response_headers: Sequence[ResponseHeader] | None = None, + response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, root_path: str = "", security: Sequence[SecurityRequirement] | None = None, @@ -347,9 +347,9 @@ def create_async_test_client( raise_server_exceptions: bool = True, request_class: type[Request] | None = None, response_cache_config: ResponseCacheConfig | None = None, - response_class: ResponseType | None = None, + response_class: type[Response] | None = None, response_cookies: ResponseCookies | None = None, - response_headers: Sequence[ResponseHeader] | None = None, + response_headers: ResponseHeaders | None = None, return_dto: type[AbstractDTO] | None | EmptyType = Empty, root_path: str = "", security: Sequence[SecurityRequirement] | None = None, diff --git a/litestar/types/__init__.py b/litestar/types/__init__.py index 35eaf014dc..90e319277c 100644 --- a/litestar/types/__init__.py +++ b/litestar/types/__init__.py @@ -73,14 +73,8 @@ ) from .empty import Empty, EmptyType from .file_types import FileInfo, FileSystemProtocol -from .helper_types import AnyIOBackend, MaybePartial, OptionalSequence, StreamType, SyncOrAsyncUnion -from .internal_types import ( - ControllerRouterHandler, - ReservedKwargs, - ResponseType, - RouteHandlerMapItem, - RouteHandlerType, -) +from .helper_types import AnyIOBackend, MaybePartial, OptionalSequence, SSEData, StreamType, SyncOrAsyncUnion +from .internal_types import ControllerRouterHandler, ReservedKwargs, RouteHandlerMapItem, RouteHandlerType from .protocols import DataclassProtocol, Logger from .serialization import DataContainerType, LitestarEncodableType @@ -146,7 +140,6 @@ "ReservedKwargs", "ResponseCookies", "ResponseHeaders", - "ResponseType", "RouteHandlerMapItem", "RouteHandlerType", "Scope", @@ -155,6 +148,7 @@ "Send", "Serializer", "StreamType", + "SSEData", "SyncOrAsyncUnion", "TypeDecodersSequence", "TypeEncodersMap", diff --git a/litestar/types/helper_types.py b/litestar/types/helper_types.py index c211c33896..588ae5409f 100644 --- a/litestar/types/helper_types.py +++ b/litestar/types/helper_types.py @@ -3,9 +3,11 @@ from functools import partial from typing import ( TYPE_CHECKING, + Any, AsyncIterable, AsyncIterator, Awaitable, + Dict, Iterable, Iterator, Literal, @@ -18,9 +20,12 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias + from litestar.response.sse import ServerSentEventMessage + + T = TypeVar("T") -__all__ = ("OptionalSequence", "SyncOrAsyncUnion", "AnyIOBackend", "StreamType", "MaybePartial") +__all__ = ("OptionalSequence", "SyncOrAsyncUnion", "AnyIOBackend", "StreamType", "MaybePartial", "SSEData") OptionalSequence: TypeAlias = Optional[Sequence[T]] """Types 'T' as union of Sequence[T] and None.""" @@ -37,3 +42,6 @@ MaybePartial: TypeAlias = Union[T, partial] """A potentially partial callable.""" + +SSEData: TypeAlias = Union[int, str, bytes, Dict[str, Any], "ServerSentEventMessage"] +"""A type alias for SSE data.""" diff --git a/litestar/types/internal_types.py b/litestar/types/internal_types.py index 499de1b6fc..d473c22677 100644 --- a/litestar/types/internal_types.py +++ b/litestar/types/internal_types.py @@ -9,7 +9,6 @@ "PathParameterDefinition", "PathParameterDefinition", "ReservedKwargs", - "ResponseType", "RouteHandlerMapItem", "RouteHandlerType", ) @@ -22,7 +21,6 @@ from litestar.handlers.asgi_handlers import ASGIRouteHandler from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.handlers.websocket_handlers import WebsocketRouteHandler - from litestar.response import Response from litestar.router import Router from litestar.template import TemplateConfig from litestar.template.config import EngineType @@ -30,7 +28,6 @@ ReservedKwargs: TypeAlias = Literal["request", "socket", "headers", "query", "cookies", "state", "data"] RouteHandlerType: TypeAlias = "HTTPRouteHandler | WebsocketRouteHandler | ASGIRouteHandler" -ResponseType: TypeAlias = "type[Response]" ControllerRouterHandler: TypeAlias = "type[Controller] | RouteHandlerType | Router | Callable[..., Any]" RouteHandlerMapItem: TypeAlias = 'dict[Method | Literal["websocket", "asgi"], RouteHandlerType]' TemplateConfigType: TypeAlias = "TemplateConfig[EngineType]" diff --git a/litestar/utils/module_loader.py b/litestar/utils/module_loader.py new file mode 100644 index 0000000000..09dbf9f8d6 --- /dev/null +++ b/litestar/utils/module_loader.py @@ -0,0 +1,92 @@ +"""General utility functions.""" + +from __future__ import annotations + +import os.path +import sys +from importlib import import_module +from importlib.util import find_spec +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from types import ModuleType + +__all__ = ( + "import_string", + "module_to_os_path", +) + + +def module_to_os_path(dotted_path: str = "app") -> Path: + """Find Module to OS Path. + + Return a path to the base directory of the project or the module + specified by `dotted_path`. + + Args: + dotted_path: The path to the module. Defaults to "app". + + Raises: + TypeError: The module could not be found. + + Returns: + Path: The path to the module. + """ + try: + if (src := find_spec(dotted_path)) is None: # pragma: no cover + raise TypeError(f"Couldn't find the path for {dotted_path}") + except ModuleNotFoundError as e: + raise TypeError(f"Couldn't find the path for {dotted_path}") from e + + return Path(str(src.origin).rsplit(os.path.sep + "__init__.py", maxsplit=1)[0]) + + +def import_string(dotted_path: str) -> Any: + """Dotted Path Import. + + Import a dotted module path and return the attribute/class designated by the + last name in the path. Raise ImportError if the import failed. + + Args: + dotted_path: The path of the module to import. + + Raises: + ImportError: Could not import the module. + + Returns: + object: The imported object. + """ + + def _is_loaded(module: ModuleType | None) -> bool: + spec = getattr(module, "__spec__", None) + initializing = getattr(spec, "_initializing", False) + return bool(module and spec and not initializing) + + def _cached_import(module_path: str, class_name: str) -> Any: + """Import and cache a class from a module. + + Args: + module_path: dotted path to module. + class_name: Class or function name. + + Returns: + object: The imported class or function + """ + # Check whether module is loaded and fully initialized. + module = sys.modules.get(module_path) + if not _is_loaded(module): + module = import_module(module_path) + return getattr(module, class_name) + + try: + module_path, class_name = dotted_path.rsplit(".", 1) + except ValueError as e: + msg = "%s doesn't look like a module path" + raise ImportError(msg, dotted_path) from e + + try: + return _cached_import(module_path, class_name) + except AttributeError as e: + msg = "Module '%s' does not define a '%s' attribute/class" + raise ImportError(msg, module_path, class_name) from e diff --git a/litestar/utils/scope/__init__.py b/litestar/utils/scope/__init__.py index 44895ebc26..e5757d3983 100644 --- a/litestar/utils/scope/__init__.py +++ b/litestar/utils/scope/__init__.py @@ -55,8 +55,8 @@ def __getattr__(name: str) -> Any: version="2.4", kind="import", removal_in="3.0", - info=f"'litestar.utils.scope.{name}' is deprecated. The Litestar scope state is private and should not be used." - "Plugin authors should maintain their own scope state namespace.", + info=f"'litestar.utils.scope.{name}' is deprecated. The Litestar scope state is private and should not be " + f"used. Plugin authors should maintain their own scope state namespace.", ) return globals()["_deprecated_names"][name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # pragma: no cover diff --git a/litestar/utils/scope/state.py b/litestar/utils/scope/state.py index bd6829d387..31f6442e61 100644 --- a/litestar/utils/scope/state.py +++ b/litestar/utils/scope/state.py @@ -96,10 +96,9 @@ def from_scope(cls, scope: Scope) -> Self: Returns: A `ConnectionState` object. """ - if state := scope["state"].get(CONNECTION_STATE_KEY): - return state # type: ignore[no-any-return] - state = scope["state"][CONNECTION_STATE_KEY] = cls() - scope["state"][CONNECTION_STATE_KEY] = state + base_scope_state = scope.setdefault("state", {}) + if (state := base_scope_state.get(CONNECTION_STATE_KEY)) is None: + state = base_scope_state[CONNECTION_STATE_KEY] = cls() return state diff --git a/litestar/utils/signature.py b/litestar/utils/signature.py index d7ea40d306..394f7ce581 100644 --- a/litestar/utils/signature.py +++ b/litestar/utils/signature.py @@ -58,7 +58,8 @@ def _unwrap_implicit_optional_hints(defaults: dict[str, Any], hints: dict[str, A .. code-block:: python - def foo(a: Optional[Union[str, int]] = None): ... + def foo(a: Optional[Union[str, int]] = None): + ... ...will become `Union[str, int, NoneType]`. @@ -66,7 +67,8 @@ def foo(a: Optional[Union[str, int]] = None): ... .. code-block:: python - def foo(a: Annotated[Optional[Union[str, int]], ...] = None): ... + def foo(a: Annotated[Optional[Union[str, int]], ...] = None): + ... ... becomes `Union[Annotated[Union[str, int, NoneType], ...], NoneType]` diff --git a/litestar/utils/typing.py b/litestar/utils/typing.py index a99a623b2c..9da6c2a6f6 100644 --- a/litestar/utils/typing.py +++ b/litestar/utils/typing.py @@ -235,6 +235,7 @@ def get_type_hints_with_generics_resolved( globalns: dict[str, Any] | None = None, localns: dict[str, Any] | None = None, include_extras: bool = False, + type_hints: dict[str, Any] | None = None, ) -> dict[str, Any]: """Get the type hints for the given object after resolving the generic types as much as possible. @@ -243,15 +244,18 @@ def get_type_hints_with_generics_resolved( globalns: The global namespace. localns: The local namespace. include_extras: A flag indicating whether to include the ``Annotated[T, ...]`` or not. + type_hints: Already resolved type hints """ origin = get_origin(annotation) if origin is None: # Implies the generic types have not been specified in the annotation - type_hints = get_type_hints(annotation, globalns=globalns, localns=localns, include_extras=include_extras) + if type_hints is None: # pragma: no cover + type_hints = get_type_hints(annotation, globalns=globalns, localns=localns, include_extras=include_extras) typevar_map = {p: p for p in annotation.__parameters__} else: - type_hints = get_type_hints(origin, globalns=globalns, localns=localns, include_extras=include_extras) + if type_hints is None: # pragma: no cover + type_hints = get_type_hints(origin, globalns=globalns, localns=localns, include_extras=include_extras) # the __parameters__ is only available on the origin itself and not the annotation typevar_map = dict(zip(origin.__parameters__, get_args(annotation))) diff --git a/pdm.lock b/pdm.lock index 8bc10c6181..8c5337b7d0 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.1" -content_hash = "sha256:63cac5a26843dd6138a7ba24d0ce45b2880331e4f6a054a95f0308c4fa8f9531" +content_hash = "sha256:d15ae45eac6b6d77a630838b16d3e06192941b526fb6c53b46cf78bd7400d394" [[package]] name = "accessible-pygments" @@ -21,7 +21,7 @@ files = [ [[package]] name = "advanced-alchemy" -version = "0.6.0" +version = "0.7.2" requires_python = ">=3.8" summary = "Ready-to-go SQLAlchemy concoctions." dependencies = [ @@ -31,8 +31,8 @@ dependencies = [ "typing-extensions>=4.0.0", ] files = [ - {file = "advanced_alchemy-0.6.0-py3-none-any.whl", hash = "sha256:27cde1f05a19f2f5aa48c9382e865e8f99c2698093f5b280e903a9c60d9c46e6"}, - {file = "advanced_alchemy-0.6.0.tar.gz", hash = "sha256:cc2a5e86148ac926de60cf06034c5c85a847a326445b0c21b2abcce35a588e70"}, + {file = "advanced_alchemy-0.7.2-py3-none-any.whl", hash = "sha256:19928c44608872f4b10ac31132a7c1c78a3ad5680f3256006c37e0ff0afccbc8"}, + {file = "advanced_alchemy-0.7.2.tar.gz", hash = "sha256:80a8cd64313aaa6be3d97258fe4669338af1efefc6b10fbfc77bb70f62a1e79a"}, ] [[package]] @@ -57,7 +57,7 @@ files = [ [[package]] name = "alembic" -version = "1.13.0" +version = "1.13.1" requires_python = ">=3.8" summary = "A database migration tool for SQLAlchemy." dependencies = [ @@ -68,8 +68,8 @@ dependencies = [ "typing-extensions>=4", ] files = [ - {file = "alembic-1.13.0-py3-none-any.whl", hash = "sha256:a23974ea301c3ee52705db809c7413cecd165290c6679b9998dd6c74342ca23a"}, - {file = "alembic-1.13.0.tar.gz", hash = "sha256:ab4b3b94d2e1e5f81e34be8a9b7b7575fc9dd5398fccb0bef351ec9b14872623"}, + {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, + {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, ] [[package]] @@ -87,17 +87,18 @@ files = [ [[package]] name = "anyio" -version = "4.1.0" +version = "4.2.0" requires_python = ">=3.8" summary = "High level compatibility layer for multiple asynchronous event loop implementations" dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", "sniffio>=1.1", + "typing-extensions>=4.1; python_version < \"3.11\"", ] files = [ - {file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"}, - {file = "anyio-4.1.0.tar.gz", hash = "sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"}, + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, ] [[package]] @@ -118,7 +119,7 @@ files = [ [[package]] name = "apeye-core" -version = "1.1.4" +version = "1.1.5" requires_python = ">=3.6.1" summary = "Core (offline) functionality for the apeye library." dependencies = [ @@ -126,8 +127,8 @@ dependencies = [ "idna>=2.5", ] files = [ - {file = "apeye_core-1.1.4-py3-none-any.whl", hash = "sha256:084bc696448d3ac428fece41c1f2eb08fa9d9ce1d1b2f4d43187e3def4528a60"}, - {file = "apeye_core-1.1.4.tar.gz", hash = "sha256:72bb89fed3baa647cb81aa28e1d851787edcbf9573853b5d2b5f87c02f50eaf5"}, + {file = "apeye_core-1.1.5-py3-none-any.whl", hash = "sha256:dc27a93f8c9e246b3b238c5ea51edf6115ab2618ef029b9f2d9a190ec8228fbf"}, + {file = "apeye_core-1.1.5.tar.gz", hash = "sha256:5de72ed3d00cc9b20fea55e54b7ab8f5ef8500eb33a5368bc162a5585e238a55"}, ] [[package]] @@ -221,12 +222,12 @@ files = [ [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" requires_python = ">=3.7" summary = "Classes Without Boilerplate" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [[package]] @@ -257,31 +258,58 @@ files = [ {file = "auto_pytabs-0.4.0.tar.gz", hash = "sha256:4c596aa02ea20c6c85809e5f60a22aa60499dcaa637e52d6313d07c58c5bb61e"}, ] +[[package]] +name = "autobahn" +version = "23.1.2" +requires_python = ">=3.7" +summary = "WebSocket client & server library, WAMP real-time framework" +dependencies = [ + "cryptography>=3.4.6", + "hyperlink>=21.0.0", + "setuptools", + "txaio>=21.2.1", +] +files = [ + {file = "autobahn-23.1.2.tar.gz", hash = "sha256:c5ef8ca7422015a1af774a883b8aef73d4954c9fcd182c9b5244e08e973f7c3a"}, +] + [[package]] name = "autodocsumm" -version = "0.2.11" +version = "0.2.12" requires_python = ">=3.7" summary = "Extended sphinx autodoc including automatic autosummaries" dependencies = [ "Sphinx<8.0,>=2.2", ] files = [ - {file = "autodocsumm-0.2.11-py3-none-any.whl", hash = "sha256:f1d0a623bf1ad64d979a9e23fd360d1fb1b8f869beaf3197f711552cddc174e2"}, - {file = "autodocsumm-0.2.11.tar.gz", hash = "sha256:183212bd9e9f3b58a96bb21b7958ee4e06224107aa45b2fd894b61b83581b9a9"}, + {file = "autodocsumm-0.2.12-py3-none-any.whl", hash = "sha256:b842b53c686c07a4f174721ca4e729b027367703dbf42e2508863a3c6d6c049c"}, + {file = "autodocsumm-0.2.12.tar.gz", hash = "sha256:848fe8c38df433c6635489499b969cb47cc389ed3d7b6e75c8ccbc94d4b3bf9e"}, +] + +[[package]] +name = "automat" +version = "22.10.0" +summary = "Self-service finite-state machines for the programmer on the go." +dependencies = [ + "attrs>=19.2.0", + "six", +] +files = [ + {file = "Automat-22.10.0-py2.py3-none-any.whl", hash = "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180"}, + {file = "Automat-22.10.0.tar.gz", hash = "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e"}, ] [[package]] name = "babel" -version = "2.13.1" +version = "2.14.0" requires_python = ">=3.7" summary = "Internationalization utilities" dependencies = [ "pytz>=2015.7; python_version < \"3.9\"", - "setuptools; python_version >= \"3.12\"", ] files = [ - {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, - {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] [[package]] @@ -300,7 +328,7 @@ files = [ [[package]] name = "beanie" -version = "1.23.6" +version = "1.25.0" requires_python = ">=3.7,<4.0" summary = "Asynchronous Python ODM for MongoDB" dependencies = [ @@ -312,26 +340,26 @@ dependencies = [ "typing-extensions>=4.7; python_version < \"3.11\"", ] files = [ - {file = "beanie-1.23.6-py3-none-any.whl", hash = "sha256:c780e0f951ee40faa688e7b0e56dc963486087c7e3970cd5a3d99cb47703e677"}, - {file = "beanie-1.23.6.tar.gz", hash = "sha256:9a9a36936188a44dcf16aa8930b450249eaa5c7ebc0edc056ad194bdec3539ca"}, + {file = "beanie-1.25.0-py3-none-any.whl", hash = "sha256:4436ac740718ccd62b21576778679ac972359fce2938557890c576adbbf5e244"}, + {file = "beanie-1.25.0.tar.gz", hash = "sha256:f153866b9ba015274102e10a397602d088fc039b705bd806cb447c898cd2979b"}, ] [[package]] name = "beautifulsoup4" -version = "4.12.2" +version = "4.12.3" requires_python = ">=3.6.0" summary = "Screen-scraping library" dependencies = [ "soupsieve>1.2", ] files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] [[package]] name = "black" -version = "23.11.0" +version = "24.1.1" requires_python = ">=3.8" summary = "The uncompromising code formatter." dependencies = [ @@ -344,37 +372,28 @@ dependencies = [ "typing-extensions>=4.0.1; python_version < \"3.11\"", ] files = [ - {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, - {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, - {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, - {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, - {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, - {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, - {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, - {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, - {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, - {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, - {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, - {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, - {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, - {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, - {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, - {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, - {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, - {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, -] - -[[package]] -name = "blacken-docs" -version = "1.16.0" -requires_python = ">=3.8" -summary = "Run Black on Python code blocks in documentation files." -dependencies = [ - "black>=22.1.0", -] -files = [ - {file = "blacken_docs-1.16.0-py3-none-any.whl", hash = "sha256:b0dcb84b28ebfb352a2539202d396f50e15a54211e204a8005798f1d1edb7df8"}, - {file = "blacken_docs-1.16.0.tar.gz", hash = "sha256:b4bdc3f3d73898dfbf0166f292c6ccfe343e65fc22ddef5319c95d1a8dcc6c1c"}, + {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, + {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, + {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, + {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, + {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, + {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, + {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, + {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, + {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, + {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, + {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, + {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, + {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, + {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, + {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, + {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, + {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, + {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, + {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, + {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, + {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, + {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, ] [[package]] @@ -447,41 +466,41 @@ files = [ [[package]] name = "cachecontrol" -version = "0.13.1" +version = "0.14.0" requires_python = ">=3.7" summary = "httplib2 caching for requests" dependencies = [ - "msgpack>=0.5.2", + "msgpack<2.0.0,>=0.5.2", "requests>=2.16.0", ] files = [ - {file = "cachecontrol-0.13.1-py3-none-any.whl", hash = "sha256:95dedbec849f46dda3137866dc28b9d133fc9af55f5b805ab1291833e4457aa4"}, - {file = "cachecontrol-0.13.1.tar.gz", hash = "sha256:f012366b79d2243a6118309ce73151bf52a38d4a5dac8ea57f09bd29087e506b"}, + {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, + {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, ] [[package]] name = "cachecontrol" -version = "0.13.1" +version = "0.14.0" extras = ["filecache"] requires_python = ">=3.7" summary = "httplib2 caching for requests" dependencies = [ - "cachecontrol==0.13.1", + "cachecontrol==0.14.0", "filelock>=3.8.0", ] files = [ - {file = "cachecontrol-0.13.1-py3-none-any.whl", hash = "sha256:95dedbec849f46dda3137866dc28b9d133fc9af55f5b805ab1291833e4457aa4"}, - {file = "cachecontrol-0.13.1.tar.gz", hash = "sha256:f012366b79d2243a6118309ce73151bf52a38d4a5dac8ea57f09bd29087e506b"}, + {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, + {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, ] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -665,6 +684,16 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "constantly" +version = "23.10.4" +requires_python = ">=3.8" +summary = "Symbolic constants in Python" +files = [ + {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, + {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, +] + [[package]] name = "covdefaults" version = "2.3.0" @@ -680,161 +709,170 @@ files = [ [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.1" requires_python = ">=3.8" summary = "Code coverage measurement for Python" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, ] [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.1" extras = ["toml"] requires_python = ">=3.8" summary = "Code coverage measurement for Python" dependencies = [ - "coverage==7.3.2", + "coverage==7.4.1", "tomli; python_full_version <= \"3.11.0a6\"", ] files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, ] [[package]] name = "cryptography" -version = "41.0.7" +version = "42.0.2" requires_python = ">=3.7" summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." dependencies = [ - "cffi>=1.12", -] -files = [ - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, - {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, - {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, - {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, + {file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, + {file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, + {file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, + {file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, + {file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, + {file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, ] [[package]] @@ -847,6 +885,21 @@ files = [ {file = "cssutils-2.9.0.tar.gz", hash = "sha256:89477b3d17d790e97b9fb4def708767061055795aae6f7c82ae32e967c9be4cd"}, ] +[[package]] +name = "daphne" +version = "4.1.0" +requires_python = ">=3.8" +summary = "Django ASGI (HTTP/WebSocket) server" +dependencies = [ + "asgiref<4,>=3.5.2", + "autobahn>=22.4.2", + "twisted[tls]>=22.4", +] +files = [ + {file = "daphne-4.1.0-py3-none-any.whl", hash = "sha256:7228cd6a3ca5a9b11c9a1c1c0414dab1bfb4ddc55ff234b545db8d71f6c24938"}, + {file = "daphne-4.1.0.tar.gz", hash = "sha256:882fab39d0b90c6b2709b38116c95f660b6cf236600115dd7c13161fb98b3448"}, +] + [[package]] name = "deprecated" version = "1.2.14" @@ -876,21 +929,21 @@ files = [ [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" summary = "Distribution utilities" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] name = "dnspython" -version = "2.4.2" -requires_python = ">=3.8,<4.0" +version = "2.5.0" +requires_python = ">=3.8" summary = "DNS toolkit" files = [ - {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, - {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, + {file = "dnspython-2.5.0-py3-none-any.whl", hash = "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6"}, + {file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"}, ] [[package]] @@ -905,17 +958,17 @@ files = [ [[package]] name = "docutils" -version = "0.18.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.20.1" +requires_python = ">=3.7" summary = "Docutils -- Python Documentation Utilities" files = [ - {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, - {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] [[package]] name = "domdf-python-tools" -version = "3.7.0" +version = "3.8.0.post2" requires_python = ">=3.6" summary = "Helpful functions for Python 🐍 🛠️" dependencies = [ @@ -924,8 +977,8 @@ dependencies = [ "typing-extensions>=3.7.4.1", ] files = [ - {file = "domdf_python_tools-3.7.0-py3-none-any.whl", hash = "sha256:7b4d1c3bdb7402b872d43953824bf921ae2e52f893adbe5c0052a21a6efa2fe4"}, - {file = "domdf_python_tools-3.7.0.tar.gz", hash = "sha256:df1af9a91649af0fb2a4e7b3a4b0a0936e4f78389dd7280dd6fd2f53a339ca71"}, + {file = "domdf_python_tools-3.8.0.post2-py3-none-any.whl", hash = "sha256:ad2c763c8d00850a7fa92ad95e9891a1918281ea25322c4dbb1734fd32f905dd"}, + {file = "domdf_python_tools-3.8.0.post2.tar.gz", hash = "sha256:a1fd255ea29f767b08de462d2da39d360262304389227d980bc307ee8aa3366a"}, ] [[package]] @@ -986,7 +1039,7 @@ files = [ [[package]] name = "faker" -version = "20.1.0" +version = "23.1.0" requires_python = ">=3.8" summary = "Faker is a Python package that generates fake data for you." dependencies = [ @@ -994,8 +1047,8 @@ dependencies = [ "typing-extensions>=3.10.0.1; python_version <= \"3.8\"", ] files = [ - {file = "Faker-20.1.0-py3-none-any.whl", hash = "sha256:aeb3e26742863d1e387f9d156f1c36e14af63bf5e6f36fb39b8c27f6a903be38"}, - {file = "Faker-20.1.0.tar.gz", hash = "sha256:562a3a09c3ed3a1a7b20e13d79f904dfdfc5e740f72813ecf95e4cf71e5a2f52"}, + {file = "Faker-23.1.0-py3-none-any.whl", hash = "sha256:60e89e5c0b584e285a7db05eceba35011a241954afdab2853cb246c8a56700a2"}, + {file = "Faker-23.1.0.tar.gz", hash = "sha256:b7f76bb1b2ac4cdc54442d955e36e477c387000f31ce46887fb9722a041be60b"}, ] [[package]] @@ -1034,68 +1087,68 @@ files = [ [[package]] name = "fsspec" -version = "2023.12.0" +version = "2024.2.0" requires_python = ">=3.8" summary = "File-system specification" files = [ - {file = "fsspec-2023.12.0-py3-none-any.whl", hash = "sha256:f807252ee2018f2223760315beb87a2166c2b9532786eeca9e6548dfcf2cfac9"}, - {file = "fsspec-2023.12.0.tar.gz", hash = "sha256:8e0bb2db2a94082968483b7ba2eaebf3949835e2dfdf09243dda387539464b31"}, + {file = "fsspec-2024.2.0-py3-none-any.whl", hash = "sha256:817f969556fa5916bc682e02ca2045f96ff7f586d45110fcb76022063ad2c7d8"}, + {file = "fsspec-2024.2.0.tar.gz", hash = "sha256:b6ad1a679f760dda52b1168c859d01b7b80648ea6f7f7c7f5a8a91dc3f3ecb84"}, ] [[package]] name = "greenlet" -version = "3.0.1" +version = "3.0.3" requires_python = ">=3.7" summary = "Lightweight in-process concurrent programming" files = [ - {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, - {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, - {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, - {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, - {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, - {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, - {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, - {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, - {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, - {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, - {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, - {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, - {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [[package]] @@ -1108,88 +1161,132 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "h2" +version = "4.1.0" +requires_python = ">=3.6.1" +summary = "HTTP/2 State-Machine based protocol implementation" +dependencies = [ + "hpack<5,>=4.0", + "hyperframe<7,>=6.0", +] +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] + [[package]] name = "hiredis" -version = "2.2.3" +version = "2.3.2" requires_python = ">=3.7" summary = "Python wrapper for hiredis" files = [ - {file = "hiredis-2.2.3-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:9a1a80a8fa767f2fdc3870316a54b84fe9fc09fa6ab6a2686783de6a228a4604"}, - {file = "hiredis-2.2.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3f006c28c885deb99b670a5a66f367a175ab8955b0374029bad7111f5357dcd4"}, - {file = "hiredis-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffaf841546905d90ff189de7397aa56413b1ce5e54547f17a98f0ebf3a3b0a3b"}, - {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cadb0ac7ba3babfd804e425946bec9717b320564a1390f163a54af9365a720a"}, - {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33bc4721632ef9708fa44e5df0066053fccc8e65410a2c48573192517a533b48"}, - {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:227c5b4bcb60f89008c275d596e4a7b6625a6b3c827b8a66ae582eace7051f71"}, - {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61995eb826009d99ed8590747bc0da683a5f4fbb4faa8788166bf3810845cd5c"}, - {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f969edc851efe23010e0f53a64269f2629a9364135e9ec81c842e8b2277d0c1"}, - {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27e560eefb57914d742a837f1da98d3b29cb22eff013c8023b7cf52ae6e051d"}, - {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3759f4789ae1913b7df278dfc9e8749205b7a106f888cd2903d19461e24a7697"}, - {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c6cb613148422c523945cdb8b6bed617856f2602fd8750e33773ede2616e55d5"}, - {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:1d274d5c511dfc03f83f997d3238eaa9b6ee3f982640979f509373cced891e98"}, - {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b7fe075e91b9d9cff40eba4fb6a8eff74964d3979a39be9a9ef58b1b4cb3604"}, - {file = "hiredis-2.2.3-cp310-cp310-win32.whl", hash = "sha256:77924b0d32fd1f493d3df15d9609ddf9d94c31a364022a6bf6b525ce9da75bea"}, - {file = "hiredis-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:dcb0569dd5bfe6004658cd0f229efa699a3169dcb4f77bd72e188adda302063d"}, - {file = "hiredis-2.2.3-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:d115790f18daa99b5c11a506e48923b630ef712e9e4b40482af942c3d40638b8"}, - {file = "hiredis-2.2.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c3b8be557e08b234774925622e196f0ee36fe4eab66cd19df934d3efd8f3743"}, - {file = "hiredis-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f5446068197b35a11ccc697720c41879c8657e2e761aaa8311783aac84cef20"}, - {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa17a3b22b3726d54d7af20394f65d4a1735a842a4e0f557dc67a90f6965c4bc"}, - {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7df645b6b7800e8b748c217fbd6a4ca8361bcb9a1ae6206cc02377833ec8a1aa"}, - {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fb9300959a0048138791f3d68359d61a788574ec9556bddf1fec07f2dbc5320"}, - {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d7e459fe7313925f395148d36d9b7f4f8dac65be06e45d7af356b187cef65fc"}, - {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8eceffca3941775b646cd585cd19b275d382de43cc3327d22f7c75d7b003d481"}, - {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b17baf702c6e5b4bb66e1281a3efbb1d749c9d06cdb92b665ad81e03118f78fc"}, - {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e43e2b5acaad09cf48c032f7e4926392bb3a3f01854416cf6d82ebff94d5467"}, - {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a7205497d7276a81fe92951a29616ef96562ed2f91a02066f72b6f93cb34b40e"}, - {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:126623b03c31cb6ac3e0d138feb6fcc36dd43dd34fc7da7b7a0c38b5d75bc896"}, - {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:071c5814b850574036506a8118034f97c3cbf2fe9947ff45a27b07a48da56240"}, - {file = "hiredis-2.2.3-cp311-cp311-win32.whl", hash = "sha256:d1be9e30e675f5bc1cb534633324578f6f0944a1bcffe53242cf632f554f83b6"}, - {file = "hiredis-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9a7c987e161e3c58f992c63b7e26fea7fe0777f3b975799d23d65bbb8cb5899"}, - {file = "hiredis-2.2.3-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:5a4bcef114fc071d5f52c386c47f35aae0a5b43673197b9288a15b584da8fa3a"}, - {file = "hiredis-2.2.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:232d0a70519865741ba56e1dfefd160a580ae78c30a1517bad47b3cf95a3bc7d"}, - {file = "hiredis-2.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9076ce8429785c85f824650735791738de7143f61f43ae9ed83e163c0ca0fa44"}, - {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec58fb7c2062f835595c12f0f02dcda76d0eb0831423cc191d1e18c9276648de"}, - {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2b34a6444b8f9c1e9f84bd2c639388e5d14f128afd14a869dfb3d9af893aa2"}, - {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:818dfd310aa1020a13cd08ee48e116dd8c3bb2e23b8161f8ac4df587dd5093d7"}, - {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d9ea6c8d4cbdeee2e0d43379ce2881e4af0454b00570677c59f33f2531cd38"}, - {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1eadbcd3de55ac42310ff82550d3302cb4efcd4e17d76646a17b6e7004bb42b"}, - {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:477c34c4489666dc73cb5e89dafe2617c3e13da1298917f73d55aac4696bd793"}, - {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:14824e457e4f5cda685c3345d125da13949bcf3bb1c88eb5d248c8d2c3dee08f"}, - {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9cd32326dfa6ce87edf754153b0105aca64486bebe93b9600ccff74fa0b224df"}, - {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:51341e70b467004dcbec3a6ce8c478d2d6241e0f6b01e4c56764afd5022e1e9d"}, - {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2443659c76b226267e2a04dbbb21bc2a3f91aa53bdc0c22964632753ae43a247"}, - {file = "hiredis-2.2.3-cp38-cp38-win32.whl", hash = "sha256:4e3e3e31423f888d396b1fc1f936936e52af868ac1ec17dd15e3eeba9dd4de24"}, - {file = "hiredis-2.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:20f509e3a1a20d6e5f5794fc37ceb21f70f409101fcfe7a8bde783894d51b369"}, - {file = "hiredis-2.2.3-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:d20891e3f33803b26d54c77fd5745878497091e33f4bbbdd454cf6e71aee8890"}, - {file = "hiredis-2.2.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:50171f985e17970f87d5a29e16603d1e5b03bdbf5c2691a37e6c912942a6b657"}, - {file = "hiredis-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9944a2cac25ffe049a7e89f306e11b900640837d1ef38d9be0eaa4a4e2b73a52"}, - {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a5c8019ff94988d56eb49b15de76fe83f6b42536d76edeb6565dbf7fe14b973"}, - {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a286ded34eb16501002e3713b3130c987366eee2ba0d58c33c72f27778e31676"}, - {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b3e974ad15eb32b1f537730dea70b93a4c3db7b026de3ad2b59da49c6f7454d"}, - {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08415ea74c1c29b9d6a4ca3dd0e810dc1af343c1d1d442e15ba133b11ab5be6a"}, - {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e17d04ea58ab8cf3f2dc52e875db16077c6357846006780086fff3189fb199d"}, - {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6ccdcb635dae85b006592f78e32d97f4bc7541cb27829d505f9c7fefcef48298"}, - {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69536b821dd1bc78058a6e7541743f8d82bf2d981b91280b14c4daa6cdc7faba"}, - {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3753df5f873d473f055e1f8837bfad0bd3b277c86f3c9bf058c58f14204cd901"}, - {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6f88cafe46612b6fa68e6dea49e25bebf160598bba00101caa51cc8c1f18d597"}, - {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33ee3ea5cad3a8cb339352cd230b411eb437a2e75d7736c4899acab32056ccdb"}, - {file = "hiredis-2.2.3-cp39-cp39-win32.whl", hash = "sha256:b4f3d06dc16671b88a13ae85d8ca92534c0b637d59e49f0558d040a691246422"}, - {file = "hiredis-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4f674e309cd055ee7a48304ceb8cf43265d859faf4d7d01d270ce45e976ae9d3"}, - {file = "hiredis-2.2.3-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8f280ab4e043b089777b43b4227bdc2035f88da5072ab36588e0ccf77d45d058"}, - {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c2a551f3b8a26f7940d6ee10b837810201754b8d7e6f6b1391655370882c5a"}, - {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c4e3c258eafaab21b174b17270a0cc093718d61cdbde8c03f85ec4bf835343"}, - {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc36a9dded458d4e37492fe3e619c6c83caae794d26ad925adbce61d592f8428"}, - {file = "hiredis-2.2.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4ed68a3b1ccb4313d2a42546fd7e7439ad4745918a48b6c9bcaa61e1e3e42634"}, - {file = "hiredis-2.2.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3bf4b5bae472630c229518e4a814b1b68f10a3d9b00aeaec45f1a330f03a0251"}, - {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33a94d264e6e12a79d9bb8af333b01dc286b9f39c99072ab5fef94ce1f018e17"}, - {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fa6811a618653164f918b891a0fa07052bd71a799defa5c44d167cac5557b26"}, - {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af33f370be90b48bbaf0dab32decbdcc522b1fa95d109020a963282086518a8e"}, - {file = "hiredis-2.2.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b9953d87418ac228f508d93898ab572775e4d3b0eeb886a1a7734553bcdaf291"}, - {file = "hiredis-2.2.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5e7bb4dd524f50b71c20ef5a12bd61da9b463f8894b18a06130942fe31509881"}, - {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89a258424158eb8b3ed9f65548d68998da334ef155d09488c5637723eb1cd697"}, - {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f4a65276f6ecdebe75f2a53f578fbc40e8d2860658420d5e0611c56bbf5054c"}, - {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:334f2738700b20faa04a0d813366fb16ed17287430a6b50584161d5ad31ca6d7"}, - {file = "hiredis-2.2.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d194decd9608f11c777946f596f31d5aacad13972a0a87829ae1e6f2d26c1885"}, - {file = "hiredis-2.2.3.tar.gz", hash = "sha256:e75163773a309e56a9b58165cf5a50e0f84b755f6ff863b2c01a38918fe92daa"}, + {file = "hiredis-2.3.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:742093f33d374098aa21c1696ac6e4874b52658c870513a297a89265a4d08fe5"}, + {file = "hiredis-2.3.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:9e14fb70ca4f7efa924f508975199353bf653f452e4ef0a1e47549e208f943d7"}, + {file = "hiredis-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d7302b4b17fcc1cc727ce84ded7f6be4655701e8d58744f73b09cb9ed2b13df"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed63e8b75c193c5e5a8288d9d7b011da076cc314fafc3bfd59ec1d8a750d48c8"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b4edee59dc089bc3948f4f6fba309f51aa2ccce63902364900aa0a553a85e97"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6481c3b7673a86276220140456c2a6fbfe8d1fb5c613b4728293c8634134824"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684840b014ce83541a087fcf2d48227196576f56ae3e944d4dfe14c0a3e0ccb7"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c4c0bcf786f0eac9593367b6279e9b89534e008edbf116dcd0de956524702c8"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66ab949424ac6504d823cba45c4c4854af5c59306a1531edb43b4dd22e17c102"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:322c668ee1c12d6c5750a4b1057e6b4feee2a75b3d25d630922a463cfe5e7478"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa73e3f163c6e8b2ec26f22285d717a5f77ab2120c97a2605d8f48b26950dac"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7f39f28ffc65de577c3bc0c7615f149e35bc927802a0f56e612db9b530f316f9"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:55ce31bf4711da879b96d511208efb65a6165da4ba91cb3a96d86d5a8d9d23e6"}, + {file = "hiredis-2.3.2-cp310-cp310-win32.whl", hash = "sha256:3dd63d0bbbe75797b743f35d37a4cca7ca7ba35423a0de742ae2985752f20c6d"}, + {file = "hiredis-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:ea002656a8d974daaf6089863ab0a306962c8b715db6b10879f98b781a2a5bf5"}, + {file = "hiredis-2.3.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:adfbf2e9c38b77d0db2fb32c3bdaea638fa76b4e75847283cd707521ad2475ef"}, + {file = "hiredis-2.3.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:80b02d27864ebaf9b153d4b99015342382eeaed651f5591ce6f07e840307c56d"}, + {file = "hiredis-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd40d2e2f82a483de0d0a6dfd8c3895a02e55e5c9949610ecbded18188fd0a56"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfa904045d7cebfb0f01dad51352551cce1d873d7c3f80c7ded7d42f8cac8f89"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28bd184b33e0dd6d65816c16521a4ba1ffbe9ff07d66873c42ea4049a62fed83"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f70481213373d44614148f0f2e38e7905be3f021902ae5167289413196de4ba4"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8797b528c1ff81eef06713623562b36db3dafa106b59f83a6468df788ff0d1"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02fc71c8333586871602db4774d3a3e403b4ccf6446dc4603ec12df563127cee"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0da56915bda1e0a49157191b54d3e27689b70960f0685fdd5c415dacdee2fbed"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e2674a5a3168349435b08fa0b82998ed2536eb9acccf7087efe26e4cd088a525"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:dc1c3fd49930494a67dcec37d0558d99d84eca8eb3f03b17198424538f2608d7"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:14c7b43205e515f538a9defb4e411e0f0576caaeeda76bb9993ed505486f7562"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7bac7e02915b970c3723a7a7c5df4ba7a11a3426d2a3f181e041aa506a1ff028"}, + {file = "hiredis-2.3.2-cp311-cp311-win32.whl", hash = "sha256:63a090761ddc3c1f7db5e67aa4e247b4b3bb9890080bdcdadd1b5200b8b89ac4"}, + {file = "hiredis-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:70d226ab0306a5b8d408235cabe51d4bf3554c9e8a72d53ce0b3c5c84cf78881"}, + {file = "hiredis-2.3.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5c614552c6bd1d0d907f448f75550f6b24fb56cbfce80c094908b7990cad9702"}, + {file = "hiredis-2.3.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9c431431abf55b64347ddc8df68b3ef840269cb0aa5bc2d26ad9506eb4b1b866"}, + {file = "hiredis-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a45857e87e9d2b005e81ddac9d815a33efd26ec67032c366629f023fe64fb415"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138d141ec5a6ec800b6d01ddc3e5561ce1c940215e0eb9960876bfde7186aae"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:387f655444d912a963ab68abf64bf6e178a13c8e4aa945cb27388fd01a02e6f1"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4852f4bf88f0e2d9bdf91279892f5740ed22ae368335a37a52b92a5c88691140"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d711c107e83117129b7f8bd08e9820c43ceec6204fff072a001fd82f6d13db9f"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92830c16885f29163e1c2da1f3c1edb226df1210ec7e8711aaabba3dd0d5470a"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:16b01d9ceae265d4ab9547be0cd628ecaff14b3360357a9d30c029e5ae8b7e7f"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5986fb5f380169270a0293bebebd95466a1c85010b4f1afc2727e4d17c452512"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:49532d7939cc51f8e99efc326090c54acf5437ed88b9c904cc8015b3c4eda9c9"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8f34801b251ca43ad70691fb08b606a2e55f06b9c9fb1fc18fd9402b19d70f7b"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7298562a49d95570ab1c7fc4051e72824c6a80e907993a21a41ba204223e7334"}, + {file = "hiredis-2.3.2-cp312-cp312-win32.whl", hash = "sha256:e1d86b75de787481b04d112067a4033e1ecfda2a060e50318a74e4e1c9b2948c"}, + {file = "hiredis-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:6dbfe1887ffa5cf3030451a56a8f965a9da2fa82b7149357752b67a335a05fc6"}, + {file = "hiredis-2.3.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bcbe47da0aebc00a7cfe3ebdcff0373b86ce2b1856251c003e3d69c9db44b5a7"}, + {file = "hiredis-2.3.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f2c9c0d910dd3f7df92f0638e7f65d8edd7f442203caf89c62fc79f11b0b73f8"}, + {file = "hiredis-2.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:01b6c24c0840ac7afafbc4db236fd55f56a9a0919a215c25a238f051781f4772"}, + {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1f567489f422d40c21e53212a73bef4638d9f21043848150f8544ef1f3a6ad1"}, + {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28adecb308293e705e44087a1c2d557a816f032430d8a2a9bb7873902a1c6d48"}, + {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27e9619847e9dc70b14b1ad2d0fb4889e7ca18996585c3463cff6c951fd6b10b"}, + {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a0026cfbf29f07649b0e34509091a2a6016ff8844b127de150efce1c3aff60b"}, + {file = "hiredis-2.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9de7586522e5da6bee83c9cf0dcccac0857a43249cb4d721a2e312d98a684d1"}, + {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e58494f282215fc461b06709e9a195a24c12ba09570f25bdf9efb036acc05101"}, + {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3a32b4b76d46f1eb42b24a918d51d8ca52411a381748196241d59a895f7c5c"}, + {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1979334ccab21a49c544cd1b8d784ffb2747f99a51cb0bd0976eebb517628382"}, + {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0c0773266e1c38a06e7593bd08870ac1503f5f0ce0f5c63f2b4134b090b5d6a4"}, + {file = "hiredis-2.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bd1cee053416183adcc8e6134704c46c60c3f66b8faaf9e65bf76191ca59a2f7"}, + {file = "hiredis-2.3.2-cp38-cp38-win32.whl", hash = "sha256:5341ce3d01ef3c7418a72e370bf028c7aeb16895e79e115fe4c954fff990489e"}, + {file = "hiredis-2.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:8fc7197ff33047ce43a67851ccf190acb5b05c52fd4a001bb55766358f04da68"}, + {file = "hiredis-2.3.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:f47775e27388b58ce52f4f972f80e45b13c65113e9e6b6bf60148f893871dc9b"}, + {file = "hiredis-2.3.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:9412a06b8a8e09abd6313d96864b6d7713c6003a365995a5c70cfb9209df1570"}, + {file = "hiredis-2.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3020b60e3fc96d08c2a9b011f1c2e2a6bdcc09cb55df93c509b88be5cb791df"}, + {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53d0f2c59bce399b8010a21bc779b4f8c32d0f582b2284ac8c98dc7578b27bc4"}, + {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57c0d0c7e308ed5280a4900d4468bbfec51f0e1b4cde1deae7d4e639bc6b7766"}, + {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d63318ca189fddc7e75f6a4af8eae9c0545863619fb38cfba5f43e81280b286"}, + {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e741ffe4e2db78a1b9dd6e5d29678ce37fbaaf65dfe132e5b82a794413302ef1"}, + {file = "hiredis-2.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb98038ccd368e0d88bd92ee575c58cfaf33e77f788c36b2a89a84ee1936dc6b"}, + {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:eae62ed60d53b3561148bcd8c2383e430af38c0deab9f2dd15f8874888ffd26f"}, + {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca33c175c1cf60222d9c6d01c38fc17ec3a484f32294af781de30226b003e00f"}, + {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c5f6972d2bdee3cd301d5c5438e31195cf1cabf6fd9274491674d4ceb46914d"}, + {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a6b54dabfaa5dbaa92f796f0c32819b4636e66aa8e9106c3d421624bd2a2d676"}, + {file = "hiredis-2.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e96cd35df012a17c87ae276196ea8f215e77d6eeca90709eb03999e2d5e3fd8a"}, + {file = "hiredis-2.3.2-cp39-cp39-win32.whl", hash = "sha256:63b99b5ea9fe4f21469fb06a16ca5244307678636f11917359e3223aaeca0b67"}, + {file = "hiredis-2.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:a50c8af811b35b8a43b1590cf890b61ff2233225257a3cad32f43b3ec7ff1b9f"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e8bf4444b09419b77ce671088db9f875b26720b5872d97778e2545cd87dba4a"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bd42d0d45ea47a2f96babd82a659fbc60612ab9423a68e4a8191e538b85542a"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80441b55edbef868e2563842f5030982b04349408396e5ac2b32025fb06b5212"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec444ab8f27562a363672d6a7372bc0700a1bdc9764563c57c5f9efa0e592b5f"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f9f606e810858207d4b4287b4ef0dc622c2aa469548bf02b59dcc616f134f811"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c3dde4ca00fe9eee3b76209711f1941bb86db42b8a75d7f2249ff9dfc026ab0e"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4dd676107a1d3c724a56a9d9db38166ad4cf44f924ee701414751bd18a784a0"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce42649e2676ad783186264d5ffc788a7612ecd7f9effb62d51c30d413a3eefe"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e3f8b1733078ac663dad57e20060e16389a60ab542f18a97931f3a2a2dd64a4"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:532a84a82156a82529ec401d1c25d677c6543c791e54a263aa139541c363995f"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d59f88c4daa36b8c38e59ac7bffed6f5d7f68eaccad471484bf587b28ccc478"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91a14dd95e24dc078204b18b0199226ee44644974c645dc54ee7b00c3157330"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb777a38797c8c7df0444533119570be18d1a4ce5478dffc00c875684df7bfcb"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d47c915897a99d0d34a39fad4be97b4b709ab3d0d3b779ebccf2b6024a8c681e"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:333b5e04866758b11bda5f5315b4e671d15755fc6ed3b7969721bc6311d0ee36"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c8937f1100435698c18e4da086968c4b5d70e86ea718376f833475ab3277c9aa"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa45f7d771094b8145af10db74704ab0f698adb682fbf3721d8090f90e42cc49"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33d5ebc93c39aed4b5bc769f8ce0819bc50e74bb95d57a35f838f1c4378978e0"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a797d8c7df9944314d309b0d9e1b354e2fa4430a05bb7604da13b6ad291bf959"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e15a408f71a6c8c87b364f1f15a6cd9c1baca12bbc47a326ac8ab99ec7ad3c64"}, + {file = "hiredis-2.3.2.tar.gz", hash = "sha256:733e2456b68f3f126ddaf2cd500a33b25146c3676b97ea843665717bda0c5d43"}, +] + +[[package]] +name = "hpack" +version = "4.0.0" +requires_python = ">=3.6.1" +summary = "Pure-Python HPACK header compression" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, ] [[package]] @@ -1266,7 +1363,7 @@ files = [ [[package]] name = "httpx" -version = "0.25.2" +version = "0.26.0" requires_python = ">=3.8" summary = "The next generation HTTP client." dependencies = [ @@ -1277,43 +1374,84 @@ dependencies = [ "sniffio", ] files = [ - {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, - {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, + {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, + {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, ] [[package]] name = "httpx-sse" -version = "0.3.1" -requires_python = ">=3.7" +version = "0.4.0" +requires_python = ">=3.8" summary = "Consume Server-Sent Event (SSE) messages with HTTPX." files = [ - {file = "httpx-sse-0.3.1.tar.gz", hash = "sha256:3bb3289b2867f50cbdb2fee3eeeefecb1e86653122e164faac0023f1ffc88aea"}, - {file = "httpx_sse-0.3.1-py3-none-any.whl", hash = "sha256:7376dd88732892f9b6b549ac0ad05a8e2341172fe7dcf9f8f9c8050934297316"}, + {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, + {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, +] + +[[package]] +name = "hypercorn" +version = "0.16.0" +requires_python = ">=3.8" +summary = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" +dependencies = [ + "h11", + "h2>=3.1.0", + "priority", + "taskgroup; python_version < \"3.11\"", + "tomli; python_version < \"3.11\"", + "wsproto>=0.14.0", +] +files = [ + {file = "hypercorn-0.16.0-py3-none-any.whl", hash = "sha256:929e45c4acde3fbf7c58edf55336d30a009d2b4cb1f1eb96e6a515d61b663f58"}, + {file = "hypercorn-0.16.0.tar.gz", hash = "sha256:3b17d1dcf4992c1f262d9f9dd799c374125d0b9a8e40e1e2d11e2938b0adfe03"}, +] + +[[package]] +name = "hyperframe" +version = "6.0.1" +requires_python = ">=3.6.1" +summary = "HTTP/2 framing layer for Python" +files = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "A featureful, immutable, and correct URL for Python." +dependencies = [ + "idna>=2.5", +] +files = [ + {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, + {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, ] [[package]] name = "hypothesis" -version = "6.91.0" +version = "6.98.4" requires_python = ">=3.8" summary = "A library for property-based testing" dependencies = [ - "attrs>=19.2.0", + "attrs>=22.2.0", "exceptiongroup>=1.0.0; python_version < \"3.11\"", "sortedcontainers<3.0.0,>=2.1.0", ] files = [ - {file = "hypothesis-6.91.0-py3-none-any.whl", hash = "sha256:316e06d6f7d5f8ab87bcc7417fca750a2b082ed3ce902b979816b413276680b3"}, - {file = "hypothesis-6.91.0.tar.gz", hash = "sha256:a9f61a2bcfc342febcc1d04b80a99e789c57b700f91cbd43bbdb5d651af385cd"}, + {file = "hypothesis-6.98.4-py3-none-any.whl", hash = "sha256:8417d1df13e7ba0eb6cba0917e0aa6c8b0b6b35a4e7fb78db6ab84dfbeb8c8fe"}, + {file = "hypothesis-6.98.4.tar.gz", hash = "sha256:785f47ddac183c7ffef9463b5ab7f2e4433ca9b2b1171e52eeb3f8c5b1f09fa2"}, ] [[package]] name = "identify" -version = "2.5.32" +version = "2.5.34" requires_python = ">=3.8" summary = "File identification library for Python" files = [ - {file = "identify-2.5.32-py2.py3-none-any.whl", hash = "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545"}, - {file = "identify-2.5.32.tar.gz", hash = "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407"}, + {file = "identify-2.5.34-py2.py3-none-any.whl", hash = "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed"}, + {file = "identify-2.5.34.tar.gz", hash = "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d"}, ] [[package]] @@ -1362,6 +1500,15 @@ files = [ {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, ] +[[package]] +name = "incremental" +version = "22.10.0" +summary = "\"A small library that versions your Python projects.\"" +files = [ + {file = "incremental-22.10.0-py2.py3-none-any.whl", hash = "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51"}, + {file = "incremental-22.10.0.tar.gz", hash = "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0"}, +] + [[package]] name = "inflection" version = "0.5.1" @@ -1384,15 +1531,15 @@ files = [ [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" requires_python = ">=3.7" summary = "A very fast and expressive template engine." dependencies = [ "MarkupSafe>=2.0", ] files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [[package]] @@ -1423,7 +1570,7 @@ files = [ [[package]] name = "litestar-sphinx-theme" version = "0.2.0" -requires_python = "<4.0,>=3.8" +requires_python = ">=3.8,<4.0" git = "https://github.com/litestar-org/litestar-sphinx-theme.git" revision = "c5ce66aadc8f910c24f54bf0d172798c237a67eb" summary = "A Sphinx theme for the Litestar organization" @@ -1447,15 +1594,15 @@ files = [ [[package]] name = "mako" -version = "1.3.0" +version = "1.3.2" requires_python = ">=3.8" summary = "A super-fast templating language that borrows the best ideas from the existing templating languages." dependencies = [ "MarkupSafe>=0.9.2", ] files = [ - {file = "Mako-1.3.0-py3-none-any.whl", hash = "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9"}, - {file = "Mako-1.3.0.tar.gz", hash = "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"}, + {file = "Mako-1.3.2-py3-none-any.whl", hash = "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"}, + {file = "Mako-1.3.2.tar.gz", hash = "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e"}, ] [[package]] @@ -1473,61 +1620,61 @@ files = [ [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" requires_python = ">=3.7" summary = "Safely add untrusted strings to HTML/XML markup." files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {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-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -1635,120 +1782,136 @@ files = [ [[package]] name = "msgspec" -version = "0.18.4" +version = "0.18.6" requires_python = ">=3.8" summary = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." files = [ - {file = "msgspec-0.18.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4d24a291a3c94a7f5e26e8f5ef93e72bf26c10dfeed4d6ae8fc87ead02f4e265"}, - {file = "msgspec-0.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9714b78965047638c01c818b4b418133d77e849017de17b0655ee37b714b47a6"}, - {file = "msgspec-0.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:241277eed9fd91037372519fca62aecf823f7229c1d351030d0be5e3302580c1"}, - {file = "msgspec-0.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d08175cbb55c1a87dd258645dce6cd00705d6088bf88e7cf510a9d5c24b0720b"}, - {file = "msgspec-0.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:da13a06e77d683204eee3b134b08ecd5e4759a79014027b1bcd7a12c614b466d"}, - {file = "msgspec-0.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73e70217ff5e4ac244c8f1b0769215cbc81e1c904e135597a5b71162857e6c27"}, - {file = "msgspec-0.18.4-cp310-cp310-win_amd64.whl", hash = "sha256:dc25e6100026f5e1ecb5120150f4e78beb909cbeb0eb724b9982361b75c86c6b"}, - {file = "msgspec-0.18.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e14287c3405093645b3812e3436598edd383b9ed724c686852e65d569f39f953"}, - {file = "msgspec-0.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acdcef2fccfff02f80ac8673dbeab205c288b680d81e05bfb5ae0be6b1502a7e"}, - {file = "msgspec-0.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b052fd7d25a8aa2ffde10126ee1d97b4c6f3d81f3f3ab1258ff759a2bd794874"}, - {file = "msgspec-0.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:826dcb0dfaac0abbcf3a3ae991749900671796eb688b017a69a82bde1e624662"}, - {file = "msgspec-0.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:86800265f87f192a0daefe668e0a9634c35bf8af94b1f297e1352ac62d2e26da"}, - {file = "msgspec-0.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:227fee75a25080a8b3677cdd95b9c0c3652e27869004a084886c65eb558b3dd6"}, - {file = "msgspec-0.18.4-cp311-cp311-win_amd64.whl", hash = "sha256:828ef92f6654915c36ef6c7d8fec92404a13be48f9ff85f060e73b30299bafe1"}, - {file = "msgspec-0.18.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8476848f4937da8faec53700891694df2e412453cb7445991f0664cdd1e2dd16"}, - {file = "msgspec-0.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f668102958841c5bbd3ba7cf569a65d17aa3bdcf22124f394dfcfcf53cc5a9b9"}, - {file = "msgspec-0.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc2405dba5af6478dedd3512bb92197b6f9d1bc0095655afbe9b54d7a426f19f"}, - {file = "msgspec-0.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99f3c13569a5add0980b0d8c6e0bd94a656f6363b26107435b3091df979d228"}, - {file = "msgspec-0.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a198409f672f93534c9c36bdc9eea9fb536827bd63ea846882365516a961356"}, - {file = "msgspec-0.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e21bc5aae6b80dfe4eb75dc1bb29af65483f967d5522e9e3812115a0ba285cac"}, - {file = "msgspec-0.18.4-cp312-cp312-win_amd64.whl", hash = "sha256:44d551aee1ec8aa2d7b64762557c266bcbf7d5109f2246955718d05becc509d6"}, - {file = "msgspec-0.18.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bbbc08d59f74de5791bda63569f26a35ae1dd6bd20c55c3ceba5567b0e5a8ef1"}, - {file = "msgspec-0.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87bc01949a35970398f5267df8ed4189c340727bb6feec99efdb9969dd05cf30"}, - {file = "msgspec-0.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96ccaef83adc0ce96d95328a03289cd5aead4fe400aac21fbe2008855a124a01"}, - {file = "msgspec-0.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6229dd49438d81ed7a3470e3cbc9646b1cc1b120d415a1786df880dabb1d1c4"}, - {file = "msgspec-0.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:55e578fd921c88de0d3a209fe5fd392bb66623924c6525b42cea37c72bf8d558"}, - {file = "msgspec-0.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e95bd0a946b5b7206f27c0f654f490231c9ad5e5a4ff65af8c986f5114dfaf0e"}, - {file = "msgspec-0.18.4-cp38-cp38-win_amd64.whl", hash = "sha256:7e95817021db96c43fd81244228e185b13b085cca3d5169af4e2dfe3ff412954"}, - {file = "msgspec-0.18.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:847d79f6f0b698671ff390aa5a66e207108f2c23b077ef9314ca4fe7819fa4ec"}, - {file = "msgspec-0.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4294158c233884f3b3220f0e96a30d3e916a4781f9502ae6d477bd57bbc80ad"}, - {file = "msgspec-0.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb11ba2709019192636042df5c8db8738e45946735627021b7e7934714526e4"}, - {file = "msgspec-0.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b01efbf80a987a99e9079257c893c026dc661d4cd05caa1f7eabf4accc7f1fbc"}, - {file = "msgspec-0.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:991aa3c76d1b1ec84e840d0b3c96692af834e1f8a1e1a3974cbd189eaf0f2276"}, - {file = "msgspec-0.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8064908ddb3d95d3261aaca48fd38abb16ccf59dc3f2d01eb4e04591fc1e9bd4"}, - {file = "msgspec-0.18.4-cp39-cp39-win_amd64.whl", hash = "sha256:5f446f16ea57d70cceec29b7cb85ec0b3bea032e3dec316806e38575ea3a69b4"}, - {file = "msgspec-0.18.4.tar.gz", hash = "sha256:cb62030bd6b1a00b01a2fcb09735016011696304e6b1d3321e58022548268d3e"}, + {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, + {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, + {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, + {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, + {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, + {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, + {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, + {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, ] [[package]] name = "multidict" -version = "6.0.4" +version = "6.0.5" requires_python = ">=3.7" summary = "multidict implementation" files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] [[package]] name = "mypy" -version = "1.7.1" +version = "1.8.0" requires_python = ">=3.8" summary = "Optional static typing for Python" dependencies = [ @@ -1757,33 +1920,33 @@ dependencies = [ "typing-extensions>=4.1.0", ] files = [ - {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, - {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, - {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, - {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, - {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, - {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, - {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, - {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, - {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, - {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, - {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, - {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, - {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, - {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, - {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, - {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, - {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, - {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, - {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, - {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, - {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, - {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, - {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, ] [[package]] @@ -1821,7 +1984,7 @@ files = [ [[package]] name = "opentelemetry-api" -version = "1.21.0" +version = "1.22.0" requires_python = ">=3.7" summary = "OpenTelemetry Python API" dependencies = [ @@ -1829,13 +1992,13 @@ dependencies = [ "importlib-metadata<7.0,>=6.0", ] files = [ - {file = "opentelemetry_api-1.21.0-py3-none-any.whl", hash = "sha256:4bb86b28627b7e41098f0e93280fe4892a1abed1b79a19aec6f928f39b17dffb"}, - {file = "opentelemetry_api-1.21.0.tar.gz", hash = "sha256:d6185fd5043e000075d921822fd2d26b953eba8ca21b1e2fa360dd46a7686316"}, + {file = "opentelemetry_api-1.22.0-py3-none-any.whl", hash = "sha256:43621514301a7e9f5d06dd8013a1b450f30c2e9372b8e30aaeb4562abf2ce034"}, + {file = "opentelemetry_api-1.22.0.tar.gz", hash = "sha256:15ae4ca925ecf9cfdfb7a709250846fbb08072260fca08ade78056c502b86bed"}, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.42b0" +version = "0.43b0" requires_python = ">=3.7" summary = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" dependencies = [ @@ -1844,60 +2007,60 @@ dependencies = [ "wrapt<2.0.0,>=1.0.0", ] files = [ - {file = "opentelemetry_instrumentation-0.42b0-py3-none-any.whl", hash = "sha256:65ae54ddb90ca2d05d2d16bf6863173e7141eba1bbbf41fc9bbb02446adbe369"}, - {file = "opentelemetry_instrumentation-0.42b0.tar.gz", hash = "sha256:6a653a1fed0f76eea32885321d77c750483e987eeefa4cbf219fc83559543198"}, + {file = "opentelemetry_instrumentation-0.43b0-py3-none-any.whl", hash = "sha256:0ff1334d7e359e27640e9d420024efeb73eacae464309c2e14ede7ba6c93967e"}, + {file = "opentelemetry_instrumentation-0.43b0.tar.gz", hash = "sha256:c3755da6c4be8033be0216d0501e11f4832690f4e2eca5a3576fbf113498f0f6"}, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.42b0" +version = "0.43b0" requires_python = ">=3.7" summary = "ASGI instrumentation for OpenTelemetry" dependencies = [ "asgiref~=3.0", "opentelemetry-api~=1.12", - "opentelemetry-instrumentation==0.42b0", - "opentelemetry-semantic-conventions==0.42b0", - "opentelemetry-util-http==0.42b0", + "opentelemetry-instrumentation==0.43b0", + "opentelemetry-semantic-conventions==0.43b0", + "opentelemetry-util-http==0.43b0", ] files = [ - {file = "opentelemetry_instrumentation_asgi-0.42b0-py3-none-any.whl", hash = "sha256:79b7278fb614aba1bf2211060960d3e8501c1d7d9314b857b30ad80ba34a2805"}, - {file = "opentelemetry_instrumentation_asgi-0.42b0.tar.gz", hash = "sha256:da1d5dd4f172c44c6c100dae352e1fd0ae36dc4f266b3fed68ce9d5ab94c9146"}, + {file = "opentelemetry_instrumentation_asgi-0.43b0-py3-none-any.whl", hash = "sha256:1f593829fa039e9367820736fb063e92acd15c25b53d7bcb5d319971b8e93fd7"}, + {file = "opentelemetry_instrumentation_asgi-0.43b0.tar.gz", hash = "sha256:3f6f19333dca31ef696672e4e36cb1c2613c71dc7e847c11ff36a37e1130dadc"}, ] [[package]] name = "opentelemetry-sdk" -version = "1.21.0" +version = "1.22.0" requires_python = ">=3.7" summary = "OpenTelemetry Python SDK" dependencies = [ - "opentelemetry-api==1.21.0", - "opentelemetry-semantic-conventions==0.42b0", + "opentelemetry-api==1.22.0", + "opentelemetry-semantic-conventions==0.43b0", "typing-extensions>=3.7.4", ] files = [ - {file = "opentelemetry_sdk-1.21.0-py3-none-any.whl", hash = "sha256:9fe633243a8c655fedace3a0b89ccdfc654c0290ea2d8e839bd5db3131186f73"}, - {file = "opentelemetry_sdk-1.21.0.tar.gz", hash = "sha256:3ec8cd3020328d6bc5c9991ccaf9ae820ccb6395a5648d9a95d3ec88275b8879"}, + {file = "opentelemetry_sdk-1.22.0-py3-none-any.whl", hash = "sha256:a730555713d7c8931657612a88a141e3a4fe6eb5523d9e2d5a8b1e673d76efa6"}, + {file = "opentelemetry_sdk-1.22.0.tar.gz", hash = "sha256:45267ac1f38a431fc2eb5d6e0c0d83afc0b78de57ac345488aa58c28c17991d0"}, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.42b0" +version = "0.43b0" requires_python = ">=3.7" summary = "OpenTelemetry Semantic Conventions" files = [ - {file = "opentelemetry_semantic_conventions-0.42b0-py3-none-any.whl", hash = "sha256:5cd719cbfec448af658860796c5d0fcea2fdf0945a2bed2363f42cb1ee39f526"}, - {file = "opentelemetry_semantic_conventions-0.42b0.tar.gz", hash = "sha256:44ae67a0a3252a05072877857e5cc1242c98d4cf12870159f1a94bec800d38ec"}, + {file = "opentelemetry_semantic_conventions-0.43b0-py3-none-any.whl", hash = "sha256:291284d7c1bf15fdaddf309b3bd6d3b7ce12a253cec6d27144439819a15d8445"}, + {file = "opentelemetry_semantic_conventions-0.43b0.tar.gz", hash = "sha256:b9576fb890df479626fa624e88dde42d3d60b8b6c8ae1152ad157a8b97358635"}, ] [[package]] name = "opentelemetry-util-http" -version = "0.42b0" +version = "0.43b0" requires_python = ">=3.7" summary = "Web util for OpenTelemetry" files = [ - {file = "opentelemetry_util_http-0.42b0-py3-none-any.whl", hash = "sha256:764069ed2f7e9a98ed1a7a87111f838000484e388e81f467405933be4b0306c6"}, - {file = "opentelemetry_util_http-0.42b0.tar.gz", hash = "sha256:665e7d372837811aa08cbb9102d4da862441d1c9b1795d649ef08386c8a3cbbd"}, + {file = "opentelemetry_util_http-0.43b0-py3-none-any.whl", hash = "sha256:f25a820784b030f6cb86b3d76e5676c769b75ed3f55a210bcdae0a5e175ebadb"}, + {file = "opentelemetry_util_http-0.43b0.tar.gz", hash = "sha256:3ff6ab361dbe99fc81200d625603c0fb890c055c6e416a3e6d661ddf47a6c7f7"}, ] [[package]] @@ -1925,17 +2088,17 @@ files = [ [[package]] name = "pathspec" -version = "0.11.2" -requires_python = ">=3.7" +version = "0.12.1" +requires_python = ">=3.8" summary = "Utility library for gitignore style pattern matching of file paths." files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "piccolo" -version = "1.1.1" +version = "1.3.0" requires_python = ">=3.8.0" summary = "A fast, user friendly ORM and query builder which supports asyncio." dependencies = [ @@ -1948,8 +2111,8 @@ dependencies = [ "typing-extensions>=4.3.0", ] files = [ - {file = "piccolo-1.1.1-py3-none-any.whl", hash = "sha256:53a061ef0dbf2445283013b527e38794ba952b7c07e2adaa372b013cd28206fe"}, - {file = "piccolo-1.1.1.tar.gz", hash = "sha256:3e7ecb2c676deec0ccf16186fdbc506d33788f34e969efd17d899f60b073ab3c"}, + {file = "piccolo-1.3.0-py3-none-any.whl", hash = "sha256:1bff419ec3548aed642284394657ad6200b5f1916c46f1a4afd1e54225a756c2"}, + {file = "piccolo-1.3.0.tar.gz", hash = "sha256:58d3379f903526e96380f2ca9bd8bba606f718c4a93163b97890cce347587bde"}, ] [[package]] @@ -1998,36 +2161,36 @@ files = [ [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.0" requires_python = ">=3.8" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" requires_python = ">=3.8" summary = "plugin and hook calling mechanisms for python" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [[package]] name = "polyfactory" -version = "2.12.0" +version = "2.14.1" requires_python = "<4.0,>=3.8" summary = "Mock data generation factories" dependencies = [ "faker", - "typing-extensions", + "typing-extensions>=4.6.0", ] files = [ - {file = "polyfactory-2.12.0-py3-none-any.whl", hash = "sha256:35c170f62763ec7e64d38b0981e4a95e3dd32870f10e1251c5f97dda0525bd64"}, - {file = "polyfactory-2.12.0.tar.gz", hash = "sha256:26dc3a52baae1ebd6386708d9a99f8ea4ef57c9d45e556815ee5e44a1cd27fc0"}, + {file = "polyfactory-2.14.1-py3-none-any.whl", hash = "sha256:8aff3be75e046501ec5c411c78c23db284322c760fef50d560ee6ed683f217c8"}, + {file = "polyfactory-2.14.1.tar.gz", hash = "sha256:8c1d5f15dad1ebfd0845d65d4a55f9791cddfa6b3096ad9f9e2fd02a4804631b"}, ] [[package]] @@ -2047,6 +2210,16 @@ files = [ {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, ] +[[package]] +name = "priority" +version = "2.0.0" +requires_python = ">=3.6.1" +summary = "A pure-Python implementation of the HTTP/2 priority tree" +files = [ + {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, + {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, +] + [[package]] name = "prometheus-client" version = "0.19.0" @@ -2057,9 +2230,24 @@ files = [ {file = "prometheus_client-0.19.0.tar.gz", hash = "sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1"}, ] +[[package]] +name = "psutil" +version = "5.9.8" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +summary = "Cross-platform lib for process and system monitoring in Python." +files = [ + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + [[package]] name = "psycopg" -version = "3.1.16" +version = "3.1.18" requires_python = ">=3.7" summary = "PostgreSQL database adapter for Python" dependencies = [ @@ -2068,84 +2256,84 @@ dependencies = [ "tzdata; sys_platform == \"win32\"", ] files = [ - {file = "psycopg-3.1.16-py3-none-any.whl", hash = "sha256:0bfe9741f4fb1c8115cadd8fe832fa91ac277e81e0652ff7fa1400f0ef0f59ba"}, - {file = "psycopg-3.1.16.tar.gz", hash = "sha256:a34d922fd7df3134595e71c3428ba6f1bd5f4968db74857fe95de12db2d6b763"}, + {file = "psycopg-3.1.18-py3-none-any.whl", hash = "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e"}, + {file = "psycopg-3.1.18.tar.gz", hash = "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b"}, ] [[package]] name = "psycopg-binary" -version = "3.1.16" +version = "3.1.18" requires_python = ">=3.7" summary = "PostgreSQL database adapter for Python -- C optimisation distribution" files = [ - {file = "psycopg_binary-3.1.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e08e333366f8583c7bee33ca6a27f84b76e05ee4e9f9f327a48e3ff81386261d"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a18dfcf7eb3db698eb7a38b4a0e82bf5b76a7bc0079068c5837df70b965570f8"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db99192d9f448829322c4f59a584994ce747b8d586ec65788b4c65f7166cfe43"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f6053fe95596e2f67ff2c9464ea23032c748695a3b79060ca01ef878b0ea0f2"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e6092ec21c08ed4ae4ff343c93a3bbb1d39c87dee181860ce40fa3b5c46f4ae"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f81e880d1bd935433efab1c2883a02031df84e739eadcb2c6a715e9c2f41c19"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:430f8843e381199cdc39ce9506a2cdbc27a569c99a0d80193844c787ce7de94d"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:92bda36f0570a5f9a3d6aeb897bad219f1f23fc4e1d0e7780935798771efb536"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b256d500ec0121ad7875bc3539c43c82dc004535d55256a13c49df2d43f07ad8"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:699737cecf675e1eb70b892b1995456db4016eff7189a3ad9325dca5b6715cc3"}, - {file = "psycopg_binary-3.1.16-cp310-cp310-win_amd64.whl", hash = "sha256:5e0885bcd7d9a0c0043be83d6a214069356c640d42496de798d901d0a16a34e7"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ee8be32eb8b813ef37c5f5968fe03fdddc9a6f0129190f97f6491c798a1ef57"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f8fb9677fb7873daf9797207e72e9275f61e769a308c4ea8f55dfd3153ebae7"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a611d7256493ee5bb73a070c9c60206af415be6aee01243c186fc03f1eb1a48"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d267cc92f0f0a9ea6c8ef058e95c85e58133d06c06f4ed48d63fc256aef166ab"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e996b38ffeffbaa06d236bbeab5168d33eea95941cf74de1daa0b008333861b1"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8429017cd7a3ef4699bee4ff8125a5e30b26882b817a178608d73e69fb727ab9"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7d3b2ea267e7676b3693799fadf941c672f5727fae4947efa1f0cc6e25b672c"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8290cfd475fadf935da0900dc91b845fe92f792e6d53039c0df82f9049a84ad"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:72539a0c6b9a2a9be2acca993df17f4baaa0ed00f1d76b65733725286e3e3304"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1078370a93eaef1dc5aed540055d50cbe37e9154342f3a3d73fd768a6199344d"}, - {file = "psycopg_binary-3.1.16-cp311-cp311-win_amd64.whl", hash = "sha256:adca24d273fe81ecab2312309db547b345155ec50d15676e2df82b8c5409eb06"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e1c416a7c2a699c3e5ba031357682ebca92bd58f399e553173ab5d67cc71cbc5"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e951a8cc7cf919fdc817a28d57160e7286011a4a45dcad3be21f3e4feba8be1a"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa02fe8aa9ef8c8743919fdbc92c04b0ee8c43f3d65e53f24d355776c52fb3"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e23375c14c22ce8fd26d057ac4ab827de79aafced173c68a4c0b03520ea02c70"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84472e5c83e805d4c491f331061cbae3ea4e62f80a480fc4b32200be72262ffd"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0f824565d1dc325c74c076efd5ba842b86219f8bc1b8048c8816621a8b268c"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6b856d44531475488e773ac78d2a7a91c0909a1e8bdbd20d3ebdbdce1868c9a0"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:198c4f16f806f7d2ad0c4a5b774652e17861b55249efb4e344049b1fcf9a24af"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b23d4b86acba2d745763ee0801821af1c42b127d8df75b903b7e7ca7c5f6400c"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2cfd857f1085c59da592090f2fa0751da30b67dcafea2ac52c4b404678406aae"}, - {file = "psycopg_binary-3.1.16-cp312-cp312-win_amd64.whl", hash = "sha256:46c9cca48d459d8df71fda4eef7d94a189b8333f4bc3cf1d170c1796fcbbc8cd"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2b22e2dad291a79d7a31b304866fd125038ef7fe378aba9698de0e1804a863c9"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d9e1768c46c595a8177cb709c99626c3cefbd12c2e46eb54323efd8ac4a7fc2d"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eaabc8dd2d364e1b43d3a25188356191a45abb687b77016544f6847b3fcd73a"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cda744c43b09342b1a8b5aace13d3284c1f5ddbfcefa2d385f703337503a060"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cdaf56adc9cc56df7a05e8f097a776939ba49d5e6afc907ba7b404d8bd21c89"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7232116fc5d4e0274114f152bdb9df089895d4c70f7c03268cab0a4c48a28d04"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6f03239d7c18666f5d6ca82ea972235de4d4d3604287098af6cdc256b76a0ca5"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:edd1b630652bdfff84662b46d11878fbab8ab2966003c1876fcde56650e99e3f"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:481e9dafca1ed9532552e097105e6664ee7f14686270ed0ee0b1d6c78c2cdb11"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d43aa3aa55b5fa964ffa78cf6abdbd51ff33a759f290e9159a9f974ffa3178fa"}, - {file = "psycopg_binary-3.1.16-cp38-cp38-win_amd64.whl", hash = "sha256:51e66b282d8689bc33d81bde3a1e14d0c88a39200c2d9436b028b394d24f1f99"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfae154f3c88e67f3ed592765ad56531b6076acfe80796e28cccc05727c1cf5b"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9f4bc3d366951359a68833c8031cc83faf5084b3bc80dd2d24f0add593d4418"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37d682d7ff57cc2573b1011740ef1566749fc94ae6ac1456405510592735c0a"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0be876e3a8ee359f6a985b662c6b02a094a50b37adf1bd756a655004bddf167a"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f79192b0edd60ef24acb0af5b83319cbb65d4187576757b690646b290de8307"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcc5996b1db4e7fb948ea47b610456df317625d92474c779a20f92ca8cbcec92"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3f2ceb04f8137462f9312a324bea5402de0a4f0503cd5442f4264911e4b6265b"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:47517d2da63bb10c80c2cf35c80a936db79636534849524fd57940b5f0bbd7bd"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2a6bd83d0b934aa03897e93acb6897972ccc3827ae61c903589bc92ed423f75d"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:08fb94928e785571ac90d3ab9e09f2721e0d895c2504ecfb8de91c5ea807b267"}, - {file = "psycopg_binary-3.1.16-cp39-cp39-win_amd64.whl", hash = "sha256:cf13807b61315130a59ea8d0950bda2ac875bae9fadc0b1a9aca9b4ef6d62c7b"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c323103dfa663b88204cf5f028e83c77d7a715f9b6f51d2bbc8184b99ddd90a"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:887f8d856c91510148be942c7acd702ccf761a05f59f8abc123c22ab77b5a16c"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d322ba72cde4ca2eefc2196dad9ad7e52451acd2f04e3688d590290625d0c970"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:489aa4fe5a0b653b68341e9e44af247dedbbc655326854aa34c163ef1bcb3143"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ff0948457bfa8c0d35c46e3a75193906d1c275538877ba65907fd67aa059ad"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15e3653c82384b043d820fc637199b5c6a36b37fa4a4943e0652785bb2bad5d"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f8ff3bc08b43f36fdc24fedb86d42749298a458c4724fb588c4d76823ac39f54"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1729d0e3dfe2546d823841eb7a3d003144189d6f5e138ee63e5227f8b75276a5"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:13bcd3742112446037d15e360b27a03af4b5afcf767f5ee374ef8f5dd7571b31"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:320047e3d3554b857e16c2b6b615a85e0db6a02426f4d203a4594a2f125dfe57"}, + {file = "psycopg_binary-3.1.18-cp310-cp310-win_amd64.whl", hash = "sha256:888a72c2aca4316ca6d4a619291b805677bae99bba2f6e31a3c18424a48c7e4d"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e4de16a637ec190cbee82e0c2dc4860fed17a23a35f7a1e6dc479a5c6876722"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6432047b8b24ef97e3fbee1d1593a0faaa9544c7a41a2c67d1f10e7621374c83"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d684227ef8212e27da5f2aff9d4d303cc30b27ac1702d4f6881935549486dd5"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67284e2e450dc7a9e4d76e78c0bd357dc946334a3d410defaeb2635607f632cd"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c9b6bd7fb5c6638cb32469674707649b526acfe786ba6d5a78ca4293d87bae4"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7121acc783c4e86d2d320a7fb803460fab158a7f0a04c5e8c5d49065118c1e73"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e28ff8f3de7b56588c2a398dc135fd9f157d12c612bd3daa7e6ba9872337f6f5"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c84a0174109f329eeda169004c7b7ca2e884a6305acab4a39600be67f915ed38"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:531381f6647fc267383dca88dbe8a70d0feff433a8e3d0c4939201fea7ae1b82"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b293e01057e63c3ac0002aa132a1071ce0fdb13b9ee2b6b45d3abdb3525c597d"}, + {file = "psycopg_binary-3.1.18-cp311-cp311-win_amd64.whl", hash = "sha256:780a90bcb69bf27a8b08bc35b958e974cb6ea7a04cdec69e737f66378a344d68"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87dd9154b757a5fbf6d590f6f6ea75f4ad7b764a813ae04b1d91a70713f414a1"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f876ebbf92db70125f6375f91ab4bc6b27648aa68f90d661b1fc5affb4c9731c"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d2f0cb45e4574f8b2fe7c6d0a0e2eb58903a4fd1fbaf60954fba82d595ab7"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd27f713f2e5ef3fd6796e66c1a5203a27a30ecb847be27a78e1df8a9a5ae68c"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c38a4796abf7380f83b1653c2711cb2449dd0b2e5aca1caa75447d6fa5179c69"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2f7f95746efd1be2dc240248cc157f4315db3fd09fef2adfcc2a76e24aa5741"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4085f56a8d4fc8b455e8f44380705c7795be5317419aa5f8214f315e4205d804"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2e2484ae835dedc80cdc7f1b1a939377dc967fed862262cfd097aa9f50cade46"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3c2b039ae0c45eee4cd85300ef802c0f97d0afc78350946a5d0ec77dd2d7e834"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f54978c4b646dec77fefd8485fa82ec1a87807f334004372af1aaa6de9539a5"}, + {file = "psycopg_binary-3.1.18-cp312-cp312-win_amd64.whl", hash = "sha256:9ffcbbd389e486d3fd83d30107bbf8b27845a295051ccabde240f235d04ed921"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:02bd4da45d5ee9941432e2e9bf36fa71a3ac21c6536fe7366d1bd3dd70d6b1e7"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39242546383f6b97032de7af30edb483d237a0616f6050512eee7b218a2aa8ee"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46ae44d66bf6058a812467f6ae84e4e157dee281bfb1cfaeca07dee07452e85"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad35ac7fd989184bf4d38a87decfb5a262b419e8ba8dcaeec97848817412c64a"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:247474af262bdd5559ee6e669926c4f23e9cf53dae2d34c4d991723c72196404"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ebecbf2406cd6875bdd2453e31067d1bd8efe96705a9489ef37e93b50dc6f09"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1859aeb2133f5ecdd9cbcee155f5e38699afc06a365f903b1512c765fd8d457e"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:da917f6df8c6b2002043193cb0d74cc173b3af7eb5800ad69c4e1fbac2a71c30"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9e24e7b6a68a51cc3b162d0339ae4e1263b253e887987d5c759652f5692b5efe"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e252d66276c992319ed6cd69a3ffa17538943954075051e992143ccbf6dc3d3e"}, + {file = "psycopg_binary-3.1.18-cp38-cp38-win_amd64.whl", hash = "sha256:5d6e860edf877d4413e4a807e837d55e3a7c7df701e9d6943c06e460fa6c058f"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eea5f14933177ffe5c40b200f04f814258cc14b14a71024ad109f308e8bad414"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:824a1bfd0db96cc6bef2d1e52d9e0963f5bf653dd5bc3ab519a38f5e6f21c299"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a87e9eeb80ce8ec8c2783f29bce9a50bbcd2e2342a340f159c3326bf4697afa1"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91074f78a9f890af5f2c786691575b6b93a4967ad6b8c5a90101f7b8c1a91d9c"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e05f6825f8db4428782135e6986fec79b139210398f3710ed4aa6ef41473c008"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f68ac2364a50d4cf9bb803b4341e83678668f1881a253e1224574921c69868c"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7ac1785d67241d5074f8086705fa68e046becea27964267ab3abd392481d7773"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:cd2a9f7f0d4dacc5b9ce7f0e767ae6cc64153264151f50698898c42cabffec0c"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3e4b0bb91da6f2238dbd4fbb4afc40dfb4f045bb611b92fce4d381b26413c686"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:74e498586b72fb819ca8ea82107747d0cb6e00ae685ea6d1ab3f929318a8ce2d"}, + {file = "psycopg_binary-3.1.18-cp39-cp39-win_amd64.whl", hash = "sha256:d4422af5232699f14b7266a754da49dc9bcd45eba244cf3812307934cd5d6679"}, ] [[package]] name = "psycopg-pool" -version = "3.2.0" +version = "3.2.1" requires_python = ">=3.8" summary = "Connection Pool for Psycopg" dependencies = [ - "typing-extensions>=3.10", + "typing-extensions>=4.4", ] 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"}, + {file = "psycopg-pool-3.2.1.tar.gz", hash = "sha256:6509a75c073590952915eddbba7ce8b8332a440a31e77bba69561483492829ad"}, + {file = "psycopg_pool-3.2.1-py3-none-any.whl", hash = "sha256:060b551d1b97a8d358c668be58b637780b884de14d861f4f5ecc48b7563aafb7"}, ] [[package]] @@ -2219,18 +2407,18 @@ files = [ [[package]] name = "psycopg" -version = "3.1.16" +version = "3.1.18" extras = ["binary", "pool"] requires_python = ">=3.7" summary = "PostgreSQL database adapter for Python" dependencies = [ - "psycopg-binary==3.1.16; implementation_name != \"pypy\"", + "psycopg-binary==3.1.18; implementation_name != \"pypy\"", "psycopg-pool", - "psycopg==3.1.16", + "psycopg==3.1.18", ] files = [ - {file = "psycopg-3.1.16-py3-none-any.whl", hash = "sha256:0bfe9741f4fb1c8115cadd8fe832fa91ac277e81e0652ff7fa1400f0ef0f59ba"}, - {file = "psycopg-3.1.16.tar.gz", hash = "sha256:a34d922fd7df3134595e71c3428ba6f1bd5f4968db74857fe95de12db2d6b763"}, + {file = "psycopg-3.1.18-py3-none-any.whl", hash = "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e"}, + {file = "psycopg-3.1.18.tar.gz", hash = "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b"}, ] [[package]] @@ -2243,6 +2431,19 @@ files = [ {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, ] +[[package]] +name = "pyasn1-modules" +version = "0.3.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "A collection of ASN.1-based protocols modules" +dependencies = [ + "pyasn1<0.6.0,>=0.4.6", +] +files = [ + {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, + {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, +] + [[package]] name = "pycparser" version = "2.21" @@ -2255,149 +2456,135 @@ files = [ [[package]] name = "pydantic" -version = "2.5.2" -requires_python = ">=3.7" +version = "2.6.1" +requires_python = ">=3.8" summary = "Data validation using Python type hints" dependencies = [ "annotated-types>=0.4.0", - "pydantic-core==2.14.5", + "pydantic-core==2.16.2", "typing-extensions>=4.6.1", ] files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, + {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, + {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, ] [[package]] name = "pydantic-core" -version = "2.14.5" -requires_python = ">=3.7" +version = "2.16.2" +requires_python = ">=3.8" summary = "" dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, - {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, - {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, - {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, - {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, - {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, - {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, - {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, - {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, - {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, - {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, - {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, - {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, + {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, + {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, + {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, + {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, + {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, + {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, + {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, + {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, + {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, + {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, + {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, + {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, + {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, ] [[package]] name = "pydantic-extra-types" -version = "2.1.0" -requires_python = ">=3.7" +version = "2.5.0" +requires_python = ">=3.8" summary = "Extra Pydantic types." dependencies = [ - "pydantic>=2.0.3", + "pydantic>=2.5.2", ] files = [ - {file = "pydantic_extra_types-2.1.0-py3-none-any.whl", hash = "sha256:1b8aa83a2986b0bc6a7179834fdb423c5e0bcef6b2b4cd9261bf753ad7dcc483"}, - {file = "pydantic_extra_types-2.1.0.tar.gz", hash = "sha256:d07b869e733d33712b07d6b8cd7b0223077c23ae5a1e23bd0699a00401259ec7"}, + {file = "pydantic_extra_types-2.5.0-py3-none-any.whl", hash = "sha256:7346873019cac32061b471adf2cdac711664ddb7a6ede04219bed2da34888c4d"}, + {file = "pydantic_extra_types-2.5.0.tar.gz", hash = "sha256:46b85240093dc63ad4a8f3cab49e03d76ae0577e4f99e2bbff7d32f99d009bf9"}, ] [[package]] name = "pydantic" -version = "2.5.2" +version = "2.6.1" extras = ["email"] -requires_python = ">=3.7" +requires_python = ">=3.8" summary = "Data validation using Python type hints" dependencies = [ "email-validator>=2.0.0", - "pydantic==2.5.2", + "pydantic==2.6.1", ] files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, + {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, + {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, ] [[package]] @@ -2506,6 +2693,19 @@ files = [ {file = "pymongo-4.6.1.tar.gz", hash = "sha256:31dab1f3e1d0cdd57e8df01b645f52d43cc1b653ed3afd535d2891f4fc4f9712"}, ] +[[package]] +name = "pyopenssl" +version = "24.0.0" +requires_python = ">=3.7" +summary = "Python wrapper module around the OpenSSL library" +dependencies = [ + "cryptography<43,>=41.0.5", +] +files = [ + {file = "pyOpenSSL-24.0.0-py3-none-any.whl", hash = "sha256:ba07553fb6fd6a7a2259adb9b84e12302a9a8a75c44046e8bb5d3e5ee887e3c3"}, + {file = "pyOpenSSL-24.0.0.tar.gz", hash = "sha256:6aa33039a93fffa4563e655b61d11364d01264be8ccb49906101e02a334530bf"}, +] + [[package]] name = "pyright" version = "1.1.344" @@ -2521,7 +2721,7 @@ files = [ [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" requires_python = ">=3.7" summary = "pytest: simple powerful testing with Python" dependencies = [ @@ -2533,21 +2733,21 @@ dependencies = [ "tomli>=1.0.0; python_version < \"3.11\"", ] files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [[package]] name = "pytest-asyncio" -version = "0.23.2" +version = "0.23.5" requires_python = ">=3.8" summary = "Pytest support for asyncio" dependencies = [ - "pytest>=7.0.0", + "pytest<9,>=7.0.0", ] files = [ - {file = "pytest-asyncio-0.23.2.tar.gz", hash = "sha256:c16052382554c7b22d48782ab3438d5b10f8cf7a4bdcae7f0f67f097d95beecc"}, - {file = "pytest_asyncio-0.23.2-py3-none-any.whl", hash = "sha256:ea9021364e32d58f0be43b91c6233fb8d2224ccef2398d6837559e587682808f"}, + {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, + {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, ] [[package]] @@ -2645,12 +2845,12 @@ files = [ [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" requires_python = ">=3.8" summary = "Read key-value pairs from a .env file and set them as environment variables" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [[package]] @@ -2669,11 +2869,11 @@ files = [ [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" summary = "World timezone definitions, modern and historical" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] @@ -2700,6 +2900,7 @@ files = [ {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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {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"}, @@ -2783,7 +2984,7 @@ files = [ [[package]] name = "rich-click" -version = "1.7.2" +version = "1.7.3" requires_python = ">=3.7" summary = "Format click help output nicely with rich" dependencies = [ @@ -2792,8 +2993,8 @@ dependencies = [ "typing-extensions", ] files = [ - {file = "rich-click-1.7.2.tar.gz", hash = "sha256:22f93439a3d65f4a04e07cd584f4d01d132d96899766af92ed287618156abbe2"}, - {file = "rich_click-1.7.2-py3-none-any.whl", hash = "sha256:a42bcdcb8696c4ca7a3b1a39e1aba3d2cb64ad00690b4c022fdcb2cbccebc3fc"}, + {file = "rich-click-1.7.3.tar.gz", hash = "sha256:bced1594c497dc007ab49508ff198bb437c576d01291c13a61658999066481f4"}, + {file = "rich_click-1.7.3-py3-none-any.whl", hash = "sha256:bc4163d4e2a3361e21c4d72d300eca6eb8896dfc978667923cb1d4937b8769a3"}, ] [[package]] @@ -2811,15 +3012,15 @@ files = [ [[package]] name = "ruamel-yaml" -version = "0.18.5" +version = "0.18.6" requires_python = ">=3.7" summary = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" dependencies = [ "ruamel-yaml-clib>=0.2.7; platform_python_implementation == \"CPython\" and python_version < \"3.13\"", ] files = [ - {file = "ruamel.yaml-0.18.5-py3-none-any.whl", hash = "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada"}, - {file = "ruamel.yaml-0.18.5.tar.gz", hash = "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e"}, + {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, + {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, ] [[package]] @@ -2873,37 +3074,53 @@ files = [ [[package]] name = "ruff" -version = "0.1.6" +version = "0.2.1" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, - {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, - {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, - {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, - {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, + {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, + {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, + {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, + {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, + {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, + {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, +] + +[[package]] +name = "service-identity" +version = "24.1.0" +requires_python = ">=3.8" +summary = "Service identity verification for pyOpenSSL & cryptography." +dependencies = [ + "attrs>=19.1.0", + "cryptography", + "pyasn1", + "pyasn1-modules", +] +files = [ + {file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"}, + {file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"}, ] [[package]] name = "setuptools" -version = "69.0.2" +version = "69.0.3" requires_python = ">=3.8" summary = "Easily download, build, install, upgrade, and uninstall Python packages" files = [ - {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, - {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [[package]] @@ -3015,15 +3232,15 @@ files = [ [[package]] name = "sphinx-autodoc-typehints" -version = "1.25.2" +version = "2.0.0" requires_python = ">=3.8" summary = "Type hints (PEP 484) support for the Sphinx autodoc extension" dependencies = [ "sphinx>=7.1.2", ] files = [ - {file = "sphinx_autodoc_typehints-1.25.2-py3-none-any.whl", hash = "sha256:5ed05017d23ad4b937eab3bee9fae9ab0dd63f0b42aa360031f1fad47e47f673"}, - {file = "sphinx_autodoc_typehints-1.25.2.tar.gz", hash = "sha256:3cabc2537e17989b2f92e64a399425c4c8bf561ed73f087bc7414a5003616a50"}, + {file = "sphinx_autodoc_typehints-2.0.0-py3-none-any.whl", hash = "sha256:12c0e161f6fe191c2cdfd8fa3caea271f5387d9fbc67ebcd6f4f1f24ce880993"}, + {file = "sphinx_autodoc_typehints-2.0.0.tar.gz", hash = "sha256:7f2cdac2e70fd9787926b6e9e541cd4ded1e838d2b46fda2a1bb0a75ec5b7f3a"}, ] [[package]] @@ -3095,17 +3312,17 @@ files = [ [[package]] name = "sphinx-tabs" -version = "3.4.4" +version = "3.4.5" requires_python = "~=3.7" summary = "Tabbed views for Sphinx" dependencies = [ - "docutils~=0.18.0", + "docutils", "pygments", "sphinx", ] files = [ - {file = "sphinx-tabs-3.4.4.tar.gz", hash = "sha256:f1b72c4f23d1ba9cdcaf880fd883524bc70689f561b9785719b8b3c3c5ed0aca"}, - {file = "sphinx_tabs-3.4.4-py3-none-any.whl", hash = "sha256:85939b689a0b0a24bf0da418b9acf14b0b0fca7a7a5cd35461ee452a2d4e716b"}, + {file = "sphinx-tabs-3.4.5.tar.gz", hash = "sha256:ba9d0c1e3e37aaadd4b5678449eb08176770e0fc227e769b6ce747df3ceea531"}, + {file = "sphinx_tabs-3.4.5-py3-none-any.whl", hash = "sha256:92cc9473e2ecf1828ca3f6617d0efc0aa8acb06b08c56ba29d1413f2f0f6cf09"}, ] [[package]] @@ -3209,61 +3426,61 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.23" +version = "2.0.25" requires_python = ">=3.7" summary = "Database Abstraction Library" dependencies = [ "greenlet!=0.4.17; platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"", - "typing-extensions>=4.2.0", -] -files = [ - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, - {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, - {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, + "typing-extensions>=4.6.0", +] +files = [ + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, + {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, + {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, ] [[package]] name = "starlette" -version = "0.33.0" +version = "0.37.1" requires_python = ">=3.8" summary = "The little ASGI library that shines." dependencies = [ @@ -3271,18 +3488,18 @@ dependencies = [ "typing-extensions>=3.10.0; python_version < \"3.10\"", ] files = [ - {file = "starlette-0.33.0-py3-none-any.whl", hash = "sha256:6d492f0f7dfb2dd646ac7d80444af4989cd5c5c78f622fac99e39a66c3e01f06"}, - {file = "starlette-0.33.0.tar.gz", hash = "sha256:8c21f9592451b2016300c5bbc54b181063367b62720a4048656c070319238897"}, + {file = "starlette-0.37.1-py3-none-any.whl", hash = "sha256:92a816002d4e8c552477b089520e3085bb632e854eb32cef99acb6f6f7830b69"}, + {file = "starlette-0.37.1.tar.gz", hash = "sha256:345cfd562236b557e76a045715ac66fdc355a1e7e617b087834a76a87dcc6533"}, ] [[package]] name = "structlog" -version = "23.2.0" +version = "24.1.0" requires_python = ">=3.8" summary = "Structured Logging for Python" files = [ - {file = "structlog-23.2.0-py3-none-any.whl", hash = "sha256:16a167e87b9fa7fae9a972d5d12805ef90e04857a93eba479d4be3801a6a1482"}, - {file = "structlog-23.2.0.tar.gz", hash = "sha256:334666b94707f89dbc4c81a22a8ccd34449f0201d5b1ee097a030b577fa8c858"}, + {file = "structlog-24.1.0-py3-none-any.whl", hash = "sha256:3f6efe7d25fab6e86f277713c218044669906537bb717c1807a09d46bca0714d"}, + {file = "structlog-24.1.0.tar.gz", hash = "sha256:41a09886e4d55df25bdcb9b5c9674bccfab723ff43e0a86a1b7b236be8e57b16"}, ] [[package]] @@ -3309,6 +3526,18 @@ files = [ {file = "targ-0.3.8.tar.gz", hash = "sha256:6e32e0cd76ab90ead61ed164f4a7131279f8a3721000f419bf219821727253bb"}, ] +[[package]] +name = "taskgroup" +version = "0.0.0a4" +summary = "backport of asyncio.TaskGroup, asyncio.Runner and asyncio.timeout" +dependencies = [ + "exceptiongroup", +] +files = [ + {file = "taskgroup-0.0.0a4-py2.py3-none-any.whl", hash = "sha256:5c1bd0e4c06114e7a4128583ab75c987597d5378a33948a3b74c662b90f61277"}, + {file = "taskgroup-0.0.0a4.tar.gz", hash = "sha256:eb08902d221e27661950f2a0320ddf3f939f579279996f81fe30779bca3a159c"}, +] + [[package]] name = "time-machine" version = "2.13.0" @@ -3417,66 +3646,146 @@ files = [ [[package]] name = "trio" -version = "0.23.1" +version = "0.24.0" requires_python = ">=3.8" summary = "A friendly Python library for async concurrency and I/O" dependencies = [ "attrs>=20.1.0", "cffi>=1.14; os_name == \"nt\" and implementation_name != \"pypy\"", - "exceptiongroup>=1.0.0rc9; python_version < \"3.11\"", + "exceptiongroup; python_version < \"3.11\"", "idna", "outcome", "sniffio>=1.3.0", "sortedcontainers", ] files = [ - {file = "trio-0.23.1-py3-none-any.whl", hash = "sha256:bb4abb3f4af23f96679e7c8cdabb8b234520f2498550d2cf63ebfd95f2ce27fe"}, - {file = "trio-0.23.1.tar.gz", hash = "sha256:16f89f7dcc8f7b9dcdec1fcd863e0c039af6d0f9a22f8dfd56f75d75ec73fd48"}, + {file = "trio-0.24.0-py3-none-any.whl", hash = "sha256:c3bd3a4e3e3025cd9a2241eae75637c43fe0b9e88b4c97b9161a55b9e54cd72c"}, + {file = "trio-0.24.0.tar.gz", hash = "sha256:ffa09a74a6bf81b84f8613909fb0beaee84757450183a7a2e0b47b455c0cac5d"}, ] [[package]] -name = "types-beautifulsoup4" -version = "4.12.0.7" +name = "twisted" +version = "23.10.0" +requires_python = ">=3.8.0" +summary = "An asynchronous networking framework written in Python" +dependencies = [ + "attrs>=21.3.0", + "automat>=0.8.0", + "constantly>=15.1", + "hyperlink>=17.1.1", + "incremental>=22.10.0", + "twisted-iocpsupport<2,>=1.0.2; platform_system == \"Windows\"", + "typing-extensions>=4.2.0", + "zope-interface>=5", +] +files = [ + {file = "twisted-23.10.0-py3-none-any.whl", hash = "sha256:4ae8bce12999a35f7fe6443e7f1893e6fe09588c8d2bed9c35cdce8ff2d5b444"}, + {file = "twisted-23.10.0.tar.gz", hash = "sha256:987847a0790a2c597197613686e2784fd54167df3a55d0fb17c8412305d76ce5"}, +] + +[[package]] +name = "twisted-iocpsupport" +version = "1.0.4" +summary = "An extension for use in the twisted I/O Completion Ports reactor." +files = [ + {file = "twisted-iocpsupport-1.0.4.tar.gz", hash = "sha256:858096c0d15e33f15ac157f455d8f86f2f2cdd223963e58c0f682a3af8362d89"}, + {file = "twisted_iocpsupport-1.0.4-cp310-cp310-win32.whl", hash = "sha256:afa2b630797f9ed2f27f3d9f55e3f72b4244911e45a8c82756f44babbf0b243e"}, + {file = "twisted_iocpsupport-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:0058c963c8957bcd3deda62122e89953c9de1e867a274facc9b15dde1a9f31e8"}, + {file = "twisted_iocpsupport-1.0.4-cp311-cp311-win32.whl", hash = "sha256:196f7c7ccad4ba4d1783b1c4e1d1b22d93c04275cd780bf7498d16c77319ad6e"}, + {file = "twisted_iocpsupport-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:4e5f97bcbabdd79cbaa969b63439b89801ea560f11d42b0a387634275c633623"}, + {file = "twisted_iocpsupport-1.0.4-cp312-cp312-win32.whl", hash = "sha256:6081bd7c2f4fcf9b383dcdb3b3385d75a26a7c9d2be25b6950c3d8ea652d2d2d"}, + {file = "twisted_iocpsupport-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:76f7e67cec1f1d097d1f4ed7de41be3d74546e1a4ede0c7d56e775c4dce5dfb0"}, + {file = "twisted_iocpsupport-1.0.4-cp38-cp38-win32.whl", hash = "sha256:cc86c2ef598c15d824a243c2541c29459881c67fc3c0adb6efe2242f8f0ec3af"}, + {file = "twisted_iocpsupport-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c27985e949b9b1a1fb4c20c71d315c10ea0f93fdf3ccdd4a8c158b5926edd8c8"}, + {file = "twisted_iocpsupport-1.0.4-cp39-cp39-win32.whl", hash = "sha256:e311dfcb470696e3c077249615893cada598e62fa7c4e4ca090167bd2b7d331f"}, + {file = "twisted_iocpsupport-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4574eef1f3bb81501fb02f911298af3c02fe8179c31a33b361dd49180c3e644d"}, + {file = "twisted_iocpsupport-1.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:872747a3b64e2909aee59c803ccd0bceb9b75bf27915520ebd32d69687040fa2"}, + {file = "twisted_iocpsupport-1.0.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:c2712b778bacf1db434e3e065adfed3db300754186a29aecac1efae9ef4bcaff"}, + {file = "twisted_iocpsupport-1.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7c66fa0aa4236b27b3c61cb488662d85dae746a6d1c7b0d91cf7aae118445adf"}, + {file = "twisted_iocpsupport-1.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:300437af17396a945a58dcfffd77863303a8b6d9e65c6e81f1d2eed55b50d444"}, +] + +[[package]] +name = "twisted" +version = "23.10.0" +extras = ["tls"] +requires_python = ">=3.8.0" +summary = "An asynchronous networking framework written in Python" +dependencies = [ + "idna>=2.4", + "pyopenssl>=21.0.0", + "service-identity>=18.1.0", + "twisted==23.10.0", +] +files = [ + {file = "twisted-23.10.0-py3-none-any.whl", hash = "sha256:4ae8bce12999a35f7fe6443e7f1893e6fe09588c8d2bed9c35cdce8ff2d5b444"}, + {file = "twisted-23.10.0.tar.gz", hash = "sha256:987847a0790a2c597197613686e2784fd54167df3a55d0fb17c8412305d76ce5"}, +] + +[[package]] +name = "txaio" +version = "23.1.1" requires_python = ">=3.7" +summary = "Compatibility API between asyncio/Twisted/Trollius" +files = [ + {file = "txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490"}, + {file = "txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704"}, +] + +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20240106" +requires_python = ">=3.8" summary = "Typing stubs for beautifulsoup4" dependencies = [ "types-html5lib", ] files = [ - {file = "types-beautifulsoup4-4.12.0.7.tar.gz", hash = "sha256:59980028d29bf55d0db359efa305b75bacf0cb92e3f3f6b3fd408f2531df274c"}, - {file = "types_beautifulsoup4-4.12.0.7-py3-none-any.whl", hash = "sha256:8b03b054cb2e62abf82bbbeda57a07257026f4ed9010ef17d8f8eff43bb1f9b7"}, + {file = "types-beautifulsoup4-4.12.0.20240106.tar.gz", hash = "sha256:98d628985b71b140bd3bc22a8cb0ab603c2f2d08f20d37925965eb4a21739be8"}, + {file = "types_beautifulsoup4-4.12.0.20240106-py3-none-any.whl", hash = "sha256:cbdd60ab8aeac737ac014431b6e921b43e84279c0405fdd25a6900bb0e71da5b"}, ] [[package]] name = "types-html5lib" -version = "1.1.11.15" +version = "1.1.11.20240106" +requires_python = ">=3.8" summary = "Typing stubs for html5lib" files = [ - {file = "types-html5lib-1.1.11.15.tar.gz", hash = "sha256:80e1a2062d22a3affe5c28d97da30bffbf3a076d393c80fc6f1671216c1bd492"}, - {file = "types_html5lib-1.1.11.15-py3-none-any.whl", hash = "sha256:16fe936d99b9f7fc210e2e21a2aed1b6bbbc554ad8242a6ef75f6f2bddb27e58"}, + {file = "types-html5lib-1.1.11.20240106.tar.gz", hash = "sha256:fc3a1b18eb601b3eeaf92c900bd67675c0a4fa1dd1d2a2893ebdb46923547ee9"}, + {file = "types_html5lib-1.1.11.20240106-py3-none-any.whl", hash = "sha256:61993cb89220107481e0f1da65c388ff8cf3d8c5f6e8483c97559639a596b697"}, +] + +[[package]] +name = "types-psutil" +version = "5.9.5.20240205" +requires_python = ">=3.8" +summary = "Typing stubs for psutil" +files = [ + {file = "types-psutil-5.9.5.20240205.tar.gz", hash = "sha256:51df36a361aa597bf483dcc5b58f2ab7aa87452a36d2da97c90994d6a81ef743"}, + {file = "types_psutil-5.9.5.20240205-py3-none-any.whl", hash = "sha256:3ec9bd8b95a64fe1269241d3ffb74b94a45df2d0391da1402423cd33f29745ca"}, ] [[package]] name = "types-pyasn1" -version = "0.5.0.1" -requires_python = ">=3.7" +version = "0.5.0.20240205" +requires_python = ">=3.8" summary = "Typing stubs for pyasn1" files = [ - {file = "types-pyasn1-0.5.0.1.tar.gz", hash = "sha256:023e903f5920ec9585555235f95bb2d2756b7b58023d3f94890ee8d1d4d9d1ff"}, - {file = "types_pyasn1-0.5.0.1-py3-none-any.whl", hash = "sha256:1bbbe3fcf16a65064e4a5bd7f1be43c375ba241054f8f361b5e6c61c8deb3935"}, + {file = "types-pyasn1-0.5.0.20240205.tar.gz", hash = "sha256:b42b4e967d2ad780bde2ce47d7627a00dfb11b37a451f3e73b264ec6e97e50c7"}, + {file = "types_pyasn1-0.5.0.20240205-py3-none-any.whl", hash = "sha256:40b205856c6a01d2ce6fa47a0be2a238a5556b04f47a2875a2aba680a65a959f"}, ] [[package]] name = "types-pyopenssl" -version = "23.3.0.0" -requires_python = ">=3.7" +version = "24.0.0.20240130" +requires_python = ">=3.8" summary = "Typing stubs for pyOpenSSL" dependencies = [ "cryptography>=35.0.0", ] files = [ - {file = "types-pyOpenSSL-23.3.0.0.tar.gz", hash = "sha256:5ffb077fe70b699c88d5caab999ae80e192fe28bf6cda7989b7e79b1e4e2dcd3"}, - {file = "types_pyOpenSSL-23.3.0.0-py3-none-any.whl", hash = "sha256:00171433653265843b7469ddb9f3c86d698668064cc33ef10537822156130ebf"}, + {file = "types-pyOpenSSL-24.0.0.20240130.tar.gz", hash = "sha256:c812e5c1c35249f75ef5935708b2a997d62abf9745be222e5f94b9595472ab25"}, + {file = "types_pyOpenSSL-24.0.0.20240130-py3-none-any.whl", hash = "sha256:24a255458b5b8a7fca8139cf56f2a8ad5a4f1a5f711b73a5bb9cb50dc688fab5"}, ] [[package]] @@ -3490,14 +3799,15 @@ files = [ [[package]] name = "types-python-jose" -version = "3.3.4.8" +version = "3.3.4.20240106" +requires_python = ">=3.8" summary = "Typing stubs for python-jose" dependencies = [ "types-pyasn1", ] files = [ - {file = "types-python-jose-3.3.4.8.tar.gz", hash = "sha256:3c316675c3cee059ccb9aff87358254344915239fa7f19cee2787155a7db14ac"}, - {file = "types_python_jose-3.3.4.8-py3-none-any.whl", hash = "sha256:95592273443b45dc5cc88f7c56aa5a97725428753fb738b794e63ccb4904954e"}, + {file = "types-python-jose-3.3.4.20240106.tar.gz", hash = "sha256:b18cf8c5080bbfe1ef7c3b707986435d9efca3e90889acb6a06f65e06bc3405a"}, + {file = "types_python_jose-3.3.4.20240106-py3-none-any.whl", hash = "sha256:b515a6c0c61f5e2a53bc93e3a2b024cbd42563e2e19cbde9fd1c2cc2cfe77ccc"}, ] [[package]] @@ -3511,26 +3821,26 @@ files = [ [[package]] name = "types-redis" -version = "4.6.0.11" -requires_python = ">=3.7" +version = "4.6.0.20240106" +requires_python = ">=3.8" summary = "Typing stubs for redis" dependencies = [ "cryptography>=35.0.0", "types-pyOpenSSL", ] files = [ - {file = "types-redis-4.6.0.11.tar.gz", hash = "sha256:c8cfc84635183deca2db4a528966c5566445fd3713983f0034fb0f5a09e0890d"}, - {file = "types_redis-4.6.0.11-py3-none-any.whl", hash = "sha256:94fc61118601fb4f79206b33b9f4344acff7ca1d7bba67834987fb0efcf6a770"}, + {file = "types-redis-4.6.0.20240106.tar.gz", hash = "sha256:2b2fa3a78f84559616242d23f86de5f4130dfd6c3b83fb2d8ce3329e503f756e"}, + {file = "types_redis-4.6.0.20240106-py3-none-any.whl", hash = "sha256:912de6507b631934bd225cdac310b04a58def94391003ba83939e5a10e99568d"}, ] [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.9.0" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] @@ -3545,17 +3855,17 @@ files = [ [[package]] name = "urllib3" -version = "2.1.0" +version = "2.2.0" requires_python = ">=3.8" summary = "HTTP library with thread-safe connection pooling, file post, and more." files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, ] [[package]] name = "uvicorn" -version = "0.24.0.post1" +version = "0.27.1" requires_python = ">=3.8" summary = "The lightning-fast ASGI server." dependencies = [ @@ -3564,13 +3874,13 @@ dependencies = [ "typing-extensions>=4.0; python_version < \"3.11\"", ] files = [ - {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, - {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, + {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, + {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, ] [[package]] name = "uvicorn" -version = "0.24.0.post1" +version = "0.27.1" extras = ["standard"] requires_python = ">=3.8" summary = "The lightning-fast ASGI server." @@ -3579,14 +3889,14 @@ dependencies = [ "httptools>=0.5.0", "python-dotenv>=0.13", "pyyaml>=5.1", - "uvicorn==0.24.0.post1", + "uvicorn==0.27.1", "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", "watchfiles>=0.13", "websockets>=10.4", ] files = [ - {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, - {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, + {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, + {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, ] [[package]] @@ -3878,6 +4188,19 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[[package]] +name = "wsproto" +version = "1.2.0" +requires_python = ">=3.7.0" +summary = "WebSockets state-machine based protocol implementation" +dependencies = [ + "h11<1,>=0.9.0", +] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + [[package]] name = "zipp" version = "3.17.0" @@ -3887,3 +4210,45 @@ files = [ {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] + +[[package]] +name = "zope-interface" +version = "6.1" +requires_python = ">=3.7" +summary = "Interfaces for Python" +dependencies = [ + "setuptools", +] +files = [ + {file = "zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb"}, + {file = "zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41"}, + {file = "zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f"}, + {file = "zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1"}, + {file = "zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"}, + {file = "zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1"}, + {file = "zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a"}, + {file = "zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0"}, + {file = "zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b"}, + {file = "zope.interface-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f"}, + {file = "zope.interface-6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43"}, + {file = "zope.interface-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d"}, + {file = "zope.interface-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179"}, + {file = "zope.interface-6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac"}, + {file = "zope.interface-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40"}, + {file = "zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309"}, +] diff --git a/pyproject.toml b/pyproject.toml index fec4959d43..ee1e2920be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ maintainers = [ name = "litestar" readme = "README.md" requires-python = ">=3.8,<4.0" -version = "2.5.2" +version = "2.6.3" [project.urls] Blog = "https://blog.litestar.dev" @@ -93,6 +93,21 @@ sqlalchemy = ["advanced-alchemy>=0.2.2,<1.0.0"] standard = ["jinja2", "jsbeautifier", "uvicorn[standard]", "uvloop>=0.18.0; sys_platform != 'win32'", "fast-query-parsers>=1.0.2"] structlog = ["structlog"] +[project.scripts] +litestar = "litestar.__main__:run_cli" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.sdist] +include = [ + 'docs/PYPI_README.md', + '/Makefile', + '/litestar', + '/tests', + '/CHANGELOG.rst', +] + [tool.pdm] ignore_package_warnings = ["sphinx", "slotscheck"] @@ -110,6 +125,9 @@ dev = [ "asyncpg>=0.29.0", "psycopg[pool,binary]>=3.1.10", "psycopg2-binary", + "psutil>=5.9.8", + "hypercorn>=0.16.0", + "daphne>=4.0.0", ] dev-contrib = ["opentelemetry-sdk", "httpx-sse"] docs = [ @@ -117,7 +135,6 @@ docs = [ "sphinx-autobuild>=2021.3.14", "sphinx-copybutton>=0.5.2", "sphinx-toolbox>=3.5.0", - "blacken-docs>=1.16.0", "sphinx-design>=0.5.0", "sphinx-click>=4.4.0", "sphinxcontrib-mermaid>=0.9.2", @@ -125,22 +142,22 @@ docs = [ "litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git", ] linting = [ - "ruff", + "ruff>=0.2.1", "mypy", "pre-commit", "slotscheck", "pyright==1.1.344", - "blacken-docs", "asyncpg-stubs", "types-beautifulsoup4", "types-pytest-lazy-fixture", "types-python-jose", "types-pyyaml", "types-redis", + "types-psutil", ] test = [ "covdefaults", - "pytest", + "pytest<8.0.0", "pytest-asyncio", "pytest-cov", "pytest-lazy-fixture", @@ -151,31 +168,16 @@ test = [ "time-machine", ] -[project.scripts] -litestar = "litestar.__main__:run_cli" - -[tool.hatch.metadata] -allow-direct-references = true - -[tool.hatch.build.targets.sdist] -include = [ - 'docs/PYPI_README.md', - '/Makefile', - '/litestar', - '/tests', - '/CHANGELOG.rst', -] - -[build-system] -build-backend = "hatchling.build" -requires = ["hatchling"] - [tool.pdm.scripts] ci = {composite = ["lint", "test"]} docs-serve = "sphinx-autobuild docs docs/_build/ -j auto --watch litestar --watch docs --watch tests --port 8002" lint = "pre-commit run --all-files" test = "pytest tests docs/examples" +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + [tool.codespell] ignore-words-list = "selectin" skip = 'pdm.lock,docs/examples/contrib/sqlalchemy/us_state_lookup.json' @@ -196,7 +198,7 @@ exclude_lines = [ fail_under = 96 [tool.pytest.ini_options] -addopts = "--strict-markers --strict-config --dist=loadgroup" +addopts = "--strict-markers --strict-config --dist=loadgroup -m 'not server_integration'" asyncio_mode = "auto" filterwarnings = [ "ignore::trio.TrioDeprecationWarning:anyio._backends._trio*:", @@ -209,12 +211,13 @@ filterwarnings = [ "ignore::DeprecationWarning:litestar.*", "ignore::pydantic.PydanticDeprecatedSince20::", "ignore:`general_plain_validator_function`:DeprecationWarning::", - "ignore: 'RichMultiCommand':DeprecationWarning::" # this is coming from rich_click itself, nothing we can do about - # that for now + "ignore: 'RichMultiCommand':DeprecationWarning::", # this is coming from rich_click itself, nothing we can do about # that for now ] markers = [ "sqlalchemy_integration: SQLAlchemy integration tests", + "server_integration: Test integration with ASGI server", ] +testpaths = ["tests", "docs/examples/testing"] xfail_strict = true [tool.mypy] @@ -252,6 +255,8 @@ module = [ "fsspec.*", "jsbeautifier.*", "pytimeparse.*", + "importlib_resources", + "exceptiongroup", ] [tool.pydantic-mypy] @@ -262,7 +267,6 @@ warn_untyped_fields = true [tool.pyright] disableBytesTypePromotions = true -reportUnnecessaryTypeIgnoreComments = true exclude = [ "test_apps", "tools", @@ -275,12 +279,13 @@ exclude = [ ] include = ["litestar", "tests"] pythonVersion = "3.8" +reportUnnecessaryTypeIgnoreComments = true [tool.slotscheck] strict-imports = false [tool.ruff] -select = [ +lint.select = [ "A", # flake8-builtins "B", # flake8-bugbear "BLE", # flake8-blind-except @@ -317,7 +322,8 @@ select = [ "YTT", # flake8-2020 ] -ignore = [ +line-length = 120 +lint.ignore = [ "A003", # flake8-builtins - class attribute {name} is shadowing a python builtin "B010", # flake8-bugbear - do not call setattr with a constant attribute value "D100", # pydocstyle - missing docstring in public module @@ -331,22 +337,21 @@ ignore = [ "D202", # pydocstyle - no blank lines allowed after function docstring "D205", # pydocstyle - 1 blank line required between summary line and description "D415", # pydocstyle - first line should end with a period, question mark, or exclamation point - "E501", # pycodestyle line too long, handled by black + "E501", # pycodestyle line too long, handled by ruff format "PLW2901", # pylint - for loop variable overwritten by assignment target "RUF012", # Ruff-specific rule - annotated with classvar "ISC001", # Ruff formatter incompatible ] -line-length = 120 src = ["litestar", "tests", "docs/examples"] target-version = "py38" -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "google" -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 12 -[tool.ruff.pep8-naming] +[tool.ruff.lint.pep8-naming] classmethod-decorators = [ "classmethod", "pydantic.root_validator", @@ -356,10 +361,10 @@ classmethod-decorators = [ "sqlalchemy.orm.declared_attr", ] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["litestar", "tests", "examples"] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "docs/**/*.*" = ["S", "B", "DTZ", "A", "TCH", "ERA", "D", "RET"] "docs/examples/**" = ["T201"] "docs/examples/application_hooks/before_send_hook.py" = ["UP006"] @@ -398,6 +403,10 @@ known-first-party = ["litestar", "tests", "examples"] "tools/**/*.*" = ["D", "ARG", "EM", "TRY", "G", "FBT"] "tools/prepare_release.py" = ["S603", "S607"] +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 88 + [tool.unasyncd] add_editors_note = true ruff_fix = true diff --git a/test_apps/sse/__init__.py b/test_apps/sse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test_apps/sse/sse.html b/test_apps/sse/sse.html new file mode 100644 index 0000000000..8998ccf93a --- /dev/null +++ b/test_apps/sse/sse.html @@ -0,0 +1,30 @@ + + + +

Server-Sent Events

+
+ + + diff --git a/test_apps/sse/sse_empty.py b/test_apps/sse/sse_empty.py new file mode 100644 index 0000000000..0e727f64fd --- /dev/null +++ b/test_apps/sse/sse_empty.py @@ -0,0 +1,23 @@ +from typing import AsyncIterator + +import uvicorn + +from litestar import Litestar, get +from litestar.config.cors import CORSConfig +from litestar.response import ServerSentEvent, ServerSentEventMessage +from litestar.types import SSEData + + +@get("/test_sse_empty") +async def handler() -> ServerSentEvent: + async def generate() -> AsyncIterator[SSEData]: + event = ServerSentEventMessage(event="empty") + yield event + + return ServerSentEvent(generate()) + + +app = Litestar(route_handlers=[handler], cors_config=CORSConfig(allow_origins=["*"])) + +if __name__ == "__main__": + uvicorn.run("sse_empty:app") diff --git a/test_apps/structlog_app/__init__.py b/test_apps/structlog_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test_apps/structlog_app/main.py b/test_apps/structlog_app/main.py new file mode 100644 index 0000000000..be98ae2150 --- /dev/null +++ b/test_apps/structlog_app/main.py @@ -0,0 +1,26 @@ +from typing import Dict + +from litestar import Litestar, Request, get +from litestar.logging.config import StructLoggingConfig +from litestar.middleware.logging import LoggingMiddlewareConfig + + +@get("/") +async def handler(request: Request) -> Dict[str, str]: + request.logger.info("Logging in the handler") + return {"hello": "world"} + + +logging_middleware_config = LoggingMiddlewareConfig() + +app = Litestar( + route_handlers=[handler], + logging_config=StructLoggingConfig(log_exceptions="always"), + middleware=[logging_middleware_config.middleware], +) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app) diff --git a/tests/e2e/test_dependency_injection/test_injection_of_classes.py b/tests/e2e/test_dependency_injection/test_injection_of_classes.py index 795c4f657e..6091015b0e 100644 --- a/tests/e2e/test_dependency_injection/test_injection_of_classes.py +++ b/tests/e2e/test_dependency_injection/test_injection_of_classes.py @@ -1,3 +1,7 @@ +from dataclasses import dataclass + +import msgspec + from litestar import Controller, get from litestar.di import Provide from litestar.testing import create_test_client @@ -37,3 +41,32 @@ def test_function(self, container: HandlerDependency) -> str: with create_test_client(MyController) as client: response = client.get(f"/test/{path_param_value}?query_param={query_param_value}") assert response.text == "15" + + +def test_inject_dataclass() -> None: + @dataclass + class Foo: + bar: str + + @get("/", dependencies={"foo": Provide(Foo, sync_to_thread=False)}) + async def handler(foo: Foo) -> Foo: + return foo + + with create_test_client([handler]) as client: + res = client.get("/?bar=baz") + assert res.status_code == 200 + assert res.json() == {"bar": "baz"} + + +def test_inject_msgspec_struct() -> None: + class Foo(msgspec.Struct): + bar: str + + @get("/", dependencies={"foo": Provide(Foo, sync_to_thread=False)}) + async def handler(foo: Foo) -> Foo: + return foo + + with create_test_client([handler]) as client: + res = client.get("/?bar=baz") + assert res.status_code == 200 + assert res.json() == {"bar": "baz"} diff --git a/tests/e2e/test_routing/conftest.py b/tests/e2e/test_routing/conftest.py new file mode 100644 index 0000000000..eaa178e1ad --- /dev/null +++ b/tests/e2e/test_routing/conftest.py @@ -0,0 +1,41 @@ +import subprocess +import time +from pathlib import Path +from typing import Callable, List + +import httpx +import psutil +import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch + + +@pytest.fixture() +def run_server(tmp_path: Path, request: FixtureRequest, monkeypatch: MonkeyPatch) -> Callable[[str, List[str]], None]: + def runner(app: str, server_command: List[str]) -> None: + tmp_path.joinpath("app.py").write_text(app) + monkeypatch.chdir(tmp_path) + + proc = psutil.Popen( + server_command, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + def kill() -> None: + for child in proc.children(recursive=True): + child.kill() + proc.kill() + + request.addfinalizer(kill) + + for _ in range(50): + try: + httpx.get("http://127.0.0.1:9999/", timeout=0.1) + break + except httpx.TransportError: + time.sleep(0.1) + else: + raise RuntimeError("App failed to come online") + + return runner diff --git a/tests/e2e/test_routing/test_path_mounting.py b/tests/e2e/test_routing/test_path_mounting.py index 138c5eaaa5..c317bb0421 100644 --- a/tests/e2e/test_routing/test_path_mounting.py +++ b/tests/e2e/test_routing/test_path_mounting.py @@ -1,6 +1,9 @@ -from typing import TYPE_CHECKING +from pathlib import Path +from typing import TYPE_CHECKING, Callable, List +import httpx import pytest +from _pytest.monkeypatch import MonkeyPatch from litestar import Litestar, MediaType, asgi, get, websocket from litestar.exceptions import ImproperlyConfiguredException @@ -92,3 +95,36 @@ async def regular_handler(socket: "WebSocket") -> None: with pytest.raises(ImproperlyConfiguredException): Litestar(route_handlers=[asgi_handler, regular_handler]) + + +@pytest.mark.parametrize( + "server_command", + [ + pytest.param(["uvicorn", "app:app", "--port", "9999"], id="uvicorn"), + pytest.param(["hypercorn", "app:app", "--bind", "127.0.0.1:9999"], id="hypercorn"), + pytest.param(["daphne", "app:app", "--port", "9999"], id="daphne"), + ], +) +@pytest.mark.xdist_group("live_server_test") +@pytest.mark.server_integration +def test_path_mounting_live_server( + tmp_path: Path, monkeypatch: MonkeyPatch, server_command: List[str], run_server: Callable[[str, List[str]], None] +) -> None: + app = """ +from litestar import asgi, Litestar +from litestar.types import Receive, Scope, Send +from litestar.response.base import ASGIResponse + +@asgi("/sub/path", is_mount=True) +async def handler(scope: Scope, receive: Receive, send: Send) -> None: + response = ASGIResponse(body=scope["path"].encode()) + await response(scope, receive, send) + + +app = Litestar(route_handlers=[handler]) +""" + run_server(app, server_command) + + res = httpx.get("http://127.0.0.1:9999/sub/path/fragment") + assert res.status_code == 200 + assert res.text == "/fragment/" diff --git a/tests/e2e/test_routing/test_path_resolution.py b/tests/e2e/test_routing/test_path_resolution.py index 0d5afebc00..81386c379e 100644 --- a/tests/e2e/test_routing/test_path_resolution.py +++ b/tests/e2e/test_routing/test_path_resolution.py @@ -1,7 +1,9 @@ from pathlib import Path from typing import Any, Callable, List, Optional, Type +import httpx import pytest +from _pytest.monkeypatch import MonkeyPatch from litestar import Controller, MediaType, Router, delete, get, post from litestar.status_codes import ( @@ -253,7 +255,7 @@ def upper_handler(string_param: str, path_param: Path) -> str: assert response.status_code == HTTP_200_OK -def test_root_path_param_resolution() -> None: +def test_base_path_param_resolution() -> None: # https://github.com/litestar-org/litestar/issues/1830 @get("/{name:str}") async def hello_world(name: str) -> str: @@ -271,7 +273,7 @@ async def hello_world(name: str) -> str: assert response.status_code == HTTP_404_NOT_FOUND -def test_root_path_param_resolution_2() -> None: +def test_base_path_param_resolution_2() -> None: # https://github.com/litestar-org/litestar/issues/1830#issuecomment-1642291149 @get("/{name:str}") async def name_greeting(name: str) -> str: @@ -295,3 +297,67 @@ async def age_greeting(name: str, age: int) -> str: response = client.get("/name/jon/bon") assert response.status_code == HTTP_404_NOT_FOUND + + +@pytest.mark.parametrize( + "server_command", + [ + pytest.param(["uvicorn", "app:app", "--port", "9999", "--root-path", "/test"], id="uvicorn"), + pytest.param(["hypercorn", "app:app", "--bind", "127.0.0.1:9999", "--root-path", "/test"], id="hypercorn"), + pytest.param(["daphne", "app:app", "--port", "9999", "--root-path", "/test"], id="daphne"), + ], +) +@pytest.mark.xdist_group("live_server_test") +@pytest.mark.server_integration +def test_server_root_path_handling( + tmp_path: Path, monkeypatch: MonkeyPatch, server_command: List[str], run_server: Callable[[str, List[str]], None] +) -> None: + # https://github.com/litestar-org/litestar/issues/2998 + app = """ +from litestar import Litestar, get, Request +from typing import List + +@get("/handler") +async def handler(request: Request) -> List[str]: + return [request.scope["path"], request.scope["root_path"]] + +app = Litestar(route_handlers=[handler]) + """ + + run_server(app, server_command) + + assert httpx.get("http://127.0.0.1:9999/handler").json() == ["/handler", "/test"] + + +@pytest.mark.parametrize( + "server_command", + [ + pytest.param(["uvicorn", "app:app", "--port", "9999", "--root-path", "/test"], id="uvicorn"), + pytest.param(["hypercorn", "app:app", "--bind", "127.0.0.1:9999", "--root-path", "/test"], id="hypercorn"), + pytest.param(["daphne", "app:app", "--port", "9999", "--root-path", "/test"], id="daphne"), + ], +) +@pytest.mark.xdist_group("live_server_test") +@pytest.mark.server_integration +def test_server_root_path_handling_empty_path( + tmp_path: Path, monkeypatch: MonkeyPatch, server_command: List[str], run_server: Callable[[str, List[str]], None] +) -> None: + # https://github.com/litestar-org/litestar/issues/3041 + app = """ +from pathlib import Path + +from litestar import Litestar +from litestar.handlers import get +from typing import Optional + +@get(path=["/", "/{path:path}"]) +async def pathfinder(path: Optional[Path]) -> str: + return str(path) + +app = Litestar(route_handlers=[pathfinder], debug=True) + """ + + run_server(app, server_command) + + assert httpx.get("http://127.0.0.1:9999/").text == "None" + assert httpx.get("http://127.0.0.1:9999/something").text == "/something" diff --git a/tests/examples/test_openapi.py b/tests/examples/test_openapi.py index 22dd6dec4d..e83dd1f41d 100644 --- a/tests/examples/test_openapi.py +++ b/tests/examples/test_openapi.py @@ -28,7 +28,7 @@ def test_schema_generation() -> None: "components": { "schemas": { "IdModel": { - "properties": {"id": {"type": "string", "format": "uuid", "description": "Any UUID string"}}, + "properties": {"id": {"type": "string", "format": "uuid"}}, "type": "object", "required": ["id"], "title": "IdContainer", diff --git a/tests/examples/test_plugins/test_di_plugin.py b/tests/examples/test_plugins/test_di_plugin.py new file mode 100644 index 0000000000..c42786b068 --- /dev/null +++ b/tests/examples/test_plugins/test_di_plugin.py @@ -0,0 +1,10 @@ +from docs.examples.plugins.di_plugin import app + +from litestar.testing import TestClient + + +def test_di_plugin_example() -> None: + with TestClient(app) as client: + res = client.get("/?param=hello") + assert res.status_code == 200 + assert res.text == "hello" diff --git a/tests/examples/test_static_files.py b/tests/examples/test_static_files.py new file mode 100644 index 0000000000..7d48603992 --- /dev/null +++ b/tests/examples/test_static_files.py @@ -0,0 +1,69 @@ +import secrets +from pathlib import Path + +import pytest +from _pytest.monkeypatch import MonkeyPatch + +from litestar.testing import TestClient + + +@pytest.fixture(autouse=True) +def _chdir(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) + + +@pytest.fixture() +def assets_file(tmp_path: Path) -> str: + content = secrets.token_hex() + assets_path = tmp_path / "assets" + assets_path.mkdir() + assets_path.joinpath("test.txt").write_text(content) + return content + + +def test_custom_router() -> None: + from docs.examples.static_files import custom_router # noqa: F401 + + +def test_full_example() -> None: + from docs.examples.static_files import full_example + + with TestClient(full_example.app) as client: + assert client.get("/static/hello.txt").text == "Hello, world!" + + +def test_html_mode() -> None: + from docs.examples.static_files import html_mode + + with TestClient(html_mode.app) as client: + assert client.get("/").text == "Hello, world!" + assert client.get("/index.html").text == "Hello, world!" + assert client.get("/something").text == "

Not found

" + + +def test_passing_options() -> None: + from docs.examples.static_files import passing_options # noqa: F401 + + +def test_route_reverse(capsys) -> None: + from docs.examples.static_files import route_reverse # noqa: F401 + + assert capsys.readouterr().out.strip() == "/static/some_file.txt" + + +def test_send_as_attachment(tmp_path: Path, assets_file: str) -> None: + from docs.examples.static_files import send_as_attachment + + with TestClient(send_as_attachment.app) as client: + res = client.get("/static/test.txt") + assert res.text == assets_file + assert res.headers["content-disposition"].startswith("attachment") + + +def test_upgrade_from_static(tmp_path: Path, assets_file: str) -> None: + from docs.examples.static_files import upgrade_from_static_1, upgrade_from_static_2 + + for app in [upgrade_from_static_1.app, upgrade_from_static_2.app]: + with TestClient(app) as client: + res = client.get("/static/test.txt") + assert res.text == assets_file diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index d677785e76..f64bb18ff6 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from dataclasses import fields -from typing import TYPE_CHECKING, List, Tuple +from typing import TYPE_CHECKING, Callable, List, Tuple from unittest.mock import MagicMock, Mock, PropertyMock import pytest @@ -164,15 +164,7 @@ def test_app_config_object_used(app_config_object: AppConfig, monkeypatch: pytes # have been accessed during app instantiation. property_mocks: List[Tuple[str, Mock]] = [] for field in fields(AppConfig): - if field.name == "response_cache_config": - property_mock = PropertyMock(return_value=ResponseCacheConfig()) - if field.name in ["event_emitter_backend", "response_cache_config"]: - property_mock = PropertyMock(return_value=Mock()) - else: - # default iterable return value allows the mock properties that need to be iterated over in - # `Litestar.__init__()` to not blow up, for other properties it shouldn't matter what the value is for the - # sake of this test. - property_mock = PropertyMock(return_value=[]) + property_mock = PropertyMock() property_mocks.append((field.name, property_mock)) monkeypatch.setattr(type(app_config_object), field.name, property_mock, raising=False) @@ -221,6 +213,22 @@ def modify_state_in_hook(app_config: AppConfig) -> AppConfig: assert app.state._state == {"a": "b", "c": "D", "e": "f"} +async def test_dont_override_initial_state(create_scope: Callable[..., Scope]) -> None: + app = Litestar() + + scope = create_scope(headers=[], state={"foo": "bar"}) + + async def send(message: Message) -> None: + pass + + async def receive() -> None: + pass + + await app(scope, receive, send) # type: ignore[arg-type] + + assert scope["state"].get("foo") == "bar" + + def test_app_from_config(app_config_object: AppConfig) -> None: Litestar.from_config(app_config_object) diff --git a/tests/unit/test_cli/test_core_commands.py b/tests/unit/test_cli/test_core_commands.py index 8330604aee..46c89d68d1 100644 --- a/tests/unit/test_cli/test_core_commands.py +++ b/tests/unit/test_cli/test_core_commands.py @@ -40,16 +40,19 @@ def mock_show_app_info(mocker: MockerFixture) -> MagicMock: @pytest.mark.parametrize("custom_app_file,", [Path("my_app.py"), None]) @pytest.mark.parametrize("app_dir", ["custom_subfolder", None]) @pytest.mark.parametrize( - "reload, reload_dir, web_concurrency", + "reload, reload_dir, reload_include, reload_exclude, web_concurrency", [ - (None, None, None), - (True, None, None), - (False, None, None), - (True, [".", "../somewhere_else"], None), - (False, [".", "../somewhere_else"], None), - (None, None, 2), - (True, None, 2), - (False, None, 2), + (None, None, None, None, None), + (True, None, None, None, None), + (False, None, None, None, None), + (True, [".", "../somewhere_else"], None, None, None), + (False, [".", "../somewhere_else"], None, None, None), + (True, None, ["*.rst", "*.yml"], None, None), + (False, None, None, ["*.py"], None), + (False, None, ["*.yml", "*.rst"], None, None), + (None, None, None, None, 2), + (True, None, None, None, 2), + (False, None, None, None, 2), ], ) def test_run_command( @@ -64,6 +67,8 @@ def test_run_command( web_concurrency: Optional[int], app_dir: Optional[str], reload_dir: Optional[List[str]], + reload_include: Optional[List[str]], + reload_exclude: Optional[List[str]], custom_app_file: Optional[Path], create_app_file: CreateAppFileFixture, set_in_env: bool, @@ -131,6 +136,18 @@ def test_run_command( else: args.extend([f"--reload-dir={s}" for s in reload_dir]) + if reload_include is not None: + if set_in_env: + monkeypatch.setenv("LITESTAR_RELOAD_INCLUDES", ",".join(reload_include)) + else: + args.extend([f"--reload-include={s}" for s in reload_include]) + + if reload_exclude is not None: + if set_in_env: + monkeypatch.setenv("LITESTAR_RELOAD_EXCLUDES", ",".join(reload_exclude)) + else: + args.extend([f"--reload-exclude={s}" for s in reload_exclude]) + path = create_app_file(custom_app_file or "app.py", directory=app_dir) result = runner.invoke(cli_command, args) @@ -138,7 +155,7 @@ def test_run_command( assert result.exception is None assert result.exit_code == 0 - if reload or reload_dir or web_concurrency > 1: + if reload or reload_dir or reload_include or reload_exclude or web_concurrency > 1: expected_args = [ sys.executable, "-m", @@ -151,12 +168,16 @@ def test_run_command( expected_args.append(f"--fd={fd}") if uds is not None: expected_args.append(f"--uds={uds}") - if reload or reload_dir: + if reload or reload_dir or reload_include or reload_exclude: expected_args.append("--reload") if web_concurrency: expected_args.append(f"--workers={web_concurrency}") if reload_dir: expected_args.extend([f"--reload-dir={s}" for s in reload_dir]) + if reload_include: + expected_args.extend([f"--reload-include={s}" for s in reload_include]) + if reload_exclude: + expected_args.extend([f"--reload-exclude={s}" for s in reload_exclude]) mock_subprocess_run.assert_called_once() assert sorted(mock_subprocess_run.call_args_list[0].args[0]) == sorted(expected_args) else: diff --git a/tests/unit/test_contrib/test_attrs/test_inject_attrs_class.py b/tests/unit/test_contrib/test_attrs/test_inject_attrs_class.py new file mode 100644 index 0000000000..bc6f526b68 --- /dev/null +++ b/tests/unit/test_contrib/test_attrs/test_inject_attrs_class.py @@ -0,0 +1,20 @@ +from attrs import define + +from litestar import get +from litestar.di import Provide +from litestar.testing import create_test_client + + +def test_inject_attrs_class() -> None: + @define + class Foo: + bar: str + + @get("/", dependencies={"foo": Provide(Foo, sync_to_thread=False)}) + async def handler(foo: Foo) -> Foo: + return foo + + with create_test_client([handler]) as client: + res = client.get("/?bar=baz") + assert res.status_code == 200 + assert res.json() == {"bar": "baz"} diff --git a/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py b/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py index 2a8c897642..6df59aaedb 100644 --- a/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py +++ b/tests/unit/test_contrib/test_piccolo_orm/test_piccolo_orm_dto.py @@ -58,8 +58,20 @@ def test_serializing_single_piccolo_table(scaffold_piccolo: Callable) -> None: def test_serializing_multiple_piccolo_tables(scaffold_piccolo: Callable) -> None: with create_test_client(route_handlers=[retrieve_venues]) as client: response = client.get("/venues") + + sanitized_venues = [] + for v in venues: + non_secret_data = { + column._meta.db_column_name: v[column._meta.db_column_name] + for column in v.all_columns() + if not column._meta.secret + } + sanitized_venues.append(Venue(**non_secret_data)) + assert response.status_code == HTTP_200_OK - assert [str(Venue(**value).querystring) for value in response.json()] == [str(v.querystring) for v in venues] + assert [str(Venue(**value).querystring) for value in response.json()] == [ + str(v.querystring) for v in sanitized_venues + ] @pytest.mark.parametrize( @@ -154,7 +166,6 @@ def test_piccolo_dto_openapi_spec_generation() -> None: assert venue_schema assert venue_schema.to_schema() == { "properties": { - "capacity": {"oneOf": [{"type": "null"}, {"type": "integer"}]}, "id": {"oneOf": [{"type": "null"}, {"type": "integer"}]}, "name": {"oneOf": [{"type": "null"}, {"type": "string"}]}, }, diff --git a/tests/unit/test_contrib/test_pydantic/models.py b/tests/unit/test_contrib/test_pydantic/models.py index e2a5c1f670..cfa33f01f1 100644 --- a/tests/unit/test_contrib/test_pydantic/models.py +++ b/tests/unit/test_contrib/test_pydantic/models.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union from pydantic import BaseModel from pydantic.dataclasses import dataclass as pydantic_dataclass @@ -15,6 +15,7 @@ class PydanticDataclassPerson: id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] + union: Union[int, List[str]] pets: Optional[List[DataclassPet]] = None @@ -24,6 +25,7 @@ class PydanticPerson(BaseModel): id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] + union: Union[int, List[str]] pets: Optional[List[DataclassPet]] = None @@ -33,6 +35,7 @@ class PydanticV1Person(BaseModelV1): id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] + union: Union[int, List[str]] pets: Optional[List[DataclassPet]] = None @@ -43,4 +46,5 @@ class PydanticV1DataclassPerson: id: str optional: Optional[str] complex: Dict[str, List[Dict[str, str]]] + union: Union[int, List[str]] pets: Optional[List[DataclassPet]] = None diff --git a/tests/unit/test_contrib/test_pydantic/test_inject_pydantic.py b/tests/unit/test_contrib/test_pydantic/test_inject_pydantic.py new file mode 100644 index 0000000000..ed7a53267c --- /dev/null +++ b/tests/unit/test_contrib/test_pydantic/test_inject_pydantic.py @@ -0,0 +1,22 @@ +import pydantic as pydantic_v2 +import pytest +from pydantic import v1 as pydantic_v1 + +from litestar import get +from litestar.di import Provide +from litestar.testing import create_test_client + + +@pytest.mark.parametrize("base_model", [pydantic_v1.BaseModel, pydantic_v2.BaseModel]) +def test_inject_pydantic_model(base_model: type) -> None: + class Foo(base_model): # type: ignore[misc] + bar: str + + @get("/", dependencies={"foo": Provide(Foo, sync_to_thread=False)}) + async def handler(foo: Foo) -> Foo: + return foo + + with create_test_client([handler]) as client: + res = client.get("/?bar=baz") + assert res.status_code == 200 + assert res.json() == {"bar": "baz"} diff --git a/tests/unit/test_contrib/test_pydantic/test_integration.py b/tests/unit/test_contrib/test_pydantic/test_integration.py index 2df020302e..9a566f727d 100644 --- a/tests/unit/test_contrib/test_pydantic/test_integration.py +++ b/tests/unit/test_contrib/test_pydantic/test_integration.py @@ -98,7 +98,7 @@ def my_route_handler(param: int, data: PydanticPerson) -> None: response = client.post("/123", json={"first_name": "moishe"}) extra = response.json().get("extra") assert extra is not None - assert len(extra) == 4 + assert len(extra) == 5 def test_default_error_handling_v1() -> None: @@ -110,7 +110,7 @@ def my_route_handler(param: int, data: PydanticV1Person) -> None: response = client.post("/123", json={"first_name": "moishe"}) extra = response.json().get("extra") assert extra is not None - assert len(extra) == 3 + assert len(extra) == 4 def test_signature_model_invalid_input( @@ -172,3 +172,35 @@ def test( "key": "other_child.val.1", }, ] + + +class V1ModelWithPrivateFields(pydantic_v1.BaseModel): + class Config: + underscore_fields_are_private = True + + _field: str = pydantic_v1.PrivateAttr() + # include an invalid annotation here to ensure we never touch those fields + _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + bar: str + + +class V2ModelWithPrivateFields(pydantic_v2.BaseModel): + class Config: + underscore_fields_are_private = True + + _field: str = pydantic_v2.PrivateAttr() + # include an invalid annotation here to ensure we never touch those fields + _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + bar: str + + +@pytest.mark.parametrize("model_type", [V1ModelWithPrivateFields, V2ModelWithPrivateFields]) +def test_private_fields(model_type: Type[Union[pydantic_v1.BaseModel, pydantic_v2.BaseModel]]) -> None: + @post("/") + async def handler(data: V2ModelWithPrivateFields) -> V2ModelWithPrivateFields: + return data + + with create_test_client([handler]) as client: + res = client.post("/", json={"bar": "value"}) + assert res.status_code == 201 + assert res.json() == {"bar": "value"} diff --git a/tests/unit/test_contrib/test_pydantic/test_openapi.py b/tests/unit/test_contrib/test_pydantic/test_openapi.py index 14fa6e6491..124362062e 100644 --- a/tests/unit/test_contrib/test_pydantic/test_openapi.py +++ b/tests/unit/test_contrib/test_pydantic/test_openapi.py @@ -1,4 +1,4 @@ -from datetime import date, timedelta +from datetime import date, datetime, timedelta from decimal import Decimal from types import ModuleType from typing import Any, Callable, Pattern, Type, Union, cast @@ -257,10 +257,18 @@ def test_create_date_constrained_field_schema_pydantic_v1(annotation: Any) -> No schema = create_date_constrained_field_schema(field_definition.annotation, field_definition.kwarg_definition) assert schema.type == OpenAPIType.STRING assert schema.format == OpenAPIFormat.DATE - assert (date.fromtimestamp(schema.exclusive_minimum) if schema.exclusive_minimum else None) == annotation.gt - assert (date.fromtimestamp(schema.minimum) if schema.minimum else None) == annotation.ge - assert (date.fromtimestamp(schema.exclusive_maximum) if schema.exclusive_maximum else None) == annotation.lt - assert (date.fromtimestamp(schema.maximum) if schema.maximum else None) == annotation.le + assert (datetime.utcfromtimestamp(schema.exclusive_minimum) if schema.exclusive_minimum else None) == ( + datetime.fromordinal(annotation.gt.toordinal()) if annotation.gt is not None else None + ) + assert (datetime.utcfromtimestamp(schema.minimum) if schema.minimum else None) == ( + datetime.fromordinal(annotation.ge.toordinal()) if annotation.ge is not None else None + ) + assert (datetime.utcfromtimestamp(schema.exclusive_maximum) if schema.exclusive_maximum else None) == ( + datetime.fromordinal(annotation.lt.toordinal()) if annotation.lt is not None else None + ) + assert (datetime.utcfromtimestamp(schema.maximum) if schema.maximum else None) == ( + datetime.fromordinal(annotation.le.toordinal()) if annotation.le is not None else None + ) @pytest.mark.parametrize("annotation", constrained_dates_v2) @@ -272,22 +280,26 @@ def test_create_date_constrained_field_schema_pydantic_v2(annotation: Any) -> No assert schema.type == OpenAPIType.STRING assert schema.format == OpenAPIFormat.DATE assert any( - getattr(m, "gt", None) == (date.fromtimestamp(schema.exclusive_minimum) if schema.exclusive_minimum else None) + (datetime.fromordinal(getattr(m, "gt", None).toordinal()) if getattr(m, "gt", None) is not None else None) # type: ignore[union-attr] + == (datetime.utcfromtimestamp(schema.exclusive_minimum) if schema.exclusive_minimum else None) for m in field_definition.metadata if m ) assert any( - getattr(m, "ge", None) == (date.fromtimestamp(schema.minimum) if schema.minimum else None) + (datetime.fromordinal(getattr(m, "ge", None).toordinal()) if getattr(m, "ge", None) is not None else None) # type: ignore[union-attr] + == (datetime.utcfromtimestamp(schema.minimum) if schema.minimum else None) for m in field_definition.metadata if m ) assert any( - getattr(m, "lt", None) == (date.fromtimestamp(schema.exclusive_maximum) if schema.exclusive_maximum else None) + (datetime.fromordinal(getattr(m, "lt", None).toordinal()) if getattr(m, "lt", None) is not None else None) # type: ignore[union-attr] + == (datetime.utcfromtimestamp(schema.exclusive_maximum) if schema.exclusive_maximum else None) for m in field_definition.metadata if m ) assert any( - getattr(m, "le", None) == (date.fromtimestamp(schema.maximum) if schema.maximum else None) + (datetime.fromordinal(getattr(m, "le", None).toordinal()) if getattr(m, "le", None) is not None else None) # type: ignore[union-attr] + == (datetime.utcfromtimestamp(schema.maximum) if schema.maximum else None) for m in field_definition.metadata if m ) @@ -334,6 +346,7 @@ def handler(data: cls) -> cls: "items": {"type": "object", "additionalProperties": {"type": "string"}}, }, }, + "union": {"oneOf": [{"type": "integer"}, {"items": {"type": "string"}, "type": "array"}]}, "pets": { "oneOf": [ {"type": "null"}, @@ -345,7 +358,7 @@ def handler(data: cls) -> cls: }, }, "type": "object", - "required": ["complex", "first_name", "id", "last_name"], + "required": ["complex", "first_name", "id", "last_name", "union"], "title": f"{cls.__name__}", } @@ -549,7 +562,7 @@ def test_create_schema_for_pydantic_model_with_annotated_model_attribute( f""" {'from __future__ import annotations' if with_future_annotations else ''} from typing_extensions import Annotated -{'from pydantic import BaseModel' if pydantic_version == 'v1' else 'from pydantic.v1 import BaseModel'} +{'from pydantic import BaseModel' if pydantic_version == 'v2' else 'from pydantic.v1 import BaseModel'} class Foo(BaseModel): foo: Annotated[int, "Foo description"] diff --git a/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py b/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py index d719ae8763..df43853fd6 100644 --- a/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py +++ b/tests/unit/test_contrib/test_pydantic/test_schema_plugin.py @@ -8,6 +8,7 @@ from pydantic.v1.generics import GenericModel from typing_extensions import Annotated +from litestar._openapi.schema_generation import SchemaCreator from litestar.contrib.pydantic.pydantic_schema_plugin import PydanticSchemaPlugin from litestar.openapi.spec import OpenAPIType from litestar.openapi.spec.schema import Schema @@ -65,3 +66,64 @@ def test_schema_generation_with_generic_classes(model: Type[Union[PydanticV1Gene ) def test_is_pydantic_constrained_field(constrained: Any) -> None: PydanticSchemaPlugin.is_constrained_field(FieldDefinition.from_annotation(constrained)) + + +def test_v2_constrained_secrets() -> None: + # https://github.com/litestar-org/litestar/issues/3148 + class Model(pydantic_v2.BaseModel): + string: pydantic_v2.SecretStr = pydantic_v2.Field(min_length=1) + bytes_: pydantic_v2.SecretBytes = pydantic_v2.Field(min_length=1) + + schema = PydanticSchemaPlugin.for_pydantic_model( + FieldDefinition.from_annotation(Model), schema_creator=SchemaCreator(plugins=[PydanticSchemaPlugin()]) + ) + assert schema.properties + assert schema.properties["string"] == Schema(min_length=1, type=OpenAPIType.STRING) + assert schema.properties["bytes_"] == Schema(min_length=1, type=OpenAPIType.STRING) + + +class V1ModelWithPrivateFields(pydantic_v1.BaseModel): + class Config: + underscore_fields_are_private = True + + _field: str = pydantic_v1.PrivateAttr() + # include an invalid annotation here to ensure we never touch those fields + _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + + +class V1GenericModelWithPrivateFields(pydantic_v1.generics.GenericModel, Generic[T]): # pyright: ignore + class Config: + underscore_fields_are_private = True + + _field: str = pydantic_v1.PrivateAttr() + # include an invalid annotation here to ensure we never touch those fields + _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + + +class V2ModelWithPrivateFields(pydantic_v2.BaseModel): + _field: str = pydantic_v2.PrivateAttr() + # include an invalid annotation here to ensure we never touch those fields + _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + + +class V2GenericModelWithPrivateFields(pydantic_v2.BaseModel, Generic[T]): + _field: str = pydantic_v2.PrivateAttr() + # include an invalid annotation here to ensure we never touch those fields + _underscore_field: "foo" # type: ignore[name-defined] # noqa: F821 + + +@pytest.mark.parametrize( + "model_class", + [ + V1ModelWithPrivateFields, + V1GenericModelWithPrivateFields, + V2ModelWithPrivateFields, + V2GenericModelWithPrivateFields, + ], +) +def test_exclude_private_fields(model_class: Type[Union[pydantic_v1.BaseModel, pydantic_v2.BaseModel]]) -> None: + # https://github.com/litestar-org/litestar/issues/3150 + schema = PydanticSchemaPlugin.for_pydantic_model( + FieldDefinition.from_annotation(model_class), schema_creator=SchemaCreator(plugins=[PydanticSchemaPlugin()]) + ) + assert not schema.properties diff --git a/tests/unit/test_data_extractors.py b/tests/unit/test_data_extractors.py index 31c6103ee8..b204707bd1 100644 --- a/tests/unit/test_data_extractors.py +++ b/tests/unit/test_data_extractors.py @@ -1,6 +1,8 @@ from typing import Any, List +from unittest.mock import AsyncMock import pytest +from pytest_mock import MockFixture from litestar import Request from litestar.connection.base import empty_receive @@ -108,3 +110,18 @@ async def send(message: "Any") -> None: assert extracted_data.get("body") == b'{"hello":"world"}' assert extracted_data.get("headers") == {**headers, "content-length": "17"} assert extracted_data.get("cookies") == {"Path": "/", "SameSite": "lax", "auth": "", "regular": ""} + + +async def test_request_data_extractor_skip_keys() -> None: + req = factory.get() + extractor = ConnectionDataExtractor() + assert (await extractor.extract(req, {"body"})).keys() == {"body"} + + +async def test_skip_parse_malformed_body_false_raises(mocker: MockFixture) -> None: + mocker.patch("litestar.testing.request_factory.Request.json", new=AsyncMock(side_effect=ValueError())) + req = factory.post(headers={"Content-Type": "application/json"}) + extractor = ConnectionDataExtractor(parse_body=True, skip_parse_malformed_body=False) + + with pytest.raises(ValueError): + await extractor.extract(req, {"body"}) diff --git a/tests/unit/test_dto/test_factory/test_backends/test_backends.py b/tests/unit/test_dto/test_factory/test_backends/test_backends.py index fd57614e49..7aca7e3cca 100644 --- a/tests/unit/test_dto/test_factory/test_backends/test_backends.py +++ b/tests/unit/test_dto/test_factory/test_backends/test_backends.py @@ -207,7 +207,7 @@ def handler(data: DC) -> DC: assert c.type == "array" assert isinstance(c.items, Schema) assert c.items.type == "integer" - assert isinstance(nested := schema.properties["nested"], Reference) + assert isinstance(nested := schema.properties["nested"], Reference) # noqa: RUF018 nested_schema = schemas[nested.value] assert nested_schema.properties is not None nested_a, nested_b = nested_schema.properties["a"], nested_schema.properties["b"] diff --git a/tests/unit/test_dto/test_factory/test_integration.py b/tests/unit/test_dto/test_factory/test_integration.py index d10ca7ab23..d78496a2a3 100644 --- a/tests/unit/test_dto/test_factory/test_integration.py +++ b/tests/unit/test_dto/test_factory/test_integration.py @@ -848,3 +848,53 @@ def test(data: Optional[Foo] = None) -> dict: with create_test_client([test]) as client: response = client.post("/") assert response.json() == {"foo": None} + + +@pytest.mark.parametrize( + "field_type, constraint_name, constraint_value, request_data", + [ + (int, "gt", 2, 2), + (int, "ge", 2, 1), + (int, "lt", 2, 2), + (int, "le", 2, 3), + (int, "multiple_of", 2, 3), + (str, "min_length", 2, "1"), + (str, "max_length", 1, "12"), + (str, "pattern", r"\d", "a"), + ], +) +def test_msgspec_dto_copies_constraints( + field_type: Any, constraint_name: str, constraint_value: Any, request_data: Any, use_experimental_dto_backend: bool +) -> None: + # https://github.com/litestar-org/litestar/issues/3026 + struct = msgspec.defstruct( + "Foo", + fields=[("bar", Annotated[field_type, msgspec.Meta(**{constraint_name: constraint_value})])], # type: ignore[list-item] + ) + + @post( + "/", + dto=Annotated[MsgspecDTO[struct], DTOConfig(experimental_codegen_backend=use_experimental_dto_backend)], # type: ignore[arg-type, valid-type] + signature_namespace={"struct": struct}, + ) + def handler(data: struct) -> None: # type: ignore[valid-type] + pass + + with create_test_client([handler]) as client: + assert client.post("/", json={"bar": request_data}).status_code == 400 + + +def test_msgspec_dto_dont_copy_length_constraint_for_partial_dto() -> None: + class Foo(msgspec.Struct): + bar: Annotated[str, msgspec.Meta(min_length=2)] + baz: Annotated[str, msgspec.Meta(max_length=2)] + + class FooDTO(MsgspecDTO[Foo]): + config = DTOConfig(partial=True) + + @post("/", dto=FooDTO, signature_types={Foo}) + def handler(data: Foo) -> None: + pass + + with create_test_client([handler]) as client: + assert client.post("/", json={"bar": "1", "baz": "123"}).status_code == 201 diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index e1b5174674..fdfb064258 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -111,15 +111,13 @@ def test_create_exception_response_utility_litestar_http_exception(media_type: M @pytest.mark.parametrize("media_type", [MediaType.JSON, MediaType.TEXT]) def test_create_exception_response_utility_starlette_http_exception(media_type: MediaType) -> None: - exc = StarletteHTTPException(detail="starlette http exception", status_code=HTTP_400_BAD_REQUEST) - request = RequestFactory(handler_kwargs={"media_type": media_type}).get() - response = create_exception_response(request=request, exc=exc) - assert response.status_code == HTTP_400_BAD_REQUEST - assert response.media_type == media_type - if media_type == MediaType.JSON: - assert response.content == {"status_code": 400, "detail": "starlette http exception"} - else: - assert response.content == b'{"status_code":400,"detail":"starlette http exception"}' + @get("/", media_type=media_type) + def handler() -> str: + raise StarletteHTTPException(status_code=400) + + with create_test_client(handler) as client: + response = client.get("/", headers={"Accept": media_type}) + assert response.json() == {"status_code": 400, "detail": "Bad Request"} @pytest.mark.parametrize("media_type", [MediaType.JSON, MediaType.TEXT]) @@ -171,3 +169,30 @@ def handler() -> None: assert response.json().get("details").startswith("Traceback (most recent call last") else: assert response.text.startswith("Traceback (most recent call last") + + +def test_non_litestar_exception_with_status_code_is_500() -> None: + # https://github.com/litestar-org/litestar/issues/3082 + class MyException(Exception): + status_code: int = 400 + + @get("/") + def handler() -> None: + raise MyException("hello") + + with create_test_client([handler]) as client: + assert client.get("/").status_code == 500 + + +def test_non_litestar_exception_with_detail_is_not_included() -> None: + # https://github.com/litestar-org/litestar/issues/3082 + class MyException(Exception): + status_code: int = 400 + detail: str = "hello" + + @get("/") + def handler() -> None: + raise MyException() + + with create_test_client([handler], debug=False) as client: + assert client.get("/", headers={"Accept": MediaType.JSON}).json().get("detail") == "Internal Server Error" diff --git a/tests/unit/test_handlers/test_base_handlers/test_resolution.py b/tests/unit/test_handlers/test_base_handlers/test_resolution.py index 079c4a2cdc..12809610a4 100644 --- a/tests/unit/test_handlers/test_base_handlers/test_resolution.py +++ b/tests/unit/test_handlers/test_base_handlers/test_resolution.py @@ -52,3 +52,18 @@ async def handler(self) -> None: "controller": Provide(controller_dependency), "handler": Provide(handler_dependency), } + + +def test_resolve_dependencies_cached() -> None: + dependency = Provide(function_factory()) + + @get(dependencies={"foo": dependency}) + async def handler() -> None: + pass + + @get(dependencies={"foo": dependency}) + async def handler_2() -> None: + pass + + assert handler.resolve_dependencies() is handler.resolve_dependencies() + assert handler_2.resolve_dependencies() is handler_2.resolve_dependencies() diff --git a/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py b/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py index 231da33b97..73ed586b50 100644 --- a/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py +++ b/tests/unit/test_handlers/test_http_handlers/test_kwarg_handling.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, Optional, Type import pytest from hypothesis import given @@ -9,7 +9,6 @@ from litestar.handlers.http_handlers import HTTPRouteHandler from litestar.handlers.http_handlers._utils import get_default_status_code from litestar.status_codes import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT -from litestar.types import ResponseType from litestar.utils import normalize_path @@ -30,7 +29,7 @@ def test_route_handler_kwarg_handling( http_method: Any, media_type: MediaType, include_in_schema: bool, - response_class: Optional[ResponseType], + response_class: Optional[Type[Response]], response_headers: Any, status_code: Any, path: Any, diff --git a/tests/unit/test_logging/test_logging_config.py b/tests/unit/test_logging/test_logging_config.py index f86ad0809c..e850305d5a 100644 --- a/tests/unit/test_logging/test_logging_config.py +++ b/tests/unit/test_logging/test_logging_config.py @@ -144,6 +144,27 @@ def test_root_logger(handlers: Any, listener: Any) -> None: assert isinstance(root_logger.handlers[0], listener) # type: ignore +@pytest.mark.parametrize( + "handlers, listener", + [ + [default_handlers, StandardQueueListenerHandler], + [default_picologging_handlers, PicologgingQueueListenerHandler], + ], +) +def test_root_logger_no_config(handlers: Any, listener: Any) -> None: + logging_config = LoggingConfig(handlers=handlers, configure_root_logger=False) + get_logger = logging_config.configure() + root_logger = get_logger() + for handler in root_logger.handlers: # type: ignore[attr-defined] + root_logger.removeHandler(handler) # type: ignore[attr-defined] + get_logger = logging_config.configure() + root_logger = get_logger() + if handlers["console"]["class"] == "logging.StreamHandler": + assert not isinstance(root_logger.handlers[0], listener) # type: ignore[attr-defined] + else: + assert len(root_logger.handlers) < 1 # type: ignore[attr-defined] + + @pytest.mark.parametrize( "handlers, listener", [ @@ -159,7 +180,6 @@ def test_root_logger(handlers: Any, listener: Any) -> None: ) def test_customizing_handler(handlers: Any, listener: Any, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setitem(handlers["queue_listener"], "handlers", ["cfg://handlers.console"]) - logging_config = LoggingConfig(handlers=handlers) get_logger = logging_config.configure() root_logger = get_logger() diff --git a/tests/unit/test_logging/test_structlog_config.py b/tests/unit/test_logging/test_structlog_config.py index 4983a8f92b..4f8e695496 100644 --- a/tests/unit/test_logging/test_structlog_config.py +++ b/tests/unit/test_logging/test_structlog_config.py @@ -1,8 +1,16 @@ +import datetime +import sys +from typing import Callable + +import pytest +import structlog from pytest import CaptureFixture +from structlog import BytesLoggerFactory, get_logger from structlog.processors import JSONRenderer -from structlog.types import BindableLogger +from structlog.types import BindableLogger, WrappedLogger -from litestar.logging.config import StructLoggingConfig, default_json_serializer +from litestar.logging.config import LoggingConfig, StructlogEventFilter, StructLoggingConfig, default_json_serializer +from litestar.plugins.structlog import StructlogConfig, StructlogPlugin from litestar.serialization import decode_json from litestar.testing import create_test_client @@ -10,7 +18,94 @@ # Because we want to test processors, use capsys instead -def test_structlog_config_default(capsys: CaptureFixture) -> None: +def test_event_filter() -> None: + """Functionality test for the event filter processor.""" + event_filter = StructlogEventFilter(["a_key"]) + log_event = {"a_key": "a_val", "b_key": "b_val"} + log_event = event_filter(..., "", log_event) # type:ignore[assignment] + assert log_event == {"b_key": "b_val"} + + +def test_set_level_custom_logger_factory() -> None: + """Functionality test for the event filter processor.""" + + def custom_logger_factory() -> Callable[..., WrappedLogger]: + """Set the default logger factory for structlog. + + Returns: + An optional logger factory. + """ + return BytesLoggerFactory() + + log_config = StructLoggingConfig(logger_factory=custom_logger_factory, wrapper_class=structlog.stdlib.BoundLogger) + logger = get_logger() + assert logger.bind().__class__.__name__ != "BoundLoggerFilteringAtDebug" + log_config.set_level(logger, 10) + logger.info("a message") + assert logger.bind().__class__.__name__ == "BoundLoggerFilteringAtDebug" + + +def test_structlog_plugin(capsys: CaptureFixture) -> None: + with create_test_client([], plugins=[StructlogPlugin()]) as client: + assert client.app.logger + assert isinstance(client.app.logger.bind(), BindableLogger) + client.app.logger.info("message", key="value") + + log_messages = [decode_json(value=x) for x in capsys.readouterr().out.splitlines()] + assert len(log_messages) == 1 + + # Format should be: {event: message, key: value, level: info, timestamp: isoformat} + log_messages[0].pop("timestamp") # Assume structlog formats timestamp correctly + assert log_messages[0] == {"event": "message", "key": "value", "level": "info"} + + +def test_structlog_plugin_config(capsys: CaptureFixture) -> None: + config = StructlogConfig() + with create_test_client([], plugins=[StructlogPlugin(config=config)]) as client: + assert client.app.logger + assert isinstance(client.app.logger.bind(), BindableLogger) + client.app.logger.info("message", key="value") + + log_messages = [decode_json(value=x) for x in capsys.readouterr().out.splitlines()] + assert len(log_messages) == 1 + assert client.app.plugins.get(StructlogPlugin)._config == config + + +def test_structlog_plugin_config_custom_standard_logger() -> None: + standard_logging_config = LoggingConfig() + structlog_logging_config = StructLoggingConfig(standard_lib_logging_config=standard_logging_config) + config = StructlogConfig(structlog_logging_config=structlog_logging_config) + with create_test_client([], plugins=[StructlogPlugin(config=config)]) as client: + assert client.app.plugins.get(StructlogPlugin)._config == config + assert ( + client.app.plugins.get(StructlogPlugin)._config.structlog_logging_config.standard_lib_logging_config + == standard_logging_config + ) + + +def test_structlog_plugin_config_custom() -> None: + structlog_logging_config = StructLoggingConfig(standard_lib_logging_config=None) + config = StructlogConfig(structlog_logging_config=structlog_logging_config) + with create_test_client([], plugins=[StructlogPlugin(config=config)]) as client: + assert client.app.plugins.get(StructlogPlugin)._config == config + assert client.app.plugins.get(StructlogPlugin)._config.structlog_logging_config == structlog_logging_config + assert ( + client.app.plugins.get(StructlogPlugin)._config.structlog_logging_config.standard_lib_logging_config + is not None + ) + + +def test_structlog_plugin_config_with_existing_logging_config(capsys: CaptureFixture) -> None: + existing_log_config = StructLoggingConfig() + standard_logging_config = LoggingConfig() + structlog_logging_config = StructLoggingConfig(standard_lib_logging_config=standard_logging_config) + config = StructlogConfig(structlog_logging_config=structlog_logging_config) + with create_test_client([], logging_config=existing_log_config, plugins=[StructlogPlugin(config=config)]) as client: + assert client.app.plugins.get(StructlogPlugin)._config == config + assert "Found pre-configured" in capsys.readouterr().out + + +def test_structlog_config_no_tty_default(capsys: CaptureFixture) -> None: with create_test_client([], logging_config=StructLoggingConfig()) as client: assert client.app.logger assert isinstance(client.app.logger.bind(), BindableLogger) @@ -24,6 +119,25 @@ def test_structlog_config_default(capsys: CaptureFixture) -> None: assert log_messages[0] == {"event": "message", "key": "value", "level": "info"} +def test_structlog_config_tty_default(capsys: CaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None: + from sys import stderr + + monkeypatch.setattr(stderr, "isatty", lambda: True) + + with create_test_client([], logging_config=StructLoggingConfig()) as client: + assert client.app.logger + assert isinstance(client.app.logger.bind(), BindableLogger) + client.app.logger.info("message", key="value") + + log_messages = capsys.readouterr().out.splitlines() + assert len(log_messages) == 1 + + if sys.platform.startswith("win"): + assert log_messages[0].startswith(str(datetime.datetime.now().year)) + else: + assert log_messages[0].startswith("\x1b[") + + def test_structlog_config_specify_processors(capsys: CaptureFixture) -> None: logging_config = StructLoggingConfig(processors=[JSONRenderer(serializer=default_json_serializer)]) diff --git a/tests/unit/test_middleware/test_compression_middleware.py b/tests/unit/test_middleware/test_compression_middleware.py index e1c3ec08bb..c71784f81e 100644 --- a/tests/unit/test_middleware/test_compression_middleware.py +++ b/tests/unit/test_middleware/test_compression_middleware.py @@ -1,4 +1,6 @@ -from typing import AsyncIterator, Callable, Literal +import zlib +from io import BytesIO +from typing import AsyncIterator, Callable, Literal, Union from unittest.mock import MagicMock import pytest @@ -9,6 +11,7 @@ from litestar.exceptions import ImproperlyConfiguredException from litestar.handlers import HTTPRouteHandler from litestar.middleware.compression import CompressionMiddleware +from litestar.middleware.compression.facade import CompressionFacade from litestar.response.streaming import Stream from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client @@ -146,9 +149,9 @@ def test_config_minimum_size_validation(minimum_size: int, should_raise: bool) - def test_config_gzip_compress_level_validation(gzip_compress_level: int, should_raise: bool) -> None: if should_raise: with pytest.raises(ImproperlyConfiguredException): - CompressionConfig(backend="brotli", brotli_gzip_fallback=False, gzip_compress_level=gzip_compress_level) + CompressionConfig(backend="gzip", brotli_gzip_fallback=False, gzip_compress_level=gzip_compress_level) else: - CompressionConfig(backend="brotli", brotli_gzip_fallback=False, gzip_compress_level=gzip_compress_level) + CompressionConfig(backend="gzip", brotli_gzip_fallback=False, gzip_compress_level=gzip_compress_level) @pytest.mark.parametrize("brotli_quality, should_raise", ((0, False), (1, False), (-1, True), (12, True), (11, False))) @@ -216,3 +219,33 @@ def handler_fn() -> str: assert response.text == "_litestar_" * 4000 assert response.headers["Content-Encoding"] == compression_encoding assert int(response.headers["Content-Length"]) < 40000 + + +def test_compression_with_custom_backend(handler: HTTPRouteHandler) -> None: + class ZlibCompression(CompressionFacade): + encoding = "deflate" + + def __init__( + self, + buffer: BytesIO, + compression_encoding: Union[Literal[CompressionEncoding.GZIP], str], + config: CompressionConfig, + ) -> None: + self.buffer = buffer + self.compression_encoding = compression_encoding + self.config = config + + def write(self, body: bytes) -> None: + self.buffer.write(zlib.compress(body, level=self.config.backend_config["level"])) + + def close(self) -> None: + ... + + zlib_config = {"level": 9} + config = CompressionConfig(backend="deflate", compression_facade=ZlibCompression, backend_config=zlib_config) + with create_test_client([handler], compression_config=config) as client: + response = client.get("/", headers={"Accept-Encoding": "deflate"}) + assert response.status_code == HTTP_200_OK + assert response.text == "_litestar_" * 4000 + assert response.headers["Content-Encoding"] == "deflate" + assert int(response.headers["Content-Length"]) < 40000 diff --git a/tests/unit/test_middleware/test_cors_middleware.py b/tests/unit/test_middleware/test_cors_middleware.py index a4b71383c1..baafeba2c9 100644 --- a/tests/unit/test_middleware/test_cors_middleware.py +++ b/tests/unit/test_middleware/test_cors_middleware.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Mapping, Optional, cast +from typing import Any, Dict, List, Literal, Mapping, Optional, Union, cast import pytest @@ -7,6 +7,7 @@ from litestar.middleware.cors import CORSMiddleware from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND from litestar.testing import create_test_client +from litestar.types.asgi_types import Method def test_setting_cors_middleware() -> None: @@ -38,16 +39,31 @@ def test_setting_cors_middleware() -> None: @pytest.mark.parametrize("origin", [None, "http://www.example.com", "https://moishe.zuchmir.com"]) @pytest.mark.parametrize("allow_origins", ["*", "http://www.example.com", "https://moishe.zuchmir.com"]) @pytest.mark.parametrize("allow_credentials", [True, False]) -@pytest.mark.parametrize("expose_headers", ["X-First-Header", "SomeOtherHeader", "X-Second-Header"]) +@pytest.mark.parametrize( + "expose_headers", [["x-first-header", "x-second-header", "x-third-header"], ["*"], ["x-first-header"]] +) +@pytest.mark.parametrize( + "allow_headers", [["x-first-header", "x-second-header", "x-third-header"], ["*"], ["x-first-header"]] +) +@pytest.mark.parametrize("allow_methods", [["GET", "POST", "PUT", "DELETE"], ["GET", "POST"], ["GET"]]) def test_cors_simple_response( - origin: Optional[str], allow_origins: List[str], allow_credentials: bool, expose_headers: List[str] + origin: Optional[str], + allow_origins: List[str], + allow_credentials: bool, + expose_headers: List[str], + allow_headers: List[str], + allow_methods: List[Union[Literal["*"], "Method"]], ) -> None: @get("/") def handler() -> Dict[str, str]: return {"hello": "world"} cors_config = CORSConfig( - allow_origins=allow_origins, allow_credentials=allow_credentials, expose_headers=expose_headers + allow_origins=allow_origins, + allow_credentials=allow_credentials, + expose_headers=expose_headers, + allow_headers=allow_headers, + allow_methods=allow_methods, ) with create_test_client(handler, cors_config=cors_config) as client: @@ -58,6 +74,8 @@ def handler() -> Dict[str, str]: assert cors_config.expose_headers == expose_headers assert cors_config.allow_origins == allow_origins assert cors_config.allow_credentials == allow_credentials + assert cors_config.allow_headers == allow_headers + assert cors_config.allow_methods == allow_methods if origin: if cors_config.is_allow_all_origins: @@ -68,10 +86,20 @@ def handler() -> Dict[str, str]: assert response.headers.get("Access-Control-Expose-Headers") == ", ".join( sorted(set(cors_config.expose_headers)) ) + if cors_config.allow_headers: + assert response.headers.get("Access-Control-Allow-Headers") == ", ".join( + sorted(set(cors_config.allow_headers)) + ) + if cors_config.allow_methods: + assert response.headers.get("Access-Control-Allow-Methods") == ", ".join( + sorted(set(cors_config.allow_methods)) + ) else: assert "Access-Control-Allow-Origin" not in response.headers assert "Access-Control-Allow-Credentials" not in response.headers assert "Access-Control-Expose-Headers" not in response.headers + assert "Access-Control-Allow-Headers" not in response.headers + assert "Access-Control-Allow-Methods" not in response.headers @pytest.mark.parametrize("origin, should_apply_cors", (("http://www.example.com", True), (None, False))) diff --git a/tests/unit/test_middleware/test_exception_handler_middleware.py b/tests/unit/test_middleware/test_exception_handler_middleware.py index cb53adc88a..2c0b1334d8 100644 --- a/tests/unit/test_middleware/test_exception_handler_middleware.py +++ b/tests/unit/test_middleware/test_exception_handler_middleware.py @@ -12,7 +12,7 @@ from litestar.logging.config import LoggingConfig, StructLoggingConfig from litestar.middleware.exceptions import ExceptionHandlerMiddleware from litestar.middleware.exceptions._debug_response import get_symbol_name -from litestar.middleware.exceptions.middleware import get_exception_handler +from litestar.middleware.exceptions.middleware import _starlette_exception_handler, get_exception_handler from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR from litestar.testing import TestClient, create_test_client from litestar.types import ExceptionHandlersMap @@ -124,7 +124,8 @@ def exception_handler(request: Request, exc: Exception) -> Response: app = Litestar(route_handlers=[handler], exception_handlers={Exception: exception_handler}, openapi_config=None) assert app.asgi_router.root_route_map_node.children["/"].asgi_handlers["GET"][0].exception_handlers == { # type: ignore - Exception: exception_handler + Exception: exception_handler, + StarletteHTTPException: _starlette_exception_handler, } @@ -233,7 +234,7 @@ def handler() -> None: assert cap_logs[0].get("connection_type") == "http" assert cap_logs[0].get("path") == "/test" assert cap_logs[0].get("traceback") - assert cap_logs[0].get("event") == "uncaught exception" + assert cap_logs[0].get("event") == "Uncaught Exception" assert cap_logs[0].get("log_level") == "error" else: assert not cap_logs diff --git a/tests/unit/test_middleware/test_logging_middleware.py b/tests/unit/test_middleware/test_logging_middleware.py index 98b1bee3e1..761a088c62 100644 --- a/tests/unit/test_middleware/test_logging_middleware.py +++ b/tests/unit/test_middleware/test_logging_middleware.py @@ -1,5 +1,5 @@ from logging import INFO -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Any, Dict import pytest from structlog.testing import capture_logs @@ -286,3 +286,17 @@ async def get_session() -> None: assert response.status_code == HTTP_200_OK assert "session" in client.cookies assert client.cookies["session"] == session_id + + +def test_structlog_invalid_request_body_handled() -> None: + # https://github.com/litestar-org/litestar/issues/3063 + @post("/") + async def hello_world(data: Dict[str, Any]) -> Dict[str, Any]: + return data + + with create_test_client( + route_handlers=[hello_world], + logging_config=StructLoggingConfig(log_exceptions="always"), + middleware=[LoggingMiddlewareConfig().middleware], + ) as client: + assert client.post("/", headers={"Content-Type": "application/json"}, content=b'{"a": "b",}').status_code == 400 diff --git a/tests/unit/test_middleware/test_middleware_handling.py b/tests/unit/test_middleware/test_middleware_handling.py index 3f19ba837e..606a3cca02 100644 --- a/tests/unit/test_middleware/test_middleware_handling.py +++ b/tests/unit/test_middleware/test_middleware_handling.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, cast import pytest -from starlette.middleware import Middleware from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from litestar import Controller, Request, Response, Router, get, post @@ -56,8 +55,8 @@ async def dispatch( # type: ignore "middleware", [ BaseMiddlewareRequestLoggingMiddleware, - Middleware(MiddlewareWithArgsAndKwargs, kwarg="123Jeronimo"), - Middleware(MiddlewareProtocolRequestLoggingMiddleware, kwarg="123Jeronimo"), + # Middleware(MiddlewareWithArgsAndKwargs, kwarg="123Jeronimo"), # pyright: ignore[reportGeneralTypeIssues] # noqa: ERA001 + # Middleware(MiddlewareProtocolRequestLoggingMiddleware, kwarg="123Jeronimo"), # type: ignore[arg-type] # pyright: ignore[reportGeneralTypeIssues] # noqa: ERA001 DefineMiddleware(MiddlewareWithArgsAndKwargs, 1, kwarg="123Jeronimo"), # type: ignore[arg-type] DefineMiddleware(MiddlewareProtocolRequestLoggingMiddleware, kwarg="123Jeronimo"), ], diff --git a/tests/unit/test_middleware/test_session/test_client_side_backend.py b/tests/unit/test_middleware/test_session/test_client_side_backend.py index b53bfcc2c3..782fa9f561 100644 --- a/tests/unit/test_middleware/test_session/test_client_side_backend.py +++ b/tests/unit/test_middleware/test_session/test_client_side_backend.py @@ -113,16 +113,28 @@ def handler(request: Request) -> None: # Then you only need to check if number of cookies set are more than the multiplying number. request.session.update(create_session(size=CHUNK_SIZE * chunks_multiplier)) + @get(path="/test_short_cookie") + def handler_short_cookie(request: Request) -> None: + # Check the naming of a cookie that's short enough to not get broken into chunks + request.session.update(create_session()) + with create_test_client( route_handlers=[handler], middleware=[cookie_session_backend_config.middleware], ) as client: response = client.get("/test") - assert len(response.cookies) > chunks_multiplier - # If it works for the multiple chunks of session, it works for the single chunk too. So, just check if "session-0" - # exists. - assert "session-0" in response.cookies + assert len(response.cookies) > chunks_multiplier + assert "session-0" in response.cookies + + with create_test_client( + route_handlers=[handler_short_cookie], + middleware=[cookie_session_backend_config.middleware], + ) as client: + response = client.get("/test_short_cookie") + + assert len(response.cookies) == 1 + assert "session" in response.cookies def test_session_cookie_name_matching(cookie_session_backend_config: "CookieBackendConfig") -> None: diff --git a/tests/unit/test_openapi/test_parameters.py b/tests/unit/test_openapi/test_parameters.py index 6c01b47f80..6439322883 100644 --- a/tests/unit/test_openapi/test_parameters.py +++ b/tests/unit/test_openapi/test_parameters.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING, List, Optional, Type, cast +from uuid import UUID import pytest from typing_extensions import Annotated @@ -324,3 +325,21 @@ async def index( assert response.json()["paths"]["/"]["get"]["parameters"][0]["examples"] == { "text-example-1": {"summary": "example summary", "value": "example value"} } + + +def test_uuid_path_description_generation() -> None: + # https://github.com/litestar-org/litestar/issues/2967 + @get("str/{id:str}") + async def str_path(id: Annotated[str, Parameter(description="String ID")]) -> str: + return id + + @get("uuid/{id:uuid}") + async def uuid_path(id: Annotated[UUID, Parameter(description="UUID ID")]) -> UUID: + return id + + with create_test_client( + [str_path, uuid_path], openapi_config=OpenAPIConfig(title="Test API", version="1.0.0") + ) as client: + response = client.get("/schema/openapi.json") + assert response.json()["paths"]["/str/{id}"]["get"]["parameters"][0]["description"] == "String ID" + assert response.json()["paths"]["/uuid/{id}"]["get"]["parameters"][0]["description"] == "UUID ID" diff --git a/tests/unit/test_openapi/test_responses.py b/tests/unit/test_openapi/test_responses.py index c8df50d4a2..1788dd1064 100644 --- a/tests/unit/test_openapi/test_responses.py +++ b/tests/unit/test_openapi/test_responses.py @@ -11,7 +11,7 @@ import pytest from typing_extensions import TypeAlias -from litestar import Controller, Litestar, MediaType, Response, get, post +from litestar import Controller, Litestar, MediaType, Response, delete, get, post from litestar._openapi.datastructures import OpenAPIContext from litestar._openapi.responses import ( ResponseFactory, @@ -35,6 +35,7 @@ from litestar.routes import HTTPRoute from litestar.status_codes import ( HTTP_200_OK, + HTTP_204_NO_CONTENT, HTTP_307_TEMPORARY_REDIRECT, HTTP_400_BAD_REQUEST, HTTP_406_NOT_ACCEPTABLE, @@ -282,6 +283,29 @@ def redirect_handler() -> Redirect: assert location.description +def test_create_success_response_no_content_explicit_responsespec( + create_factory: CreateFactoryFixture, +) -> None: + @delete( + path="/test", + responses={HTTP_204_NO_CONTENT: ResponseSpec(None, description="Custom description")}, + name="test", + ) + def handler() -> None: + return None + + handler = get_registered_route_handler(handler, "test") + factory = create_factory(handler) + responses = factory.create_additional_responses() + status, response = next(responses) + assert status == "204" + assert response.description == "Custom description" + assert not response.content + + with pytest.raises(StopIteration): + next(responses) + + def test_create_success_response_file_data(create_factory: CreateFactoryFixture) -> None: @get(path="/test", name="test") def file_handler() -> File: diff --git a/tests/unit/test_openapi/test_schema.py b/tests/unit/test_openapi/test_schema.py index 45744408c5..625287e593 100644 --- a/tests/unit/test_openapi/test_schema.py +++ b/tests/unit/test_openapi/test_schema.py @@ -1,6 +1,6 @@ import sys from dataclasses import dataclass -from datetime import date +from datetime import date, datetime from enum import Enum, auto from typing import ( # type: ignore[attr-defined] TYPE_CHECKING, @@ -305,10 +305,10 @@ class MyDataclass: constrained_int: Annotated[int, annotated_types.Gt(1), annotated_types.Lt(10)] constrained_float: Annotated[float, annotated_types.Ge(1), annotated_types.Le(10)] constrained_date: Annotated[date, annotated_types.Interval(gt=historical_date, lt=today)] - constrainted_lower_case: Annotated[str, annotated_types.LowerCase] - constrainted_upper_case: Annotated[str, annotated_types.UpperCase] - constrainted_is_ascii: Annotated[str, annotated_types.IsAscii] - constrainted_is_digit: Annotated[str, annotated_types.IsDigits] + constrained_lower_case: Annotated[str, annotated_types.LowerCase] + constrained_upper_case: Annotated[str, annotated_types.UpperCase] + constrained_is_ascii: Annotated[str, annotated_types.IsAscii] + constrained_is_digit: Annotated[str, annotated_types.IsDigits] schema = get_schema_for_field_definition(FieldDefinition.from_kwarg(name="MyDataclass", annotation=MyDataclass)) @@ -316,12 +316,16 @@ class MyDataclass: assert schema.properties["constrained_int"].exclusive_maximum == 10 # type: ignore assert schema.properties["constrained_float"].minimum == 1 # type: ignore assert schema.properties["constrained_float"].maximum == 10 # type: ignore - assert date.fromtimestamp(schema.properties["constrained_date"].exclusive_minimum) == historical_date # type: ignore - assert date.fromtimestamp(schema.properties["constrained_date"].exclusive_maximum) == today # type: ignore - assert schema.properties["constrainted_lower_case"].description == "must be in lower case" # type: ignore - assert schema.properties["constrainted_upper_case"].description == "must be in upper case" # type: ignore - assert schema.properties["constrainted_is_ascii"].pattern == "[[:ascii:]]" # type: ignore - assert schema.properties["constrainted_is_digit"].pattern == "[[:digit:]]" # type: ignore + assert datetime.utcfromtimestamp(schema.properties["constrained_date"].exclusive_minimum) == datetime.fromordinal( # type: ignore + historical_date.toordinal() + ) + assert datetime.utcfromtimestamp(schema.properties["constrained_date"].exclusive_maximum) == datetime.fromordinal( # type: ignore + today.toordinal() + ) + assert schema.properties["constrained_lower_case"].description == "must be in lower case" # type: ignore + assert schema.properties["constrained_upper_case"].description == "must be in upper case" # type: ignore + assert schema.properties["constrained_is_ascii"].pattern == "[[:ascii:]]" # type: ignore + assert schema.properties["constrained_is_digit"].pattern == "[[:digit:]]" # type: ignore def test_literal_enums() -> None: @@ -504,6 +508,7 @@ class ModelA: # pyright: ignore @dataclass class ModelB: # pyright: ignore pass + else: class ModelA(base_type): # type: ignore[no-redef, misc] @@ -533,6 +538,7 @@ class ModelA: # pyright: ignore @dataclass class ModelB: # pyright: ignore pass + else: class ModelA(base_type): # type: ignore[no-redef, misc] diff --git a/tests/unit/test_openapi/test_spec_generation.py b/tests/unit/test_openapi/test_spec_generation.py index f3287093ac..f64bd3f569 100644 --- a/tests/unit/test_openapi/test_spec_generation.py +++ b/tests/unit/test_openapi/test_spec_generation.py @@ -5,7 +5,10 @@ import pytest from msgspec import Struct -from litestar import post +from litestar import delete, post +from litestar.openapi import ResponseSpec +from litestar.openapi.spec import OpenAPI +from litestar.status_codes import HTTP_204_NO_CONTENT from litestar.testing import create_test_client from tests.models import DataclassPerson, MsgSpecStructPerson, TypedDictPerson @@ -48,6 +51,33 @@ def handler(data: cls) -> cls: } +def test_spec_generation_no_content() -> None: + @delete( + "/", + status_code=HTTP_204_NO_CONTENT, + responses={204: ResponseSpec(None, description="Custom response")}, + ) + def handler() -> None: + return None + + with create_test_client(handler) as client: + schema: OpenAPI = client.app.openapi_schema + assert schema.to_schema()["paths"] == { + "/": { + "delete": { + "summary": "Handler", + "deprecated": False, + "operationId": "Handler", + "responses": { + "204": { + "description": "Custom response", + } + }, + }, + }, + } + + def test_msgspec_schema() -> None: class CamelizedStruct(Struct, rename="camel"): field_one: int diff --git a/tests/unit/test_plugins/test_base.py b/tests/unit/test_plugins/test_base.py index d0853421d0..8598eb5c64 100644 --- a/tests/unit/test_plugins/test_base.py +++ b/tests/unit/test_plugins/test_base.py @@ -8,9 +8,10 @@ from litestar import Litestar, MediaType, get from litestar.constants import UNDEFINED_SENTINELS from litestar.contrib.attrs import AttrsSchemaPlugin -from litestar.contrib.pydantic import PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin +from litestar.contrib.pydantic import PydanticDIPlugin, PydanticInitPlugin, PydanticPlugin, PydanticSchemaPlugin from litestar.contrib.sqlalchemy.plugins import SQLAlchemySerializationPlugin from litestar.plugins import CLIPluginProtocol, InitPluginProtocol, OpenAPISchemaPlugin, PluginRegistry +from litestar.plugins.core import MsgspecDIPlugin from litestar.testing import create_test_client from litestar.typing import FieldDefinition @@ -81,6 +82,27 @@ def on_cli_init(self, cli: Group) -> None: assert PluginRegistry([cli_plugin]).get(CLIPlugin) is cli_plugin +def test_plugin_registry_stringified_get() -> None: + class CLIPlugin(CLIPluginProtocol): + def on_cli_init(self, cli: Group) -> None: + pass + + cli_plugin = CLIPlugin() + pydantic_plugin = PydanticPlugin() + with pytest.raises(KeyError): + PluginRegistry([CLIPlugin()]).get( + "litestar2.contrib.pydantic.PydanticPlugin" + ) # not a fqdn. should fail # type: ignore[list-item] + PluginRegistry([]).get("CLIPlugin") # not a fqdn. should fail # type: ignore[list-item] + + assert PluginRegistry([cli_plugin, pydantic_plugin]).get(CLIPlugin) is cli_plugin + assert PluginRegistry([cli_plugin, pydantic_plugin]).get(PydanticPlugin) is pydantic_plugin + assert PluginRegistry([cli_plugin, pydantic_plugin]).get("PydanticPlugin") is pydantic_plugin + assert ( + PluginRegistry([cli_plugin, pydantic_plugin]).get("litestar.contrib.pydantic.PydanticPlugin") is pydantic_plugin + ) + + def test_openapi_schema_plugin_is_constrained_field() -> None: assert OpenAPISchemaPlugin.is_constrained_field(FieldDefinition.from_annotation(str)) is False @@ -100,6 +122,17 @@ def test_app_get_default_plugins( any_pydantic = bool(init_plugin) or bool(schema_plugin) default_plugins = Litestar._get_default_plugins(plugins) # type: ignore[arg-type] if not any_pydantic: - assert {type(p) for p in default_plugins} == {PydanticPlugin, AttrsSchemaPlugin} + assert {type(p) for p in default_plugins} == { + PydanticPlugin, + AttrsSchemaPlugin, + PydanticDIPlugin, + MsgspecDIPlugin, + } else: - assert {type(p) for p in default_plugins} == {PydanticInitPlugin, PydanticSchemaPlugin, AttrsSchemaPlugin} + assert {type(p) for p in default_plugins} == { + PydanticInitPlugin, + PydanticSchemaPlugin, + AttrsSchemaPlugin, + PydanticDIPlugin, + MsgspecDIPlugin, + } diff --git a/tests/unit/test_response/test_sse.py b/tests/unit/test_response/test_sse.py index eade2426d5..bff7af75c2 100644 --- a/tests/unit/test_response/test_sse.py +++ b/tests/unit/test_response/test_sse.py @@ -1,4 +1,4 @@ -from typing import Any, AsyncIterator, Iterator, List +from typing import AsyncIterator, Iterator, List import anyio import pytest @@ -10,6 +10,7 @@ from litestar.response import ServerSentEvent from litestar.response.sse import ServerSentEventMessage from litestar.testing import create_async_test_client +from litestar.types import SSEData async def test_sse_steaming_response() -> None: @@ -61,7 +62,7 @@ def numbers(minimum: int, maximum: int) -> Iterator[str]: async def test_various_sse_inputs(input: str, expected_events: List[HTTPXServerSentEvent]) -> None: @get("/testme") async def handler() -> ServerSentEvent: - async def numbers() -> AsyncIterator[Any]: + async def numbers() -> AsyncIterator[SSEData]: for i in range(1, 6): await anyio.sleep(0.001) if input == "integer": diff --git a/tests/unit/test_static_files/conftest.py b/tests/unit/test_static_files/conftest.py new file mode 100644 index 0000000000..01d70d2bb1 --- /dev/null +++ b/tests/unit/test_static_files/conftest.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import asdict +from typing import Callable + +import pytest +from _pytest.fixtures import FixtureRequest +from fsspec.implementations.local import LocalFileSystem +from typing_extensions import TypeAlias + +from litestar import Router +from litestar.file_system import BaseLocalFileSystem +from litestar.static_files import StaticFilesConfig, create_static_files_router +from litestar.types import FileSystemProtocol + +MakeConfig: TypeAlias = "Callable[[StaticFilesConfig], tuple[list[StaticFilesConfig], list[Router]]]" + + +@pytest.fixture(params=["config", "handlers"]) +def make_config(request: FixtureRequest) -> MakeConfig: + def make(config: StaticFilesConfig) -> tuple[list[StaticFilesConfig], list[Router]]: + if request.param == "config": + return [config], [] + return [], [create_static_files_router(**asdict(config))] + + return make + + +@pytest.fixture(params=[BaseLocalFileSystem(), LocalFileSystem()]) +def file_system(request: FixtureRequest) -> FileSystemProtocol: + return request.param # type: ignore[no-any-return] diff --git a/tests/unit/test_static_files/test_create_static_router.py b/tests/unit/test_static_files/test_create_static_router.py new file mode 100644 index 0000000000..ffab6383ca --- /dev/null +++ b/tests/unit/test_static_files/test_create_static_router.py @@ -0,0 +1,96 @@ +from pathlib import Path +from typing import Any, Optional + +import pytest + +from litestar import Litestar, Request, Response, Router +from litestar.connection import ASGIConnection +from litestar.datastructures import CacheControlHeader +from litestar.exceptions import ValidationException +from litestar.handlers import BaseRouteHandler +from litestar.static_files import create_static_files_router +from litestar.status_codes import HTTP_200_OK +from litestar.testing.helpers import create_test_client + + +def test_route_reverse() -> None: + app = Litestar( + route_handlers=[create_static_files_router(path="/static", directories=["something"], name="static")] + ) + + assert app.route_reverse("static", file_path="foo.py") == "/static/foo.py" + + +def test_pass_options() -> None: + def guard(connection: ASGIConnection, handler: BaseRouteHandler) -> None: + pass + + def handle(request: Request, exception: Any) -> Response: + return Response(b"") + + async def after_request(response: Response) -> Response: + return Response(b"") + + async def after_response(request: Request) -> None: + pass + + async def before_request(request: Request) -> Any: + pass + + exception_handlers = {ValidationException: handle} + opts = {"foo": "bar"} + cache_control = CacheControlHeader() + security = [{"foo": ["bar"]}] + tags = ["static", "random"] + + router = create_static_files_router( + path="/", + directories=["something"], + guards=[guard], + exception_handlers=exception_handlers, # type: ignore[arg-type] + opt=opts, + after_request=after_request, + after_response=after_response, + before_request=before_request, + cache_control=cache_control, + include_in_schema=False, + security=security, + tags=tags, + ) + + assert router.guards == [guard] + assert router.exception_handlers == exception_handlers + assert router.opt == opts + assert router.after_request is after_request + assert router.after_response is after_response + assert router.before_request is before_request + assert router.cache_control is cache_control + assert router.include_in_schema is False + assert router.security == security + assert router.tags == tags + + +def test_custom_router_class() -> None: + class MyRouter(Router): + pass + + router = create_static_files_router("/", directories=["some"], router_class=MyRouter) + assert isinstance(router, MyRouter) + + +@pytest.mark.parametrize("cache_control", (None, CacheControlHeader(max_age=3600))) +def test_cache_control(tmp_path: Path, cache_control: Optional[CacheControlHeader]) -> None: + static_dir = tmp_path / "foo" + static_dir.mkdir() + static_dir.joinpath("test.txt").write_text("hello") + + router = create_static_files_router("/static", [static_dir], name="static", cache_control=cache_control) + + with create_test_client([router]) as client: + response = client.get("static/test.txt") + + assert response.status_code == HTTP_200_OK + if cache_control is not None: + assert response.headers["cache-control"] == cache_control.to_header() + else: + assert "cache-control" not in response.headers diff --git a/tests/unit/test_static_files/test_file_serving_resolution.py b/tests/unit/test_static_files/test_file_serving_resolution.py index 564799a758..3bc9c70d6a 100644 --- a/tests/unit/test_static_files/test_file_serving_resolution.py +++ b/tests/unit/test_static_files/test_file_serving_resolution.py @@ -1,125 +1,145 @@ +from __future__ import annotations + import gzip import mimetypes from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable import brotli import pytest -from fsspec.implementations.local import LocalFileSystem +from typing_extensions import TypeAlias -from litestar import MediaType, get -from litestar.file_system import BaseLocalFileSystem -from litestar.static_files.config import StaticFilesConfig +from litestar import MediaType, Router, get +from litestar.static_files import StaticFilesConfig, create_static_files_router from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client +from tests.unit.test_static_files.conftest import MakeConfig if TYPE_CHECKING: from litestar.types import FileSystemProtocol -def test_default_static_files_config(tmpdir: "Path") -> None: +def test_default_static_files_config(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir / "test.txt" path.write_text("content", "utf-8") - static_files_config = StaticFilesConfig(path="/static", directories=[tmpdir]) + static_files_config, router = make_config(StaticFilesConfig(path="/static", directories=[tmpdir])) - with create_test_client([], static_files_config=[static_files_config]) as client: + with create_test_client(router, static_files_config=static_files_config) as client: response = client.get("/static/test.txt") assert response.status_code == HTTP_200_OK, response.text assert response.text == "content" -def test_multiple_static_files_configs(tmpdir: "Path") -> None: - root1 = tmpdir.mkdir("1") # type: ignore - root2 = tmpdir.mkdir("2") # type: ignore - path1 = root1 / "test.txt" # pyright: ignore - path1.write_text("content1", "utf-8") - path2 = root2 / "test.txt" # pyright: ignore - path2.write_text("content2", "utf-8") +@pytest.fixture() +def setup_dirs(tmpdir: Path) -> tuple[Path, Path]: + paths = [] + for i in range(1, 3): + root = tmpdir / str(i) + root.mkdir() + file_path = root / f"test_{i}.txt" + file_path.write_text(f"content{i}", "utf-8") + paths.append(root) + + return paths[0], paths[1] + + +MakeConfigs: TypeAlias = ( + "Callable[[StaticFilesConfig, StaticFilesConfig], tuple[list[StaticFilesConfig], list[Router]]]" +) + - static_files_config = [ +@pytest.fixture() +def make_configs(make_config: MakeConfig) -> MakeConfigs: + def make( + first_config: StaticFilesConfig, second_config: StaticFilesConfig + ) -> tuple[list[StaticFilesConfig], list[Router]]: + configs_1, routers_1 = make_config(first_config) + configs_2, routers_2 = make_config(second_config) + return [*configs_1, *configs_2], [*routers_1, *routers_2] + + return make + + +def test_multiple_static_files_configs(setup_dirs: tuple[Path, Path], make_configs: MakeConfigs) -> None: + root1, root2 = setup_dirs + + configs, handlers = make_configs( StaticFilesConfig(path="/static_first", directories=[root1]), # pyright: ignore StaticFilesConfig(path="/static_second", directories=[root2]), # pyright: ignore - ] - with create_test_client([], static_files_config=static_files_config) as client: - response = client.get("/static_first/test.txt") + ) + with create_test_client(handlers, static_files_config=configs) as client: + response = client.get("/static_first/test_1.txt") assert response.status_code == HTTP_200_OK assert response.text == "content1" - response = client.get("/static_second/test.txt") + response = client.get("/static_second/test_2.txt") assert response.status_code == HTTP_200_OK assert response.text == "content2" -@pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) -def test_static_files_configs_with_mixed_file_systems(tmpdir: "Path", file_system: "FileSystemProtocol") -> None: - root1 = tmpdir.mkdir("1") # type: ignore - root2 = tmpdir.mkdir("2") # type: ignore - path1 = root1 / "test.txt" # pyright: ignore - path1.write_text("content1", "utf-8") - path2 = root2 / "test.txt" # pyright: ignore - path2.write_text("content2", "utf-8") +def test_static_files_configs_with_mixed_file_systems( + file_system: FileSystemProtocol, setup_dirs: tuple[Path, Path], make_configs: MakeConfigs +) -> None: + root1, root2 = setup_dirs - static_files_config = [ + configs, handlers = make_configs( StaticFilesConfig(path="/static_first", directories=[root1], file_system=file_system), # pyright: ignore StaticFilesConfig(path="/static_second", directories=[root2]), # pyright: ignore - ] - with create_test_client([], static_files_config=static_files_config) as client: - response = client.get("/static_first/test.txt") + ) + + with create_test_client(handlers, static_files_config=configs) as client: + response = client.get("/static_first/test_1.txt") assert response.status_code == HTTP_200_OK assert response.text == "content1" - response = client.get("/static_second/test.txt") + response = client.get("/static_second/test_2.txt") assert response.status_code == HTTP_200_OK assert response.text == "content2" -@pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) -def test_static_files_config_with_multiple_directories(tmpdir: "Path", file_system: "FileSystemProtocol") -> None: - root1 = tmpdir.mkdir("first") # type: ignore - root2 = tmpdir.mkdir("second") # type: ignore - path1 = root1 / "test1.txt" # pyright: ignore - path1.write_text("content1", "utf-8") - path2 = root2 / "test2.txt" # pyright: ignore - path2.write_text("content2", "utf-8") +def test_static_files_config_with_multiple_directories( + file_system: FileSystemProtocol, setup_dirs: tuple[Path, Path], make_config: MakeConfig +) -> None: + root1, root2 = setup_dirs + configs, handlers = make_config( + StaticFilesConfig(path="/static", directories=[root1, root2], file_system=file_system) + ) - with create_test_client( - [], - static_files_config=[ - StaticFilesConfig(path="/static", directories=[root1, root2], file_system=file_system) # pyright: ignore - ], - ) as client: - response = client.get("/static/test1.txt") + with create_test_client(handlers, static_files_config=configs) as client: + response = client.get("/static/test_1.txt") assert response.status_code == HTTP_200_OK assert response.text == "content1" - response = client.get("/static/test2.txt") + response = client.get("/static/test_2.txt") assert response.status_code == HTTP_200_OK assert response.text == "content2" -def test_staticfiles_for_slash_path_regular_mode(tmpdir: "Path") -> None: +def test_staticfiles_for_slash_path_regular_mode(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") - static_files_config = StaticFilesConfig(path="/", directories=[tmpdir]) - with create_test_client([], static_files_config=[static_files_config]) as client: + configs, handlers = make_config(StaticFilesConfig(path="/", directories=[tmpdir])) + + with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/text.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" -def test_staticfiles_for_slash_path_html_mode(tmpdir: "Path") -> None: +def test_staticfiles_for_slash_path_html_mode(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir / "index.html" path.write_text("", "utf-8") - static_files_config = StaticFilesConfig(path="/", directories=[tmpdir], html_mode=True) - with create_test_client([], static_files_config=[static_files_config]) as client: + configs, handlers = make_config(StaticFilesConfig(path="/", directories=[tmpdir], html_mode=True)) + + with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/") assert response.status_code == HTTP_200_OK assert response.text == "" -def test_sub_path_under_static_path(tmpdir: "Path") -> None: +def test_sub_path_under_static_path(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir / "test.txt" path.write_text("content", "utf-8") @@ -127,9 +147,10 @@ def test_sub_path_under_static_path(tmpdir: "Path") -> None: def handler(f: str) -> str: return f - with create_test_client( - handler, static_files_config=[StaticFilesConfig(path="/static", directories=[tmpdir])] - ) as client: + configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir])) + handlers.append(handler) # type: ignore[arg-type] + + with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/test.txt") assert response.status_code == HTTP_200_OK @@ -137,26 +158,26 @@ def handler(f: str) -> str: assert response.status_code == HTTP_200_OK -def test_static_substring_of_self(tmpdir: "Path") -> None: +def test_static_substring_of_self(tmpdir: Path, make_config: MakeConfig) -> None: path = tmpdir.mkdir("static_part").mkdir("static") / "test.txt" # type: ignore path.write_text("content", "utf-8") - static_files_config = StaticFilesConfig(path="/static", directories=[tmpdir]) - with create_test_client([], static_files_config=[static_files_config]) as client: + configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir])) + with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/static_part/static/test.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" @pytest.mark.parametrize("extension", ["css", "js", "html", "json"]) -def test_static_files_response_mimetype(tmpdir: "Path", extension: str) -> None: +def test_static_files_response_mimetype(tmpdir: Path, extension: str, make_config: MakeConfig) -> None: fn = f"test.{extension}" path = tmpdir / fn path.write_text("content", "utf-8") - static_files_config = StaticFilesConfig(path="/static", directories=[tmpdir]) + configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmpdir])) expected_mime_type = mimetypes.guess_type(fn)[0] - with create_test_client([], static_files_config=[static_files_config]) as client: + with create_test_client(handlers, static_files_config=configs) as client: response = client.get(f"/static/{fn}") assert expected_mime_type assert response.status_code == HTTP_200_OK @@ -164,7 +185,7 @@ def test_static_files_response_mimetype(tmpdir: "Path", extension: str) -> None: @pytest.mark.parametrize("extension", ["gz", "br"]) -def test_static_files_response_encoding(tmp_path: "Path", extension: str) -> None: +def test_static_files_response_encoding(tmp_path: Path, extension: str, make_config: MakeConfig) -> None: fn = f"test.js.{extension}" path = tmp_path / fn compressed_data = None @@ -173,10 +194,11 @@ def test_static_files_response_encoding(tmp_path: "Path", extension: str) -> Non elif extension == "gz": compressed_data = gzip.compress(b"content") path.write_bytes(compressed_data) # type: ignore[arg-type] - static_files_config = StaticFilesConfig(path="/static", directories=[tmp_path]) expected_encoding_type = mimetypes.guess_type(fn)[1] - with create_test_client([], static_files_config=[static_files_config]) as client: + configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[tmp_path])) + + with create_test_client(handlers, static_files_config=configs) as client: response = client.get(f"/static/{fn}") assert expected_encoding_type assert response.status_code == HTTP_200_OK @@ -184,45 +206,51 @@ def test_static_files_response_encoding(tmp_path: "Path", extension: str) -> Non @pytest.mark.parametrize("send_as_attachment,disposition", [(True, "attachment"), (False, "inline")]) -def test_static_files_content_disposition(tmpdir: "Path", send_as_attachment: bool, disposition: str) -> None: +def test_static_files_content_disposition( + tmpdir: Path, send_as_attachment: bool, disposition: str, make_config: MakeConfig +) -> None: path = tmpdir.mkdir("static_part").mkdir("static") / "test.txt" # type: ignore path.write_text("content", "utf-8") - static_files_config = StaticFilesConfig(path="/static", directories=[tmpdir], send_as_attachment=send_as_attachment) + configs, handlers = make_config( + StaticFilesConfig(path="/static", directories=[tmpdir], send_as_attachment=send_as_attachment) + ) - with create_test_client([], static_files_config=[static_files_config]) as client: + with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/static_part/static/test.txt") assert response.status_code == HTTP_200_OK assert response.headers["content-disposition"].startswith(disposition) -def test_service_from_relative_path_using_string(tmpdir: "Path") -> None: +def test_service_from_relative_path_using_string(tmpdir: Path, make_config: MakeConfig) -> None: sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore path = tmpdir / "test.txt" path.write_text("content", "utf-8") - static_files_config = StaticFilesConfig(path="/static", directories=[f"{sub_dir}/.."]) - with create_test_client([], static_files_config=[static_files_config]) as client: + configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[f"{sub_dir}/.."])) + + with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/test.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" -def test_service_from_relative_path_using_path(tmpdir: "Path") -> None: +def test_service_from_relative_path_using_path(tmpdir: Path, make_config: MakeConfig) -> None: sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore path = tmpdir / "test.txt" path.write_text("content", "utf-8") - static_files_config = StaticFilesConfig(path="/static", directories=[Path(f"{sub_dir}/..")]) - with create_test_client([], static_files_config=[static_files_config]) as client: + configs, handlers = make_config(StaticFilesConfig(path="/static", directories=[Path(f"{sub_dir}/..")])) + + with create_test_client(handlers, static_files_config=configs) as client: response = client.get("/static/test.txt") assert response.status_code == HTTP_200_OK assert response.text == "content" -def test_service_from_base_path_using_string(tmpdir: "Path") -> None: +def test_service_from_base_path_using_string(tmpdir: Path) -> None: sub_dir = Path(tmpdir.mkdir("low")).resolve() # type: ignore path = tmpdir / "test.txt" @@ -249,3 +277,21 @@ def sub_handler() -> dict: response = client.get("/sub") assert response.status_code == HTTP_200_OK assert response.json() == {"hello": "world"} + + +@pytest.mark.parametrize("resolve", [True, False]) +def test_resolve_symlinks(tmp_path: Path, resolve: bool) -> None: + source_dir = tmp_path / "foo" + source_dir.mkdir() + linked_dir = tmp_path / "bar" + linked_dir.symlink_to(source_dir, target_is_directory=True) + source_dir.joinpath("test.txt").write_text("hello") + + router = create_static_files_router(path="/", directories=[linked_dir], resolve_symlinks=resolve) + + with create_test_client(router) as client: + if not resolve: + linked_dir.unlink() + assert client.get("/test.txt").status_code == 404 + else: + assert client.get("/test.txt").status_code == 200 diff --git a/tests/unit/test_static_files/test_html_mode.py b/tests/unit/test_static_files/test_html_mode.py index fa58044da8..c1f792d55a 100644 --- a/tests/unit/test_static_files/test_html_mode.py +++ b/tests/unit/test_static_files/test_html_mode.py @@ -1,12 +1,11 @@ -from typing import TYPE_CHECKING +from __future__ import annotations -import pytest -from fsspec.implementations.local import LocalFileSystem +from typing import TYPE_CHECKING -from litestar.file_system import BaseLocalFileSystem -from litestar.static_files.config import StaticFilesConfig +from litestar.static_files import StaticFilesConfig from litestar.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND from litestar.testing import create_test_client +from tests.unit.test_static_files.conftest import MakeConfig if TYPE_CHECKING: from pathlib import Path @@ -14,14 +13,14 @@ from litestar.types import FileSystemProtocol -@pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) -def test_staticfiles_is_html_mode(tmpdir: "Path", file_system: "FileSystemProtocol") -> None: +def test_staticfiles_is_html_mode(tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig) -> None: path = tmpdir / "index.html" path.write_text("content", "utf-8") - static_files_config = StaticFilesConfig( - path="/static", directories=[tmpdir], html_mode=True, file_system=file_system + static_files_config, handlers = make_config( + StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system) ) - with create_test_client([], static_files_config=[static_files_config]) as client: + + with create_test_client(handlers, static_files_config=static_files_config) as client: response = client.get("/static") assert response.status_code == HTTP_200_OK assert response.text == "content" @@ -29,28 +28,28 @@ def test_staticfiles_is_html_mode(tmpdir: "Path", file_system: "FileSystemProtoc assert response.headers["content-disposition"].startswith("inline") -@pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) -def test_staticfiles_is_html_mode_serves_404_when_present(tmpdir: "Path", file_system: "FileSystemProtocol") -> None: +def test_staticfiles_is_html_mode_serves_404_when_present( + tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig +) -> None: path = tmpdir / "404.html" path.write_text("not found", "utf-8") - static_files_config = StaticFilesConfig( - path="/static", directories=[tmpdir], html_mode=True, file_system=file_system + static_files_config, handlers = make_config( + StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system) ) - with create_test_client([], static_files_config=[static_files_config]) as client: + with create_test_client(handlers, static_files_config=static_files_config) as client: response = client.get("/static") assert response.status_code == HTTP_404_NOT_FOUND assert response.text == "not found" assert response.headers["content-type"] == "text/html; charset=utf-8" -@pytest.mark.parametrize("file_system", (BaseLocalFileSystem(), LocalFileSystem())) def test_staticfiles_is_html_mode_raises_exception_when_no_404_html_is_present( - tmpdir: "Path", file_system: "FileSystemProtocol" + tmpdir: Path, file_system: FileSystemProtocol, make_config: MakeConfig ) -> None: - static_files_config = StaticFilesConfig( - path="/static", directories=[tmpdir], html_mode=True, file_system=file_system + static_files_config, handlers = make_config( + StaticFilesConfig(path="/static", directories=[tmpdir], html_mode=True, file_system=file_system) ) - with create_test_client([], static_files_config=[static_files_config]) as client: + with create_test_client(handlers, static_files_config=static_files_config) as client: response = client.get("/static") assert response.status_code == HTTP_404_NOT_FOUND assert response.json() == {"status_code": 404, "detail": "no file or directory match the path . was found"} diff --git a/tests/unit/test_static_files/test_static_files_validation.py b/tests/unit/test_static_files/test_static_files_validation.py index 075b93d47d..56eeda3c19 100644 --- a/tests/unit/test_static_files/test_static_files_validation.py +++ b/tests/unit/test_static_files/test_static_files_validation.py @@ -1,47 +1,51 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, List import pytest from litestar import HttpMethod, Litestar, MediaType, get from litestar.exceptions import ImproperlyConfiguredException -from litestar.static_files.config import StaticFilesConfig -from litestar.status_codes import HTTP_200_OK, HTTP_405_METHOD_NOT_ALLOWED +from litestar.static_files import StaticFilesConfig, create_static_files_router +from litestar.status_codes import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_405_METHOD_NOT_ALLOWED from litestar.testing import create_test_client if TYPE_CHECKING: from pathlib import Path -def test_config_validation_of_directories() -> None: +@pytest.mark.parametrize("directories", [[], [""]]) +@pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router]) +def test_config_validation_of_directories(func: Any, directories: List[str]) -> None: with pytest.raises(ImproperlyConfiguredException): - StaticFilesConfig(path="/static", directories=[]) + func(path="/static", directories=directories) -def test_config_validation_of_path(tmpdir: "Path") -> None: +@pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router]) +def test_config_validation_of_path(tmpdir: "Path", func: Any) -> None: path = tmpdir / "text.txt" path.write_text("content", "utf-8") with pytest.raises(ImproperlyConfiguredException): - StaticFilesConfig(path="", directories=[tmpdir]) + func(path="", directories=[tmpdir]) with pytest.raises(ImproperlyConfiguredException): - StaticFilesConfig(path="/{param:int}", directories=[tmpdir]) + func(path="/{param:int}", directories=[tmpdir]) -def test_config_validation_of_file_system(tmpdir: "Path") -> None: +@pytest.mark.parametrize("func", [StaticFilesConfig, create_static_files_router]) +def test_config_validation_of_file_system(tmpdir: "Path", func: Any) -> None: class FSWithoutOpen: def info(self) -> None: return with pytest.raises(ImproperlyConfiguredException): - StaticFilesConfig(path="/static", directories=[tmpdir], file_system=FSWithoutOpen()) + func(path="/static", directories=[tmpdir], file_system=FSWithoutOpen()) class FSWithoutInfo: def open(self) -> None: return with pytest.raises(ImproperlyConfiguredException): - StaticFilesConfig(path="/static", directories=[tmpdir], file_system=FSWithoutInfo()) + func(path="/static", directories=[tmpdir], file_system=FSWithoutInfo()) class ImplementedFS: def info(self) -> None: @@ -50,7 +54,7 @@ def info(self) -> None: def open(self) -> None: return - assert StaticFilesConfig(path="/static", directories=[tmpdir], file_system=ImplementedFS()) + assert func(path="/static", directories=[tmpdir], file_system=ImplementedFS()) def test_runtime_validation_of_static_path_and_path_parameter(tmpdir: "Path") -> None: @@ -79,7 +83,7 @@ def handler(f: str) -> str: (HttpMethod.OPTIONS, HTTP_405_METHOD_NOT_ALLOWED), ), ) -def test_runtime_validation_of_request_method(tmpdir: "Path", method: HttpMethod, expected: int) -> None: +def test_runtime_validation_of_request_method_legacy_config(tmpdir: "Path", method: HttpMethod, expected: int) -> None: path = tmpdir / "test.txt" path.write_text("content", "utf-8") @@ -88,3 +92,24 @@ def test_runtime_validation_of_request_method(tmpdir: "Path", method: HttpMethod ) as client: response = client.request(method, "/static/test.txt") assert response.status_code == expected + + +@pytest.mark.parametrize( + "method, expected", + ( + (HttpMethod.GET, HTTP_200_OK), + (HttpMethod.HEAD, HTTP_200_OK), + (HttpMethod.OPTIONS, HTTP_204_NO_CONTENT), + (HttpMethod.PUT, HTTP_405_METHOD_NOT_ALLOWED), + (HttpMethod.PATCH, HTTP_405_METHOD_NOT_ALLOWED), + (HttpMethod.POST, HTTP_405_METHOD_NOT_ALLOWED), + (HttpMethod.DELETE, HTTP_405_METHOD_NOT_ALLOWED), + ), +) +def test_runtime_validation_of_request_method_create_handler(tmpdir: "Path", method: HttpMethod, expected: int) -> None: + path = tmpdir / "test.txt" + path.write_text("content", "utf-8") + + with create_test_client(create_static_files_router(path="/static", directories=[tmpdir])) as client: + response = client.request(method, "/static/test.txt") + assert response.status_code == expected diff --git a/tests/unit/test_stores.py b/tests/unit/test_stores.py index da3761ff6c..e09ffa6391 100644 --- a/tests/unit/test_stores.py +++ b/tests/unit/test_stores.py @@ -366,3 +366,17 @@ async def test_file_store_handle_rename_fail(file_store: FileStore, mocker: Mock await file_store.set("foo", "bar") mock_unlink.assert_called_once() assert Path(mock_unlink.call_args_list[0].args[0]).with_suffix("") == file_store.path.joinpath("foo") + + +@pytest.mark.xdist_group("redis") +async def test_redis_store_with_client_shutdown(redis_service: None) -> None: + redis_store = RedisStore.with_client(url="redis://localhost:6397") + assert await redis_store._redis.ping() + # remove the private shutdown and the assert below fails + # the check on connection is a mimic of https://github.com/redis/redis-py/blob/d529c2ad8d2cf4dcfb41bfd93ea68cfefd81aa66/tests/test_asyncio/test_connection_pool.py#L35-L39 + await redis_store._shutdown() + assert not any( + x.is_connected + for x in redis_store._redis.connection_pool._available_connections + + list(redis_store._redis.connection_pool._in_use_connections) + ) diff --git a/tests/unit/test_template/test_template.py b/tests/unit/test_template/test_template.py index 328c25e5de..4466524da5 100644 --- a/tests/unit/test_template/test_template.py +++ b/tests/unit/test_template/test_template.py @@ -98,15 +98,15 @@ def index() -> Template: @pytest.mark.parametrize( "extension,expected_type", [ - (".json", MediaType.JSON), - (".html", MediaType.HTML), - (".html.other", MediaType.HTML), - (".css", MediaType.CSS), - (".xml", MediaType.XML), - (".xml.other", MediaType.XML), - (".txt", MediaType.TEXT), - (".unknown", MediaType.TEXT), - ("", MediaType.TEXT), + (".json", MediaType.JSON.value), + (".html", MediaType.HTML.value), + (".html.other", MediaType.HTML.value), + (".css", MediaType.CSS.value), + (".xml", MediaType.XML.value), + (".xml.other", MediaType.XML.value), + (".txt", MediaType.TEXT.value), + (".unknown", MediaType.TEXT.value), + ("", MediaType.TEXT.value), ], ) @pytest.mark.skipif(sys.platform == "win32", reason="mimetypes.guess_types is unreliable on windows") @@ -123,7 +123,7 @@ def index() -> Template: ) as client: res = client.get("/") assert res.status_code == 200 - assert res.headers["content-type"].startswith(expected_type.value) + assert res.headers["content-type"].startswith(expected_type) def test_before_request_handler_content_type(tmp_path: Path) -> None: diff --git a/tests/unit/test_utils/test_module_loader.py b/tests/unit/test_utils/test_module_loader.py new file mode 100644 index 0000000000..b76315980f --- /dev/null +++ b/tests/unit/test_utils/test_module_loader.py @@ -0,0 +1,39 @@ +from pathlib import Path + +import pytest +from _pytest.monkeypatch import MonkeyPatch + +from litestar.config.compression import CompressionConfig +from litestar.utils.module_loader import import_string, module_to_os_path + + +def test_import_string() -> None: + cls = import_string("litestar.config.compression.CompressionConfig") + assert type(cls) == type(CompressionConfig) + + with pytest.raises(ImportError): + _ = import_string("CompressionConfigNew") + _ = import_string("litestar.config.compression.CompressionConfigNew") + _ = import_string("imaginary_module_that_doesnt_exist.Config") # a random nonexistent class + + +def test_module_path() -> None: + the_path = module_to_os_path("litestar.config.compression") + assert the_path.exists() + + with pytest.raises(TypeError): + _ = module_to_os_path("litestar.config.compression.Config") + _ = module_to_os_path("litestar.config.compression.extra.module") + + +def test_import_non_existing_attribute_raises() -> None: + with pytest.raises(ImportError): + import_string("litestar.app.some_random_string") + + +def test_import_string_cached(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + tmp_path.joinpath("testmodule.py").write_text("x = 'foo'") + monkeypatch.chdir(tmp_path) + monkeypatch.syspath_prepend(tmp_path) + + assert import_string("testmodule.x") == "foo" diff --git a/tests/unit/test_utils/test_scope.py b/tests/unit/test_utils/test_scope.py index 1e4d215e48..2a7e2f43fc 100644 --- a/tests/unit/test_utils/test_scope.py +++ b/tests/unit/test_utils/test_scope.py @@ -10,7 +10,7 @@ get_litestar_scope_state, set_litestar_scope_state, ) -from litestar.utils.scope.state import ScopeState +from litestar.utils.scope.state import CONNECTION_STATE_KEY, ScopeState if TYPE_CHECKING: from litestar.types.asgi_types import Scope @@ -21,6 +21,12 @@ def scope(create_scope: Callable[..., Scope]) -> Scope: return create_scope() +def test_from_scope_without_state() -> None: + scope = {} # type: ignore[var-annotated] + state = ScopeState.from_scope(scope) # type: ignore[arg-type] + assert scope["state"][CONNECTION_STATE_KEY] is state + + @pytest.mark.parametrize(("pop",), [(True,), (False,)]) def test_get_litestar_scope_state_arbitrary_value(pop: bool, scope: Scope) -> None: key = "test" diff --git a/tools/sphinx_ext/run_examples.py b/tools/sphinx_ext/run_examples.py index 8b6a716d9f..c34e294ff2 100644 --- a/tools/sphinx_ext/run_examples.py +++ b/tools/sphinx_ext/run_examples.py @@ -29,8 +29,6 @@ RGX_RUN = re.compile(r"# +?run:(.*)") -AVAILABLE_PORTS = list(range(9000, 9999)) - logger = logging.getLogger("sphinx") @@ -49,44 +47,56 @@ def _load_app_from_path(path: Path) -> Litestar: raise RuntimeError(f"No Litestar app found in {path}") +def _get_available_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + # Bind to a free port provided by the host + try: + sock.bind(("localhost", 0)) + except OSError as e: + raise StartupError("Could not find an open port") from e + else: + return sock.getsockname()[1] + + @contextmanager def run_app(path: Path) -> Generator[int, None, None]: """Run an example app from a python file. The first ``Litestar`` instance found in the file will be used as target to run. """ - while AVAILABLE_PORTS: - port = AVAILABLE_PORTS.pop(0) - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - if sock.connect_ex(("127.0.0.1", port)) != 0: - break - else: - raise StartupError("Could not find an open port") + port = _get_available_port() app = _load_app_from_path(path) def run() -> None: with redirect_stderr(Path(os.devnull).open()): uvicorn.run(app, port=port, access_log=False) - proc = multiprocessing.Process(target=run) - proc.start() + count = 0 + while count < 100: + proc = multiprocessing.Process(target=run) + proc.start() + try: + for _ in range(100): + try: + httpx.get(f"http://127.0.0.1:{port}", timeout=0.1) + break + except httpx.TransportError: + time.sleep(0.1) + else: + raise StartupError(f"App {path} failed to come online") + + yield port + break + except StartupError: + time.sleep(0.2) + count += 1 + port = _get_available_port() + finally: + proc.kill() - try: - for _ in range(100): - try: - httpx.get(f"http://127.0.0.1:{port}", timeout=0.1) - break - except httpx.TransportError: - time.sleep(0.1) - else: - raise StartupError(f"App {path} failed to come online") - - yield port - - finally: - proc.kill() - AVAILABLE_PORTS.append(port) + else: + raise StartupError(f"App {path} failed to come online") def extract_run_args(content: str) -> tuple[str, list[list[str]]]: