├── src └── email_simplified │ ├── py.typed │ ├── handlers │ ├── __init__.py │ ├── test.py │ ├── base.py │ └── smtp.py │ ├── __init__.py │ ├── address.py │ ├── attachment.py │ └── message.py ├── CHANGES.md ├── docs ├── changes.md ├── license.md ├── _static │ └── theme.css ├── testing.md ├── api.md ├── config.md ├── conf.py ├── index.md ├── handler.md ├── start.md ├── message.md └── smtp.md ├── .gitignore ├── .editorconfig ├── .readthedocs.yaml ├── tests ├── handlers │ ├── test_test.py │ ├── test_config.py │ └── test_smtp.py ├── test_attachment.py ├── test_address.py └── test_message.py ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── pre-commit.yaml │ ├── tests.yaml │ └── publish.yaml ├── LICENSE.txt ├── README.md ├── pyproject.toml └── uv.lock /src/email_simplified/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## Version 0.1.0 2 | 3 | Released 2025-02-11 4 | -------------------------------------------------------------------------------- /docs/changes.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ```{include} ../CHANGES.md 4 | ``` 5 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | ```{literalinclude} ../LICENSE.txt 4 | :language: text 5 | ``` 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | __pycache__/ 4 | dist/ 5 | .coverage* 6 | htmlcov/ 7 | .tox/ 8 | docs/_build/ 9 | -------------------------------------------------------------------------------- /docs/_static/theme.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap'); 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | charset = utf-8 10 | max_line_length = 88 11 | 12 | [*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-24.04 4 | tools: 5 | python: '3.13' 6 | commands: 7 | - asdf plugin add uv 8 | - asdf install uv latest 9 | - asdf global uv latest 10 | - uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html 11 | -------------------------------------------------------------------------------- /src/email_simplified/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import EmailHandler 2 | from .base import get_handler_class 3 | from .smtp import SMTPEmailHandler 4 | from .test import TestEmailHandler 5 | 6 | __all__ = [ 7 | "get_handler_class", 8 | "EmailHandler", 9 | "SMTPEmailHandler", 10 | "TestEmailHandler", 11 | ] 12 | -------------------------------------------------------------------------------- /src/email_simplified/__init__.py: -------------------------------------------------------------------------------- 1 | from .attachment import Attachment 2 | from .handlers.base import get_handler_class 3 | from .handlers.smtp import SMTPEmailHandler 4 | from .handlers.test import TestEmailHandler 5 | from .message import Message 6 | 7 | __all__ = [ 8 | "get_handler_class", 9 | "Attachment", 10 | "Message", 11 | "SMTPEmailHandler", 12 | "TestEmailHandler", 13 | ] 14 | -------------------------------------------------------------------------------- /tests/handlers/test_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | 5 | from email_simplified import Message 6 | from email_simplified import TestEmailHandler 7 | 8 | 9 | def test_handler() -> None: 10 | handler = TestEmailHandler() 11 | handler.send([Message(subject="a")]) 12 | asyncio.run(handler.send_async([Message(subject="b")])) 13 | assert len(handler.outbox) == 2 14 | 15 | 16 | def test_config() -> None: 17 | TestEmailHandler.from_config({"invalid": True}) 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: 895ebb389825c29bd4e0addcf7579d6c69d199cc # frozen: v0.9.6 4 | hooks: 5 | - id: ruff 6 | - id: ruff-format 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0 9 | hooks: 10 | - id: check-merge-conflict 11 | - id: debug-statements 12 | - id: fix-byte-order-marker 13 | - id: trailing-whitespace 14 | - id: end-of-file-fixer 15 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | During testing, you probably don't want to actually send messages. Instead, you 4 | can use the built-in {class}`.TestEmailHandler`, which stores all messages in a 5 | list called {attr}`~.TestEmailHandler.outbox`. After sending messages, you can 6 | check that the length or content of the outbox is what you expect. 7 | 8 | ```python 9 | from email_simplified import Message 10 | from email_simplified import TestEmailHandler 11 | 12 | test_email = TestEmailHandler() 13 | test_email.send(Message(...)) 14 | assert len(test_email.outbox) == 1 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | Anything documented here is part of the public API that Email-Simplified 4 | provides, unless otherwise indicated. Anything not documented here is considered 5 | internal or private and may change at any time. 6 | 7 | ## Handlers 8 | 9 | ```{eval-rst} 10 | .. currentmodule:: email_simplified 11 | 12 | .. autofunction:: get_handler_class 13 | 14 | .. autoclass:: SMTPEmailHandler 15 | :members: 16 | 17 | .. autoclass:: TestEmailHandler 18 | :members: 19 | 20 | .. currentmodule:: email_simplified.handlers 21 | 22 | .. autoclass:: EmailHandler 23 | :members: 24 | ``` 25 | 26 | ## Messages 27 | 28 | ```{eval-rst} 29 | 30 | .. currentmodule:: email_simplified 31 | 32 | .. autoclass:: Message 33 | :members: 34 | 35 | .. autoclass:: Attachment 36 | :members: 37 | ``` 38 | -------------------------------------------------------------------------------- /src/email_simplified/handlers/test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as t 4 | from email.message import EmailMessage as _EmailMessage 5 | 6 | from ..message import Message 7 | from .base import EmailHandler 8 | 9 | 10 | class TestEmailHandler(EmailHandler): 11 | """Email handler that appends messages to a list rather than sending them. 12 | Useful when testing. 13 | """ 14 | 15 | __test__ = False # don't get collected by pytest 16 | 17 | def __init__(self) -> None: 18 | self.outbox: list[Message | _EmailMessage] = [] 19 | """List of messages that have been sent with this handler.""" 20 | 21 | @classmethod 22 | def from_config(cls, config: dict[str, t.Any]) -> t.Self: 23 | return cls() 24 | 25 | def send(self, messages: list[Message | _EmailMessage]) -> None: 26 | self.outbox.extend(messages) 27 | 28 | async def send_async(self, messages: list[Message | _EmailMessage]) -> None: 29 | self.send(messages) 30 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main, stable] 6 | jobs: 7 | main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2 12 | with: 13 | enable-cache: true 14 | prune-cache: false 15 | - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 16 | id: setup-python 17 | with: 18 | python-version-file: pyproject.toml 19 | - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 20 | with: 21 | path: ~/.cache/pre-commit 22 | key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }} 23 | - run: uv run --locked --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files 24 | - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 25 | if: ${{ !cancelled() }} 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright David Lord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/handlers/test_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from email_simplified import get_handler_class 6 | from email_simplified import SMTPEmailHandler 7 | from email_simplified import TestEmailHandler 8 | from email_simplified.handlers import EmailHandler 9 | 10 | 11 | @pytest.mark.parametrize( 12 | ("name", "expect"), [("smtp", SMTPEmailHandler), ("test", TestEmailHandler)] 13 | ) 14 | def test_get_entry_point(name: str, expect: type[EmailHandler]) -> None: 15 | cls = get_handler_class(name) 16 | assert cls is expect 17 | 18 | 19 | def test_get_import() -> None: 20 | cls = get_handler_class("email_simplified.handlers.smtp:SMTPEmailHandler") 21 | assert cls is SMTPEmailHandler 22 | 23 | 24 | def test_get_class() -> None: 25 | cls = get_handler_class(SMTPEmailHandler) 26 | assert cls is SMTPEmailHandler 27 | 28 | 29 | def test_get_error() -> None: 30 | with pytest.raises(ValueError): 31 | get_handler_class("nothing") 32 | 33 | 34 | def test_from_config() -> None: 35 | handler = SMTPEmailHandler.from_config({"port": 1025}) 36 | assert handler.port == 1025 37 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Email-Simplified provides a way to load a handler and its config dynamically. 4 | This is useful in applications where you want your users to be able to 5 | configure how email is sent, rather than hard-coding a handler and config. 6 | 7 | See [Flask-Email-Simplified] for an example of using this dynamic config. 8 | 9 | [Flask-Email-Simplified]: https://flask-email-simplified.readthedocs.io 10 | 11 | {func}`.get_handler_class` can be used to get a handler class by name. 12 | Packages can register handler classes under simple names using Python's 13 | entry point system. For example, the built-in classes are registered as 14 | `"smtp"` and `"test"`. You can also pass a Python import path like 15 | `"module.submodule:handler_class"`, or an already imported class. 16 | 17 | Each handler class implements a {meth}`~.EmailHandler.from_config` class method. 18 | This can be used to create an instance of the loaded handler from dict keys and 19 | values rather than needing to pass keyword arguments. Handlers should document 20 | what keys they use. 21 | 22 | ```python 23 | from email_simplified import get_handler_class 24 | 25 | email = get_handler_class("smtp").from_config({"port": 1025}) 26 | ``` 27 | -------------------------------------------------------------------------------- /tests/test_attachment.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from email_simplified import Attachment 6 | 7 | 8 | @pytest.mark.parametrize( 9 | ("value", "expect"), [("a", "text/plain"), (b"a", "application/octet-stream")] 10 | ) 11 | def test_default_mimetype(value: str | bytes, expect: str) -> None: 12 | assert Attachment(value).mimetype == expect 13 | 14 | 15 | @pytest.mark.parametrize( 16 | ("value", "expect"), [("a.html", "text/html"), ("a.invalid", "text/plain")] 17 | ) 18 | def test_guess_mimetype(value: str, expect: str) -> None: 19 | assert Attachment("a", filename=value).mimetype == expect 20 | 21 | 22 | def test_given_mimetype() -> None: 23 | assert Attachment("a", mimetype="text/html").mimetype == "text/html" 24 | 25 | 26 | def test_lazy_cid() -> None: 27 | data = Attachment("a") 28 | assert data._cid is None # pyright: ignore 29 | 30 | 31 | def test_generate_cid() -> None: 32 | data = Attachment("a") 33 | assert data.cid 34 | assert data._cid is not None # pyright: ignore 35 | assert data.cid == data.cid 36 | 37 | 38 | def test_set_cid() -> None: 39 | data = Attachment("a") 40 | data.cid = "test" 41 | assert data._cid is not None # pyright: ignore 42 | assert data.cid == "test" 43 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | project = "Email-Simplified" 2 | 3 | default_role = "code" 4 | 5 | extensions = [ 6 | "myst_parser", 7 | "sphinx.ext.autodoc", 8 | "sphinx.ext.extlinks", 9 | "sphinx.ext.intersphinx", 10 | ] 11 | 12 | autodoc_member_order = "bysource" 13 | autodoc_typehints = "description" 14 | autodoc_preserve_defaults = True 15 | 16 | myst_enable_extensions = [ 17 | "colon_fence", 18 | "fieldlist", 19 | ] 20 | myst_heading_anchors = 2 21 | 22 | extlinks = { 23 | "issue": ("https://github.com/davidism/email-simplified/issues/%s", "#%s"), 24 | "pr": ("https://github.com/davidism/email-simplified/pull/%s", "#%s"), 25 | } 26 | 27 | intersphinx_mapping = { 28 | "python": ("https://docs.python.org/3/", None), 29 | } 30 | 31 | html_theme = "furo" 32 | html_static_path = ["_static"] 33 | html_css_files = ["theme.css"] 34 | html_copy_source = False 35 | html_theme_options = { 36 | "source_repository": "https://github.com/davidism/email-simplified/", 37 | "source_branch": "main", 38 | "source_directory": "docs/", 39 | "light_css_variables": { 40 | "font-stack": "'Atkinson Hyperlegible', sans-serif", 41 | "font-stack--monospace": "'Source Code Pro', monospace", 42 | }, 43 | } 44 | pygments_style = "default" 45 | pygments_style_dark = "github-dark" 46 | html_show_copyright = False 47 | html_use_index = False 48 | html_domain_indices = False 49 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main, stable] 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python: ['3.13', '3.12', '3.11'] 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2 16 | with: 17 | enable-cache: true 18 | prune-cache: false 19 | - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 20 | with: 21 | python-version: ${{ matrix.python }} 22 | - run: uv run --locked tox run -e ${{ format('py{0}', matrix.python) }} 23 | typing: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 27 | - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2 28 | with: 29 | enable-cache: true 30 | prune-cache: false 31 | - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 32 | with: 33 | python-version-file: pyproject.toml 34 | - name: cache mypy 35 | uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 36 | with: 37 | path: ./.mypy_cache 38 | key: mypy|${{ hashFiles('pyproject.toml') }} 39 | - run: uv run --locked tox run -e typing 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | push: 4 | tags: ['*'] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 10 | - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2 11 | with: 12 | enable-cache: true 13 | prune-cache: false 14 | - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 15 | with: 16 | python-version-file: pyproject.toml 17 | - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV 18 | - run: uv build 19 | - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 20 | with: 21 | path: ./dist 22 | create-release: 23 | needs: [build] 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: write 27 | steps: 28 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 29 | - name: create release 30 | run: > 31 | gh release create --draft --repo ${{ github.repository }} 32 | ${{ github.ref_name }} artifact/* 33 | env: 34 | GH_TOKEN: ${{ github.token }} 35 | publish-pypi: 36 | needs: [build] 37 | environment: 38 | name: publish 39 | url: https://pypi.org/project/email-simplified/${{ github.ref_name }} 40 | runs-on: ubuntu-latest 41 | permissions: 42 | id-token: write 43 | steps: 44 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 45 | - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 46 | with: 47 | packages-dir: artifact/ 48 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Email-Simplified 2 | 3 | Email-Simplified provides a much simpler interface for creating and sending 4 | email messages compared to Python's `email` and `smtplib` modules. It also 5 | defines an interface for using other email sending providers that offer an API 6 | other than SMTP. 7 | 8 | [Flask-Email-Simplified] is an extension that integrates Email-Simplified with 9 | Flask's configuration. 10 | 11 | [Flask-Email-Simplified]: https://flask-email-simplified.readthedocs.io 12 | 13 | ## Install 14 | 15 | Install from [PyPI]: 16 | 17 | ``` 18 | $ pip install email-simplified 19 | ``` 20 | 21 | [pypi]: https://pypi.org/project/email-simplified 22 | 23 | ## Source 24 | 25 | The project is hosted on GitHub: . 26 | 27 | ## Features 28 | 29 | - Properly sets up TLS and uses the operating system trust store by default. 30 | - Handles international domain names. Allows Unicode in names (assuming server 31 | SMTPUTF8 support). 32 | - Generates text from HTML if only HTML content is given (although you should 33 | still give both to get better results). 34 | - Handles inline HTML attachments with CID generation. 35 | - Sync and async sending. 36 | - Can convert to and from MIME messages, and can send MIME messages in addition 37 | to its own message class. This allows supporting arbitrary MIME constructs if 38 | you need to. 39 | - Can batch recipients for sending with servers that restrict the number of 40 | recipients per message. 41 | - Provides a test handler as well to collect instead of send messages during tests. 42 | - Can be configured by name and config dict rather than imports and arguments, 43 | for framework integration. 44 | - Complete static typing. 45 | 46 | ```{toctree} 47 | :hidden: 48 | 49 | start 50 | message 51 | smtp 52 | testing 53 | config 54 | handler 55 | api 56 | license 57 | changes 58 | ``` 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Email-Simplified 2 | 3 | Email-Simplified provides a much simpler interface for creating and sending 4 | email messages compared to Python's `email` and `smtplib` modules. It also 5 | defines an interface for using other email sending providers that offer an API 6 | other than SMTP. 7 | 8 | [Flask-Email-Simplified] is an extension that integrates Email-Simplified with 9 | Flask's configuration. 10 | 11 | [Flask-Email-Simplified]: https://flask-email-simplified.readthedocs.io 12 | 13 | ## Install 14 | 15 | Install from [PyPI]: 16 | 17 | ``` 18 | $ pip install email-simplified 19 | ``` 20 | 21 | [pypi]: https://pypi.org/project/email-simplified/ 22 | 23 | ## Example 24 | 25 | ```python 26 | from email_simplified.handlers import SMTPEmailHandler 27 | from email_simplified import Message 28 | 29 | email = SMTPEmailHandler() 30 | message = Message(subject="Hello", text="Hello, World!", to=["world@example.test"]) 31 | email.send([message]) 32 | ``` 33 | 34 | ## Features 35 | 36 | - Properly sets up TLS and uses the operating system trust store by default. 37 | - Handles international domain names. Allows Unicode in names (assuming server 38 | SMTPUTF8 support). 39 | - Generates text from HTML if only HTML content is given (although you should 40 | still give both to get better results). 41 | - Handles inline HTML attachments with CID generation. 42 | - Sync and async sending. 43 | - Can convert to and from MIME messages, and can send MIME messages in addition 44 | to its own message class. This allows supporting arbitrary MIME constructs if 45 | you need to. 46 | - Can batch recipients for sending with servers that restrict the number of 47 | recipients per message. 48 | - Provides a test handler as well to collect instead of send messages during tests. 49 | - Can be configured by name and config dict rather than imports and arguments, 50 | for framework integration. 51 | - Complete static typing. 52 | -------------------------------------------------------------------------------- /docs/handler.md: -------------------------------------------------------------------------------- 1 | # Writing a Handler 2 | 3 | The built-in SMTP handler will be appropriate for most use cases. However, you 4 | may wish to use another SMTP library, or use an email service that has an HTTP 5 | API. Most services with HTTP still have SMTP as well, but they may offer 6 | additional features not possible with SMTP. 7 | 8 | Creating a new handler involves subclassing the base handler and overriding a 9 | few methods. If your custom handler is for a service that others may use, you 10 | should package and publish it on PyPI. 11 | 12 | ## Implementation 13 | 14 | Subclass {class}`.EmailHandler` and override at least 15 | {meth}`~.EmailHandler.send`. Override `__init__` and 16 | {meth}`~.EmailHandler.from_config` as well to allow configuration. 17 | 18 | ```python 19 | from email.message import EmailMessage as _EmailMessage 20 | from typing import Any 21 | from typing import Self 22 | from email_simplified.handlers import EmailHandler 23 | from email_simplified import Message 24 | 25 | class MyEmailHandler(EmailHandler): 26 | def __init__(self, ...) -> None: 27 | ... 28 | 29 | @classmethod 30 | def from_config(cls, config: dict[str, Any]) -> Self: 31 | ... 32 | return cls(...) 33 | 34 | def send(self, messages: list[Message | _EmailMessage]) -> None: 35 | ... 36 | ``` 37 | 38 | Beyond that, your class can have any additional attributes and 39 | methods as needed. See the built-in {class}`.SMTPEmailHandler` for an example. 40 | 41 | ### Async 42 | 43 | The default implementation of {meth}`~.EmailHandler.send_async` uses 44 | {func}`asyncio.to_thread` to call the sync `send` method without blocking. 45 | Override this if you're using an async library such as [aiosmtplib] or [HTTPX], 46 | or if you're using a different event loop such as [Trio]. 47 | 48 | [aiosmtplib]: https://pypi.org/project/aiosmtplib/ 49 | [HTTPX]: https://www.python-httpx.org 50 | [Trio]: https://trio.readthedocs.io 51 | 52 | ## Entry Point 53 | 54 | When packaging your handler, you can specify an entry point with a simple name 55 | for loading with {func}`.get_handler_class`. The entry point group is named 56 | `email_simplified.handler`, and each key should correspond to a handler class. 57 | In `pyproject.toml`: 58 | 59 | ```toml 60 | [project.entry-points."email_simplified.handler"] 61 | my-email = "my_email.handler:MyEmailHandler" 62 | ``` 63 | 64 | Pick a unique name, such as the name of the service. Behavior is undefined when 65 | two packages provide the same entry point name, although it would be unlikely 66 | for a user to install two packages for the same provider. 67 | 68 | ```python 69 | get_handler_class("my-email") 70 | ``` 71 | 72 | If you don't provide this, users can still pass 73 | `"my_email.handler:MyEmailHandler"`, or import it directly. 74 | -------------------------------------------------------------------------------- /tests/test_address.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from email.headerregistry import Address 4 | 5 | import pytest 6 | 7 | from email_simplified.address import AddressList 8 | from email_simplified.address import prepare_address 9 | 10 | 11 | @pytest.mark.parametrize( 12 | ("value", "expect"), 13 | [ 14 | ("a@a.test", Address(addr_spec="a@a.test")), 15 | ("あ@あ.test", Address(username="あ", domain="xn--l8j.test")), 16 | ("A ", Address(display_name="A", addr_spec="a@a.test")), 17 | (Address(addr_spec="a@a.test"), Address(addr_spec="a@a.test")), 18 | ( 19 | Address(username="あ", domain="あ.test"), 20 | Address(username="あ", domain="xn--l8j.test"), 21 | ), 22 | ], 23 | ) 24 | def test_prepare_address(value: str | Address, expect: Address) -> None: 25 | assert prepare_address(value) == expect 26 | 27 | 28 | def test_list_init() -> None: 29 | data = AddressList(["a@a.test"]) 30 | assert data[0] == Address(addr_spec="a@a.test") 31 | 32 | 33 | def test_list_init_empty() -> None: 34 | data = AddressList() 35 | assert not data 36 | 37 | 38 | def test_list_append() -> None: 39 | data = AddressList() 40 | data.append("a@a.test") 41 | assert data[0] == Address(addr_spec="a@a.test") 42 | 43 | 44 | def test_list_extend() -> None: 45 | data = AddressList() 46 | data.extend(["a@a.test", "b@a.test"]) 47 | assert len(data) == 2 48 | assert all(isinstance(x, Address) for x in data) 49 | 50 | 51 | def test_list_index() -> None: 52 | data = AddressList(["b@a.test", "a@a.test"]) 53 | assert data.index("a@a.test") == 1 54 | 55 | 56 | def test_list_count() -> None: 57 | data = AddressList(["a@a.test", "a@a.test"]) 58 | assert data.count("a@a.test") == 2 59 | 60 | 61 | def test_list_insert() -> None: 62 | data = AddressList(["a@a.test"]) 63 | data.insert(0, "b@a.test") 64 | assert data == [Address(addr_spec="b@a.test"), Address(addr_spec="a@a.test")] 65 | 66 | 67 | def test_list_remove() -> None: 68 | data = AddressList(["b@a.test", "a@a.test"]) 69 | data.remove("b@a.test") 70 | assert data == [Address(addr_spec="a@a.test")] 71 | 72 | 73 | def test_list_setitem() -> None: 74 | data = AddressList(["b@a.test", "a@a.test"]) 75 | data[1] = "c@a.test" 76 | assert data[1] == Address(addr_spec="c@a.test") 77 | data[:] = ["d@a.test", "e@a.test"] 78 | assert data == [Address(addr_spec="d@a.test"), Address(addr_spec="e@a.test")] 79 | 80 | 81 | def test_list_iadd() -> None: 82 | data = AddressList(["a@a.test"]) 83 | data += ["b@a.test", "c@a.test"] 84 | assert len(data) == 3 85 | 86 | 87 | def test_list_contains() -> None: 88 | data = AddressList(["a@a.test"]) 89 | assert "a@a.test" in data 90 | assert "b@a.test" not in data 91 | assert object() not in data 92 | -------------------------------------------------------------------------------- /docs/start.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Initialize the Handler 4 | 5 | A handler implements sending email messages to an email service. The built-in 6 | SMTP handler will be fine for most cases. 7 | 8 | ```python 9 | from email_simplified import SMTPEmailHandler 10 | 11 | email = SMTPEmailHandler() 12 | ``` 13 | 14 | This {class}`.SMTPEmailHandler` wraps Python's {mod}`smtplib`. It takes various 15 | arguments to configure the client, but all arguments have default values and are 16 | not required. By default, it connects to `localhost:25`. 17 | 18 | See {doc}`config` for information on configuring a handler dynamically. 19 | 20 | ### Local Mail Server 21 | 22 | The SMTP handler will fail to send if there's no server at the configured 23 | location. During development, you can use a tool such as [mailcatcher], which 24 | acts as a simple, local SMTP server as well as a UI for displaying any messages 25 | it receives. 26 | 27 | [mailcatcher]: https://github.com/sj26/mailcatcher 28 | 29 | ## Create a Message 30 | 31 | Email-Simplified provides a {class}`.Message` class that can be used to define 32 | the fields of a "standard" email MIME structure. It supports a text part, HTML 33 | part, HTML inline attachments, and download attachments. 34 | 35 | See {doc}`message` for more details. 36 | 37 | ```python 38 | from email_simplified import Message 39 | 40 | message = Message( 41 | subject="Hello", 42 | text="Hello, World!", 43 | to=["world@example.test"], 44 | ) 45 | ``` 46 | 47 | Alternatively, you may need to create a {class}`email.message.EmailMessage` 48 | directly if you need a more complex, non-standard MIME structure. 49 | Email-Simplified handlers can support sending both types of messages. SMTP 50 | handlers will always convert to MIME, but other handlers for HTTP-based services 51 | may be able to make an API call or send the MIME content directly depending on 52 | the message structure. 53 | 54 | ## Send a Message 55 | 56 | Call the handler's {meth}`~.EmailHandler.send` method to send the message you 57 | created. You can pass a single message or a list of messages. Sending a list 58 | of messages is more efficient than making multiple `send` calls, as it reuses 59 | the same connection. 60 | 61 | ```python 62 | email.send(message) 63 | ``` 64 | 65 | See {doc}`smtp` for more details on sending with SMTP. See {doc}`testing` for 66 | more details on the test handler, which only stores and does not send messages 67 | externally. See {doc}`handler` for more details on writing a handler for another 68 | service. 69 | 70 | ### Performance 71 | 72 | Sending messages can be a slow operation, which will delay your code from 73 | continuing. It can be useful to send messages in background tasks, using a 74 | system such as [RQ] or [Celery]. 75 | 76 | [RQ]: https://python-rq.org 77 | [Celery]: https://docs.celeryq.dev 78 | 79 | ### Async 80 | 81 | If you're calling this from an `async` function, you should `await` 82 | {meth}`~.EmailHandler.send_async` instead. 83 | 84 | The built-in SMTP handler uses {func}`asyncio.to_thread` to run the sync `send` 85 | function in a thread. Other handlers can be more efficient by using an async 86 | library such as [aiosmtplib] or [HTTPX]. 87 | 88 | [aiosmtplib]: https://pypi.org/project/aiosmtplib/ 89 | [HTTPX]: https://www.python-httpx.org 90 | -------------------------------------------------------------------------------- /src/email_simplified/handlers/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import importlib.metadata 5 | import pkgutil 6 | import typing as t 7 | from email.message import EmailMessage as _EmailMessage 8 | from inspect import isclass 9 | 10 | from ..message import Message 11 | 12 | 13 | class EmailHandler: 14 | """Interface for sending email messages. Subclasses will define how to send 15 | using a service, such as SMTP, an email provider's API, etc. 16 | """ 17 | 18 | def send(self, messages: list[Message | _EmailMessage]) -> None: 19 | """Send one or more email messages. 20 | 21 | Messages should typically be :class:`.Message`. However, they may also 22 | be :class:`email.message.EmailMessage` for cases where a non-standard 23 | MIME construction is needed. It is up to the handler to decide whether 24 | that is supported based on what their service is capable of. 25 | 26 | :param messages: A list of messages to send. 27 | """ 28 | raise NotImplementedError 29 | 30 | async def send_async(self, messages: list[Message | _EmailMessage]) -> None: 31 | """Send one or more email messages, as with :meth:`send`, but in an 32 | ``async`` context. 33 | """ 34 | await asyncio.to_thread(self.send, messages) 35 | 36 | @classmethod 37 | def from_config(cls, config: dict[str, t.Any]) -> t.Self: 38 | """Create an instance of this handler using arguments from ``config`` as 39 | needed. This is useful for frameworks when paired with 40 | :func:`get_handler_class`, allowing the deployment to configure an 41 | arbitrary handler and its arguments. 42 | """ 43 | raise NotImplementedError 44 | 45 | 46 | _handler_classes: dict[str, type[EmailHandler]] = {} 47 | 48 | 49 | def get_handler_class(name: str | type[EmailHandler]) -> type[EmailHandler]: 50 | """Get a :class:`.EmailHandler` implementation by name. 51 | 52 | First looks up if the name is in the ``email_simplified.handler`` entry 53 | point namespace, and loads the referenced class if it is. Libraries 54 | providing handler implementations should register a handler's name so that 55 | it's automatically available when installed. 56 | 57 | If an entry point is not found, attempts to treat the name as an import 58 | path in the form ``module.submodule:handler_class``. This is useful for 59 | users writing their own handler locally where adding an entry point may not 60 | be possible. May also be passed an actual handler class, in which case it's 61 | returned directly. 62 | 63 | :param name: The registered entry point name, import path, or handler class 64 | to load and return. 65 | """ 66 | if not isinstance(name, str): 67 | return name 68 | 69 | if not _handler_classes: 70 | for ep in importlib.metadata.entry_points(group="email_simplified.handler"): 71 | obj = ep.load() 72 | _handler_classes[ep.name] = obj 73 | 74 | if name in _handler_classes: 75 | return _handler_classes[name] 76 | 77 | try: 78 | obj = pkgutil.resolve_name(name) 79 | except ImportError: 80 | obj = None 81 | 82 | if isclass(obj) and issubclass(obj, EmailHandler): 83 | _handler_classes[name] = obj 84 | return obj 85 | 86 | raise ValueError(f"Could not find installed entry point or import: '{name}'.") 87 | -------------------------------------------------------------------------------- /src/email_simplified/address.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import collections.abc as cabc 4 | import email.utils 5 | import sys 6 | import typing as t 7 | from email.headerregistry import Address 8 | 9 | 10 | class AddressList(list[Address]): 11 | """A :class:`list` subclass that contains class:`email.headerregistry.Address` 12 | items, but accepts strings as well for various operations. Domains are IDNA 13 | encoded if needed. 14 | """ 15 | 16 | def __init__(self, value: cabc.Iterable[str | Address] = (), /) -> None: 17 | super().__init__(prepare_address(v) for v in value) 18 | 19 | def append(self, value: str | Address, /) -> None: 20 | super().append(prepare_address(value)) 21 | 22 | def extend(self, value: cabc.Iterable[str | Address], /) -> None: 23 | super().extend(prepare_address(v) for v in value) 24 | 25 | def index( 26 | self, 27 | value: str | Address, 28 | start: t.SupportsIndex = 0, 29 | stop: t.SupportsIndex = sys.maxsize, 30 | /, 31 | ) -> int: 32 | return super().index(prepare_address(value)) 33 | 34 | def count(self, value: str | Address, /) -> int: 35 | return super().count(prepare_address(value)) 36 | 37 | def insert(self, index: t.SupportsIndex, value: str | Address, /) -> None: 38 | super().insert(index, prepare_address(value)) 39 | 40 | def remove(self, value: str | Address, /) -> None: 41 | super().remove(prepare_address(value)) 42 | 43 | @t.overload 44 | def __setitem__(self, key: t.SupportsIndex, value: str | Address) -> None: ... 45 | 46 | @t.overload 47 | def __setitem__(self, key: slice, value: cabc.Iterable[str | Address]) -> None: ... 48 | 49 | def __setitem__( 50 | self, 51 | key: t.SupportsIndex | slice, 52 | value: str | Address | cabc.Iterable[str | Address], 53 | ) -> None: 54 | if not isinstance(key, slice): 55 | super().__setitem__(key, prepare_address(value)) # type: ignore[arg-type] 56 | else: 57 | super().__setitem__(key, (prepare_address(v) for v in value)) # type: ignore[union-attr] 58 | 59 | def __iadd__(self, other: cabc.Iterable[str | Address]) -> t.Self: # type: ignore[override, misc] 60 | return super().__iadd__(prepare_address(v) for v in other) 61 | 62 | def __contains__(self, value: object) -> bool: 63 | if not isinstance(value, str | Address): 64 | return False 65 | 66 | return super().__contains__(prepare_address(value)) 67 | 68 | 69 | def prepare_address(address: str | Address) -> Address: 70 | """Convert a string or :class:`email.headerregistry.Address` to an 71 | ``Address`` with the domain part IDNA encoded if needed. 72 | 73 | :param address: Address to process. 74 | """ 75 | if isinstance(address, Address): 76 | domain = _idna_if_needed(address.domain) 77 | 78 | if domain == address.domain: 79 | return address 80 | 81 | return Address(address.display_name, address.username, domain) 82 | 83 | name, addr = email.utils.parseaddr(address) 84 | username, _, domain = addr.rpartition("@") 85 | domain = _idna_if_needed(domain) 86 | return Address(name, username, domain) 87 | 88 | 89 | def _idna_if_needed(domain: str) -> str: 90 | """If the domain is non-ASCII, IDNA encode it. 91 | 92 | :param domain: The domain to encode. 93 | """ 94 | try: 95 | domain.encode("ascii") 96 | except UnicodeEncodeError: 97 | return domain.encode("idna").decode("ascii") 98 | 99 | return domain 100 | -------------------------------------------------------------------------------- /tests/handlers/test_smtp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from smtplib import SMTP 5 | from unittest.mock import create_autospec 6 | from unittest.mock import MagicMock 7 | from unittest.mock import patch 8 | 9 | from email_simplified import Message 10 | from email_simplified import SMTPEmailHandler 11 | 12 | 13 | def test_tls_port() -> None: 14 | assert SMTPEmailHandler(port=465).use_tls 15 | 16 | 17 | def test_tls_disables_starttls() -> None: 18 | assert not SMTPEmailHandler(use_tls=True, use_starttls=True).use_starttls 19 | 20 | 21 | def test_tls_context() -> None: 22 | assert SMTPEmailHandler(use_tls=True).tls_context 23 | assert SMTPEmailHandler(use_starttls=True).tls_context 24 | 25 | 26 | def test_from_config() -> None: 27 | assert SMTPEmailHandler.from_config({"port": 1025, "invalid": True}).port == 1025 28 | 29 | 30 | @patch("email_simplified.handlers.smtp.SMTP", autospec=True) 31 | def test_connect(smtp_cls: MagicMock) -> None: 32 | handler = SMTPEmailHandler() 33 | client: MagicMock 34 | 35 | with handler.connect() as client: # type: ignore[assignment] 36 | pass 37 | 38 | assert smtp_cls.call_args.kwargs.keys() == { 39 | "host", 40 | "port", 41 | "local_hostname", 42 | "timeout", 43 | } 44 | client.starttls.assert_not_called() 45 | client.login.assert_not_called() 46 | 47 | 48 | @patch("email_simplified.handlers.smtp.SMTP", autospec=True) 49 | def test_connect_setup(smtp_cls: MagicMock) -> None: 50 | handler = SMTPEmailHandler(use_starttls=True, username="a", password="b") 51 | client: MagicMock 52 | 53 | with handler.connect() as client: # type: ignore[assignment] 54 | pass 55 | 56 | client.starttls.assert_called() 57 | client.login.assert_called() 58 | 59 | 60 | @patch("email_simplified.handlers.smtp.SMTP_SSL", autospec=True) 61 | def test_connect_tls(smtp_ssl_cls: MagicMock) -> None: 62 | handler = SMTPEmailHandler(use_tls=True) 63 | 64 | with handler.connect(): 65 | pass 66 | 67 | assert "context" in smtp_ssl_cls.call_args.kwargs 68 | 69 | 70 | @patch.object(SMTPEmailHandler, "connect") 71 | def test_send(connect: MagicMock) -> None: 72 | ctx = create_autospec(SMTP, instance=True) 73 | connect.return_value.__enter__.return_value = ctx 74 | handler = SMTPEmailHandler() 75 | handler.send( 76 | [ 77 | Message(subject="a"), 78 | Message(subject="b", from_addr="a@example.test").to_mime(), 79 | ] 80 | ) 81 | connect.assert_called() 82 | assert ctx.send_message.call_count == 2 83 | 84 | 85 | @patch.object(SMTPEmailHandler, "connect") 86 | def test_send_empty(connect: MagicMock) -> None: 87 | handler = SMTPEmailHandler() 88 | handler.send([]) 89 | connect.assert_not_called() 90 | 91 | 92 | @patch.object(SMTPEmailHandler, "connect") 93 | def test_send_batch(connect: MagicMock) -> None: 94 | ctx = create_autospec(SMTP, instance=True) 95 | connect.return_value.__enter__.return_value = ctx 96 | handler = SMTPEmailHandler(recipients_per_message=4) 97 | handler.send([Message(subject="a", to=[f"a{x}@example.test" for x in range(10)])]) 98 | assert ctx.send_message.call_count == 3 99 | 100 | 101 | @patch.object(SMTPEmailHandler, "connect") 102 | def test_send_async(connect: MagicMock) -> None: 103 | ctx = create_autospec(SMTP, instance=True) 104 | connect.return_value.__enter__.return_value = ctx 105 | handler = SMTPEmailHandler() 106 | asyncio.run(handler.send_async([Message(subject="a")])) 107 | connect.assert_called() 108 | assert ctx.send_message.call_count == 1 109 | -------------------------------------------------------------------------------- /src/email_simplified/attachment.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import email.utils 4 | import mimetypes 5 | import socket 6 | import typing as t 7 | from email.message import EmailMessage 8 | 9 | 10 | class Attachment: 11 | """Structured representation of an email attachment. 12 | 13 | :param data: Text or bytes data to attach. 14 | :param filename: Filename to show for the attachment. 15 | :param mimetype: Mimetype describing the attached data. Defaults to guessing 16 | from ``filename`` if possible, or ``text/plain`` for text data or 17 | ``application/octet-stream`` for bytes data. 18 | """ 19 | 20 | def __init__( 21 | self, 22 | data: str | bytes, 23 | *, 24 | filename: str | None = None, 25 | mimetype: str | None = None, 26 | ): 27 | if mimetype is None and filename is not None: 28 | guess = mimetypes.guess_type(filename)[0] 29 | 30 | if guess is not None: 31 | mimetype = guess 32 | 33 | if mimetype is None: 34 | if isinstance(data, str): 35 | mimetype = "text/plain" 36 | else: 37 | mimetype = "application/octet-stream" 38 | 39 | self.data = data 40 | """Text or bytes data to attach.""" 41 | 42 | self.filename = filename 43 | """Filename to show for the attachment.""" 44 | 45 | self.mimetype: str = mimetype 46 | """Mimetype describing the attached data. Defaults to guessing from 47 | ``filename`` if possible, or ``text/plain`` for text data or 48 | ``application/octet-stream`` for bytes data. 49 | """ 50 | 51 | self._cid: str | None = None 52 | 53 | @property 54 | def cid(self) -> str: 55 | """The id used to refer to the inline attachment in HTML. Generated on 56 | access if not set. 57 | """ 58 | if self._cid is None: 59 | self._cid = email.utils.make_msgid(domain=local_hostname()) 60 | 61 | return self._cid 62 | 63 | @cid.setter 64 | def cid(self, value: str | None) -> None: 65 | self._cid = value 66 | 67 | def add_to_mime(self, message: EmailMessage, *, inline: bool = False) -> None: 68 | """Add this attachment to the given :class:`email.message.EmailMessage. 69 | When attaching inline, include the :attr:`cid`, otherwise the 70 | :attr:`filename`. 71 | 72 | :param message: The message to attach to. 73 | :param inline: Attach inline for linking from HTML. 74 | """ 75 | kwargs: dict[str, t.Any] = {} 76 | main_type, _, kwargs["subtype"] = self.mimetype.partition("/") 77 | data: str | bytes = self.data 78 | 79 | if isinstance(data, str): 80 | if main_type != "text": 81 | # For example application/json from a .json filename. 82 | # message.add_attachment insists on text/* mimetypes for str 83 | # data, so encode it as utf8 to get around that. 84 | data = data.encode() 85 | kwargs["maintype"] = main_type 86 | else: 87 | kwargs["maintype"] = main_type 88 | 89 | if inline: 90 | kwargs["cid"] = self.cid 91 | message.add_related(data, **kwargs) 92 | else: 93 | if self.filename: 94 | kwargs["filename"] = self.filename 95 | 96 | message.add_attachment(data, **kwargs) 97 | 98 | 99 | _local_hostname: str | None = None 100 | """Cached value for :func:`local_hostname`.""" 101 | 102 | 103 | def local_hostname() -> str: 104 | """Return the result of :func:`socket.getfqdn`, caching the value for 105 | subsequent calls. Used to generate attachment cids and with SMTP. 106 | """ 107 | global _local_hostname 108 | 109 | if _local_hostname is None: 110 | _local_hostname = socket.getfqdn() 111 | 112 | return _local_hostname 113 | -------------------------------------------------------------------------------- /tests/test_message.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from email.headerregistry import Address 4 | 5 | import pytest 6 | 7 | from email_simplified import Attachment 8 | from email_simplified import Message 9 | 10 | 11 | def test_init() -> None: 12 | m = Message( 13 | subject="a", 14 | text="a", 15 | from_addr="a@a.test", 16 | reply_to="b@a.test", 17 | to=["c@a.test"], 18 | cc=["d@a.test"], 19 | bcc=["e@a.test"], 20 | html="

a

", 21 | ) 22 | assert m.subject == "a" 23 | assert m.text == "a" 24 | assert m.html == "

a

" 25 | assert m.from_addr == Address(addr_spec="a@a.test") 26 | assert m.reply_to == Address(addr_spec="b@a.test") 27 | assert m.to == [Address(addr_spec="c@a.test")] 28 | assert m.cc == [Address(addr_spec="d@a.test")] 29 | assert m.bcc == [Address(addr_spec="e@a.test")] 30 | 31 | 32 | def test_init_empty() -> None: 33 | m = Message() 34 | assert m.subject is None 35 | assert m.text is None 36 | assert m.html is None 37 | assert m.from_addr is None 38 | assert m.reply_to is None 39 | assert m.to == [] 40 | assert m.cc == [] 41 | assert m.bcc == [] 42 | 43 | 44 | @pytest.mark.parametrize("attr", ["from_addr", "reply_to"]) 45 | def test_set_from(attr: str) -> None: 46 | m = Message() 47 | setattr(m, attr, "a@a.test") 48 | assert getattr(m, attr) == Address(addr_spec="a@a.test") 49 | setattr(m, attr, None) 50 | assert getattr(m, attr) is None 51 | 52 | 53 | @pytest.mark.parametrize("attr", ["to", "cc", "bcc"]) 54 | def test_set_to(attr: str) -> None: 55 | m = Message() 56 | addr = Address(addr_spec="a@a.test") 57 | setattr(m, attr, [addr]) 58 | assert getattr(m, attr) == [addr] 59 | setattr(m, attr, []) 60 | assert getattr(m, attr) == [] 61 | 62 | 63 | def test_mime_headers() -> None: 64 | m = Message.from_mime( 65 | Message( 66 | subject="a", 67 | text="a", 68 | from_addr="あ@あ.test", 69 | reply_to="B ", 70 | to=["c1@a.test", "c2@a.test"], 71 | cc=["d1@a.test", "d2@a.test"], 72 | bcc=["e1@a.test", "e2@a.test"], 73 | ).to_mime() 74 | ) 75 | assert m.subject == "a" 76 | assert str(m.from_addr) == "あ@xn--l8j.test" 77 | assert str(m.reply_to) == "B " 78 | assert len(m.to) == 2 79 | assert len(m.cc) == 2 80 | assert len(m.bcc) == 2 81 | 82 | 83 | def test_to_mime_headers_empty() -> None: 84 | m = Message.from_mime(Message(text="a").to_mime()) 85 | assert m.subject is None 86 | assert m.from_addr is None 87 | assert m.reply_to is None 88 | assert not m.to 89 | assert not m.cc 90 | assert not m.bcc 91 | 92 | 93 | def test_mime_text() -> None: 94 | m = Message.from_mime(Message(text="a").to_mime()) 95 | assert m.text == "a\n" 96 | assert m.html is None 97 | 98 | 99 | def test_mime_html() -> None: 100 | m = Message.from_mime(Message(html="

b

\n

c

").to_mime()) 101 | assert m.text == "b\nc\n" 102 | assert m.html == "

b

\n

c

\n" 103 | 104 | 105 | def test_mime_text_html() -> None: 106 | m = Message.from_mime(Message(text="a", html="

b

").to_mime()) 107 | assert m.text == "a\n" 108 | assert m.html == "

b

\n" 109 | 110 | 111 | def test_attachments() -> None: 112 | m = Message.from_mime( 113 | Message( 114 | html="a", 115 | attachments=[Attachment(data="null", filename="a.json")], 116 | inline_attachments=[Attachment(data=b"null", filename="b.json")], 117 | ).to_mime() 118 | ) 119 | assert len(m.attachments) == 1 120 | assert len(m.inline_attachments) == 1 121 | 122 | 123 | def test_no_html_attachments() -> None: 124 | m = Message.from_mime( 125 | Message( 126 | text="a", 127 | attachments=[Attachment(data="a")], 128 | inline_attachments=[Attachment(data="b")], 129 | ).to_mime() 130 | ) 131 | assert len(m.attachments) == 1 132 | assert len(m.inline_attachments) == 0 133 | -------------------------------------------------------------------------------- /docs/message.md: -------------------------------------------------------------------------------- 1 | # Creating Messages 2 | 3 | The {class}`.Message` class provides an interface for creating "standard" email 4 | messages. This includes text, HTML, download attachments, and inline 5 | attachments. This wraps Python's {mod}`email.message.EmailMessage` in a much 6 | simpler interface. 7 | 8 | ## Attachments 9 | 10 | Attachments are represented with the {class}`.Attachment` class. Pass a list of 11 | these attachment instances to the `attachments` parameter when creating a 12 | message, or append to the {attr}`.Message.attachments` list later. 13 | 14 | Email-Simplified also refers to these as "download attachments", in contrast to 15 | "inline attachments" described below. Download attachments are typically shown 16 | next to the message subject and can be saved and opened by the user. 17 | 18 | ## HTML and Text 19 | 20 | A message the contains HTML content should also contain text content. This 21 | ensures support for clients that don't support HTML or prefer viewing plain 22 | text. 23 | 24 | If you only set {attr}`~.Message.html` and not {attr}`~.Message.text`, then 25 | Email-Simplified will extract the text content from the HTML. This may fail if 26 | the HTML is invalid. Depending on the complexity of the HTML, this could look 27 | pretty bad. It's better to pass `text` content yourself. 28 | 29 | ## HTML Inline Attachments 30 | 31 | When specifying an HTML body for the message, you can also specify inline 32 | attachments. These can be referenced by a special URL in the HTML, such as 33 | in `` tags. 34 | 35 | Pass a list of these attachment instances to the `inline_attachments` parameter 36 | when creating a message, or append to the {attr}`.Message.inline_attachments` 37 | list later. 38 | 39 | Your message must contain an {attr}`~.Message.html` body, otherwise inline 40 | attachments will be omitted. 41 | 42 | To reference inline attachments in HTML, you need the attachment's CID. This 43 | will be automatically generated when accessing {attr}`.Attachment.cid`, or can 44 | be assigned to a custom value. 45 | 46 | ```python 47 | from email_simplified import Attachment, Message 48 | 49 | with open("logo.png", "rb") as f: 50 | image = Attachment(f.read(), filename="logo.png") 51 | 52 | message = Message( 53 | text="Hello, World!", 54 | html=f"""\ 55 |

Hello, World!


56 | 57 | """, 58 | inline_attachments=[image], 59 | ) 60 | ``` 61 | 62 | :::{warning} 63 | The f-string in the example above is only for demonstration purposes and can 64 | result in unsafe HTML. If you render any user input into the HTML, you must use 65 | a library such as [MarkupSafe] to escape the values. Or use a template library 66 | such as [Jinja] which handles escaping. 67 | ::: 68 | 69 | [MarkupSafe]: https://markupsafe.palletsprojects.com 70 | [Jinja]: https://jinja.palletsprojects.com 71 | 72 | ## Addresses 73 | 74 | Email-Simplified handles various complexities in email addresses. Addresses can 75 | be `user@domain` or `Name `. If the domain part contains Unicode, 76 | it will be IDNA encoded. If the name or user part contains Unicode, it will not 77 | be encoded, and the server must support the SMTPUTF8 extension. There are some 78 | rules about what addresses are valid. 79 | 80 | Addresses are stored as Python {class}`email.headerregistry.Address` objects. 81 | Strings can be passed to the various parameters and lists, and will be 82 | converted. `Address` objects can be passed as well, and will IDNA be encoded if 83 | needed. This handles keeping all values normalized and valid ahead of time. 84 | 85 | ## MIME 86 | 87 | While unlikely, email MIME messages can be constructed pretty much arbitrarily. 88 | The {class}`.Message` class only represents a "common"/"standard" message 89 | structure with text, HTML, inline attachments, and download attachments. 90 | 91 | Esoteric MIME constructs beyond that are still supported by 92 | {meth}`.EmailHandler.send`, but must be constructed separately with Python's 93 | {mod}`email.message`. 94 | 95 | If you have a MIME {class}`email.message.EmailMessage` from somewhere else, you 96 | can call Email-Simplified's {meth}`.Message.from_mime` to convert it for easier 97 | modification in your own code. Call {meth}`.Message.to_mime` if you need to 98 | pass it back to code that works with MIME. Both of these only support the 99 | "standard" message structure: 100 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "email-simplified" 3 | version = "0.1.0" 4 | description = "Email builder and sending handler interface." 5 | readme = "README.md" 6 | authors = [{ name = "David Lord" }] 7 | license = "MIT" 8 | license-files = ["LICENSE.txt"] 9 | requires-python = ">=3.11" 10 | 11 | [project.urls] 12 | Documentation = "https://email-simplified.readthedocs.io" 13 | Changes = "https://email-simplified.readthedocs.io/en/latest/changes/" 14 | Source = "https://github.com/davidism/email-simplified/" 15 | 16 | [project.entry-points."email_simplified.handler"] 17 | smtp = "email_simplified.handlers.smtp:SMTPEmailHandler" 18 | test = "email_simplified.handlers.test:TestEmailHandler" 19 | 20 | [build-system] 21 | requires = ["pdm-backend>=2.4"] 22 | build-backend = "pdm.backend" 23 | 24 | [dependency-groups] 25 | dev = [ 26 | "mypy", 27 | "pre-commit", 28 | "pyright", 29 | "pytest", 30 | "pytest-cov", 31 | "ruff", 32 | "tox", 33 | "tox-uv", 34 | ] 35 | docs = [ 36 | "furo", 37 | "myst-parser", 38 | "sphinx", 39 | ] 40 | docs-auto = [ 41 | "sphinx-autobuild", 42 | ] 43 | gha-update = [ 44 | "gha-update ; python_version>='3.12'", 45 | ] 46 | pre-commit = [ 47 | "pre-commit", 48 | ] 49 | tests = [ 50 | "pytest", 51 | "pytest-cov", 52 | ] 53 | typing = [ 54 | "mypy", 55 | "pyright", 56 | "pytest", 57 | ] 58 | 59 | [tool.pytest.ini_options] 60 | testpaths = ["tests"] 61 | filterwarnings = [ 62 | "error", 63 | ] 64 | 65 | [tool.coverage.run] 66 | branch = true 67 | source = ["email_simplified", "tests"] 68 | 69 | [tool.coverage.paths] 70 | source = ["src", "*/site-packages"] 71 | 72 | [tool.coverage.report] 73 | exclude_also = [ 74 | "if t.TYPE_CHECKING", 75 | "raise NotImplementedError", 76 | ": \\.{3}", 77 | ] 78 | 79 | [tool.mypy] 80 | python_version = "3.11" 81 | files = ["src", "tests"] 82 | show_error_codes = true 83 | pretty = true 84 | strict = true 85 | 86 | [tool.pyright] 87 | pythonVersion = "3.11" 88 | include = ["src", "tests"] 89 | typeCheckingMode = "strict" 90 | 91 | [tool.ruff] 92 | src = ["src"] 93 | fix = true 94 | show-fixes = true 95 | output-format = "full" 96 | 97 | [tool.ruff.lint] 98 | select = [ 99 | "B", # flake8-bugbear 100 | "E", # pycodestyle error 101 | "F", # pyflakes 102 | "I", # isort 103 | "UP", # pyupgrade 104 | "W", # pycodestyle warning 105 | ] 106 | 107 | [tool.ruff.lint.isort] 108 | force-single-line = true 109 | order-by-type = false 110 | 111 | [tool.gha-update] 112 | tag-only = [ 113 | "slsa-framework/slsa-github-generator", 114 | ] 115 | 116 | [tool.tox] 117 | env_list = [ 118 | "py3.13", "py3.12", "py3.11", 119 | "style", 120 | "typing", 121 | "docs", 122 | ] 123 | 124 | [tool.tox.env_run_base] 125 | runner = "uv-venv-lock-runner" 126 | package = "wheel" 127 | wheel_build_env = ".pkg" 128 | constrain_package_deps = true 129 | use_frozen_constraints = true 130 | dependency_groups = ["tests"] 131 | commands = [[ 132 | "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", 133 | "--cov", "--cov-report=term-missing", 134 | { replace = "posargs", default = [], extend = true }, 135 | ]] 136 | 137 | [tool.tox.env.style] 138 | dependency_groups = ["pre-commmit"] 139 | skip_install = true 140 | commands = [["pre-commit", "run", "--all-files"]] 141 | 142 | [tool.tox.env.typing] 143 | dependency_groups = ["typing"] 144 | commands = [ 145 | ["mypy"], 146 | ["pyright"], 147 | ["pyright", "--verifytypes", "email_simplified", "--ignoreexternal"], 148 | ] 149 | 150 | [tool.tox.env.docs] 151 | dependency_groups = ["docs"] 152 | commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]] 153 | 154 | [tool.tox.env.docs-auto] 155 | dependency_groups = ["docs", "docs-auto"] 156 | commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]] 157 | 158 | [tool.tox.env.update-actions] 159 | labels = ["update"] 160 | dependency_groups = ["gha-update"] 161 | skip_install = true 162 | commands = [["gha-update"]] 163 | 164 | [tool.tox.env.update-pre_commit] 165 | labels = ["update"] 166 | dependency_groups = ["pre-commit"] 167 | skip_install = true 168 | commands = [["pre-commit", "autoupdate", "--freeze", "-j4"]] 169 | 170 | [tool.tox.env.update-requirements] 171 | labels = ["update"] 172 | skip_install = true 173 | commands = [["uv", "lock", { replace = "posargs", default = ["-U"], extend = true }]] 174 | -------------------------------------------------------------------------------- /docs/smtp.md: -------------------------------------------------------------------------------- 1 | # SMTP Handler 2 | 3 | The built-in SMTP handler wraps Python's {mod}`smtplib` with a much simpler 4 | interface. It handles setting up and using the client, which contains some 5 | steps that are easy to forget or get wrong when using `smtplib` manually. 6 | 7 | Many email services offer HTTP APIs to send messages. However, even if they 8 | don't emphasize it, they probably still offer SMTP as well. For most use cases, 9 | SMTP will be fine. If you need specific service behavior, you can write and 10 | publish a handler as described in {doc}`handler`. 11 | 12 | ## Configuration 13 | 14 | All arguments to {class}`.SMTPEmailHandler` are optional. They are passed on 15 | to {class}`smtplib.SMTP`, which has defaults for all values. In particular, the 16 | default host and port is `localhost:25`. When using port 465, TLS is 17 | automatically configured and operating system trust is used by default. 18 | 19 | ## TLS 20 | 21 | If you pass `port=465`, this will automatically enable 22 | {attr}`~.SMTPEmailHandler.use_tls`. You can also pass `use_tls=True` if the 23 | service you use requires a different port. 24 | 25 | When `use_tls` or `use_starttls` are enabled, a TLS context is required, which 26 | defines what server certificates to trust. You should _never_ disable this, as 27 | it makes TLS essentially insecure. If you don't pass a context, 28 | {func}`ssl.create_default_context` returns a context that uses your operating 29 | system's trust store, which is appropriate for publicly-accessible services. If 30 | you're using custom certificates, you can use the same function and pass the 31 | appropriate public, private, and CA certificates. 32 | 33 | If both params are passed, `use_tls` takes precedence over `use_starttls`. 34 | STARTTLS is often confused with true TLS, and should generally be avoided as 35 | it is an older and less secure option. TLS will secure the entire connection, 36 | STARTTLS will only secure the connection after an initial unsecure portion. 37 | Make sure you find your provider's TLS port, and not their STARTTLS port. You 38 | may also see TLS referred to as its earlier predecessor SSL. 39 | 40 | ## Gmail and OAuth 41 | 42 | Gmail and some other providers require stricter authentication than your 43 | username and password. This is so that you don't accidentally reveal your 44 | full Google account credentials. Many email readers now support OAuth 45 | authentication flows, but this is difficult to implement in applications that 46 | don't provide direct user auth. For example, a web application needs to start 47 | up without an admin present to go through the OAuth flow. 48 | 49 | Look for a feature in your provider called something like "app passwords". This 50 | generates a password that only works in limited scopes, and cannot be used to 51 | log into your entire account. For Gmail, you can find that here 52 | and here 53 | . 54 | 55 | It is possible to use OAuth rather than an app password, but how to do so is 56 | outside the scope of these docs. In general, you'd need to start a temporary 57 | local server to receive the authentication callback, then send the user to the 58 | service's authentication page with the correct settings. Storing the returned 59 | token for some time is also a good idea to avoid having to go through the auth 60 | flow every time. 61 | 62 | ## Don't Send With a Personal Account 63 | 64 | Using your regular mail account can seem convenient, but is probably not a good 65 | idea. It ties your application to your identity, and often times personal mail 66 | accounts have limits that make using them for applications difficult. Sending 67 | "transactional" emails, such as notifications and password resets, through a 68 | personal account can also cause the service to start considering it a source of 69 | spam. 70 | 71 | In development, consider using a local SMTP application such as [mailcatcher], 72 | which can also display the emails and never sends them externally. In 73 | production, choose a service that is designed for applications and sending large 74 | numbers of transactional emails. Most services offer a free tier that will be 75 | more than enough for most hobby applications. 76 | 77 | [mailcatcher]: https://github.com/sj26/mailcatcher 78 | 79 | ## Batching Recipients 80 | 81 | Some SMTP servers enforce a limit on the number of recipients for a single 82 | message. We've observed this especially with Microsoft Exchange servers. 83 | 84 | Pass `recipients_per_message=N`, where N <= the server's limit, and the handler 85 | will automatically batch the recipients into multiple low level send calls. 86 | 87 | At the low level, this limit seems to be the number of `RCPT` commands before a 88 | `DATA` command. This is relevant when one message has many recipients, but is not 89 | the same as sending many messages each with different recipients. If your server 90 | limits the number of messages (`DATA` commands) in a single connection, you'll 91 | need to implement batching the calls to {meth}`.SMTPEmailHandler.send` instead. 92 | -------------------------------------------------------------------------------- /src/email_simplified/handlers/smtp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ssl 4 | import typing as t 5 | from contextlib import contextmanager 6 | from email.message import EmailMessage as _EmailMessage 7 | from itertools import islice 8 | from smtplib import SMTP 9 | from smtplib import SMTP_SSL 10 | from smtplib import SMTP_SSL_PORT 11 | from ssl import SSLContext 12 | 13 | from ..attachment import local_hostname 14 | from ..message import Message 15 | from .base import EmailHandler 16 | 17 | 18 | class SMTPEmailHandler(EmailHandler): 19 | """Email handler that sends with SMTP using Python's built-in 20 | :mod:`smtplib`. All arguments are optional, :class:`smtplib.SMTP` uses 21 | default values if something is not given. 22 | 23 | :param host: Host to connect to. 24 | :param port: Port to connect to. 25 | :param use_tls: The connection should be established with TLS. Default is 26 | ``True`` if ``port`` is 465. 27 | :param use_starttls: The connection should be upgraded to TLS after 28 | establishing a plain connection first. You should prefer ``use_tls`` 29 | instead if your SMTP server supports it. 30 | :param tls_context: An :class:`ssl.SSLContext` to use when ``use_tls`` or 31 | ``use_starttls`` is enabled. Defaults to :func:`ssl.create_default_context`, 32 | which enables verifying any server cert trusted by the OS. You should 33 | only need to change this if the server uses a custom cert, or needs a 34 | client cert sent as well. 35 | :param timeout: Connection timeout. Default is no timeout. 36 | :param username: Username to log in with. 37 | :param password: Password to log in with. 38 | :param default_from: Default address to send from if the ``Sender`` or 39 | ``From`` header is not set on a message. 40 | :param recipients_per_message: If a message has more than this number of 41 | recipients, split the send calls to batches of this size. This is to 42 | support servers that have a limit configured. By default, no batching 43 | is done. 44 | """ 45 | 46 | def __init__( 47 | self, 48 | *, 49 | host: str | None = None, 50 | port: int | None = None, 51 | use_tls: bool | None = None, 52 | use_starttls: bool = False, 53 | tls_context: SSLContext | None = None, 54 | timeout: float | None = None, 55 | username: str | None = None, 56 | password: str | None = None, 57 | default_from: str | None = None, 58 | recipients_per_message: int | None = None, 59 | ): 60 | self.host = host 61 | """Host to connect to.""" 62 | 63 | self.port = port 64 | """Port to connect to.""" 65 | 66 | self.timeout = timeout 67 | """Connection timeout.""" 68 | 69 | self.username = username 70 | """Username to log in with.""" 71 | 72 | self.password = password 73 | """Password to log in with.""" 74 | 75 | self.default_from = default_from 76 | """Default address to send from if the ``Sender`` or ``From`` header is 77 | not set on a message. 78 | """ 79 | 80 | self.recipients_per_message = recipients_per_message 81 | """If a message has more than this number of recipients, split the 82 | low-level send calls to batches of this size. This is to support servers 83 | that have a limit configured. By default no batching is done. 84 | """ 85 | 86 | if use_tls is None: 87 | use_tls = port == SMTP_SSL_PORT 88 | 89 | self.use_tls = use_tls 90 | """The connection should be established with TLS.""" 91 | 92 | self.use_starttls: bool = not use_tls and use_starttls 93 | """The connection should be upgraded to TLS after establishing a plain 94 | connection first. 95 | """ 96 | 97 | if use_tls or use_starttls and tls_context is None: 98 | tls_context = ssl.create_default_context() 99 | 100 | self.tls_context = tls_context 101 | """An :class:`ssl.SSLContext` to use when ``use_tls`` or 102 | ``use_starttls`` is enabled. 103 | """ 104 | 105 | @classmethod 106 | def from_config(cls, config: dict[str, t.Any]) -> t.Self: 107 | """Create a handler from a config dict. Config keys match the 108 | arguments to :class:`.SMTPEmailHandler`, and are all optional. 109 | """ 110 | return cls( 111 | host=config.get("host"), 112 | port=config.get("port"), 113 | use_tls=config.get("use_tls"), 114 | use_starttls=config.get("use_starttls", False), 115 | tls_context=config.get("tls_context"), 116 | timeout=config.get("timeout"), 117 | username=config.get("username"), 118 | password=config.get("password"), 119 | default_from=config.get("default_from"), 120 | ) 121 | 122 | @contextmanager 123 | def connect(self) -> t.Iterator[SMTP]: 124 | """Context manager that creates an :class:`smtplib.SMTP` client, connects, 125 | logs in, then closes when exiting the block. 126 | """ 127 | smtp_cls: type[SMTP | SMTP_SSL] = SMTP 128 | smtp_args: dict[str, t.Any] = { 129 | "host": self.host, 130 | "port": self.port, 131 | "local_hostname": local_hostname(), 132 | "timeout": self.timeout, 133 | } 134 | 135 | if self.use_tls: 136 | smtp_cls = SMTP_SSL 137 | smtp_args["context"] = self.tls_context 138 | 139 | with smtp_cls(**smtp_args) as client: 140 | if self.use_starttls: 141 | client.starttls(context=self.tls_context) 142 | 143 | if self.username is not None and self.password is not None: 144 | client.login(self.username, self.password) 145 | 146 | yield client 147 | 148 | def _send_batched( 149 | self, client: SMTP, message: _EmailMessage, from_addr: str | None 150 | ) -> None: 151 | assert self.recipients_per_message is not None 152 | recipient_fields = (message["to"], message["cc"], message["bcc"]) 153 | recipients = ( 154 | a.addr_spec for f in recipient_fields if f is not None for a in f.addresses 155 | ) 156 | 157 | # itertools.batched for Python < 3.12 158 | while batch := tuple(islice(recipients, self.recipients_per_message)): 159 | client.send_message(message, from_addr=from_addr, to_addrs=batch) 160 | 161 | def send(self, messages: list[Message | _EmailMessage]) -> None: 162 | if not messages: 163 | return 164 | 165 | with self.connect() as client: 166 | for message in messages: 167 | if isinstance(message, Message): 168 | mime_message = message.to_mime() 169 | else: 170 | mime_message = message 171 | 172 | from_addr = mime_message["sender"] or mime_message["from"] 173 | 174 | if from_addr: 175 | from_addr = from_addr.addresses[0].addr_spec 176 | else: 177 | from_addr = self.default_from 178 | 179 | if self.recipients_per_message: 180 | self._send_batched(client, mime_message, from_addr) 181 | else: 182 | client.send_message(mime_message, from_addr=from_addr) 183 | -------------------------------------------------------------------------------- /src/email_simplified/message.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import collections.abc as cabc 4 | import html.parser 5 | import typing as t 6 | from email.headerregistry import Address 7 | from email.headerregistry import AddressHeader 8 | from email.message import EmailMessage as _EmailMessage 9 | 10 | from .address import AddressList 11 | from .address import prepare_address 12 | from .attachment import Attachment 13 | 14 | 15 | class Message: 16 | """A representation of the typical data found in an email message. Can be 17 | converted to and from an :class:`email.message.EmailMessage`. 18 | 19 | While unlikely, MIME messages can be constructed pretty much arbitrarily. 20 | This class only represents a "common"/"standard" message structure with 21 | text, HTML, download attachments, and inline attachments. 22 | 23 | Addresses passed to the various arguments/attributes can be strings in the 24 | form ``user@domain`` or ``Name ``, or instances of 25 | :class:`email.headerregistry.Address`. If the ``user`` part has non-ASCII 26 | characters, SMTP servers must support the ``SMTPUTF8`` extension. The domain 27 | part will be encoded using IDNA. 28 | 29 | :param subject: The text in the subject line of the message. 30 | :param text: The plain text content. 31 | :param from_addr: The address to show the message was sent from. 32 | :param reply_to: Instruct users to reply to this address rather than 33 | ``from_addr``. 34 | :param to: The primary recipients, visible and replied to by all other 35 | recipients. 36 | :param cc: The secondary recipients, visible and replied to by all other 37 | recipients. 38 | :param bcc: Hidden recipients. Not visible or replied to by any other 39 | recipients. When sending mass email, use this instead of ``to`` and 40 | ``cc`` to avoid "reply all email storms". 41 | :param attachments: Downloadable files separate from the content. 42 | :param html: The HTML text content. ``text`` content should also be 43 | provided, but if it's not then text is extracted from the HTML. 44 | :param inline_attachments: Files that can be linked and displayed inside 45 | HTML content. Only relevant when ``html`` content is provided. 46 | """ 47 | 48 | def __init__( 49 | self, 50 | *, 51 | subject: str | None = None, 52 | text: str | None = None, 53 | from_addr: str | Address | None = None, 54 | reply_to: str | Address | None = None, 55 | to: list[str | Address] | None = None, 56 | cc: list[str | Address] | None = None, 57 | bcc: list[str | Address] | None = None, 58 | attachments: list[Attachment] | None = None, 59 | html: str | None = None, 60 | inline_attachments: list[Attachment] | None = None, 61 | ): 62 | self.subject: str | None = subject 63 | """The text in the subject line of the message.""" 64 | 65 | self.text: str | None = text 66 | """The plain text content.""" 67 | 68 | self.html: str | None = html 69 | """The HTML text content. :attr:`text` content should also be provided, 70 | but if it's not then text is extracted from the HTML. 71 | """ 72 | 73 | if from_addr: 74 | self._from_addr: Address | None = prepare_address(from_addr) 75 | else: 76 | self._from_addr = None 77 | 78 | if reply_to: 79 | self._reply_to: Address | None = prepare_address(reply_to) 80 | else: 81 | self._reply_to = None 82 | 83 | self._to: AddressList = AddressList(to or ()) 84 | self._cc: AddressList = AddressList(cc or ()) 85 | self._bcc: AddressList = AddressList(bcc or ()) 86 | 87 | self.attachments: list[Attachment] = attachments or [] 88 | """Downloadable files separate from the content.""" 89 | 90 | self.inline_attachments: list[Attachment] = inline_attachments or [] 91 | """Files that can be linked and displayed inside HTML content. Only 92 | relevant when :attr:`html` content is provided. 93 | """ 94 | 95 | @property 96 | def from_addr(self) -> Address | None: 97 | """The address to show the message was sent from.""" 98 | return self._from_addr 99 | 100 | @from_addr.setter 101 | def from_addr(self, value: str | Address | None) -> None: 102 | if value: 103 | self._from_addr = prepare_address(value) 104 | else: 105 | self._from_addr = None 106 | 107 | @property 108 | def reply_to(self) -> Address | None: 109 | """Instruct users to reply to this address rather than :attr:`from_addr`.""" 110 | return self._reply_to 111 | 112 | @reply_to.setter 113 | def reply_to(self, value: str | Address | None) -> None: 114 | if value: 115 | self._reply_to = prepare_address(value) 116 | else: 117 | self._reply_to = None 118 | 119 | @property 120 | def to(self) -> AddressList: 121 | """The primary recipients, visible and replied to by all other recipients.""" 122 | return self._to 123 | 124 | @to.setter 125 | def to(self, value: cabc.Iterable[str | Address]) -> None: 126 | self._to[:] = value 127 | 128 | @property 129 | def cc(self) -> AddressList: 130 | """The secondary recipients, visible and replied to by all other recipients.""" 131 | return self._cc 132 | 133 | @cc.setter 134 | def cc(self, value: cabc.Iterable[str | Address]) -> None: 135 | self._cc[:] = value 136 | 137 | @property 138 | def bcc(self) -> AddressList: 139 | """Hidden recipients. Not visible or replied to by any other recipients 140 | When sending mass email, use this instead of :attr:`to` and :attr:`cc` 141 | to avoid "reply all email storms". 142 | """ 143 | return self._bcc 144 | 145 | @bcc.setter 146 | def bcc(self, value: cabc.Iterable[str | Address]) -> None: 147 | self._bcc[:] = value 148 | 149 | def to_mime(self) -> _EmailMessage: 150 | """Convert this :class:`email_simplified.Message` to an 151 | :class:`email.message.EmailMessage`. 152 | """ 153 | message = _EmailMessage() 154 | 155 | if self.subject: 156 | message["Subject"] = self.subject 157 | 158 | if self.from_addr: 159 | message["From"] = self.from_addr 160 | 161 | if self.reply_to: 162 | message["Reply-To"] = self.reply_to 163 | 164 | if self.to: 165 | message["To"] = self.to 166 | 167 | if self.cc: 168 | message["CC"] = self.cc 169 | 170 | if self.bcc: 171 | message["BCC"] = self.bcc 172 | 173 | if self.text: 174 | message.set_content(self.text) 175 | elif self.html: 176 | text = _HTMLToText.process(self.html) 177 | message.set_content(text) 178 | 179 | if self.html: 180 | message.add_alternative(self.html, subtype="html") 181 | part = t.cast(_EmailMessage, message.get_payload(1)) 182 | 183 | for attachment in self.inline_attachments: 184 | attachment.add_to_mime(part, inline=True) 185 | 186 | for attachment in self.attachments: 187 | attachment.add_to_mime(message) 188 | 189 | return message 190 | 191 | @classmethod 192 | def from_mime(cls, message: _EmailMessage) -> t.Self: 193 | """Convert an :class:`email.message.EmailMessage` message to a 194 | :class:`email_simplified.Message`. 195 | 196 | MIME messages can potentially have arbitrary structures. This only 197 | supports converting from a "standard" structure where a message 198 | has text, html, inline attachment, and download attachment parts. Can 199 | be one of the following: 200 | 201 | - One ``text/plain`` part. 202 | - One ``multipart/alternative`` part containing one ``text/plain`` 203 | part then either: 204 | 205 | - One ``text/html`` part. 206 | - One ``multipart/related`` part containing one ``text/html`` part 207 | then one or more inline attachment parts. 208 | 209 | - One ``multipart/mixed`` part containing one of the above parts then 210 | one or more download attachment parts. 211 | 212 | :param message: The MIME part message to convert. 213 | """ 214 | original = message 215 | content_type = original.get_content_type() 216 | parts: list[_EmailMessage] 217 | text: str 218 | html: str | None 219 | attachments: list[Attachment] = [] 220 | inline_attachments: list[Attachment] = [] 221 | 222 | if content_type == "multipart/mixed": 223 | message, *parts = t.cast(list[_EmailMessage], original.get_payload()) 224 | content_type = message.get_content_type() 225 | 226 | for part in parts: 227 | attachments.append( 228 | Attachment( 229 | data=part.get_content(), 230 | filename=part.get_filename(), 231 | mimetype=part.get_content_type(), 232 | ) 233 | ) 234 | 235 | if content_type == "multipart/alternative": 236 | text_part: _EmailMessage 237 | html_part: _EmailMessage 238 | text_part, html_part = t.cast(list[_EmailMessage], message.get_payload()) 239 | text = text_part.get_content() 240 | 241 | if html_part.get_content_type() == "multipart/related": 242 | html_part, *parts = t.cast(list[_EmailMessage], html_part.get_payload()) 243 | 244 | for part in parts: 245 | attachment = Attachment( 246 | data=part.get_content(), 247 | filename=part.get_filename(), 248 | mimetype=part.get_content_type(), 249 | ) 250 | attachment.cid = part["content-id"] 251 | inline_attachments.append(attachment) 252 | 253 | html = html_part.get_content() 254 | else: 255 | text = message.get_content() 256 | html = None 257 | 258 | from_addr: AddressHeader = original["from"] 259 | reply_to: AddressHeader = original["reply-to"] 260 | to: AddressHeader = original["to"] 261 | cc: AddressHeader = original["cc"] 262 | bcc: AddressHeader = original["bcc"] 263 | return cls( 264 | subject=original["subject"], 265 | text=text, 266 | from_addr=from_addr.addresses[0] if from_addr else None, 267 | reply_to=reply_to.addresses[0] if reply_to else None, 268 | to=list(to.addresses) if to else [], 269 | cc=list(cc.addresses) if cc else [], 270 | bcc=list(bcc.addresses) if bcc else [], 271 | attachments=attachments, 272 | html=html, 273 | inline_attachments=inline_attachments, 274 | ) 275 | 276 | 277 | class _HTMLToText(html.parser.HTMLParser): 278 | """Extract all text data from an HTML document. Used to create text content 279 | for an email if only HTML content is given. 280 | """ 281 | 282 | def __init__(self) -> None: 283 | super().__init__(convert_charrefs=True) 284 | self.data: list[str] = [] 285 | """Accumulates the text parts extracted during parsing.""" 286 | 287 | @classmethod 288 | def process(cls, html: str) -> str: 289 | """Convert an HTML document into plain text. 290 | 291 | :param html: The HTML to convert. 292 | """ 293 | parser = cls() 294 | parser.feed(html) 295 | return parser.text 296 | 297 | @property 298 | def text(self) -> str: 299 | """Join the text parts into a single string.""" 300 | return "".join(self.data) 301 | 302 | def handle_data(self, data: str) -> None: 303 | self.data.append(data) 304 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.11" 3 | resolution-markers = [ 4 | "python_full_version >= '3.12'", 5 | "python_full_version < '3.12'", 6 | ] 7 | 8 | [[package]] 9 | name = "alabaster" 10 | version = "1.0.0" 11 | source = { registry = "https://pypi.org/simple" } 12 | sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } 13 | wheels = [ 14 | { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, 15 | ] 16 | 17 | [[package]] 18 | name = "anyio" 19 | version = "4.8.0" 20 | source = { registry = "https://pypi.org/simple" } 21 | dependencies = [ 22 | { name = "idna" }, 23 | { name = "sniffio" }, 24 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 25 | ] 26 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 27 | wheels = [ 28 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 29 | ] 30 | 31 | [[package]] 32 | name = "babel" 33 | version = "2.17.0" 34 | source = { registry = "https://pypi.org/simple" } 35 | sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } 36 | wheels = [ 37 | { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, 38 | ] 39 | 40 | [[package]] 41 | name = "beautifulsoup4" 42 | version = "4.13.3" 43 | source = { registry = "https://pypi.org/simple" } 44 | dependencies = [ 45 | { name = "soupsieve" }, 46 | { name = "typing-extensions" }, 47 | ] 48 | sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } 49 | wheels = [ 50 | { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, 51 | ] 52 | 53 | [[package]] 54 | name = "cachetools" 55 | version = "5.5.1" 56 | source = { registry = "https://pypi.org/simple" } 57 | sdist = { url = "https://files.pythonhosted.org/packages/d9/74/57df1ab0ce6bc5f6fa868e08de20df8ac58f9c44330c7671ad922d2bbeae/cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95", size = 28044 } 58 | wheels = [ 59 | { url = "https://files.pythonhosted.org/packages/ec/4e/de4ff18bcf55857ba18d3a4bd48c8a9fde6bb0980c9d20b263f05387fd88/cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb", size = 9530 }, 60 | ] 61 | 62 | [[package]] 63 | name = "certifi" 64 | version = "2025.1.31" 65 | source = { registry = "https://pypi.org/simple" } 66 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 67 | wheels = [ 68 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 69 | ] 70 | 71 | [[package]] 72 | name = "cfgv" 73 | version = "3.4.0" 74 | source = { registry = "https://pypi.org/simple" } 75 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } 76 | wheels = [ 77 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, 78 | ] 79 | 80 | [[package]] 81 | name = "chardet" 82 | version = "5.2.0" 83 | source = { registry = "https://pypi.org/simple" } 84 | sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } 85 | wheels = [ 86 | { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, 87 | ] 88 | 89 | [[package]] 90 | name = "charset-normalizer" 91 | version = "3.4.1" 92 | source = { registry = "https://pypi.org/simple" } 93 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 94 | wheels = [ 95 | { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, 96 | { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, 97 | { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, 98 | { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, 99 | { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, 100 | { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, 101 | { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, 102 | { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, 103 | { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, 104 | { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, 105 | { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, 106 | { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, 107 | { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, 108 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, 109 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, 110 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, 111 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, 112 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, 113 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, 114 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, 115 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, 116 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, 117 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, 118 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, 119 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, 120 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, 121 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 122 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 123 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 124 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 125 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 126 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 127 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 128 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 129 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 130 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 131 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 132 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 133 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 134 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 135 | ] 136 | 137 | [[package]] 138 | name = "click" 139 | version = "8.1.8" 140 | source = { registry = "https://pypi.org/simple" } 141 | dependencies = [ 142 | { name = "colorama", marker = "sys_platform == 'win32'" }, 143 | ] 144 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 145 | wheels = [ 146 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 147 | ] 148 | 149 | [[package]] 150 | name = "colorama" 151 | version = "0.4.6" 152 | source = { registry = "https://pypi.org/simple" } 153 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 154 | wheels = [ 155 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 156 | ] 157 | 158 | [[package]] 159 | name = "coverage" 160 | version = "7.6.12" 161 | source = { registry = "https://pypi.org/simple" } 162 | sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } 163 | wheels = [ 164 | { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, 165 | { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, 166 | { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, 167 | { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, 168 | { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, 169 | { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, 170 | { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, 171 | { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, 172 | { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, 173 | { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, 174 | { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, 175 | { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, 176 | { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, 177 | { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, 178 | { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, 179 | { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, 180 | { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, 181 | { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, 182 | { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, 183 | { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, 184 | { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, 185 | { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, 186 | { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, 187 | { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, 188 | { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, 189 | { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, 190 | { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, 191 | { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, 192 | { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, 193 | { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, 194 | { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, 195 | { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, 196 | { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, 197 | { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, 198 | { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, 199 | { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, 200 | { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, 201 | { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, 202 | { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, 203 | { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, 204 | { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, 205 | ] 206 | 207 | [package.optional-dependencies] 208 | toml = [ 209 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 210 | ] 211 | 212 | [[package]] 213 | name = "distlib" 214 | version = "0.3.9" 215 | source = { registry = "https://pypi.org/simple" } 216 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } 217 | wheels = [ 218 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, 219 | ] 220 | 221 | [[package]] 222 | name = "docutils" 223 | version = "0.21.2" 224 | source = { registry = "https://pypi.org/simple" } 225 | sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } 226 | wheels = [ 227 | { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, 228 | ] 229 | 230 | [[package]] 231 | name = "email-simplified" 232 | version = "0.1.0" 233 | source = { editable = "." } 234 | 235 | [package.dev-dependencies] 236 | dev = [ 237 | { name = "mypy" }, 238 | { name = "pre-commit" }, 239 | { name = "pyright" }, 240 | { name = "pytest" }, 241 | { name = "pytest-cov" }, 242 | { name = "ruff" }, 243 | { name = "tox" }, 244 | { name = "tox-uv" }, 245 | ] 246 | docs = [ 247 | { name = "furo" }, 248 | { name = "myst-parser" }, 249 | { name = "sphinx" }, 250 | ] 251 | docs-auto = [ 252 | { name = "sphinx-autobuild" }, 253 | ] 254 | gha-update = [ 255 | { name = "gha-update", marker = "python_full_version >= '3.12'" }, 256 | ] 257 | pre-commit = [ 258 | { name = "pre-commit" }, 259 | ] 260 | tests = [ 261 | { name = "pytest" }, 262 | { name = "pytest-cov" }, 263 | ] 264 | typing = [ 265 | { name = "mypy" }, 266 | { name = "pyright" }, 267 | { name = "pytest" }, 268 | ] 269 | 270 | [package.metadata] 271 | 272 | [package.metadata.requires-dev] 273 | dev = [ 274 | { name = "mypy" }, 275 | { name = "pre-commit" }, 276 | { name = "pyright" }, 277 | { name = "pytest" }, 278 | { name = "pytest-cov" }, 279 | { name = "ruff" }, 280 | { name = "tox" }, 281 | { name = "tox-uv" }, 282 | ] 283 | docs = [ 284 | { name = "furo" }, 285 | { name = "myst-parser" }, 286 | { name = "sphinx" }, 287 | ] 288 | docs-auto = [{ name = "sphinx-autobuild" }] 289 | gha-update = [{ name = "gha-update", marker = "python_full_version >= '3.12'" }] 290 | pre-commit = [{ name = "pre-commit" }] 291 | tests = [ 292 | { name = "pytest" }, 293 | { name = "pytest-cov" }, 294 | ] 295 | typing = [ 296 | { name = "mypy" }, 297 | { name = "pyright" }, 298 | { name = "pytest" }, 299 | ] 300 | 301 | [[package]] 302 | name = "filelock" 303 | version = "3.17.0" 304 | source = { registry = "https://pypi.org/simple" } 305 | sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } 306 | wheels = [ 307 | { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, 308 | ] 309 | 310 | [[package]] 311 | name = "furo" 312 | version = "2024.8.6" 313 | source = { registry = "https://pypi.org/simple" } 314 | dependencies = [ 315 | { name = "beautifulsoup4" }, 316 | { name = "pygments" }, 317 | { name = "sphinx" }, 318 | { name = "sphinx-basic-ng" }, 319 | ] 320 | sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 } 321 | wheels = [ 322 | { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, 323 | ] 324 | 325 | [[package]] 326 | name = "gha-update" 327 | version = "0.1.0" 328 | source = { registry = "https://pypi.org/simple" } 329 | dependencies = [ 330 | { name = "click", marker = "python_full_version >= '3.12'" }, 331 | { name = "httpx", marker = "python_full_version >= '3.12'" }, 332 | ] 333 | sdist = { url = "https://files.pythonhosted.org/packages/d5/ac/f1b6699a529bd298a777199861a8232590bb612eac92e15bf1033134f123/gha_update-0.1.0.tar.gz", hash = "sha256:8c0f55ed7bdc11fb061d67984814fd642bd3a1872028e34c15c913cd59202d53", size = 3345 } 334 | wheels = [ 335 | { url = "https://files.pythonhosted.org/packages/98/06/832338d1b5f82f17e1f4985146d81050cd2709ea54dab3f15b343ad227cc/gha_update-0.1.0-py3-none-any.whl", hash = "sha256:ec110088749eed66b5f55cc7f15f41a6af037446a5ac6a435b0768a57d52e087", size = 4513 }, 336 | ] 337 | 338 | [[package]] 339 | name = "h11" 340 | version = "0.14.0" 341 | source = { registry = "https://pypi.org/simple" } 342 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 343 | wheels = [ 344 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 345 | ] 346 | 347 | [[package]] 348 | name = "httpcore" 349 | version = "1.0.7" 350 | source = { registry = "https://pypi.org/simple" } 351 | dependencies = [ 352 | { name = "certifi", marker = "python_full_version >= '3.12'" }, 353 | { name = "h11", marker = "python_full_version >= '3.12'" }, 354 | ] 355 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 356 | wheels = [ 357 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 358 | ] 359 | 360 | [[package]] 361 | name = "httpx" 362 | version = "0.28.1" 363 | source = { registry = "https://pypi.org/simple" } 364 | dependencies = [ 365 | { name = "anyio", marker = "python_full_version >= '3.12'" }, 366 | { name = "certifi", marker = "python_full_version >= '3.12'" }, 367 | { name = "httpcore", marker = "python_full_version >= '3.12'" }, 368 | { name = "idna", marker = "python_full_version >= '3.12'" }, 369 | ] 370 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 371 | wheels = [ 372 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 373 | ] 374 | 375 | [[package]] 376 | name = "identify" 377 | version = "2.6.7" 378 | source = { registry = "https://pypi.org/simple" } 379 | sdist = { url = "https://files.pythonhosted.org/packages/83/d1/524aa3350f78bcd714d148ade6133d67d6b7de2cdbae7d99039c024c9a25/identify-2.6.7.tar.gz", hash = "sha256:3fa266b42eba321ee0b2bb0936a6a6b9e36a1351cbb69055b3082f4193035684", size = 99260 } 380 | wheels = [ 381 | { url = "https://files.pythonhosted.org/packages/03/00/1fd4a117c6c93f2dcc5b7edaeaf53ea45332ef966429be566ca16c2beb94/identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0", size = 99097 }, 382 | ] 383 | 384 | [[package]] 385 | name = "idna" 386 | version = "3.10" 387 | source = { registry = "https://pypi.org/simple" } 388 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 389 | wheels = [ 390 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 391 | ] 392 | 393 | [[package]] 394 | name = "imagesize" 395 | version = "1.4.1" 396 | source = { registry = "https://pypi.org/simple" } 397 | sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } 398 | wheels = [ 399 | { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, 400 | ] 401 | 402 | [[package]] 403 | name = "iniconfig" 404 | version = "2.0.0" 405 | source = { registry = "https://pypi.org/simple" } 406 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 407 | wheels = [ 408 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 409 | ] 410 | 411 | [[package]] 412 | name = "jinja2" 413 | version = "3.1.5" 414 | source = { registry = "https://pypi.org/simple" } 415 | dependencies = [ 416 | { name = "markupsafe" }, 417 | ] 418 | sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } 419 | wheels = [ 420 | { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, 421 | ] 422 | 423 | [[package]] 424 | name = "markdown-it-py" 425 | version = "3.0.0" 426 | source = { registry = "https://pypi.org/simple" } 427 | dependencies = [ 428 | { name = "mdurl" }, 429 | ] 430 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 431 | wheels = [ 432 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 433 | ] 434 | 435 | [[package]] 436 | name = "markupsafe" 437 | version = "3.0.2" 438 | source = { registry = "https://pypi.org/simple" } 439 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } 440 | wheels = [ 441 | { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, 442 | { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, 443 | { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, 444 | { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, 445 | { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, 446 | { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, 447 | { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, 448 | { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, 449 | { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, 450 | { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, 451 | { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, 452 | { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, 453 | { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, 454 | { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, 455 | { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, 456 | { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, 457 | { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, 458 | { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, 459 | { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, 460 | { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, 461 | { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, 462 | { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, 463 | { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, 464 | { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, 465 | { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, 466 | { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, 467 | { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, 468 | { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, 469 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, 470 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, 471 | { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, 472 | { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, 473 | { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, 474 | { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, 475 | { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, 476 | { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, 477 | { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, 478 | { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, 479 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, 480 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, 481 | ] 482 | 483 | [[package]] 484 | name = "mdit-py-plugins" 485 | version = "0.4.2" 486 | source = { registry = "https://pypi.org/simple" } 487 | dependencies = [ 488 | { name = "markdown-it-py" }, 489 | ] 490 | sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } 491 | wheels = [ 492 | { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, 493 | ] 494 | 495 | [[package]] 496 | name = "mdurl" 497 | version = "0.1.2" 498 | source = { registry = "https://pypi.org/simple" } 499 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 500 | wheels = [ 501 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 502 | ] 503 | 504 | [[package]] 505 | name = "mypy" 506 | version = "1.15.0" 507 | source = { registry = "https://pypi.org/simple" } 508 | dependencies = [ 509 | { name = "mypy-extensions" }, 510 | { name = "typing-extensions" }, 511 | ] 512 | sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } 513 | wheels = [ 514 | { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, 515 | { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, 516 | { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, 517 | { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, 518 | { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, 519 | { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, 520 | { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, 521 | { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, 522 | { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, 523 | { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, 524 | { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, 525 | { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, 526 | { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, 527 | { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, 528 | { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, 529 | { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, 530 | { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, 531 | { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, 532 | { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, 533 | ] 534 | 535 | [[package]] 536 | name = "mypy-extensions" 537 | version = "1.0.0" 538 | source = { registry = "https://pypi.org/simple" } 539 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } 540 | wheels = [ 541 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, 542 | ] 543 | 544 | [[package]] 545 | name = "myst-parser" 546 | version = "4.0.0" 547 | source = { registry = "https://pypi.org/simple" } 548 | dependencies = [ 549 | { name = "docutils" }, 550 | { name = "jinja2" }, 551 | { name = "markdown-it-py" }, 552 | { name = "mdit-py-plugins" }, 553 | { name = "pyyaml" }, 554 | { name = "sphinx" }, 555 | ] 556 | sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858 } 557 | wheels = [ 558 | { url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 }, 559 | ] 560 | 561 | [[package]] 562 | name = "nodeenv" 563 | version = "1.9.1" 564 | source = { registry = "https://pypi.org/simple" } 565 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } 566 | wheels = [ 567 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, 568 | ] 569 | 570 | [[package]] 571 | name = "packaging" 572 | version = "24.2" 573 | source = { registry = "https://pypi.org/simple" } 574 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 575 | wheels = [ 576 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 577 | ] 578 | 579 | [[package]] 580 | name = "platformdirs" 581 | version = "4.3.6" 582 | source = { registry = "https://pypi.org/simple" } 583 | sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } 584 | wheels = [ 585 | { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, 586 | ] 587 | 588 | [[package]] 589 | name = "pluggy" 590 | version = "1.5.0" 591 | source = { registry = "https://pypi.org/simple" } 592 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 593 | wheels = [ 594 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 595 | ] 596 | 597 | [[package]] 598 | name = "pre-commit" 599 | version = "4.1.0" 600 | source = { registry = "https://pypi.org/simple" } 601 | dependencies = [ 602 | { name = "cfgv" }, 603 | { name = "identify" }, 604 | { name = "nodeenv" }, 605 | { name = "pyyaml" }, 606 | { name = "virtualenv" }, 607 | ] 608 | sdist = { url = "https://files.pythonhosted.org/packages/2a/13/b62d075317d8686071eb843f0bb1f195eb332f48869d3c31a4c6f1e063ac/pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4", size = 193330 } 609 | wheels = [ 610 | { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, 611 | ] 612 | 613 | [[package]] 614 | name = "pygments" 615 | version = "2.19.1" 616 | source = { registry = "https://pypi.org/simple" } 617 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 618 | wheels = [ 619 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 620 | ] 621 | 622 | [[package]] 623 | name = "pyproject-api" 624 | version = "1.9.0" 625 | source = { registry = "https://pypi.org/simple" } 626 | dependencies = [ 627 | { name = "packaging" }, 628 | ] 629 | sdist = { url = "https://files.pythonhosted.org/packages/7e/66/fdc17e94486836eda4ba7113c0db9ac7e2f4eea1b968ee09de2fe75e391b/pyproject_api-1.9.0.tar.gz", hash = "sha256:7e8a9854b2dfb49454fae421cb86af43efbb2b2454e5646ffb7623540321ae6e", size = 22714 } 630 | wheels = [ 631 | { url = "https://files.pythonhosted.org/packages/b0/1d/92b7c765df46f454889d9610292b0ccab15362be3119b9a624458455e8d5/pyproject_api-1.9.0-py3-none-any.whl", hash = "sha256:326df9d68dea22d9d98b5243c46e3ca3161b07a1b9b18e213d1e24fd0e605766", size = 13131 }, 632 | ] 633 | 634 | [[package]] 635 | name = "pyright" 636 | version = "1.1.393" 637 | source = { registry = "https://pypi.org/simple" } 638 | dependencies = [ 639 | { name = "nodeenv" }, 640 | { name = "typing-extensions" }, 641 | ] 642 | sdist = { url = "https://files.pythonhosted.org/packages/f4/c1/aede6c74e664ab103673e4f1b7fd3d058fef32276be5c43572f4067d4a8e/pyright-1.1.393.tar.gz", hash = "sha256:aeeb7ff4e0364775ef416a80111613f91a05c8e01e58ecfefc370ca0db7aed9c", size = 3790430 } 643 | wheels = [ 644 | { url = "https://files.pythonhosted.org/packages/92/47/f0dd0f8afce13d92e406421ecac6df0990daee84335fc36717678577d3e0/pyright-1.1.393-py3-none-any.whl", hash = "sha256:8320629bb7a44ca90944ba599390162bf59307f3d9fb6e27da3b7011b8c17ae5", size = 5646057 }, 645 | ] 646 | 647 | [[package]] 648 | name = "pytest" 649 | version = "8.3.4" 650 | source = { registry = "https://pypi.org/simple" } 651 | dependencies = [ 652 | { name = "colorama", marker = "sys_platform == 'win32'" }, 653 | { name = "iniconfig" }, 654 | { name = "packaging" }, 655 | { name = "pluggy" }, 656 | ] 657 | sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } 658 | wheels = [ 659 | { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, 660 | ] 661 | 662 | [[package]] 663 | name = "pytest-cov" 664 | version = "6.0.0" 665 | source = { registry = "https://pypi.org/simple" } 666 | dependencies = [ 667 | { name = "coverage", extra = ["toml"] }, 668 | { name = "pytest" }, 669 | ] 670 | sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } 671 | wheels = [ 672 | { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, 673 | ] 674 | 675 | [[package]] 676 | name = "pyyaml" 677 | version = "6.0.2" 678 | source = { registry = "https://pypi.org/simple" } 679 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 680 | wheels = [ 681 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, 682 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, 683 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, 684 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, 685 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, 686 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, 687 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, 688 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, 689 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, 690 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, 691 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, 692 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, 693 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, 694 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, 695 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, 696 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, 697 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, 698 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, 699 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, 700 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, 701 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, 702 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, 703 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, 704 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, 705 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, 706 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 707 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 708 | ] 709 | 710 | [[package]] 711 | name = "requests" 712 | version = "2.32.3" 713 | source = { registry = "https://pypi.org/simple" } 714 | dependencies = [ 715 | { name = "certifi" }, 716 | { name = "charset-normalizer" }, 717 | { name = "idna" }, 718 | { name = "urllib3" }, 719 | ] 720 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 721 | wheels = [ 722 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 723 | ] 724 | 725 | [[package]] 726 | name = "ruff" 727 | version = "0.9.6" 728 | source = { registry = "https://pypi.org/simple" } 729 | sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } 730 | wheels = [ 731 | { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, 732 | { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, 733 | { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, 734 | { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, 735 | { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, 736 | { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, 737 | { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, 738 | { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, 739 | { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, 740 | { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, 741 | { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, 742 | { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, 743 | { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, 744 | { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, 745 | { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, 746 | { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, 747 | { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, 748 | ] 749 | 750 | [[package]] 751 | name = "sniffio" 752 | version = "1.3.1" 753 | source = { registry = "https://pypi.org/simple" } 754 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 755 | wheels = [ 756 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 757 | ] 758 | 759 | [[package]] 760 | name = "snowballstemmer" 761 | version = "2.2.0" 762 | source = { registry = "https://pypi.org/simple" } 763 | sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } 764 | wheels = [ 765 | { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, 766 | ] 767 | 768 | [[package]] 769 | name = "soupsieve" 770 | version = "2.6" 771 | source = { registry = "https://pypi.org/simple" } 772 | sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } 773 | wheels = [ 774 | { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, 775 | ] 776 | 777 | [[package]] 778 | name = "sphinx" 779 | version = "8.1.3" 780 | source = { registry = "https://pypi.org/simple" } 781 | dependencies = [ 782 | { name = "alabaster" }, 783 | { name = "babel" }, 784 | { name = "colorama", marker = "sys_platform == 'win32'" }, 785 | { name = "docutils" }, 786 | { name = "imagesize" }, 787 | { name = "jinja2" }, 788 | { name = "packaging" }, 789 | { name = "pygments" }, 790 | { name = "requests" }, 791 | { name = "snowballstemmer" }, 792 | { name = "sphinxcontrib-applehelp" }, 793 | { name = "sphinxcontrib-devhelp" }, 794 | { name = "sphinxcontrib-htmlhelp" }, 795 | { name = "sphinxcontrib-jsmath" }, 796 | { name = "sphinxcontrib-qthelp" }, 797 | { name = "sphinxcontrib-serializinghtml" }, 798 | ] 799 | sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } 800 | wheels = [ 801 | { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, 802 | ] 803 | 804 | [[package]] 805 | name = "sphinx-autobuild" 806 | version = "2024.10.3" 807 | source = { registry = "https://pypi.org/simple" } 808 | dependencies = [ 809 | { name = "colorama" }, 810 | { name = "sphinx" }, 811 | { name = "starlette" }, 812 | { name = "uvicorn" }, 813 | { name = "watchfiles" }, 814 | { name = "websockets" }, 815 | ] 816 | sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023 } 817 | wheels = [ 818 | { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908 }, 819 | ] 820 | 821 | [[package]] 822 | name = "sphinx-basic-ng" 823 | version = "1.0.0b2" 824 | source = { registry = "https://pypi.org/simple" } 825 | dependencies = [ 826 | { name = "sphinx" }, 827 | ] 828 | sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 } 829 | wheels = [ 830 | { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496 }, 831 | ] 832 | 833 | [[package]] 834 | name = "sphinxcontrib-applehelp" 835 | version = "2.0.0" 836 | source = { registry = "https://pypi.org/simple" } 837 | sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } 838 | wheels = [ 839 | { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, 840 | ] 841 | 842 | [[package]] 843 | name = "sphinxcontrib-devhelp" 844 | version = "2.0.0" 845 | source = { registry = "https://pypi.org/simple" } 846 | sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } 847 | wheels = [ 848 | { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, 849 | ] 850 | 851 | [[package]] 852 | name = "sphinxcontrib-htmlhelp" 853 | version = "2.1.0" 854 | source = { registry = "https://pypi.org/simple" } 855 | sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } 856 | wheels = [ 857 | { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, 858 | ] 859 | 860 | [[package]] 861 | name = "sphinxcontrib-jsmath" 862 | version = "1.0.1" 863 | source = { registry = "https://pypi.org/simple" } 864 | sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } 865 | wheels = [ 866 | { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, 867 | ] 868 | 869 | [[package]] 870 | name = "sphinxcontrib-qthelp" 871 | version = "2.0.0" 872 | source = { registry = "https://pypi.org/simple" } 873 | sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } 874 | wheels = [ 875 | { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, 876 | ] 877 | 878 | [[package]] 879 | name = "sphinxcontrib-serializinghtml" 880 | version = "2.0.0" 881 | source = { registry = "https://pypi.org/simple" } 882 | sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } 883 | wheels = [ 884 | { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, 885 | ] 886 | 887 | [[package]] 888 | name = "starlette" 889 | version = "0.45.3" 890 | source = { registry = "https://pypi.org/simple" } 891 | dependencies = [ 892 | { name = "anyio" }, 893 | ] 894 | sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } 895 | wheels = [ 896 | { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, 897 | ] 898 | 899 | [[package]] 900 | name = "tomli" 901 | version = "2.2.1" 902 | source = { registry = "https://pypi.org/simple" } 903 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 904 | wheels = [ 905 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 906 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 907 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 908 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 909 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 910 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 911 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 912 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 913 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 914 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 915 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 916 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 917 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 918 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 919 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 920 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 921 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 922 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 923 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 924 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 925 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 926 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 927 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 928 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 929 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 930 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 931 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 932 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 933 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 934 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 935 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 936 | ] 937 | 938 | [[package]] 939 | name = "tox" 940 | version = "4.24.1" 941 | source = { registry = "https://pypi.org/simple" } 942 | dependencies = [ 943 | { name = "cachetools" }, 944 | { name = "chardet" }, 945 | { name = "colorama" }, 946 | { name = "filelock" }, 947 | { name = "packaging" }, 948 | { name = "platformdirs" }, 949 | { name = "pluggy" }, 950 | { name = "pyproject-api" }, 951 | { name = "virtualenv" }, 952 | ] 953 | sdist = { url = "https://files.pythonhosted.org/packages/cf/7b/97f757e159983737bdd8fb513f4c263cd411a846684814ed5433434a1fa9/tox-4.24.1.tar.gz", hash = "sha256:083a720adbc6166fff0b7d1df9d154f9d00bfccb9403b8abf6bc0ee435d6a62e", size = 194742 } 954 | wheels = [ 955 | { url = "https://files.pythonhosted.org/packages/ab/04/b0d1c1b44c98583cab9eabb4acdba964fdf6b6c597c53cfb8870fd08cbbf/tox-4.24.1-py3-none-any.whl", hash = "sha256:57ba7df7d199002c6df8c2db9e6484f3de6ca8f42013c083ea2d4d1e5c6bdc75", size = 171829 }, 956 | ] 957 | 958 | [[package]] 959 | name = "tox-uv" 960 | version = "1.23.0" 961 | source = { registry = "https://pypi.org/simple" } 962 | dependencies = [ 963 | { name = "packaging" }, 964 | { name = "tox" }, 965 | { name = "uv" }, 966 | ] 967 | sdist = { url = "https://files.pythonhosted.org/packages/9b/bf/88041224a87804774d321e2b0caaf38b4705fcf62d7c272d1bb8c2d18e80/tox_uv-1.23.0.tar.gz", hash = "sha256:37b32014b5e0154f275f0868d05c666454accee1acb839da02901009dfbe2702", size = 19440 } 968 | wheels = [ 969 | { url = "https://files.pythonhosted.org/packages/68/f6/f9cf2584e3b19b5b3523147b257aee54f039e48888e5f883147952d5570c/tox_uv-1.23.0-py3-none-any.whl", hash = "sha256:5ca40a3d2fe52c5c0ab4dd639309d8763d9ff5665a00fec6a1299f437b9b612f", size = 14941 }, 970 | ] 971 | 972 | [[package]] 973 | name = "typing-extensions" 974 | version = "4.12.2" 975 | source = { registry = "https://pypi.org/simple" } 976 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 977 | wheels = [ 978 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 979 | ] 980 | 981 | [[package]] 982 | name = "urllib3" 983 | version = "2.3.0" 984 | source = { registry = "https://pypi.org/simple" } 985 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } 986 | wheels = [ 987 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, 988 | ] 989 | 990 | [[package]] 991 | name = "uv" 992 | version = "0.5.30" 993 | source = { registry = "https://pypi.org/simple" } 994 | sdist = { url = "https://files.pythonhosted.org/packages/e0/41/ba1c4ed43d59a2403ba653345cc43da09aecc203726a033d851b3b0798c0/uv-0.5.30.tar.gz", hash = "sha256:e40c77c012d087a51ae9a33189e7c59aa25da40f883c06e034a841b7a05c6639", size = 2860983 } 995 | wheels = [ 996 | { url = "https://files.pythonhosted.org/packages/ee/42/1d5122a959be3f11992f3f25f60bdfceb8d3c2cd45a77de60123aeebc3fc/uv-0.5.30-py3-none-linux_armv6l.whl", hash = "sha256:b4ad4c4597f27d97f9273aa2b06654dab97380d1567582c7e719624220556eb2", size = 15435555 }, 997 | { url = "https://files.pythonhosted.org/packages/53/19/47ec2ea94895d383852922785cb573f6f0dfb32f105d46841870b3496861/uv-0.5.30-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:20a3fbe5662aa12d9196d1c842d267f375195818e53d0687761ae1571676cd40", size = 15617182 }, 998 | { url = "https://files.pythonhosted.org/packages/4b/90/40197a57f374ad3d9c9a86ddb43cfdac4459b0ea14f18553d7a2d90b72cc/uv-0.5.30-py3-none-macosx_11_0_arm64.whl", hash = "sha256:98aacbaa74393710e1125382688b74d1080fb3fdeb8659484b3a30120106524b", size = 14510030 }, 999 | { url = "https://files.pythonhosted.org/packages/f2/25/0dd9b0261e51e1702631be30c5d25a71f3a9bd5fdf453402e42ee114fd81/uv-0.5.30-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c39834b2ba5ed4ce27641dcdd6b601fc091d0c45c8bc95d2f684148beb35d032", size = 14970225 }, 1000 | { url = "https://files.pythonhosted.org/packages/92/56/5b41cab8292cf27ed510d6d9eb6adc595171cf8369eae2bde377829c7aab/uv-0.5.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7eaa0ea685b2962c995fa68c817740002379327767d25b6bfc4449afd9d28350", size = 15164087 }, 1001 | { url = "https://files.pythonhosted.org/packages/19/d0/5aac4892d0d8c2a85de8adca905f87506d451ef1a60472e9cd2846e3f502/uv-0.5.30-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a35a297e8835ac686492228085c18799a4f9e4502b97830d9fa629ab33c628fc", size = 15938782 }, 1002 | { url = "https://files.pythonhosted.org/packages/5f/c7/f772bea86b87d642100ba908a8cd6ebd6f3d171991b55a361ab6cae25fb2/uv-0.5.30-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e3323a6aef65d7c35ce557a1dfe32c18b2c98b14361e6991e8903473cdc1c80a", size = 16884983 }, 1003 | { url = "https://files.pythonhosted.org/packages/28/dc/93ec4bbe0df4edee1292673cc1edb13fa6b8cd90b4893d7d5bdf0b0760d0/uv-0.5.30-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39d0daa24e41b0d7f69cced458eb69cd32f1259edb7f1c7018ed8906694c5af9", size = 16624144 }, 1004 | { url = "https://files.pythonhosted.org/packages/dc/02/69cf46866ba9a7308c88d378bd42a0e096817af8e5a88451709c80994145/uv-0.5.30-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f442f1962c325921d5141f47a970aeb0454a1808f1901e27e25a958e0055244a", size = 20959582 }, 1005 | { url = "https://files.pythonhosted.org/packages/16/f2/96c61ee44ea4c08645a96c1b18a53ffa2a19044ce60c9e3a0b3712ea1a11/uv-0.5.30-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a46b72bdb1855789b35277f894dac2b15fc0a084146ea8821b7cc7cae559a901", size = 16256029 }, 1006 | { url = "https://files.pythonhosted.org/packages/ae/70/304e89f486c06bbd924b37833c2cec7c8f4bde607b467d7748e51460939f/uv-0.5.30-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ee63749036afe168f477006e5d198cce618fcb6accf036fa33d4006f7e787e12", size = 15256649 }, 1007 | { url = "https://files.pythonhosted.org/packages/51/eb/01ed61dbf91eb64916d0581c1646dba7710a63006eba0bf1e4306cf63a5c/uv-0.5.30-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0a2624d586e71f4c8d27fb45fe7c27f8585b2669cfb85344be435bea5932a774", size = 15162449 }, 1008 | { url = "https://files.pythonhosted.org/packages/86/fd/fb18df5324a8e67671a3dbb899746e1e93253a7d1ef5789816c82f9c031f/uv-0.5.30-py3-none-musllinux_1_1_i686.whl", hash = "sha256:194891c7473eb9cedfcd0ddd25fe7c1f208df639f67474068459c53f2f1ac034", size = 15560853 }, 1009 | { url = "https://files.pythonhosted.org/packages/6f/93/89b390fd6bc941c341d4b6cae85a67473ba2cfc67334931796fb9432dfe3/uv-0.5.30-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:79dd27c2c0fdf887aadc9796345339786f27a07de7f80c9a892696e5740251c4", size = 16381075 }, 1010 | { url = "https://files.pythonhosted.org/packages/45/1a/b42793b982dd6d3a94a489d408acd745d1a1a733e10cc2707985f79e93b6/uv-0.5.30-py3-none-win32.whl", hash = "sha256:5d42cd9051ab6d1bd18ca1cceb8099963a28315bcd8c9cd4104ffdb896af3075", size = 15607311 }, 1011 | { url = "https://files.pythonhosted.org/packages/31/cc/9c9dadb39959bddf5b7884123b0230067de91cc975d99c5346df99cde8a8/uv-0.5.30-py3-none-win_amd64.whl", hash = "sha256:a8ebb553230ae811c16b2c4889095f7a8c39f657d75cf39f6f3fa5a38a5b9731", size = 16936894 }, 1012 | { url = "https://files.pythonhosted.org/packages/bb/6f/d6ea64ffc7d1e0f0875cb75620ff70845c7a210a1c220629223e10d2a80a/uv-0.5.30-py3-none-win_arm64.whl", hash = "sha256:c6b359832c7caf58c43b37e156bfeabf3adc8f2a894a0f325d617cd41a57578e", size = 15752133 }, 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "uvicorn" 1017 | version = "0.34.0" 1018 | source = { registry = "https://pypi.org/simple" } 1019 | dependencies = [ 1020 | { name = "click" }, 1021 | { name = "h11" }, 1022 | ] 1023 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 1024 | wheels = [ 1025 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "virtualenv" 1030 | version = "20.29.2" 1031 | source = { registry = "https://pypi.org/simple" } 1032 | dependencies = [ 1033 | { name = "distlib" }, 1034 | { name = "filelock" }, 1035 | { name = "platformdirs" }, 1036 | ] 1037 | sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acadb4bcbfd4e3e86df8be75527116c91d8f9784f5e9cab/virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728", size = 4320272 } 1038 | wheels = [ 1039 | { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "watchfiles" 1044 | version = "1.0.4" 1045 | source = { registry = "https://pypi.org/simple" } 1046 | dependencies = [ 1047 | { name = "anyio" }, 1048 | ] 1049 | sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 } 1050 | wheels = [ 1051 | { url = "https://files.pythonhosted.org/packages/0f/bb/8461adc4b1fed009546fb797fc0d5698dcfe5e289cb37e1b8f16a93cdc30/watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19", size = 394869 }, 1052 | { url = "https://files.pythonhosted.org/packages/55/88/9ebf36b3547176d1709c320de78c1fa3263a46be31b5b1267571d9102686/watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235", size = 384905 }, 1053 | { url = "https://files.pythonhosted.org/packages/03/8a/04335ce23ef78d8c69f0913e8b20cf7d9233e3986543aeef95ef2d6e43d2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202", size = 449944 }, 1054 | { url = "https://files.pythonhosted.org/packages/17/4e/c8d5dcd14fe637f4633616dabea8a4af0a10142dccf3b43e0f081ba81ab4/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6", size = 456020 }, 1055 | { url = "https://files.pythonhosted.org/packages/5e/74/3e91e09e1861dd7fbb1190ce7bd786700dc0fbc2ccd33bb9fff5de039229/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317", size = 482983 }, 1056 | { url = "https://files.pythonhosted.org/packages/a1/3d/e64de2d1ce4eb6a574fd78ce3a28c279da263be9ef3cfcab6f708df192f2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee", size = 520320 }, 1057 | { url = "https://files.pythonhosted.org/packages/2c/bd/52235f7063b57240c66a991696ed27e2a18bd6fcec8a1ea5a040b70d0611/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49", size = 500988 }, 1058 | { url = "https://files.pythonhosted.org/packages/3a/b0/ff04194141a5fe650c150400dd9e42667916bc0f52426e2e174d779b8a74/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c", size = 452573 }, 1059 | { url = "https://files.pythonhosted.org/packages/3d/9d/966164332c5a178444ae6d165082d4f351bd56afd9c3ec828eecbf190e6a/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1", size = 615114 }, 1060 | { url = "https://files.pythonhosted.org/packages/94/df/f569ae4c1877f96ad4086c153a8eee5a19a3b519487bf5c9454a3438c341/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226", size = 613076 }, 1061 | { url = "https://files.pythonhosted.org/packages/15/ae/8ce5f29e65d5fa5790e3c80c289819c55e12be2e1b9f5b6a0e55e169b97d/watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105", size = 271013 }, 1062 | { url = "https://files.pythonhosted.org/packages/a4/c6/79dc4a7c598a978e5fafa135090aaf7bbb03b8dec7bada437dfbe578e7ed/watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74", size = 284229 }, 1063 | { url = "https://files.pythonhosted.org/packages/37/3d/928633723211753f3500bfb138434f080363b87a1b08ca188b1ce54d1e05/watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3", size = 276824 }, 1064 | { url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 }, 1065 | { url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 }, 1066 | { url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 }, 1067 | { url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 }, 1068 | { url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 }, 1069 | { url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 }, 1070 | { url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 }, 1071 | { url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 }, 1072 | { url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 }, 1073 | { url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 }, 1074 | { url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 }, 1075 | { url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 }, 1076 | { url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 }, 1077 | { url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 }, 1078 | { url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 }, 1079 | { url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 }, 1080 | { url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 }, 1081 | { url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 }, 1082 | { url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 }, 1083 | { url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 }, 1084 | { url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 }, 1085 | { url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 }, 1086 | { url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 }, 1087 | { url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 }, 1088 | { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 }, 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "websockets" 1093 | version = "14.2" 1094 | source = { registry = "https://pypi.org/simple" } 1095 | sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 } 1096 | wheels = [ 1097 | { url = "https://files.pythonhosted.org/packages/15/b6/504695fb9a33df0ca56d157f5985660b5fc5b4bf8c78f121578d2d653392/websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166", size = 163088 }, 1098 | { url = "https://files.pythonhosted.org/packages/81/26/ebfb8f6abe963c795122439c6433c4ae1e061aaedfc7eff32d09394afbae/websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f", size = 160745 }, 1099 | { url = "https://files.pythonhosted.org/packages/a1/c6/1435ad6f6dcbff80bb95e8986704c3174da8866ddb751184046f5c139ef6/websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910", size = 160995 }, 1100 | { url = "https://files.pythonhosted.org/packages/96/63/900c27cfe8be1a1f2433fc77cd46771cf26ba57e6bdc7cf9e63644a61863/websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c", size = 170543 }, 1101 | { url = "https://files.pythonhosted.org/packages/00/8b/bec2bdba92af0762d42d4410593c1d7d28e9bfd952c97a3729df603dc6ea/websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473", size = 169546 }, 1102 | { url = "https://files.pythonhosted.org/packages/6b/a9/37531cb5b994f12a57dec3da2200ef7aadffef82d888a4c29a0d781568e4/websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473", size = 169911 }, 1103 | { url = "https://files.pythonhosted.org/packages/60/d5/a6eadba2ed9f7e65d677fec539ab14a9b83de2b484ab5fe15d3d6d208c28/websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56", size = 170183 }, 1104 | { url = "https://files.pythonhosted.org/packages/76/57/a338ccb00d1df881c1d1ee1f2a20c9c1b5b29b51e9e0191ee515d254fea6/websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142", size = 169623 }, 1105 | { url = "https://files.pythonhosted.org/packages/64/22/e5f7c33db0cb2c1d03b79fd60d189a1da044e2661f5fd01d629451e1db89/websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d", size = 169583 }, 1106 | { url = "https://files.pythonhosted.org/packages/aa/2e/2b4662237060063a22e5fc40d46300a07142afe30302b634b4eebd717c07/websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a", size = 163969 }, 1107 | { url = "https://files.pythonhosted.org/packages/94/a5/0cda64e1851e73fc1ecdae6f42487babb06e55cb2f0dc8904b81d8ef6857/websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b", size = 164408 }, 1108 | { url = "https://files.pythonhosted.org/packages/c1/81/04f7a397653dc8bec94ddc071f34833e8b99b13ef1a3804c149d59f92c18/websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c", size = 163096 }, 1109 | { url = "https://files.pythonhosted.org/packages/ec/c5/de30e88557e4d70988ed4d2eabd73fd3e1e52456b9f3a4e9564d86353b6d/websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967", size = 160758 }, 1110 | { url = "https://files.pythonhosted.org/packages/e5/8c/d130d668781f2c77d106c007b6c6c1d9db68239107c41ba109f09e6c218a/websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990", size = 160995 }, 1111 | { url = "https://files.pythonhosted.org/packages/a6/bc/f6678a0ff17246df4f06765e22fc9d98d1b11a258cc50c5968b33d6742a1/websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda", size = 170815 }, 1112 | { url = "https://files.pythonhosted.org/packages/d8/b2/8070cb970c2e4122a6ef38bc5b203415fd46460e025652e1ee3f2f43a9a3/websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95", size = 169759 }, 1113 | { url = "https://files.pythonhosted.org/packages/81/da/72f7caabd94652e6eb7e92ed2d3da818626e70b4f2b15a854ef60bf501ec/websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3", size = 170178 }, 1114 | { url = "https://files.pythonhosted.org/packages/31/e0/812725b6deca8afd3a08a2e81b3c4c120c17f68c9b84522a520b816cda58/websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9", size = 170453 }, 1115 | { url = "https://files.pythonhosted.org/packages/66/d3/8275dbc231e5ba9bb0c4f93144394b4194402a7a0c8ffaca5307a58ab5e3/websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267", size = 169830 }, 1116 | { url = "https://files.pythonhosted.org/packages/a3/ae/e7d1a56755ae15ad5a94e80dd490ad09e345365199600b2629b18ee37bc7/websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe", size = 169824 }, 1117 | { url = "https://files.pythonhosted.org/packages/b6/32/88ccdd63cb261e77b882e706108d072e4f1c839ed723bf91a3e1f216bf60/websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205", size = 163981 }, 1118 | { url = "https://files.pythonhosted.org/packages/b3/7d/32cdb77990b3bdc34a306e0a0f73a1275221e9a66d869f6ff833c95b56ef/websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce", size = 164421 }, 1119 | { url = "https://files.pythonhosted.org/packages/82/94/4f9b55099a4603ac53c2912e1f043d6c49d23e94dd82a9ce1eb554a90215/websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e", size = 163102 }, 1120 | { url = "https://files.pythonhosted.org/packages/8e/b7/7484905215627909d9a79ae07070057afe477433fdacb59bf608ce86365a/websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad", size = 160766 }, 1121 | { url = "https://files.pythonhosted.org/packages/a3/a4/edb62efc84adb61883c7d2c6ad65181cb087c64252138e12d655989eec05/websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03", size = 160998 }, 1122 | { url = "https://files.pythonhosted.org/packages/f5/79/036d320dc894b96af14eac2529967a6fc8b74f03b83c487e7a0e9043d842/websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f", size = 170780 }, 1123 | { url = "https://files.pythonhosted.org/packages/63/75/5737d21ee4dd7e4b9d487ee044af24a935e36a9ff1e1419d684feedcba71/websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5", size = 169717 }, 1124 | { url = "https://files.pythonhosted.org/packages/2c/3c/bf9b2c396ed86a0b4a92ff4cdaee09753d3ee389be738e92b9bbd0330b64/websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a", size = 170155 }, 1125 | { url = "https://files.pythonhosted.org/packages/75/2d/83a5aca7247a655b1da5eb0ee73413abd5c3a57fc8b92915805e6033359d/websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20", size = 170495 }, 1126 | { url = "https://files.pythonhosted.org/packages/79/dd/699238a92761e2f943885e091486378813ac8f43e3c84990bc394c2be93e/websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2", size = 169880 }, 1127 | { url = "https://files.pythonhosted.org/packages/c8/c9/67a8f08923cf55ce61aadda72089e3ed4353a95a3a4bc8bf42082810e580/websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307", size = 169856 }, 1128 | { url = "https://files.pythonhosted.org/packages/17/b1/1ffdb2680c64e9c3921d99db460546194c40d4acbef999a18c37aa4d58a3/websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc", size = 163974 }, 1129 | { url = "https://files.pythonhosted.org/packages/14/13/8b7fc4cb551b9cfd9890f0fd66e53c18a06240319915533b033a56a3d520/websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f", size = 164420 }, 1130 | { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 }, 1131 | ] 1132 | --------------------------------------------------------------------------------