From 73a28649277692c154fe90e0c44534be2b264491 Mon Sep 17 00:00:00 2001
From: lcd1232 <8745863+lcd1232@users.noreply.github.com>
Date: Thu, 9 Jan 2025 16:59:31 +0400
Subject: [PATCH 01/24] Add .idea files Update .gitignore Add init commit
---
.github/workflows/publish.yml | 34 ++
.gitignore | 311 ++++++++++++++++++
.idea/.gitignore | 8 +
.idea/GitLink.xml | 6 +
.../inspectionProfiles/profiles_settings.xml | 6 +
.idea/vcs.xml | 6 +
pyproject.toml | 35 ++
setup.py | 4 +
tempmail/__init__.py | 3 +
tempmail/py.typed | 0
10 files changed, 413 insertions(+)
create mode 100644 .github/workflows/publish.yml
create mode 100644 .gitignore
create mode 100644 .idea/.gitignore
create mode 100644 .idea/GitLink.xml
create mode 100644 .idea/inspectionProfiles/profiles_settings.xml
create mode 100644 .idea/vcs.xml
create mode 100644 pyproject.toml
create mode 100644 setup.py
create mode 100644 tempmail/__init__.py
create mode 100644 tempmail/py.typed
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..fd16f1c
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,34 @@
+name: Publish to PyPI
+
+on:
+ push:
+ tags:
+ # Semver versioning pattern
+ - 'v[0-9]+.[0-9]+.[0-9]+'
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.9"
+
+ - name: Install build tools
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install build twine
+
+ - name: Build package
+ run: python -m build
+
+ - name: Publish package to PyPI
+ run: |
+ python -m twine upload dist/*
+ env:
+ TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
+ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4e6a182
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,311 @@
+# Created by https://www.toptal.com/developers/gitignore/api/python,linux,macos,pycharm+iml
+# Edit at https://www.toptal.com/developers/gitignore?templates=python,linux,macos,pycharm+iml
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### PyCharm+iml ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### PyCharm+iml Patch ###
+# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
+
+*.iml
+modules.xml
+.idea/misc.xml
+*.ipr
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+### Python Patch ###
+# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
+poetry.toml
+
+# ruff
+.ruff_cache/
+
+# LSP config files
+pyrightconfig.json
+
+# End of https://www.toptal.com/developers/gitignore/api/python,linux,macos,pycharm+iml
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/GitLink.xml b/.idea/GitLink.xml
new file mode 100644
index 0000000..009597c
--- /dev/null
+++ b/.idea/GitLink.xml
@@ -0,0 +1,6 @@
+
+
Test body
", + "created_at": "2023-01-01T00:00:00Z", + } + ] + } + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + messages = client.list_email_messages("test@temp.io") + + assert len(messages) == 1 + message = messages[0] + assert isinstance(message, EmailMessage) + assert message.id == "msg1" + assert message.from_addr == "sender@example.com" + assert message.to_addr == "test@temp.io" + assert message.subject == "Test Subject" + assert message.body_text == "Test body" + assert message.body_html == "Test body
" + + @patch("tempmail.client.requests.Session.request") + def test_list_email_messages_with_options(self, mock_request): + """Test message listing with options.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"messages": []} + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + options = ListMessagesOptions(limit=10, offset=5) + client.list_email_messages("test@temp.io", options) + + # Verify the request was made with correct parameters + mock_request.assert_called_once() + call_args = mock_request.call_args + expected_params = {"email": "test@temp.io", "limit": 10, "offset": 5} + assert call_args[1]["params"] == expected_params + + def test_list_email_messages_no_email(self): + """Test message listing fails without email.""" + client = TempMailClient("test-api-key") + with pytest.raises(ValidationError, match="Email address is required"): + client.list_email_messages("") + + @patch("tempmail.client.requests.Session.request") + def test_delete_message_success(self, mock_request): + """Test successful message deletion.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + result = client.delete_message("msg123") + + assert result is True + + # Verify correct endpoint was called + mock_request.assert_called_once() + call_args = mock_request.call_args + assert "/messages/msg123" in call_args[1]["url"] + assert call_args[1]["method"] == "DELETE" + + def test_delete_message_no_id(self): + """Test message deletion fails without message ID.""" + client = TempMailClient("test-api-key") + with pytest.raises(ValidationError, match="Message ID is required"): + client.delete_message("") + + @patch("tempmail.client.requests.Session.request") + def test_authentication_error(self, mock_request): + """Test authentication error handling.""" + mock_response = Mock() + mock_response.status_code = 401 + mock_response.content = b"" + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + with pytest.raises(AuthenticationError, match="Invalid API key"): + client.create_email() + + @patch("tempmail.client.requests.Session.request") + def test_rate_limit_error(self, mock_request): + """Test rate limit error handling.""" + mock_response = Mock() + mock_response.status_code = 429 + mock_response.content = b"" + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + with pytest.raises(RateLimitError, match="Rate limit exceeded"): + client.create_email() + + @patch("tempmail.client.requests.Session.request") + def test_validation_error(self, mock_request): + """Test validation error handling.""" + mock_response = Mock() + mock_response.status_code = 400 + mock_response.content = b'{"message": "Invalid domain"}' + mock_response.json.return_value = {"message": "Invalid domain"} + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + with pytest.raises(ValidationError, match="Invalid domain"): + client.create_email() + + @patch("tempmail.client.requests.Session.request") + def test_api_error(self, mock_request): + """Test generic API error handling.""" + mock_response = Mock() + mock_response.status_code = 500 + mock_response.content = b'{"message": "Internal server error"}' + mock_response.json.return_value = {"message": "Internal server error"} + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + with pytest.raises(APIError) as exc_info: + client.create_email() + + assert exc_info.value.status_code == 500 + assert "Internal server error" in str(exc_info.value) + + @patch("tempmail.client.requests.Session.request") + def test_rate_limit_headers(self, mock_request): + """Test rate limit information from headers.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_response.headers = { + "X-RateLimit-Limit": "100", + "X-RateLimit-Remaining": "95", + "X-RateLimit-Reset": "1640995200", + } + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + rate_limit = client.get_rate_limit() + + assert rate_limit is not None + assert rate_limit.limit == 100 + assert rate_limit.remaining == 95 + assert rate_limit.reset == 1640995200 + + @patch("tempmail.client.requests.Session.request") + def test_retry_on_request_exception(self, mock_request): + """Test retry mechanism on request exceptions.""" + from requests.exceptions import ConnectionError + + # Mock to fail 2 times then succeed + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"domains": []} + mock_response.headers = {} + + mock_request.side_effect = [ + ConnectionError("Connection failed"), + ConnectionError("Connection failed"), + mock_response, + ] + + client = TempMailClient("test-api-key") + + with patch("time.sleep"): # Mock sleep to speed up test + domains = client.list_domains() + + assert len(domains) == 0 + assert mock_request.call_count == 3 + + @patch("tempmail.client.requests.Session.request") + def test_max_retries_exceeded(self, mock_request): + """Test failure after max retries exceeded.""" + from requests.exceptions import ConnectionError + from tempmail.exceptions import TempMailError + + mock_request.side_effect = ConnectionError("Connection failed") + + client = TempMailClient("test-api-key") + + with patch("time.sleep"): # Mock sleep to speed up test + with pytest.raises(TempMailError, match="Request failed after 4 attempts"): + client.list_domains() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d22e265 --- /dev/null +++ b/uv.lock @@ -0,0 +1,937 @@ +version = 1 +revision = 3 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] + +[[package]] +name = "black" +version = "24.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pathspec", marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810, upload-time = "2024-08-02T17:43:18.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092, upload-time = "2024-08-02T17:47:26.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529, upload-time = "2024-08-02T17:47:29.109Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443, upload-time = "2024-08-02T17:46:20.306Z" }, + { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012, upload-time = "2024-08-02T17:47:20.33Z" }, + { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080, upload-time = "2024-08-02T17:48:05.467Z" }, + { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143, upload-time = "2024-08-02T17:47:30.247Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774, upload-time = "2024-08-02T17:46:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503, upload-time = "2024-08-02T17:46:22.654Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132, upload-time = "2024-08-02T17:49:52.843Z" }, + { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665, upload-time = "2024-08-02T17:47:54.479Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458, upload-time = "2024-08-02T17:46:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109, upload-time = "2024-08-02T17:46:52.97Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322, upload-time = "2024-08-02T17:51:20.203Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108, upload-time = "2024-08-02T17:50:40.824Z" }, + { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786, upload-time = "2024-08-02T17:46:02.939Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754, upload-time = "2024-08-02T17:46:38.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706, upload-time = "2024-08-02T17:49:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429, upload-time = "2024-08-02T17:49:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488, upload-time = "2024-08-02T17:46:08.067Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721, upload-time = "2024-08-02T17:46:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504, upload-time = "2024-08-02T17:43:15.747Z" }, +] + +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/63a45bfc36f73efe46731a3a71cb84e2112f7e0b049507025ce477f0f052/charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", size = 198805, upload-time = "2025-08-09T07:56:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/0c/52/8b0c6c3e53f7e546a5e49b9edb876f379725914e1130297f3b423c7b71c5/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", size = 142862, upload-time = "2025-08-09T07:56:57.751Z" }, + { url = "https://files.pythonhosted.org/packages/59/c0/a74f3bd167d311365e7973990243f32c35e7a94e45103125275b9e6c479f/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", size = 155104, upload-time = "2025-08-09T07:56:58.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/79/ae516e678d6e32df2e7e740a7be51dc80b700e2697cb70054a0f1ac2c955/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", size = 152598, upload-time = "2025-08-09T07:57:00.201Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/ef9c88464b126fa176f4ef4a317ad9b6f4d30b2cffbc43386062367c3e2c/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", size = 147391, upload-time = "2025-08-09T07:57:01.441Z" }, + { url = "https://files.pythonhosted.org/packages/7a/03/cbb6fac9d3e57f7e07ce062712ee80d80a5ab46614684078461917426279/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", size = 145037, upload-time = "2025-08-09T07:57:02.638Z" }, + { url = "https://files.pythonhosted.org/packages/64/d1/f9d141c893ef5d4243bc75c130e95af8fd4bc355beff06e9b1e941daad6e/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", size = 156425, upload-time = "2025-08-09T07:57:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/c5/35/9c99739250742375167bc1b1319cd1cec2bf67438a70d84b2e1ec4c9daa3/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", size = 153734, upload-time = "2025-08-09T07:57:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/50/10/c117806094d2c956ba88958dab680574019abc0c02bcf57b32287afca544/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", size = 148551, upload-time = "2025-08-09T07:57:06.823Z" }, + { url = "https://files.pythonhosted.org/packages/61/c5/dc3ba772489c453621ffc27e8978a98fe7e41a93e787e5e5bde797f1dddb/charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", size = 98459, upload-time = "2025-08-09T07:57:08.031Z" }, + { url = "https://files.pythonhosted.org/packages/05/35/bb59b1cd012d7196fc81c2f5879113971efc226a63812c9cf7f89fe97c40/charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", size = 105887, upload-time = "2025-08-09T07:57:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, + { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, + { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, + { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.9'" }, +] + +[[package]] +name = "coverage" +version = "7.10.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" }, + { url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180, upload-time = "2025-08-29T15:33:01.608Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" }, + { url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909, upload-time = "2025-08-29T15:33:07.74Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" }, + { url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521, upload-time = "2025-08-29T15:33:10.599Z" }, + { url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417, upload-time = "2025-08-29T15:33:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931, upload-time = "2025-08-29T15:33:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714, upload-time = "2025-08-29T15:33:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562, upload-time = "2025-08-29T15:33:24.717Z" }, + { url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453, upload-time = "2025-08-29T15:33:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127, upload-time = "2025-08-29T15:33:27.777Z" }, + { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, + { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724, upload-time = "2025-08-29T15:33:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536, upload-time = "2025-08-29T15:33:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171, upload-time = "2025-08-29T15:33:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, + { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, + { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, + { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, + { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, + { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, + { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, + { url = "https://files.pythonhosted.org/packages/91/70/f73ad83b1d2fd2d5825ac58c8f551193433a7deaf9b0d00a8b69ef61cd9a/coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352", size = 217009, upload-time = "2025-08-29T15:34:57.381Z" }, + { url = "https://files.pythonhosted.org/packages/01/e8/099b55cd48922abbd4b01ddd9ffa352408614413ebfc965501e981aced6b/coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612", size = 217400, upload-time = "2025-08-29T15:34:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d1/c6bac7c9e1003110a318636fef3b5c039df57ab44abcc41d43262a163c28/coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b", size = 243835, upload-time = "2025-08-29T15:35:00.541Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/82c6c061838afbd2172e773156c0aa84a901d59211b4975a4e93accf5c89/coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144", size = 245658, upload-time = "2025-08-29T15:35:02.135Z" }, + { url = "https://files.pythonhosted.org/packages/81/6a/35674445b1d38161148558a3ff51b0aa7f0b54b1def3abe3fbd34efe05bc/coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b", size = 247433, upload-time = "2025-08-29T15:35:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/98c99e7cafb288730a93535092eb433b5503d529869791681c4f2e2012a8/coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862", size = 245315, upload-time = "2025-08-29T15:35:05.629Z" }, + { url = "https://files.pythonhosted.org/packages/09/05/123e0dba812408c719c319dea05782433246f7aa7b67e60402d90e847545/coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2", size = 243385, upload-time = "2025-08-29T15:35:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/67/52/d57a42502aef05c6325f28e2e81216c2d9b489040132c18725b7a04d1448/coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78", size = 244343, upload-time = "2025-08-29T15:35:09.55Z" }, + { url = "https://files.pythonhosted.org/packages/6b/22/7f6fad7dbb37cf99b542c5e157d463bd96b797078b1ec506691bc836f476/coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c", size = 219530, upload-time = "2025-08-29T15:35:11.167Z" }, + { url = "https://files.pythonhosted.org/packages/62/30/e2fda29bfe335026027e11e6a5e57a764c9df13127b5cf42af4c3e99b937/coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf", size = 220432, upload-time = "2025-08-29T15:35:12.902Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "isort" +version = "5.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, +] + +[[package]] +name = "isort" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, + { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, + { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" }, + { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" }, + { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" }, + { url = "https://files.pythonhosted.org/packages/14/55/e13d0dcd276975927d1f4e9e2ec4fd409e199f01bdc671717e673cc63a22/mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d", size = 9512564, upload-time = "2025-07-31T07:53:12.346Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "iniconfig", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pygments", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "coverage", version = "7.10.6", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.9'" }, + { name = "idna", marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.9'" }, + { name = "idna", marker = "python_full_version >= '3.9'" }, + { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "temp-mail-io" +source = { editable = "." } +dependencies = [ + { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "black", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "isort", version = "5.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "isort", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "types-requests", version = "2.32.0.20241016", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "types-requests", version = "2.32.4.20250809", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=21.0.0" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.0.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=0.800" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=6.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=2.10.0" }, + { name = "requests", specifier = ">=2.25.0" }, + { name = "types-requests", marker = "extra == 'dev'" }, +] +provides-extras = ["dev"] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { 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, upload-time = "2024-11-27T22:37:54.956Z" }, + { 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, upload-time = "2024-11-27T22:37:56.698Z" }, + { 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, upload-time = "2024-11-27T22:37:57.63Z" }, + { 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, upload-time = "2024-11-27T22:37:59.344Z" }, + { 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, upload-time = "2024-11-27T22:38:00.429Z" }, + { 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, upload-time = "2024-11-27T22:38:02.094Z" }, + { 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, upload-time = "2024-11-27T22:38:03.206Z" }, + { 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, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { 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, upload-time = "2024-11-27T22:38:07.731Z" }, + { 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, upload-time = "2024-11-27T22:38:09.384Z" }, + { 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, upload-time = "2024-11-27T22:38:10.329Z" }, + { 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, upload-time = "2024-11-27T22:38:11.443Z" }, + { 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, upload-time = "2024-11-27T22:38:13.099Z" }, + { 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, upload-time = "2024-11-27T22:38:14.766Z" }, + { 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, upload-time = "2024-11-27T22:38:15.843Z" }, + { 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, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { 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, upload-time = "2024-11-27T22:38:21.659Z" }, + { 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, upload-time = "2024-11-27T22:38:22.693Z" }, + { 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, upload-time = "2024-11-27T22:38:24.367Z" }, + { 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, upload-time = "2024-11-27T22:38:26.081Z" }, + { 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, upload-time = "2024-11-27T22:38:27.921Z" }, + { 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, upload-time = "2024-11-27T22:38:29.591Z" }, + { 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, upload-time = "2024-11-27T22:38:30.639Z" }, + { 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, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.0.20241016" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/3c/4f2a430c01a22abd49a583b6b944173e39e7d01b688190a5618bd59a2e22/types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", size = 18065, upload-time = "2024-10-16T02:46:10.818Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/01/485b3026ff90e5190b5e24f1711522e06c79f4a56c8f4b95848ac072e20f/types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747", size = 15836, upload-time = "2024-10-16T02:46:09.734Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250809" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] From 81521c033ccd89c9515e5c6e38c0ded5fdf5db01 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:27:17 +0400 Subject: [PATCH 03/24] Some improvements --- README.md | 7 +- examples/basic_usage.py | 86 ----------------- tempmail/client.py | 206 ++++++++++++++++------------------------ tempmail/models.py | 36 ++++--- tests/test_client.py | 93 ++++++------------ 5 files changed, 131 insertions(+), 297 deletions(-) delete mode 100644 examples/basic_usage.py diff --git a/README.md b/README.md index 4b1c492..df9b309 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,8 @@ client = TempMailClient("your-api-key") # With custom parameters client = TempMailClient( "your-api-key", - base_url="https://api.temp-mail.org", - timeout=30, - max_retries=3 + base_url="https://api.temp-mail.io", + timeout=30 ) ``` @@ -176,4 +175,4 @@ MIT License - see [LICENSE](LICENSE) file for details. - [Temp Mail API Documentation](https://docs.temp-mail.io) - [GitHub Repository](https://github.com/temp-mail-io/temp-mail-python) -- [PyPI Package](https://pypi.org/project/temp-mail-io/) \ No newline at end of file +- [PyPI Package](https://pypi.org/project/temp-mail-io/) diff --git a/examples/basic_usage.py b/examples/basic_usage.py deleted file mode 100644 index ecd45f2..0000000 --- a/examples/basic_usage.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -""" -Basic usage example for the Temp Mail Python library. - -This example demonstrates how to: -1. Create a temporary email address -2. List available domains -3. Check for received messages -4. Delete messages -""" - -import os -import time -from tempmail import TempMailClient, CreateEmailOptions - - -def main(): - # Initialize the client with your API key - api_key = os.getenv("TEMPMAIL_API_KEY") - if not api_key: - print("Please set the TEMPMAIL_API_KEY environment variable") - return - - client = TempMailClient(api_key) - - try: - # 1. List available domains - print("Getting available domains...") - domains = client.list_domains() - print(f"Available domains: {[d.domain for d in domains[:5]]}") # Show first 5 - - # 2. Create a temporary email address - print("\nCreating a temporary email address...") - - # Option 1: Let the API choose a random email - email = client.create_email() - print(f"Created email: {email.email}") - - # Option 2: Create with specific domain and prefix - if domains: - options = CreateEmailOptions( - domain=domains[0].domain, - prefix="mytest" - ) - custom_email = client.create_email(options) - print(f"Created custom email: {custom_email.email}") - - # 3. Check for messages (initially should be empty) - print(f"\nChecking messages for {email.email}...") - messages = client.list_email_messages(email.email) - print(f"Current messages: {len(messages)}") - - # 4. Send a test email to the temporary address - print(f"\nYou can now send test emails to: {email.email}") - print("Waiting 10 seconds to check for new messages...") - time.sleep(10) - - # 5. Check for new messages - messages = client.list_email_messages(email.email) - print(f"Messages after waiting: {len(messages)}") - - for i, message in enumerate(messages): - print(f"\nMessage {i+1}:") - print(f" From: {message.from_addr}") - print(f" Subject: {message.subject}") - print(f" Body: {message.body_text[:100]}...") # First 100 chars - - # 6. Delete the message - print(f" Deleting message {message.id}...") - client.delete_message(message.id) - print(" Message deleted!") - - # 7. Check rate limit information - rate_limit = client.get_rate_limit() - if rate_limit: - print(f"\nRate limit info:") - print(f" Limit: {rate_limit.limit}") - print(f" Remaining: {rate_limit.remaining}") - print(f" Reset: {rate_limit.reset}") - - except Exception as e: - print(f"Error: {e}") - - -if __name__ == "__main__": - main() diff --git a/tempmail/client.py b/tempmail/client.py index 5a4fa4c..d6d540a 100644 --- a/tempmail/client.py +++ b/tempmail/client.py @@ -2,17 +2,16 @@ import json import time -from typing import Optional, List, Dict, Any, Union +from typing import Optional, List, Dict, Any from urllib.parse import urljoin import requests +from . import __version__ from .models import ( RateLimit, Domain, EmailAddress, EmailMessage, - CreateEmailOptions, - ListMessagesOptions, ) from .exceptions import ( TempMailError, @@ -29,18 +28,16 @@ class TempMailClient: def __init__( self, api_key: str, - base_url: str = "https://api.temp-mail.org", + base_url: str = "https://api.temp-mail.io", timeout: int = 30, - max_retries: int = 3, ): """ Initialize the Temp Mail client. Args: api_key: Your Temp Mail API key - base_url: Base URL for the API (default: https://api.temp-mail.org) + base_url: Base URL for the API (default: https://api.temp-mail.io) timeout: Request timeout in seconds (default: 30) - max_retries: Maximum number of retry attempts (default: 3) """ if not api_key: raise AuthenticationError("API key is required") @@ -48,13 +45,12 @@ def __init__( self.api_key = api_key self.base_url = base_url self.timeout = timeout - self.max_retries = max_retries self.session = requests.Session() self.session.headers.update({ - "Authorization": f"Bearer {api_key}", + "X-API-Key": api_key, "Content-Type": "application/json", - "User-Agent": "temp-mail-python/1.0.0", + "User-Agent": f"temp-mail-python/{__version__}", }) self._last_rate_limit: Optional[RateLimit] = None @@ -69,90 +65,88 @@ def _make_request( """Make an HTTP request to the API.""" url = urljoin(self.base_url, endpoint) - for attempt in range(self.max_retries + 1): - try: - response = self.session.request( - method=method, - url=url, - params=params, - json=json_data, - timeout=self.timeout, + try: + response = self.session.request( + method=method, + url=url, + params=params, + json=json_data, + timeout=self.timeout, + ) + + # Update rate limit info from headers + self._update_rate_limit_from_headers(response.headers) + + # Handle different status codes + if response.status_code >= 200 and response.status_code < 300: + return response.json() + elif response.status_code == 401: + raise AuthenticationError("Invalid API key") + elif response.status_code == 429: + raise RateLimitError("Rate limit exceeded") + elif response.status_code == 400: + error_data = response.json() if response.content else {} + error_message = self._extract_error_message(error_data, "Invalid request parameters") + raise ValidationError(error_message) + else: + error_data = response.json() if response.content else {} + error_message = self._extract_error_message( + error_data, + f"API request failed with status {response.status_code}" + ) + raise APIError( + error_message, + status_code=response.status_code, + response_data=error_data, ) - # Update rate limit info from headers - self._update_rate_limit_from_headers(response.headers) - - # Handle different status codes - if response.status_code == 200 or response.status_code == 201: - return response.json() - elif response.status_code == 401: - raise AuthenticationError("Invalid API key") - elif response.status_code == 429: - raise RateLimitError("Rate limit exceeded") - elif response.status_code == 400: - error_data = response.json() if response.content else {} - raise ValidationError( - error_data.get("message", "Invalid request parameters") - ) - else: - error_data = response.json() if response.content else {} - raise APIError( - error_data.get( - "message", - f"API request failed with status {response.status_code}", - ), - status_code=response.status_code, - response_data=error_data, - ) - - except requests.exceptions.RequestException as e: - if attempt == self.max_retries: - raise TempMailError( - f"Request failed after {self.max_retries + 1} attempts: {str(e)}" - ) - time.sleep(2**attempt) # Exponential backoff - - # This should never be reached due to the raise in the except block - raise TempMailError("Unexpected error in request handling") + except requests.exceptions.RequestException as e: + raise TempMailError(f"Request failed: {str(e)}") + + def _extract_error_message(self, error_data: Dict[str, Any], default_message: str) -> str: + """Extract error message from API response.""" + # Try new API format: {"error": {"detail": "message"}} + if "error" in error_data and isinstance(error_data["error"], dict): + error_obj = error_data["error"] + if "detail" in error_obj: + return str(error_obj["detail"]) + if "message" in error_obj: + return str(error_obj["message"]) + + # Try old format: {"message": "error"} + if "message" in error_data: + return str(error_data["message"]) + + return default_message def _update_rate_limit_from_headers(self, headers: Any) -> None: """Update rate limit info from response headers.""" - try: - if "X-RateLimit-Limit" in headers: - self._last_rate_limit = RateLimit( - limit=int(headers["X-RateLimit-Limit"]), - remaining=int(headers.get("X-RateLimit-Remaining", 0)), - reset=int(headers.get("X-RateLimit-Reset", 0)), - ) - except (ValueError, KeyError): - pass + self._last_rate_limit = RateLimit( + limit=int(headers["X-RateLimit-Limit"]), + remaining=int(headers.get("X-RateLimit-Remaining", 0)), + reset=int(headers.get("X-RateLimit-Reset", 0)), + ) def create_email( - self, options: Optional[CreateEmailOptions] = None + self, email: Optional[str] = None, domain: Optional[str] = None, domain_type: Optional[str] = None, ) -> EmailAddress: """ - Generate a new temporary email address. - - Args: - options: Configuration options for email creation - - Returns: - EmailAddress: The generated email address + Create a new temporary email address. + :param email: Optional specific email address to create + :param domain: Optional domain to use + :param domain_type: Optional domain type (e.g., "public", "custom", "premium") """ params: Dict[str, Any] = {} - if options: - if options.domain: - params["domain"] = options.domain - if options.prefix: - params["prefix"] = options.prefix - - data = self._make_request("POST", "/emails", params=params) - - return EmailAddress( - email=data["email"], - domain=data["domain"], - created_at=data.get("created_at"), - ) + if email: + params["email"] = email + if domain: + params["domain"] = domain + if domain_type: + params["domain_type"] = domain_type + + data = self._make_request("POST", "/v1/emails", params=params) + + return EmailAddress.from_json(data) def list_domains(self) -> List[Domain]: """ @@ -161,34 +155,14 @@ def list_domains(self) -> List[Domain]: Returns: List[Domain]: Available domains """ - data = self._make_request("GET", "/domains") + data = self._make_request("GET", "/v1/domains") - return [Domain(domain=domain) for domain in data.get("domains", [])] + return [Domain.from_json(domain) for domain in data["domains"]] def list_email_messages( - self, email: str, options: Optional[ListMessagesOptions] = None + self, email: str, ) -> List[EmailMessage]: - """ - Get messages for a specific email address. - - Args: - email: The email address to get messages for - options: Options for filtering messages - - Returns: - List[EmailMessage]: List of email messages - """ - if not email: - raise ValidationError("Email address is required") - - params: Dict[str, Any] = {"email": email} - if options: - if options.limit: - params["limit"] = options.limit - if options.offset: - params["offset"] = options.offset - - data = self._make_request("GET", "/messages", params=params) + data = self._make_request("GET", f"/v1/emails/{email}/messages") messages = [] for msg_data in data.get("messages", []): @@ -208,22 +182,10 @@ def list_email_messages( return messages def delete_message(self, message_id: str) -> bool: - """ - Delete a specific message. - - Args: - message_id: ID of the message to delete - - Returns: - bool: True if deletion was successful - """ - if not message_id: - raise ValidationError("Message ID is required") - - self._make_request("DELETE", f"/messages/{message_id}") + self._make_request("DELETE", f"/v1/messages/{message_id}") return True - def get_rate_limit(self) -> Optional[RateLimit]: + def get_rate_limit(self) -> RateLimit: """ Get current rate limit information. @@ -231,7 +193,7 @@ def get_rate_limit(self) -> Optional[RateLimit]: RateLimit: Current rate limit status, or None if not available """ # Make a lightweight request to get fresh rate limit info - self._make_request("GET", "/rate-limit") + self._make_request("GET", "/v1/rate-limit") return self._last_rate_limit @property diff --git a/tempmail/models.py b/tempmail/models.py index fb96895..92f7f72 100644 --- a/tempmail/models.py +++ b/tempmail/models.py @@ -17,17 +17,29 @@ class RateLimit: @dataclass class Domain: """Email domain information.""" + name: str + type: str # e.g., "public", "custom", "premium" - domain: str + @classmethod + def from_json(cls, data: Dict[str, Any]) -> 'Domain': + return cls( + name=data['name'], + type=data['type'] + ) @dataclass class EmailAddress: """Generated temporary email address.""" - email: str - domain: str - created_at: Optional[datetime] = None + ttl: int # Time to live in seconds + + @classmethod + def from_json(cls, data: Dict[str, Any]) -> 'EmailAddress': + return cls( + email=data['email'], + ttl=data['ttl'] + ) @dataclass @@ -42,19 +54,3 @@ class EmailMessage: body_html: Optional[str] = None created_at: Optional[datetime] = None attachments: Optional[List[Dict[str, Any]]] = None - - -@dataclass -class CreateEmailOptions: - """Options for creating a new email address.""" - - domain: Optional[str] = None - prefix: Optional[str] = None - - -@dataclass -class ListMessagesOptions: - """Options for listing email messages.""" - - limit: Optional[int] = None - offset: Optional[int] = None diff --git a/tests/test_client.py b/tests/test_client.py index b5f7aff..3140141 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -34,22 +34,17 @@ def test_client_initialization_with_custom_params(self): client = TempMailClient( "test-api-key", base_url="https://custom.api.com", - timeout=60, - max_retries=5 + timeout=60 ) assert client.base_url == "https://custom.api.com" assert client.timeout == 60 - assert client.max_retries == 5 - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_create_email_success(self, mock_request): """Test successful email creation.""" mock_response = Mock() mock_response.status_code = 201 - mock_response.json.return_value = { - "email": "test@example.com", - "domain": "example.com", - } + mock_response.json.return_value = {"email": "test@example.com", "domain": "example.com"} mock_response.headers = {} mock_request.return_value = mock_response @@ -60,15 +55,12 @@ def test_create_email_success(self, mock_request): assert email.email == "test@example.com" assert email.domain == "example.com" - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_create_email_with_options(self, mock_request): """Test email creation with options.""" mock_response = Mock() mock_response.status_code = 201 - mock_response.json.return_value = { - "email": "custom@mydomain.com", - "domain": "mydomain.com", - } + mock_response.json.return_value = {"email": "custom@mydomain.com", "domain": "mydomain.com"} mock_response.headers = {} mock_request.return_value = mock_response @@ -82,16 +74,14 @@ def test_create_email_with_options(self, mock_request): # Verify request was made with correct parameters mock_request.assert_called_once() call_args = mock_request.call_args - assert call_args[1]["params"] == {"domain": "mydomain.com", "prefix": "custom"} + assert call_args[1]['params'] == {"domain": "mydomain.com", "prefix": "custom"} - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_list_domains_success(self, mock_request): """Test successful domain listing.""" mock_response = Mock() mock_response.status_code = 200 - mock_response.json.return_value = { - "domains": ["example.com", "test.org", "temp.io"] - } + mock_response.json.return_value = {"domains": ["example.com", "test.org", "temp.io"]} mock_response.headers = {} mock_request.return_value = mock_response @@ -104,7 +94,7 @@ def test_list_domains_success(self, mock_request): assert domains[1].domain == "test.org" assert domains[2].domain == "temp.io" - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_list_email_messages_success(self, mock_request): """Test successful message listing.""" mock_response = Mock() @@ -118,7 +108,7 @@ def test_list_email_messages_success(self, mock_request): "subject": "Test Subject", "body_text": "Test body", "body_html": "Test body
", - "created_at": "2023-01-01T00:00:00Z", + "created_at": "2023-01-01T00:00:00Z" } ] } @@ -138,7 +128,7 @@ def test_list_email_messages_success(self, mock_request): assert message.body_text == "Test body" assert message.body_html == "Test body
" - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_list_email_messages_with_options(self, mock_request): """Test message listing with options.""" mock_response = Mock() @@ -155,7 +145,7 @@ def test_list_email_messages_with_options(self, mock_request): mock_request.assert_called_once() call_args = mock_request.call_args expected_params = {"email": "test@temp.io", "limit": 10, "offset": 5} - assert call_args[1]["params"] == expected_params + assert call_args[1]['params'] == expected_params def test_list_email_messages_no_email(self): """Test message listing fails without email.""" @@ -163,7 +153,7 @@ def test_list_email_messages_no_email(self): with pytest.raises(ValidationError, match="Email address is required"): client.list_email_messages("") - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_delete_message_success(self, mock_request): """Test successful message deletion.""" mock_response = Mock() @@ -180,8 +170,8 @@ def test_delete_message_success(self, mock_request): # Verify correct endpoint was called mock_request.assert_called_once() call_args = mock_request.call_args - assert "/messages/msg123" in call_args[1]["url"] - assert call_args[1]["method"] == "DELETE" + assert "/messages/msg123" in call_args[1]['url'] + assert call_args[1]['method'] == "DELETE" def test_delete_message_no_id(self): """Test message deletion fails without message ID.""" @@ -189,12 +179,12 @@ def test_delete_message_no_id(self): with pytest.raises(ValidationError, match="Message ID is required"): client.delete_message("") - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_authentication_error(self, mock_request): """Test authentication error handling.""" mock_response = Mock() mock_response.status_code = 401 - mock_response.content = b"" + mock_response.content = b'' mock_response.headers = {} mock_request.return_value = mock_response @@ -202,12 +192,12 @@ def test_authentication_error(self, mock_request): with pytest.raises(AuthenticationError, match="Invalid API key"): client.create_email() - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_rate_limit_error(self, mock_request): """Test rate limit error handling.""" mock_response = Mock() mock_response.status_code = 429 - mock_response.content = b"" + mock_response.content = b'' mock_response.headers = {} mock_request.return_value = mock_response @@ -215,7 +205,7 @@ def test_rate_limit_error(self, mock_request): with pytest.raises(RateLimitError, match="Rate limit exceeded"): client.create_email() - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_validation_error(self, mock_request): """Test validation error handling.""" mock_response = Mock() @@ -229,7 +219,7 @@ def test_validation_error(self, mock_request): with pytest.raises(ValidationError, match="Invalid domain"): client.create_email() - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_api_error(self, mock_request): """Test generic API error handling.""" mock_response = Mock() @@ -246,7 +236,7 @@ def test_api_error(self, mock_request): assert exc_info.value.status_code == 500 assert "Internal server error" in str(exc_info.value) - @patch("tempmail.client.requests.Session.request") + @patch('tempmail.client.requests.Session.request') def test_rate_limit_headers(self, mock_request): """Test rate limit information from headers.""" mock_response = Mock() @@ -255,7 +245,7 @@ def test_rate_limit_headers(self, mock_request): mock_response.headers = { "X-RateLimit-Limit": "100", "X-RateLimit-Remaining": "95", - "X-RateLimit-Reset": "1640995200", + "X-RateLimit-Reset": "1640995200" } mock_request.return_value = mock_response @@ -267,41 +257,14 @@ def test_rate_limit_headers(self, mock_request): assert rate_limit.remaining == 95 assert rate_limit.reset == 1640995200 - @patch("tempmail.client.requests.Session.request") - def test_retry_on_request_exception(self, mock_request): - """Test retry mechanism on request exceptions.""" - from requests.exceptions import ConnectionError - - # Mock to fail 2 times then succeed - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"domains": []} - mock_response.headers = {} - - mock_request.side_effect = [ - ConnectionError("Connection failed"), - ConnectionError("Connection failed"), - mock_response, - ] - - client = TempMailClient("test-api-key") - - with patch("time.sleep"): # Mock sleep to speed up test - domains = client.list_domains() - - assert len(domains) == 0 - assert mock_request.call_count == 3 - - @patch("tempmail.client.requests.Session.request") - def test_max_retries_exceeded(self, mock_request): - """Test failure after max retries exceeded.""" + @patch('tempmail.client.requests.Session.request') + def test_request_exception(self, mock_request): + """Test handling of request exceptions.""" from requests.exceptions import ConnectionError from tempmail.exceptions import TempMailError mock_request.side_effect = ConnectionError("Connection failed") client = TempMailClient("test-api-key") - - with patch("time.sleep"): # Mock sleep to speed up test - with pytest.raises(TempMailError, match="Request failed after 4 attempts"): - client.list_domains() + with pytest.raises(TempMailError, match="Request failed"): + client.list_domains() From 163e719dc6ca830f4e5a349e30a76eca144bf590 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:53:03 +0400 Subject: [PATCH 04/24] Pass some tests --- .idea/ruff.xml | 3 + tempmail/__init__.py | 4 - tempmail/client.py | 11 -- tests/test_client.py | 461 ++++++++++++++++++++++--------------------- 4 files changed, 236 insertions(+), 243 deletions(-) diff --git a/.idea/ruff.xml b/.idea/ruff.xml index ed9f86d..46029ef 100644 --- a/.idea/ruff.xml +++ b/.idea/ruff.xml @@ -2,5 +2,8 @@Test body
", - "created_at": "2023-01-01T00:00:00Z" - } + "name": "example.io", + "type": "premium", + }, ] } - mock_response.headers = {} + mock_response.headers = self._rate_limit_headers mock_request.return_value = mock_response - client = TempMailClient("test-api-key") - messages = client.list_email_messages("test@temp.io") - - assert len(messages) == 1 - message = messages[0] - assert isinstance(message, EmailMessage) - assert message.id == "msg1" - assert message.from_addr == "sender@example.com" - assert message.to_addr == "test@temp.io" - assert message.subject == "Test Subject" - assert message.body_text == "Test body" - assert message.body_html == "Test body
" - - @patch('tempmail.client.requests.Session.request') - def test_list_email_messages_with_options(self, mock_request): - """Test message listing with options.""" - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"messages": []} - mock_response.headers = {} - mock_request.return_value = mock_response - - client = TempMailClient("test-api-key") - options = ListMessagesOptions(limit=10, offset=5) - client.list_email_messages("test@temp.io", options) - - # Verify the request was made with correct parameters - mock_request.assert_called_once() - call_args = mock_request.call_args - expected_params = {"email": "test@temp.io", "limit": 10, "offset": 5} - assert call_args[1]['params'] == expected_params - - def test_list_email_messages_no_email(self): - """Test message listing fails without email.""" - client = TempMailClient("test-api-key") - with pytest.raises(ValidationError, match="Email address is required"): - client.list_email_messages("") - - @patch('tempmail.client.requests.Session.request') - def test_delete_message_success(self, mock_request): - """Test successful message deletion.""" - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {} - mock_response.headers = {} - mock_request.return_value = mock_response - - client = TempMailClient("test-api-key") - result = client.delete_message("msg123") - - assert result is True - - # Verify correct endpoint was called - mock_request.assert_called_once() - call_args = mock_request.call_args - assert "/messages/msg123" in call_args[1]['url'] - assert call_args[1]['method'] == "DELETE" - - def test_delete_message_no_id(self): - """Test message deletion fails without message ID.""" - client = TempMailClient("test-api-key") - with pytest.raises(ValidationError, match="Message ID is required"): - client.delete_message("") - - @patch('tempmail.client.requests.Session.request') - def test_authentication_error(self, mock_request): - """Test authentication error handling.""" - mock_response = Mock() - mock_response.status_code = 401 - mock_response.content = b'' - mock_response.headers = {} - mock_request.return_value = mock_response - - client = TempMailClient("test-api-key") - with pytest.raises(AuthenticationError, match="Invalid API key"): - client.create_email() - - @patch('tempmail.client.requests.Session.request') - def test_rate_limit_error(self, mock_request): - """Test rate limit error handling.""" - mock_response = Mock() - mock_response.status_code = 429 - mock_response.content = b'' - mock_response.headers = {} - mock_request.return_value = mock_response - - client = TempMailClient("test-api-key") - with pytest.raises(RateLimitError, match="Rate limit exceeded"): - client.create_email() - - @patch('tempmail.client.requests.Session.request') - def test_validation_error(self, mock_request): - """Test validation error handling.""" - mock_response = Mock() - mock_response.status_code = 400 - mock_response.content = b'{"message": "Invalid domain"}' - mock_response.json.return_value = {"message": "Invalid domain"} - mock_response.headers = {} - mock_request.return_value = mock_response - - client = TempMailClient("test-api-key") - with pytest.raises(ValidationError, match="Invalid domain"): - client.create_email() - - @patch('tempmail.client.requests.Session.request') - def test_api_error(self, mock_request): - """Test generic API error handling.""" - mock_response = Mock() - mock_response.status_code = 500 - mock_response.content = b'{"message": "Internal server error"}' - mock_response.json.return_value = {"message": "Internal server error"} - mock_response.headers = {} - mock_request.return_value = mock_response - - client = TempMailClient("test-api-key") - with pytest.raises(APIError) as exc_info: - client.create_email() - - assert exc_info.value.status_code == 500 - assert "Internal server error" in str(exc_info.value) - - @patch('tempmail.client.requests.Session.request') - def test_rate_limit_headers(self, mock_request): - """Test rate limit information from headers.""" - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {} - mock_response.headers = { - "X-RateLimit-Limit": "100", - "X-RateLimit-Remaining": "95", - "X-RateLimit-Reset": "1640995200" - } - mock_request.return_value = mock_response - - client = TempMailClient("test-api-key") - rate_limit = client.get_rate_limit() - - assert rate_limit is not None - assert rate_limit.limit == 100 - assert rate_limit.remaining == 95 - assert rate_limit.reset == 1640995200 - - @patch('tempmail.client.requests.Session.request') - def test_request_exception(self, mock_request): - """Test handling of request exceptions.""" - from requests.exceptions import ConnectionError - from tempmail.exceptions import TempMailError - - mock_request.side_effect = ConnectionError("Connection failed") - - client = TempMailClient("test-api-key") - with pytest.raises(TempMailError, match="Request failed"): - client.list_domains() + client: TempMailClient = TempMailClient("test-api-key") + domains: typing.List[Domain] = client.list_domains() + assert domains == [ + Domain(name="example.com", type="public"), + Domain(name="test.org", type="custom"), + Domain(name="example.io", type="premium"), + ] + + # + # @patch('tempmail.client.requests.Session.request') + # def test_list_email_messages_success(self, mock_request): + # """Test successful message listing.""" + # mock_response = Mock() + # mock_response.status_code = 200 + # mock_response.json.return_value = { + # "messages": [ + # { + # "id": "msg1", + # "from": "sender@example.com", + # "to": "test@temp.io", + # "subject": "Test Subject", + # "body_text": "Test body", + # "body_html": "Test body
", + # "created_at": "2023-01-01T00:00:00Z" + # } + # ] + # } + # mock_response.headers = {} + # mock_request.return_value = mock_response + # + # client = TempMailClient("test-api-key") + # messages = client.list_email_messages("test@temp.io") + # + # assert len(messages) == 1 + # message = messages[0] + # assert isinstance(message, EmailMessage) + # assert message.id == "msg1" + # assert message.from_addr == "sender@example.com" + # assert message.to_addr == "test@temp.io" + # assert message.subject == "Test Subject" + # assert message.body_text == "Test body" + # assert message.body_html == "Test body
" + # + # @patch('tempmail.client.requests.Session.request') + # def test_list_email_messages_with_options(self, mock_request): + # """Test message listing with options.""" + # mock_response = Mock() + # mock_response.status_code = 200 + # mock_response.json.return_value = {"messages": []} + # mock_response.headers = {} + # mock_request.return_value = mock_response + # + # client = TempMailClient("test-api-key") + # options = ListMessagesOptions(limit=10, offset=5) + # client.list_email_messages("test@temp.io", options) + # + # # Verify the request was made with correct parameters + # mock_request.assert_called_once() + # call_args = mock_request.call_args + # expected_params = {"email": "test@temp.io", "limit": 10, "offset": 5} + # assert call_args[1]['params'] == expected_params + # + # def test_list_email_messages_no_email(self): + # """Test message listing fails without email.""" + # client = TempMailClient("test-api-key") + # with pytest.raises(ValidationError, match="Email address is required"): + # client.list_email_messages("") + # + # @patch('tempmail.client.requests.Session.request') + # def test_delete_message_success(self, mock_request): + # """Test successful message deletion.""" + # mock_response = Mock() + # mock_response.status_code = 200 + # mock_response.json.return_value = {} + # mock_response.headers = {} + # mock_request.return_value = mock_response + # + # client = TempMailClient("test-api-key") + # result = client.delete_message("msg123") + # + # assert result is True + # + # # Verify correct endpoint was called + # mock_request.assert_called_once() + # call_args = mock_request.call_args + # assert "/messages/msg123" in call_args[1]['url'] + # assert call_args[1]['method'] == "DELETE" + # + # def test_delete_message_no_id(self): + # """Test message deletion fails without message ID.""" + # client = TempMailClient("test-api-key") + # with pytest.raises(ValidationError, match="Message ID is required"): + # client.delete_message("") + # + # @patch('tempmail.client.requests.Session.request') + # def test_authentication_error(self, mock_request): + # """Test authentication error handling.""" + # mock_response = Mock() + # mock_response.status_code = 401 + # mock_response.content = b'' + # mock_response.headers = {} + # mock_request.return_value = mock_response + # + # client = TempMailClient("test-api-key") + # with pytest.raises(AuthenticationError, match="Invalid API key"): + # client.create_email() + # + # @patch('tempmail.client.requests.Session.request') + # def test_rate_limit_error(self, mock_request): + # """Test rate limit error handling.""" + # mock_response = Mock() + # mock_response.status_code = 429 + # mock_response.content = b'' + # mock_response.headers = {} + # mock_request.return_value = mock_response + # + # client = TempMailClient("test-api-key") + # with pytest.raises(RateLimitError, match="Rate limit exceeded"): + # client.create_email() + # + # @patch('tempmail.client.requests.Session.request') + # def test_validation_error(self, mock_request): + # """Test validation error handling.""" + # mock_response = Mock() + # mock_response.status_code = 400 + # mock_response.content = b'{"message": "Invalid domain"}' + # mock_response.json.return_value = {"message": "Invalid domain"} + # mock_response.headers = {} + # mock_request.return_value = mock_response + # + # client = TempMailClient("test-api-key") + # with pytest.raises(ValidationError, match="Invalid domain"): + # client.create_email() + # + # @patch('tempmail.client.requests.Session.request') + # def test_api_error(self, mock_request): + # """Test generic API error handling.""" + # mock_response = Mock() + # mock_response.status_code = 500 + # mock_response.content = b'{"message": "Internal server error"}' + # mock_response.json.return_value = {"message": "Internal server error"} + # mock_response.headers = {} + # mock_request.return_value = mock_response + # + # client = TempMailClient("test-api-key") + # with pytest.raises(APIError) as exc_info: + # client.create_email() + # + # assert exc_info.value.status_code == 500 + # assert "Internal server error" in str(exc_info.value) + # + # @patch('tempmail.client.requests.Session.request') + # def test_rate_limit_headers(self, mock_request): + # """Test rate limit information from headers.""" + # mock_response = Mock() + # mock_response.status_code = 200 + # mock_response.json.return_value = {} + # mock_response.headers = { + # "X-RateLimit-Limit": "100", + # "X-RateLimit-Remaining": "95", + # "X-RateLimit-Reset": "1640995200" + # } + # mock_request.return_value = mock_response + # + # client = TempMailClient("test-api-key") + # rate_limit = client.get_rate_limit() + # + # assert rate_limit is not None + # assert rate_limit.limit == 100 + # assert rate_limit.remaining == 95 + # assert rate_limit.reset == 1640995200 + # + # @patch('tempmail.client.requests.Session.request') + # def test_request_exception(self, mock_request): + # """Test handling of request exceptions.""" + # from requests.exceptions import ConnectionError + # from tempmail.exceptions import TempMailError + # + # mock_request.side_effect = ConnectionError("Connection failed") + # + # client = TempMailClient("test-api-key") + # with pytest.raises(TempMailError, match="Request failed"): + # client.list_domains() From 183cf2237381b4be06f5f067176519349c73cfe1 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:07:35 +0400 Subject: [PATCH 05/24] Fix some tests --- tempmail/models.py | 27 +++++++++++++++------------ tests/test_client.py | 7 ++++--- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/tempmail/models.py b/tempmail/models.py index 92f7f72..ec1b475 100644 --- a/tempmail/models.py +++ b/tempmail/models.py @@ -1,5 +1,6 @@ """Data models for the Temp Mail API.""" +import enum from dataclasses import dataclass from typing import Optional, List, Dict, Any from datetime import datetime @@ -14,32 +15,34 @@ class RateLimit: reset: int +class DomainType(enum.Enum): + PUBLIC = "public" + CUSTOM = "custom" + PREMIUM = "premium" + + @dataclass class Domain: """Email domain information.""" + name: str - type: str # e.g., "public", "custom", "premium" + type: DomainType @classmethod - def from_json(cls, data: Dict[str, Any]) -> 'Domain': - return cls( - name=data['name'], - type=data['type'] - ) + def from_json(cls, data: Dict[str, Any]) -> "Domain": + return cls(name=data["name"], type=DomainType(data["type"])) @dataclass class EmailAddress: """Generated temporary email address.""" + email: str - ttl: int # Time to live in seconds + ttl: int # Time to live in seconds @classmethod - def from_json(cls, data: Dict[str, Any]) -> 'EmailAddress': - return cls( - email=data['email'], - ttl=data['ttl'] - ) + def from_json(cls, data: Dict[str, Any]) -> "EmailAddress": + return cls(email=data["email"], ttl=data["ttl"]) @dataclass diff --git a/tests/test_client.py b/tests/test_client.py index 8801a27..5849017 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,6 +8,7 @@ EmailAddress, Domain, ) +from tempmail.models import DomainType class TestTempMailClient: @@ -93,9 +94,9 @@ def test_list_domains_success(self, mock_request) -> None: client: TempMailClient = TempMailClient("test-api-key") domains: typing.List[Domain] = client.list_domains() assert domains == [ - Domain(name="example.com", type="public"), - Domain(name="test.org", type="custom"), - Domain(name="example.io", type="premium"), + Domain(name="example.com", type=DomainType.PUBLIC), + Domain(name="test.org", type=DomainType.CUSTOM), + Domain(name="example.io", type=DomainType.PREMIUM), ] # From ffa62a1a7d965977c23509787475c553291c323f Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:25:49 +0400 Subject: [PATCH 06/24] Add some logic --- tempmail/client.py | 126 +++++++---- tempmail/models.py | 28 ++- tests/test_client.py | 484 +++++++++++++++++++++++++++---------------- 3 files changed, 416 insertions(+), 222 deletions(-) diff --git a/tempmail/client.py b/tempmail/client.py index b4f8e9e..33cd435 100644 --- a/tempmail/client.py +++ b/tempmail/client.py @@ -1,7 +1,5 @@ """Temp Mail API client implementation.""" -import json -import time from typing import Optional, List, Dict, Any from urllib.parse import urljoin import requests @@ -36,11 +34,13 @@ def __init__( self.timeout = timeout self.session = requests.Session() - self.session.headers.update({ - "X-API-Key": api_key, - "Content-Type": "application/json", - "User-Agent": f"temp-mail-python/{__version__}", - }) + self.session.headers.update( + { + "X-API-Key": api_key, + "Content-Type": "application/json", + "User-Agent": f"temp-mail-python/{__version__}", + } + ) self._last_rate_limit: Optional[RateLimit] = None @@ -75,13 +75,14 @@ def _make_request( raise RateLimitError("Rate limit exceeded") elif response.status_code == 400: error_data = response.json() if response.content else {} - error_message = self._extract_error_message(error_data, "Invalid request parameters") + error_message = self._extract_error_message( + error_data, "Invalid request parameters" + ) raise ValidationError(error_message) else: error_data = response.json() if response.content else {} error_message = self._extract_error_message( - error_data, - f"API request failed with status {response.status_code}" + error_data, f"API request failed with status {response.status_code}" ) raise APIError( error_message, @@ -92,7 +93,9 @@ def _make_request( except requests.exceptions.RequestException as e: raise TempMailError(f"Request failed: {str(e)}") - def _extract_error_message(self, error_data: Dict[str, Any], default_message: str) -> str: + def _extract_error_message( + self, error_data: Dict[str, Any], default_message: str + ) -> str: """Extract error message from API response.""" # Try new API format: {"error": {"detail": "message"}} if "error" in error_data and isinstance(error_data["error"], dict): @@ -111,13 +114,17 @@ def _extract_error_message(self, error_data: Dict[str, Any], default_message: st def _update_rate_limit_from_headers(self, headers: Any) -> None: """Update rate limit info from response headers.""" self._last_rate_limit = RateLimit( - limit=int(headers["X-RateLimit-Limit"]), - remaining=int(headers.get("X-RateLimit-Remaining", 0)), - reset=int(headers.get("X-RateLimit-Reset", 0)), + limit=int(headers["X-Ratelimit-Limit"]), + remaining=int(headers["X-Ratelimit-Remaining"]), + reset=int(headers["X-Ratelimit-Reset"]), + used=int(headers["X-Ratelimit-Used"]), ) def create_email( - self, email: Optional[str] = None, domain: Optional[str] = None, domain_type: Optional[str] = None, + self, + email: Optional[str] = None, + domain: Optional[str] = None, + domain_type: Optional[str] = None, ) -> EmailAddress: """ Create a new temporary email address. @@ -125,15 +132,17 @@ def create_email( :param domain: Optional domain to use :param domain_type: Optional domain type (e.g., "public", "custom", "premium") """ - params: Dict[str, Any] = {} + json_data: Dict[str, Any] = {} if email: - params["email"] = email + json_data["email"] = email if domain: - params["domain"] = domain + json_data["domain"] = domain if domain_type: - params["domain_type"] = domain_type + json_data["domain_type"] = domain_type - data = self._make_request("POST", "/v1/emails", params=params) + data = self._make_request( + "POST", "/v1/emails", json_data=json_data if json_data else None + ) return EmailAddress.from_json(data) @@ -149,41 +158,80 @@ def list_domains(self) -> List[Domain]: return [Domain.from_json(domain) for domain in data["domains"]] def list_email_messages( - self, email: str, + self, + email: str, ) -> List[EmailMessage]: + """Get all messages for a specific email address.""" data = self._make_request("GET", f"/v1/emails/{email}/messages") messages = [] for msg_data in data.get("messages", []): - messages.append( - EmailMessage( - id=msg_data["id"], - from_addr=msg_data["from"], - to_addr=msg_data["to"], - subject=msg_data["subject"], - body_text=msg_data["body_text"], - body_html=msg_data.get("body_html"), - created_at=msg_data.get("created_at"), - attachments=msg_data.get("attachments"), - ) - ) + messages.append(EmailMessage.from_json(msg_data)) return messages + def get_message(self, message_id: str) -> EmailMessage: + """Get a specific message by ID.""" + data = self._make_request("GET", f"/v1/messages/{message_id}") + return EmailMessage.from_json(data) + def delete_message(self, message_id: str) -> bool: + """Delete a specific message by ID.""" self._make_request("DELETE", f"/v1/messages/{message_id}") return True + def delete_email(self, email: str) -> bool: + """Delete an email address and all its messages.""" + self._make_request("DELETE", f"/v1/emails/{email}") + return True + + def get_message_source_code(self, message_id: str) -> str: + """Get the raw source code of a message.""" + data = self._make_request("GET", f"/v1/messages/{message_id}/source_code") + return data["data"] + + def download_attachment(self, attachment_id: str) -> bytes: + """Download an attachment by ID.""" + url = f"/v1/attachments/{attachment_id}" + response = self.session.request( + method="GET", + url=self.base_url + url, + timeout=self.timeout, + ) + + # Update rate limit info from headers + self._update_rate_limit_from_headers(response.headers) + + if response.status_code >= 200 and response.status_code < 300: + return response.content + elif response.status_code == 401: + raise AuthenticationError("Invalid API key") + elif response.status_code == 429: + raise RateLimitError("Rate limit exceeded") + elif response.status_code == 400: + error_data = response.json() if response.content else {} + error_message = self._extract_error_message( + error_data, "Invalid request parameters" + ) + raise ValidationError(error_message) + else: + error_data = response.json() if response.content else {} + error_message = self._extract_error_message( + error_data, f"API request failed with status {response.status_code}" + ) + raise APIError( + error_message, + status_code=response.status_code, + response_data=error_data, + ) + def get_rate_limit(self) -> RateLimit: """ Get current rate limit information. - - Returns: - RateLimit: Current rate limit status, or None if not available + :return: RateLimit object """ - # Make a lightweight request to get fresh rate limit info - self._make_request("GET", "/v1/rate-limit") - return self._last_rate_limit + data = self._make_request("GET", "/v1/rate_limit") + return RateLimit.from_json(data) @property def last_rate_limit(self) -> Optional[RateLimit]: diff --git a/tempmail/models.py b/tempmail/models.py index ec1b475..779ba00 100644 --- a/tempmail/models.py +++ b/tempmail/models.py @@ -3,7 +3,6 @@ import enum from dataclasses import dataclass from typing import Optional, List, Dict, Any -from datetime import datetime @dataclass @@ -13,6 +12,16 @@ class RateLimit: limit: int remaining: int reset: int + used: Optional[int] = None + + @classmethod + def from_json(cls, data: Dict[str, Any]) -> "RateLimit": + return cls( + limit=data["limit"], + remaining=data["remaining"], + reset=data["reset"], + used=data.get("used"), + ) class DomainType(enum.Enum): @@ -54,6 +63,21 @@ class EmailMessage: to_addr: str subject: str body_text: str + cc: Optional[List[str]] = None body_html: Optional[str] = None - created_at: Optional[datetime] = None + created_at: Optional[str] = None attachments: Optional[List[Dict[str, Any]]] = None + + @classmethod + def from_json(cls, data: Dict[str, Any]) -> "EmailMessage": + return cls( + id=data["id"], + from_addr=data["from"], + to_addr=data["to"], + subject=data["subject"], + body_text=data["body_text"], + cc=data.get("cc", []), + body_html=data.get("body_html"), + created_at=data.get("created_at"), + attachments=data.get("attachments", []), + ) diff --git a/tests/test_client.py b/tests/test_client.py index 5849017..b8c1708 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,22 +1,28 @@ """Tests for the TempMailClient.""" import typing +import pytest from unittest.mock import Mock, patch from tempmail import ( TempMailClient, EmailAddress, Domain, + EmailMessage, + AuthenticationError, + RateLimitError, + ValidationError, + APIError, ) from tempmail.models import DomainType class TestTempMailClient: _rate_limit_headers: typing.Dict[str, str] = { - "X-RateLimit-Limit": "100", - "X-RateLimit-Remaining": "99", - "X-RateLimit-Reset": "2073044847", - "X-RateLimit-Used": "1", + "X-Ratelimit-Limit": "100", + "X-Ratelimit-Remaining": "99", + "X-Ratelimit-Reset": "2073044847", + "X-Ratelimit-Used": "1", } def test_client_initialization(self) -> None: @@ -63,8 +69,8 @@ def test_create_email_with_options(self, mock_request): mock_request.assert_called_once_with( method="POST", url="https://api.temp-mail.io/v1/emails", - params={"domain": "mydomain.com"}, - json=None, + params=None, + json={"domain": "mydomain.com"}, timeout=30, ) @@ -99,178 +105,294 @@ def test_list_domains_success(self, mock_request) -> None: Domain(name="example.io", type=DomainType.PREMIUM), ] - # - # @patch('tempmail.client.requests.Session.request') - # def test_list_email_messages_success(self, mock_request): - # """Test successful message listing.""" - # mock_response = Mock() - # mock_response.status_code = 200 - # mock_response.json.return_value = { - # "messages": [ - # { - # "id": "msg1", - # "from": "sender@example.com", - # "to": "test@temp.io", - # "subject": "Test Subject", - # "body_text": "Test body", - # "body_html": "Test body
", - # "created_at": "2023-01-01T00:00:00Z" - # } - # ] - # } - # mock_response.headers = {} - # mock_request.return_value = mock_response - # - # client = TempMailClient("test-api-key") - # messages = client.list_email_messages("test@temp.io") - # - # assert len(messages) == 1 - # message = messages[0] - # assert isinstance(message, EmailMessage) - # assert message.id == "msg1" - # assert message.from_addr == "sender@example.com" - # assert message.to_addr == "test@temp.io" - # assert message.subject == "Test Subject" - # assert message.body_text == "Test body" - # assert message.body_html == "Test body
" - # - # @patch('tempmail.client.requests.Session.request') - # def test_list_email_messages_with_options(self, mock_request): - # """Test message listing with options.""" - # mock_response = Mock() - # mock_response.status_code = 200 - # mock_response.json.return_value = {"messages": []} - # mock_response.headers = {} - # mock_request.return_value = mock_response - # - # client = TempMailClient("test-api-key") - # options = ListMessagesOptions(limit=10, offset=5) - # client.list_email_messages("test@temp.io", options) - # - # # Verify the request was made with correct parameters - # mock_request.assert_called_once() - # call_args = mock_request.call_args - # expected_params = {"email": "test@temp.io", "limit": 10, "offset": 5} - # assert call_args[1]['params'] == expected_params - # - # def test_list_email_messages_no_email(self): - # """Test message listing fails without email.""" - # client = TempMailClient("test-api-key") - # with pytest.raises(ValidationError, match="Email address is required"): - # client.list_email_messages("") - # - # @patch('tempmail.client.requests.Session.request') - # def test_delete_message_success(self, mock_request): - # """Test successful message deletion.""" - # mock_response = Mock() - # mock_response.status_code = 200 - # mock_response.json.return_value = {} - # mock_response.headers = {} - # mock_request.return_value = mock_response - # - # client = TempMailClient("test-api-key") - # result = client.delete_message("msg123") - # - # assert result is True - # - # # Verify correct endpoint was called - # mock_request.assert_called_once() - # call_args = mock_request.call_args - # assert "/messages/msg123" in call_args[1]['url'] - # assert call_args[1]['method'] == "DELETE" - # - # def test_delete_message_no_id(self): - # """Test message deletion fails without message ID.""" - # client = TempMailClient("test-api-key") - # with pytest.raises(ValidationError, match="Message ID is required"): - # client.delete_message("") - # - # @patch('tempmail.client.requests.Session.request') - # def test_authentication_error(self, mock_request): - # """Test authentication error handling.""" - # mock_response = Mock() - # mock_response.status_code = 401 - # mock_response.content = b'' - # mock_response.headers = {} - # mock_request.return_value = mock_response - # - # client = TempMailClient("test-api-key") - # with pytest.raises(AuthenticationError, match="Invalid API key"): - # client.create_email() - # - # @patch('tempmail.client.requests.Session.request') - # def test_rate_limit_error(self, mock_request): - # """Test rate limit error handling.""" - # mock_response = Mock() - # mock_response.status_code = 429 - # mock_response.content = b'' - # mock_response.headers = {} - # mock_request.return_value = mock_response - # - # client = TempMailClient("test-api-key") - # with pytest.raises(RateLimitError, match="Rate limit exceeded"): - # client.create_email() - # - # @patch('tempmail.client.requests.Session.request') - # def test_validation_error(self, mock_request): - # """Test validation error handling.""" - # mock_response = Mock() - # mock_response.status_code = 400 - # mock_response.content = b'{"message": "Invalid domain"}' - # mock_response.json.return_value = {"message": "Invalid domain"} - # mock_response.headers = {} - # mock_request.return_value = mock_response - # - # client = TempMailClient("test-api-key") - # with pytest.raises(ValidationError, match="Invalid domain"): - # client.create_email() - # - # @patch('tempmail.client.requests.Session.request') - # def test_api_error(self, mock_request): - # """Test generic API error handling.""" - # mock_response = Mock() - # mock_response.status_code = 500 - # mock_response.content = b'{"message": "Internal server error"}' - # mock_response.json.return_value = {"message": "Internal server error"} - # mock_response.headers = {} - # mock_request.return_value = mock_response - # - # client = TempMailClient("test-api-key") - # with pytest.raises(APIError) as exc_info: - # client.create_email() - # - # assert exc_info.value.status_code == 500 - # assert "Internal server error" in str(exc_info.value) - # - # @patch('tempmail.client.requests.Session.request') - # def test_rate_limit_headers(self, mock_request): - # """Test rate limit information from headers.""" - # mock_response = Mock() - # mock_response.status_code = 200 - # mock_response.json.return_value = {} - # mock_response.headers = { - # "X-RateLimit-Limit": "100", - # "X-RateLimit-Remaining": "95", - # "X-RateLimit-Reset": "1640995200" - # } - # mock_request.return_value = mock_response - # - # client = TempMailClient("test-api-key") - # rate_limit = client.get_rate_limit() - # - # assert rate_limit is not None - # assert rate_limit.limit == 100 - # assert rate_limit.remaining == 95 - # assert rate_limit.reset == 1640995200 - # - # @patch('tempmail.client.requests.Session.request') - # def test_request_exception(self, mock_request): - # """Test handling of request exceptions.""" - # from requests.exceptions import ConnectionError - # from tempmail.exceptions import TempMailError - # - # mock_request.side_effect = ConnectionError("Connection failed") - # - # client = TempMailClient("test-api-key") - # with pytest.raises(TempMailError, match="Request failed"): - # client.list_domains() + @patch("tempmail.client.requests.Session.request") + def test_list_email_messages_success(self, mock_request): + """Test successful message listing.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "messages": [ + { + "id": "msg1", + "from": "sender@example.com", + "to": "test@temp.io", + "cc": ["cc@example.com"], + "subject": "Test Subject", + "body_text": "Test body", + "body_html": "Test body
", + "created_at": "2023-01-01T00:00:00Z", + "attachments": [], + } + ] + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + messages = client.list_email_messages("test@temp.io") + + assert len(messages) == 1 + message = messages[0] + assert isinstance(message, EmailMessage) + assert message.id == "msg1" + assert message.from_addr == "sender@example.com" + assert message.to_addr == "test@temp.io" + assert message.cc == ["cc@example.com"] + assert message.subject == "Test Subject" + assert message.body_text == "Test body" + assert message.body_html == "Test body
" + assert message.created_at == "2023-01-01T00:00:00Z" + assert message.attachments == [] + + @patch("tempmail.client.requests.Session.request") + def test_list_email_messages_empty(self, mock_request): + """Test message listing with empty response.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"messages": []} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + messages = client.list_email_messages("test@temp.io") + + assert len(messages) == 0 + # Verify the request was made to correct endpoint + mock_request.assert_called_once_with( + method="GET", + url="https://api.temp-mail.io/v1/emails/test@temp.io/messages", + params=None, + json=None, + timeout=30, + ) + + @patch("tempmail.client.requests.Session.request") + def test_get_message_success(self, mock_request): + """Test successful single message retrieval.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "id": "msg1", + "from": "sender@example.com", + "to": "test@temp.io", + "cc": [], + "subject": "Test Subject", + "body_text": "Test body", + "body_html": "Test body
", + "created_at": "2023-01-01T00:00:00Z", + "attachments": [], + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + message = client.get_message("msg1") + + assert isinstance(message, EmailMessage) + assert message.id == "msg1" + assert message.from_addr == "sender@example.com" + assert message.to_addr == "test@temp.io" + assert message.subject == "Test Subject" + + # Verify correct endpoint was called + mock_request.assert_called_once_with( + method="GET", + url="https://api.temp-mail.io/v1/messages/msg1", + params=None, + json=None, + timeout=30, + ) + + @patch("tempmail.client.requests.Session.request") + def test_delete_message_success(self, mock_request): + """Test successful message deletion.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + result = client.delete_message("msg123") + + assert result is True + + # Verify correct endpoint was called + mock_request.assert_called_once_with( + method="DELETE", + url="https://api.temp-mail.io/v1/messages/msg123", + params=None, + json=None, + timeout=30, + ) + + @patch("tempmail.client.requests.Session.request") + def test_delete_email_success(self, mock_request): + """Test successful email deletion.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + result = client.delete_email("test@temp.io") + + assert result is True + + # Verify correct endpoint was called + mock_request.assert_called_once_with( + method="DELETE", + url="https://api.temp-mail.io/v1/emails/test@temp.io", + params=None, + json=None, + timeout=30, + ) + + @patch("tempmail.client.requests.Session.request") + def test_get_message_source_code_success(self, mock_request): + """Test successful message source code retrieval.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "data": "Received: from example.com...\r\nSubject: Test Subject\r\n\r\nTest body" + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + source_code = client.get_message_source_code("msg1") + + assert "Received: from example.com" in source_code + assert "Subject: Test Subject" in source_code + + # Verify correct endpoint was called + mock_request.assert_called_once_with( + method="GET", + url="https://api.temp-mail.io/v1/messages/msg1/source_code", + params=None, + json=None, + timeout=30, + ) + + @patch("tempmail.client.requests.Session.request") + def test_download_attachment_success(self, mock_request): + """Test successful attachment download.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = b"attachment content here" + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + content = client.download_attachment("attachment1") + + assert content == b"attachment content here" + + # Verify correct endpoint was called + mock_request.assert_called_once_with( + method="GET", + url="https://api.temp-mail.io/v1/attachments/attachment1", + timeout=30, + ) + + @patch("tempmail.client.requests.Session.request") + def test_authentication_error(self, mock_request): + """Test authentication error handling.""" + mock_response = Mock() + mock_response.status_code = 401 + mock_response.content = b"" + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + with pytest.raises(AuthenticationError, match="Invalid API key"): + client.create_email() + + @patch("tempmail.client.requests.Session.request") + def test_rate_limit_error(self, mock_request): + """Test rate limit error handling.""" + mock_response = Mock() + mock_response.status_code = 429 + mock_response.content = b"" + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + with pytest.raises(RateLimitError, match="Rate limit exceeded"): + client.create_email() + + @patch("tempmail.client.requests.Session.request") + def test_validation_error(self, mock_request): + """Test validation error handling.""" + mock_response = Mock() + mock_response.status_code = 400 + mock_response.content = b'{"error": {"detail": "Invalid domain"}}' + mock_response.json.return_value = {"error": {"detail": "Invalid domain"}} + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + with pytest.raises(ValidationError, match="Invalid domain"): + client.create_email() + + @patch("tempmail.client.requests.Session.request") + def test_api_error(self, mock_request): + """Test generic API error handling.""" + mock_response = Mock() + mock_response.status_code = 500 + mock_response.content = b'{"error": {"detail": "Internal server error"}}' + mock_response.json.return_value = {"error": {"detail": "Internal server error"}} + mock_response.headers = {} + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + with pytest.raises(APIError) as exc_info: + client.create_email() + + assert exc_info.value.status_code == 500 + assert "Internal server error" in str(exc_info.value) + + @patch("tempmail.client.requests.Session.request") + def test_get_rate_limit_success(self, mock_request): + """Test successful rate limit retrieval.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "limit": 100, + "remaining": 95, + "used": 5, + "reset": 1640995200, + } + mock_response.headers = { + "X-RateLimit-Limit": "100", + "X-RateLimit-Remaining": "95", + "X-RateLimit-Reset": "1640995200", + } + mock_request.return_value = mock_response + + client = TempMailClient("test-api-key") + rate_limit_data = client.get_rate_limit() + + assert rate_limit_data.limit == 100 + assert rate_limit_data.remaining == 95 + assert rate_limit_data.used == 5 + assert rate_limit_data.reset == 1640995200 + + # Verify the last rate limit was updated from headers + assert client.last_rate_limit is not None + assert client.last_rate_limit.limit == 100 + assert client.last_rate_limit.remaining == 95 + assert client.last_rate_limit.reset == 1640995200 + + @patch("tempmail.client.requests.Session.request") + def test_request_exception(self, mock_request): + """Test handling of request exceptions.""" + from requests.exceptions import ConnectionError + from tempmail.exceptions import TempMailError + + mock_request.side_effect = ConnectionError("Connection failed") + + client = TempMailClient("test-api-key") + with pytest.raises(TempMailError, match="Request failed"): + client.list_domains() From 104b341b468c68e44ea687dccb540909377a7b7b Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:59:07 +0400 Subject: [PATCH 07/24] Add some logic --- pyproject.toml | 1 + tempmail/__init__.py | 2 - tempmail/client.py | 84 ++++++++++++++---------------------------- tempmail/exceptions.py | 20 +--------- tempmail/models.py | 35 ++++++++++++++++++ tests/test_client.py | 21 ++++++++--- uv.lock | 28 ++++++++++++++ 7 files changed, 109 insertions(+), 82 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ff425a5..5123b21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dev = [ "isort>=5.0.0", "mypy>=0.800", "types-requests", + "setuptools", ] [project.urls] diff --git a/tempmail/__init__.py b/tempmail/__init__.py index 17e8522..d635f3b 100644 --- a/tempmail/__init__.py +++ b/tempmail/__init__.py @@ -14,7 +14,6 @@ AuthenticationError, RateLimitError, ValidationError, - APIError, ) __all__ = [ @@ -27,5 +26,4 @@ "AuthenticationError", "RateLimitError", "ValidationError", - "APIError", ] diff --git a/tempmail/client.py b/tempmail/client.py index 33cd435..56f49b0 100644 --- a/tempmail/client.py +++ b/tempmail/client.py @@ -1,5 +1,6 @@ """Temp Mail API client implementation.""" +import typing from typing import Optional, List, Dict, Any from urllib.parse import urljoin import requests @@ -10,13 +11,13 @@ Domain, EmailAddress, EmailMessage, + APIErrorResponse, ) from .exceptions import ( TempMailError, AuthenticationError, RateLimitError, ValidationError, - APIError, ) @@ -50,8 +51,16 @@ def _make_request( endpoint: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - """Make an HTTP request to the API.""" + return_content: bool = False, + ) -> typing.Union[Dict[str, Any], bytes]: + """ + Make an HTTP request to the API. + :param method: HTTP method (GET, POST, DELETE, etc.) + :param endpoint: API endpoint (e.g., "/v1/emails") + :param params: Query parameters + :param json_data: JSON body for POST/PUT requests + :param return_content: If True, return raw response content instead of JSON + """ url = urljoin(self.base_url, endpoint) try: @@ -63,33 +72,24 @@ def _make_request( timeout=self.timeout, ) - # Update rate limit info from headers self._update_rate_limit_from_headers(response.headers) - # Handle different status codes - if response.status_code >= 200 and response.status_code < 300: + if 200 <= response.status_code < 300: + if return_content: + return response.content return response.json() - elif response.status_code == 401: - raise AuthenticationError("Invalid API key") - elif response.status_code == 429: - raise RateLimitError("Rate limit exceeded") - elif response.status_code == 400: - error_data = response.json() if response.content else {} - error_message = self._extract_error_message( - error_data, "Invalid request parameters" - ) - raise ValidationError(error_message) else: - error_data = response.json() if response.content else {} - error_message = self._extract_error_message( - error_data, f"API request failed with status {response.status_code}" + api_response: APIErrorResponse = APIErrorResponse.from_json( + response.json() ) - raise APIError( - error_message, - status_code=response.status_code, - response_data=error_data, - ) - + if api_response.is_api_key_error(): + raise AuthenticationError(api_response.detail) + elif api_response.is_rate_limit_error(): + raise RateLimitError(api_response.detail) + elif api_response.is_validation_error(): + raise ValidationError(api_response.detail) + else: + raise TempMailError(api_response.detail) except requests.exceptions.RequestException as e: raise TempMailError(f"Request failed: {str(e)}") @@ -192,38 +192,10 @@ def get_message_source_code(self, message_id: str) -> str: def download_attachment(self, attachment_id: str) -> bytes: """Download an attachment by ID.""" - url = f"/v1/attachments/{attachment_id}" - response = self.session.request( - method="GET", - url=self.base_url + url, - timeout=self.timeout, + content: bytes = self._make_request( + "GET", f"/v1/attachments/{attachment_id}", return_content=True ) - - # Update rate limit info from headers - self._update_rate_limit_from_headers(response.headers) - - if response.status_code >= 200 and response.status_code < 300: - return response.content - elif response.status_code == 401: - raise AuthenticationError("Invalid API key") - elif response.status_code == 429: - raise RateLimitError("Rate limit exceeded") - elif response.status_code == 400: - error_data = response.json() if response.content else {} - error_message = self._extract_error_message( - error_data, "Invalid request parameters" - ) - raise ValidationError(error_message) - else: - error_data = response.json() if response.content else {} - error_message = self._extract_error_message( - error_data, f"API request failed with status {response.status_code}" - ) - raise APIError( - error_message, - status_code=response.status_code, - response_data=error_data, - ) + return content def get_rate_limit(self) -> RateLimit: """ diff --git a/tempmail/exceptions.py b/tempmail/exceptions.py index 6c01e93..ec909b1 100644 --- a/tempmail/exceptions.py +++ b/tempmail/exceptions.py @@ -1,7 +1,5 @@ """Exceptions for the Temp Mail API client.""" -from typing import Optional - class TempMailError(Exception): """Base exception for all Temp Mail API errors.""" @@ -10,13 +8,13 @@ class TempMailError(Exception): class AuthenticationError(TempMailError): - """Raised when API key is invalid or missing.""" + """Raised when API Key is invalid or missing.""" pass class RateLimitError(TempMailError): - """Raised when API rate limit is exceeded.""" + """Raised when API Rate Limit is exceeded.""" pass @@ -25,17 +23,3 @@ class ValidationError(TempMailError): """Raised when request parameters are invalid.""" pass - - -class APIError(TempMailError): - """Raised when the API returns an error response.""" - - def __init__( - self, - message: str, - status_code: Optional[int] = None, - response_data: Optional[dict] = None, - ): - super().__init__(message) - self.status_code = status_code - self.response_data = response_data diff --git a/tempmail/models.py b/tempmail/models.py index 779ba00..67231de 100644 --- a/tempmail/models.py +++ b/tempmail/models.py @@ -81,3 +81,38 @@ def from_json(cls, data: Dict[str, Any]) -> "EmailMessage": created_at=data.get("created_at"), attachments=data.get("attachments", []), ) + + +@dataclass +class APIErrorResponse: + code: str + detail: str + type: str + request_id: str + + @classmethod + def from_json(cls, data: Dict[str, Any]) -> "APIErrorResponse": + return cls( + code=data["error"]["code"], + detail=data["error"]["detail"], + type=data["error"]["type"], + request_id=data["meta"]["request_id"], + ) + + def is_api_key_error(self) -> bool: + """ + Returns True if the error is related to an invalid or missing API key. + """ + return self.code in {"api_key_invalid", "api_key_empty"} + + def is_rate_limit_error(self) -> bool: + """ + Returns True if the error is related to exceeding the API rate limit. + """ + return self.code == "rate_limited" + + def is_validation_error(self) -> bool: + """ + Returns True if the error is related to invalid request parameters. + """ + return self.code == "validation_error" diff --git a/tests/test_client.py b/tests/test_client.py index b8c1708..d8c4be9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,7 +12,6 @@ AuthenticationError, RateLimitError, ValidationError, - APIError, ) from tempmail.models import DomainType @@ -293,6 +292,8 @@ def test_download_attachment_success(self, mock_request): mock_request.assert_called_once_with( method="GET", url="https://api.temp-mail.io/v1/attachments/attachment1", + params=None, + json=None, timeout=30, ) @@ -300,8 +301,15 @@ def test_download_attachment_success(self, mock_request): def test_authentication_error(self, mock_request): """Test authentication error handling.""" mock_response = Mock() - mock_response.status_code = 401 - mock_response.content = b"" + mock_response.status_code = 400 + mock_response.json.return_value = { + "error": { + "code": "api_key_invalid", + "detail": "API token is invalid", + "type": "request_error", + }, + "meta": {"request_id": "01K510JMH7V5PTN1TNCW5HF9AE"}, + } mock_response.headers = {} mock_request.return_value = mock_response @@ -365,9 +373,10 @@ def test_get_rate_limit_success(self, mock_request): "reset": 1640995200, } mock_response.headers = { - "X-RateLimit-Limit": "100", - "X-RateLimit-Remaining": "95", - "X-RateLimit-Reset": "1640995200", + "X-Ratelimit-Limit": "100", + "X-Ratelimit-Remaining": "95", + "X-Ratelimit-Reset": "1640995200", + "X-Ratelimit-Used": "5", } mock_request.return_value = mock_response diff --git a/uv.lock b/uv.lock index d22e265..e1d7b11 100644 --- a/uv.lock +++ b/uv.lock @@ -780,6 +780,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "setuptools" +version = "75.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/01/771ea46cce201dd42cff043a5eea929d1c030fb3d1c2ee2729d02ca7814c/setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5", size = 1354489, upload-time = "2025-03-12T00:02:19.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/65/3f0dba35760d902849d39d38c0a72767794b1963227b69a587f8a336d08c/setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9", size = 1251198, upload-time = "2025-03-12T00:02:17.554Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + [[package]] name = "temp-mail-io" source = { editable = "." } @@ -800,6 +825,8 @@ dev = [ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "setuptools", version = "75.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "setuptools", version = "80.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "types-requests", version = "2.32.0.20241016", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "types-requests", version = "2.32.4.20250809", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] @@ -812,6 +839,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=6.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=2.10.0" }, { name = "requests", specifier = ">=2.25.0" }, + { name = "setuptools", marker = "extra == 'dev'" }, { name = "types-requests", marker = "extra == 'dev'" }, ] provides-extras = ["dev"] From 1f47eadefecd952588e8cb3972dba51108d462d2 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:09:51 +0400 Subject: [PATCH 08/24] Fix more tests --- tempmail/client.py | 26 +++++-------------------- tests/test_client.py | 46 +++++++++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/tempmail/client.py b/tempmail/client.py index 56f49b0..4f4df4c 100644 --- a/tempmail/client.py +++ b/tempmail/client.py @@ -72,9 +72,8 @@ def _make_request( timeout=self.timeout, ) - self._update_rate_limit_from_headers(response.headers) - if 200 <= response.status_code < 300: + self._update_rate_limit_from_headers(response.headers) if return_content: return response.content return response.json() @@ -93,24 +92,6 @@ def _make_request( except requests.exceptions.RequestException as e: raise TempMailError(f"Request failed: {str(e)}") - def _extract_error_message( - self, error_data: Dict[str, Any], default_message: str - ) -> str: - """Extract error message from API response.""" - # Try new API format: {"error": {"detail": "message"}} - if "error" in error_data and isinstance(error_data["error"], dict): - error_obj = error_data["error"] - if "detail" in error_obj: - return str(error_obj["detail"]) - if "message" in error_obj: - return str(error_obj["message"]) - - # Try old format: {"message": "error"} - if "message" in error_data: - return str(error_data["message"]) - - return default_message - def _update_rate_limit_from_headers(self, headers: Any) -> None: """Update rate limit info from response headers.""" self._last_rate_limit = RateLimit( @@ -207,5 +188,8 @@ def get_rate_limit(self) -> RateLimit: @property def last_rate_limit(self) -> Optional[RateLimit]: - """Get the last known rate limit information.""" + """ + Get the last known rate limit information. + It will be None if no requests have been made yet. + """ return self._last_rate_limit diff --git a/tests/test_client.py b/tests/test_client.py index d8c4be9..4a3c09c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,6 +12,7 @@ AuthenticationError, RateLimitError, ValidationError, + TempMailError, ) from tempmail.models import DomainType @@ -314,7 +315,7 @@ def test_authentication_error(self, mock_request): mock_request.return_value = mock_response client = TempMailClient("test-api-key") - with pytest.raises(AuthenticationError, match="Invalid API key"): + with pytest.raises(AuthenticationError, match="API token is invalid"): client.create_email() @patch("tempmail.client.requests.Session.request") @@ -322,12 +323,22 @@ def test_rate_limit_error(self, mock_request): """Test rate limit error handling.""" mock_response = Mock() mock_response.status_code = 429 - mock_response.content = b"" + mock_response.json.return_value = { + "error": { + "code": "rate_limited", + "detail": "You have reached your rate limit. Please try again later.", + "type": "request_error", + }, + "meta": {"request_id": "01K510JMH7V5PTN1TNCW5HF9AE"}, + } mock_response.headers = {} mock_request.return_value = mock_response client = TempMailClient("test-api-key") - with pytest.raises(RateLimitError, match="Rate limit exceeded"): + with pytest.raises( + RateLimitError, + match="You have reached your rate limit. Please try again later.", + ): client.create_email() @patch("tempmail.client.requests.Session.request") @@ -335,32 +346,41 @@ def test_validation_error(self, mock_request): """Test validation error handling.""" mock_response = Mock() mock_response.status_code = 400 - mock_response.content = b'{"error": {"detail": "Invalid domain"}}' - mock_response.json.return_value = {"error": {"detail": "Invalid domain"}} + mock_response.json.return_value = { + "error": { + "code": "validation_error", + "detail": "Invalid domain name", + "type": "request_error", + }, + "meta": {"request_id": "01K510JMH7V5PTN1TNCW5HF9AE"}, + } mock_response.headers = {} mock_request.return_value = mock_response client = TempMailClient("test-api-key") - with pytest.raises(ValidationError, match="Invalid domain"): - client.create_email() + with pytest.raises(ValidationError, match="Invalid domain name"): + client.create_email(domain="invalid_domain") @patch("tempmail.client.requests.Session.request") def test_api_error(self, mock_request): """Test generic API error handling.""" mock_response = Mock() mock_response.status_code = 500 - mock_response.content = b'{"error": {"detail": "Internal server error"}}' - mock_response.json.return_value = {"error": {"detail": "Internal server error"}} + mock_response.json.return_value = { + "error": { + "code": "internal_error", + "detail": "Internal server error", + "type": "api_error", + }, + "meta": {"request_id": "01K510JMH7V5PTN1TNCW5HF9AE"}, + } mock_response.headers = {} mock_request.return_value = mock_response client = TempMailClient("test-api-key") - with pytest.raises(APIError) as exc_info: + with pytest.raises(TempMailError, match="Internal server error"): client.create_email() - assert exc_info.value.status_code == 500 - assert "Internal server error" in str(exc_info.value) - @patch("tempmail.client.requests.Session.request") def test_get_rate_limit_success(self, mock_request): """Test successful rate limit retrieval.""" From cce58b6f0b216b1e7decf16842cf01a79c27e85e Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:13:55 +0400 Subject: [PATCH 09/24] Add test file --- .github/workflows/test.yml | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f7de948 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,59 @@ +name: Test + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + python-version: ["3.8", "3.9", "3.10", "3.11"] + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: | + uv sync --dev + + - name: Lint with black + run: | + uv run black --check . + + - name: Sort imports with isort + run: | + uv run isort --check-only . + + - name: Type check with mypy + run: | + uv run mypy tempmail + + - name: Test with pytest + run: | + uv run pytest tests/ -v --cov=tempmail --cov-report=xml --cov-report=term-missing + + - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false \ No newline at end of file From 7ae6c75692885bd32ad93e6a0e8fa8d7b9309514 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:17:29 +0400 Subject: [PATCH 10/24] Update test logi --- .github/workflows/test.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f7de948..87568b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,11 +35,7 @@ jobs: - name: Lint with black run: | - uv run black --check . - - - name: Sort imports with isort - run: | - uv run isort --check-only . + uvx ruff --check . - name: Type check with mypy run: | @@ -56,4 +52,4 @@ jobs: file: ./coverage.xml flags: unittests name: codecov-umbrella - fail_ci_if_error: false \ No newline at end of file + fail_ci_if_error: false From 13f023a2ec485dbc7946bf0cbb56d2e4bcab8f6f Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:23:38 +0400 Subject: [PATCH 11/24] Update test logic --- .github/workflows/test.yml | 8 ++------ .pre-commit-config.yaml | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87568b5..9e8f163 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,13 +33,9 @@ jobs: run: | uv sync --dev - - name: Lint with black + - name: Run pre-commit run: | - uvx ruff --check . - - - name: Type check with mypy - run: | - uv run mypy tempmail + uv run pre-commit run --all-files - name: Test with pytest run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..593af91 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +exclude: '.git' +default_stages: [ commit ] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: trailing-whitespace + - id: check-yaml + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.4 + hooks: + - id: ruff-check + args: [ --fix ] + - id: ruff-format + + - repo: https://github.com/astral-sh/uv-pre-commit + rev: v0.8.17 + hooks: + - id: uv-sync + args: [ --dev ] + - id: uv-lock + args: [ --dev ] From f7a9b3d7bec47c9312250390fcdf8ef1e8ac41e9 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:24:15 +0400 Subject: [PATCH 12/24] Update test logic --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e8f163..e65e625 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: - name: Run pre-commit run: | - uv run pre-commit run --all-files + uvx run pre-commit run --all-files - name: Test with pytest run: | From 449d35f8169285b78e54ce7a2567903d4d55e78f Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:24:54 +0400 Subject: [PATCH 13/24] Update test logic --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e65e625..3213021 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: - name: Run pre-commit run: | - uvx run pre-commit run --all-files + uvx pre-commit run --all-files - name: Test with pytest run: | From 00d93f3741a83008ba323b32ad32f5c8e29f15c0 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:25:47 +0400 Subject: [PATCH 14/24] Fix revision --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 593af91..7ca5662 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: ruff-format - repo: https://github.com/astral-sh/uv-pre-commit - rev: v0.8.17 + rev: 0.8.17 hooks: - id: uv-sync args: [ --dev ] From b5edf4ee8294945bf99b72a499d6c0b9e31e1de7 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:27:24 +0400 Subject: [PATCH 15/24] Fix args --- .pre-commit-config.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ca5662..92df8f6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,8 @@ repos: rev: 0.8.17 hooks: - id: uv-sync - args: [ --dev ] + args: + - --extra dev - id: uv-lock - args: [ --dev ] + args: + - --extra dev From 93ecf22c60e4afcf3bbd3ee00ba781e9a792be7b Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:30:08 +0400 Subject: [PATCH 16/24] Fix args --- .pre-commit-config.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92df8f6..28c9e76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,8 +19,6 @@ repos: rev: 0.8.17 hooks: - id: uv-sync - args: - - --extra dev + args: ["--extra", "dev"] - id: uv-lock - args: - - --extra dev + args: ["--check"] From 197016b4ad38aad0d964e51a703457875c945ee5 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:31:15 +0400 Subject: [PATCH 17/24] Fix args --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3213021..927eaf2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: - name: Install dependencies run: | - uv sync --dev + uv sync --extra dev - name: Run pre-commit run: | From b5e0d107a69f37fc815af2be6d7b479477fc95d0 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 12:35:09 +0400 Subject: [PATCH 18/24] Update codecov --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 927eaf2..b141e5b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,9 +43,10 @@ jobs: - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: unittests name: codecov-umbrella fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} From a854c178d53ad3d74e04e2e527dfa89081eb9ae4 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Sat, 13 Sep 2025 13:38:44 +0400 Subject: [PATCH 19/24] Add test publish --- .github/workflows/publish.yml | 2 +- .github/workflows/test-publish.yml | 58 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test-publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fd16f1c..a68a955 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,5 +30,5 @@ jobs: run: | python -m twine upload dist/* env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml new file mode 100644 index 0000000..7076d26 --- /dev/null +++ b/.github/workflows/test-publish.yml @@ -0,0 +1,58 @@ +name: Test Publish to TestPyPI + +on: + workflow_dispatch: + inputs: + version_suffix: + description: 'Version suffix for test release (e.g., .dev1, .rc1)' + required: false + default: '.dev1' + type: string + +jobs: + test-publish: + runs-on: ubuntu-latest + environment: test-pypi + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Install build dependencies + run: | + uv sync --extra dev + + - name: Build package + run: | + uv build + + - name: Check package + run: | + uvx twine check dist/* + + - name: List built packages + run: | + ls -la dist/ + + - name: Publish to TestPyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }} + TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ + run: | + uvx twine upload dist/* --verbose + + - name: Test installation from TestPyPI + run: | + sleep 30 # Wait for package to be available + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ temp-mail-io + python -c "import tempmail; print('✓ Package installed successfully from TestPyPI')" + python -c "from tempmail import TempMailClient; print('✓ TempMailClient imports successfully')" From 8af37c201e62e8122957dd2fb99848a4645eb6dd Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:13:05 +0400 Subject: [PATCH 20/24] Update code --- README.md | 117 +++++++++++++++++++++++++++++++------------ tempmail/client.py | 11 ++-- tests/test_client.py | 15 +++--- 3 files changed, 101 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index df9b309..6cfa0ca 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,11 @@ for message in messages: - **Create temporary email addresses** - Generate disposable emails instantly - **List available domains** - Get domains you can use for email creation - **Receive emails** - Fetch messages sent to your temporary addresses -- **Delete messages** - Clean up messages when done +- **Get individual messages** - Retrieve specific messages by ID +- **Get message source code** - Access raw email source +- **Download attachments** - Download email attachments +- **Delete messages** - Clean up individual messages +- **Delete emails** - Remove email addresses and all their messages - **Rate limit monitoring** - Track your API usage - **Error handling** - Comprehensive exception handling - **Type hints** - Full typing support for better development experience @@ -61,14 +65,17 @@ client = TempMailClient( ### Creating Email Addresses ```python -from tempmail import CreateEmailOptions - # Create random email email = client.create_email() -# Create with specific domain and prefix -options = CreateEmailOptions(domain="example.com", prefix="mytest") -email = client.create_email(options) +# Create with specific domain +email = client.create_email(domain="example.com") + +# Create with specific email address +email = client.create_email(email="mytest@example.com") + +# Create with domain type preference +email = client.create_email(domain_type="premium") ``` ### Listing Domains @@ -76,23 +83,34 @@ email = client.create_email(options) ```python domains = client.list_domains() for domain in domains: - print(domain.domain) + print(f"Domain: {domain.name}, Type: {domain.type.value}") ``` ### Managing Messages ```python -from tempmail import ListMessagesOptions - # List all messages for an email messages = client.list_email_messages("test@example.com") +for message in messages: + print(f"From: {message.from_addr}") + print(f"Subject: {message.subject}") + print(f"CC: {message.cc}") + print(f"Attachments: {len(message.attachments or [])}") + +# Get a specific message +message = client.get_message("message-id") + +# Get message source code +source_code = client.get_message_source_code("message-id") -# List with pagination -options = ListMessagesOptions(limit=10, offset=0) -messages = client.list_email_messages("test@example.com", options) +# Download an attachment +attachment_data = client.download_attachment("attachment-id") # Delete a message client.delete_message("message-id") + +# Delete an entire email address and all its messages +client.delete_email("test@example.com") ``` ### Rate Limiting @@ -100,9 +118,15 @@ client.delete_message("message-id") ```python # Get current rate limit status rate_limit = client.get_rate_limit() -if rate_limit: - print(f"Remaining requests: {rate_limit.remaining}") - print(f"Reset time: {rate_limit.reset}") +print(f"Limit: {rate_limit.limit}") +print(f"Remaining: {rate_limit.remaining}") +print(f"Used: {rate_limit.used}") +print(f"Reset time: {rate_limit.reset}") + +# Access last rate limit from any request +last_rate_limit = client.last_rate_limit +if last_rate_limit: + print(f"Last known remaining: {last_rate_limit.remaining}") ``` ## Error Handling @@ -115,7 +139,6 @@ from tempmail import ( AuthenticationError, # Invalid API key RateLimitError, # Rate limit exceeded ValidationError, # Invalid parameters - APIError # Server errors ) try: @@ -127,8 +150,8 @@ except RateLimitError: print("Rate limit exceeded") except ValidationError as e: print(f"Invalid parameters: {e}") -except APIError as e: - print(f"API error: {e.status_code} - {e}") +except TempMailError as e: + print(f"API error: {e}") ``` ## Development @@ -140,36 +163,68 @@ except APIError as e: git clone https://github.com/temp-mail-io/temp-mail-python cd temp-mail-python -# Install in development mode -pip install -e ".[dev]" +# Install uv (recommended) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install dependencies +uv sync --dev ``` ### Running Tests ```bash -pytest tests/ +uv run pytest tests/ ``` -### Code Formatting +### Code Quality ```bash -black tempmail/ tests/ -isort tempmail/ tests/ +# Run all pre-commit hooks +uv run pre-commit run --all-files + +# Or run individual tools +uv run ruff check # Linting +uv run ruff format # Formatting +uv run mypy tempmail/ # Type checking ``` -### Type Checking +## Complete Example -```bash -mypy tempmail/ -``` +```python +from tempmail import TempMailClient, AuthenticationError, RateLimitError -## Examples +# Initialize client +client = TempMailClient("your-api-key") -See the [examples/](examples/) directory for more detailed usage examples. +try: + # Create a temporary email + email = client.create_email() + print(f"Created email: {email.email}") + print(f"TTL: {email.ttl} seconds") + + # List available domains + domains = client.list_domains() + print(f"Available domains: {len(domains)}") + + # Check for messages (would be empty initially) + messages = client.list_email_messages(email.email) + print(f"Messages: {len(messages)}") + + # Check rate limit + rate_limit = client.get_rate_limit() + print(f"Requests remaining: {rate_limit.remaining}/{rate_limit.limit}") + +except AuthenticationError: + print("Invalid API key") +except RateLimitError: + print("Rate limit exceeded") +except Exception as e: + print(f"Error: {e}") +``` ## License -MIT License - see [LICENSE](LICENSE) file for details. +MIT License—see [LICENSE](LICENSE) file for details. ## Links diff --git a/tempmail/client.py b/tempmail/client.py index 4f4df4c..6886fda 100644 --- a/tempmail/client.py +++ b/tempmail/client.py @@ -52,6 +52,7 @@ def _make_request( params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None, return_content: bool = False, + update_rate_limit: bool = True, ) -> typing.Union[Dict[str, Any], bytes]: """ Make an HTTP request to the API. @@ -73,7 +74,8 @@ def _make_request( ) if 200 <= response.status_code < 300: - self._update_rate_limit_from_headers(response.headers) + if update_rate_limit: + self._update_rate_limit_from_headers(response.headers) if return_content: return response.content return response.json() @@ -183,8 +185,11 @@ def get_rate_limit(self) -> RateLimit: Get current rate limit information. :return: RateLimit object """ - data = self._make_request("GET", "/v1/rate_limit") - return RateLimit.from_json(data) + data = self._make_request("GET", "/v1/rate_limit", update_rate_limit=False) + rate_limit: RateLimit = RateLimit.from_json(data) + # Also update the last known rate limit since this method doesn't use headers + self._last_rate_limit = rate_limit + return rate_limit @property def last_rate_limit(self) -> Optional[RateLimit]: diff --git a/tests/test_client.py b/tests/test_client.py index 4a3c09c..cd352f2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -14,7 +14,7 @@ ValidationError, TempMailError, ) -from tempmail.models import DomainType +from tempmail.models import DomainType, RateLimit class TestTempMailClient: @@ -403,16 +403,15 @@ def test_get_rate_limit_success(self, mock_request): client = TempMailClient("test-api-key") rate_limit_data = client.get_rate_limit() - assert rate_limit_data.limit == 100 - assert rate_limit_data.remaining == 95 - assert rate_limit_data.used == 5 - assert rate_limit_data.reset == 1640995200 + assert rate_limit_data == RateLimit( + limit=100, remaining=95, used=5, reset=1640995200 + ) # Verify the last rate limit was updated from headers assert client.last_rate_limit is not None - assert client.last_rate_limit.limit == 100 - assert client.last_rate_limit.remaining == 95 - assert client.last_rate_limit.reset == 1640995200 + assert client.last_rate_limit == RateLimit( + limit=100, remaining=95, used=5, reset=1640995200 + ) @patch("tempmail.client.requests.Session.request") def test_request_exception(self, mock_request): From 44267f9028489abf47dc4b38fc7dd8db837f2b6e Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Wed, 17 Sep 2025 00:23:45 +0400 Subject: [PATCH 21/24] Improve code a little bit --- tempmail/__init__.py | 6 ++++-- tempmail/models.py | 20 ++++++++++++++++---- tests/test_client.py | 35 ++++++++++++++++------------------- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/tempmail/__init__.py b/tempmail/__init__.py index d635f3b..59edf41 100644 --- a/tempmail/__init__.py +++ b/tempmail/__init__.py @@ -1,6 +1,8 @@ -"""Official Temp Mail API (https://temp-mail.io) Wrapper for Python.""" +""" +Official Temp Mail API (https://temp-mail.io) Wrapper for Python. +""" -__version__ = "1.0.0" +__version__ = "0.1.0" from .client import TempMailClient from .models import ( diff --git a/tempmail/models.py b/tempmail/models.py index 67231de..a94dd38 100644 --- a/tempmail/models.py +++ b/tempmail/models.py @@ -2,6 +2,7 @@ import enum from dataclasses import dataclass +from datetime import datetime from typing import Optional, List, Dict, Any @@ -12,7 +13,7 @@ class RateLimit: limit: int remaining: int reset: int - used: Optional[int] = None + used: int @classmethod def from_json(cls, data: Dict[str, Any]) -> "RateLimit": @@ -20,7 +21,7 @@ def from_json(cls, data: Dict[str, Any]) -> "RateLimit": limit=data["limit"], remaining=data["remaining"], reset=data["reset"], - used=data.get("used"), + used=data["used"], ) @@ -54,6 +55,15 @@ def from_json(cls, data: Dict[str, Any]) -> "EmailAddress": return cls(email=data["email"], ttl=data["ttl"]) +@dataclass +class Attachment: + """Attachment information for an email message.""" + + filename: str + content_type: str + size: int # Size in bytes + + @dataclass class EmailMessage: """Email message received at temporary address.""" @@ -63,9 +73,9 @@ class EmailMessage: to_addr: str subject: str body_text: str + created_at: datetime cc: Optional[List[str]] = None body_html: Optional[str] = None - created_at: Optional[str] = None attachments: Optional[List[Dict[str, Any]]] = None @classmethod @@ -76,9 +86,11 @@ def from_json(cls, data: Dict[str, Any]) -> "EmailMessage": to_addr=data["to"], subject=data["subject"], body_text=data["body_text"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), cc=data.get("cc", []), body_html=data.get("body_html"), - created_at=data.get("created_at"), attachments=data.get("attachments", []), ) diff --git a/tests/test_client.py b/tests/test_client.py index cd352f2..d6f11d8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,5 +1,6 @@ """Tests for the TempMailClient.""" +import datetime import typing import pytest @@ -14,6 +15,7 @@ ValidationError, TempMailError, ) +from requests.exceptions import ConnectionError from tempmail.models import DomainType, RateLimit @@ -26,13 +28,11 @@ class TestTempMailClient: } def test_client_initialization(self) -> None: - """Test client initialization with API key.""" client = TempMailClient("test-api-key") assert client.api_key == "test-api-key" assert client.session.headers["X-API-Key"] == "test-api-key" def test_client_initialization_with_custom_params(self) -> None: - """Test client initialization with custom parameters.""" client = TempMailClient( "test-api-key", base_url="https://custom.api.com", timeout=60 ) @@ -41,7 +41,6 @@ def test_client_initialization_with_custom_params(self) -> None: @patch("tempmail.client.requests.Session.request") def test_create_email_success(self, mock_request) -> None: - """Test successful email creation.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"email": "test@example.com", "ttl": 86400} @@ -54,7 +53,6 @@ def test_create_email_success(self, mock_request) -> None: @patch("tempmail.client.requests.Session.request") def test_create_email_with_options(self, mock_request): - """Test email creation with options.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"email": "custom@mydomain.com", "ttl": 86400} @@ -107,7 +105,6 @@ def test_list_domains_success(self, mock_request) -> None: @patch("tempmail.client.requests.Session.request") def test_list_email_messages_success(self, mock_request): - """Test successful message listing.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { @@ -129,20 +126,22 @@ def test_list_email_messages_success(self, mock_request): mock_request.return_value = mock_response client = TempMailClient("test-api-key") - messages = client.list_email_messages("test@temp.io") + messages: typing.List[EmailMessage] = client.list_email_messages("test@temp.io") assert len(messages) == 1 - message = messages[0] - assert isinstance(message, EmailMessage) - assert message.id == "msg1" - assert message.from_addr == "sender@example.com" - assert message.to_addr == "test@temp.io" - assert message.cc == ["cc@example.com"] - assert message.subject == "Test Subject" - assert message.body_text == "Test body" - assert message.body_html == "Test body
" - assert message.created_at == "2023-01-01T00:00:00Z" - assert message.attachments == [] + assert messages[0] == EmailMessage( + id="msg1", + from_addr="sender@example.com", + to_addr="test@temp.io", + cc=["cc@example.com"], + subject="Test Subject", + body_text="Test body", + body_html="Test body
", + created_at=datetime.datetime( + 2023, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), + attachments=[], + ) @patch("tempmail.client.requests.Session.request") def test_list_email_messages_empty(self, mock_request): @@ -416,8 +415,6 @@ def test_get_rate_limit_success(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_request_exception(self, mock_request): """Test handling of request exceptions.""" - from requests.exceptions import ConnectionError - from tempmail.exceptions import TempMailError mock_request.side_effect = ConnectionError("Connection failed") From 267e6eeee1f129895c6dc024d9309795fb232ab1 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Wed, 17 Sep 2025 02:02:20 +0400 Subject: [PATCH 22/24] Improve code a little bit --- tempmail/client.py | 6 ++---- tests/test_client.py | 48 ++++++++++++++------------------------------ 2 files changed, 17 insertions(+), 37 deletions(-) diff --git a/tempmail/client.py b/tempmail/client.py index 6886fda..77e093c 100644 --- a/tempmail/client.py +++ b/tempmail/client.py @@ -158,15 +158,13 @@ def get_message(self, message_id: str) -> EmailMessage: data = self._make_request("GET", f"/v1/messages/{message_id}") return EmailMessage.from_json(data) - def delete_message(self, message_id: str) -> bool: + def delete_message(self, message_id: str) -> None: """Delete a specific message by ID.""" self._make_request("DELETE", f"/v1/messages/{message_id}") - return True - def delete_email(self, email: str) -> bool: + def delete_email(self, email: str) -> None: """Delete an email address and all its messages.""" self._make_request("DELETE", f"/v1/emails/{email}") - return True def get_message_source_code(self, message_id: str) -> str: """Get the raw source code of a message.""" diff --git a/tests/test_client.py b/tests/test_client.py index d6f11d8..c0a5000 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,5 +1,3 @@ -"""Tests for the TempMailClient.""" - import datetime import typing import pytest @@ -145,7 +143,6 @@ def test_list_email_messages_success(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_list_email_messages_empty(self, mock_request): - """Test message listing with empty response.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"messages": []} @@ -156,7 +153,6 @@ def test_list_email_messages_empty(self, mock_request): messages = client.list_email_messages("test@temp.io") assert len(messages) == 0 - # Verify the request was made to correct endpoint mock_request.assert_called_once_with( method="GET", url="https://api.temp-mail.io/v1/emails/test@temp.io/messages", @@ -167,7 +163,6 @@ def test_list_email_messages_empty(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_get_message_success(self, mock_request): - """Test successful single message retrieval.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { @@ -187,13 +182,19 @@ def test_get_message_success(self, mock_request): client = TempMailClient("test-api-key") message = client.get_message("msg1") - assert isinstance(message, EmailMessage) - assert message.id == "msg1" - assert message.from_addr == "sender@example.com" - assert message.to_addr == "test@temp.io" - assert message.subject == "Test Subject" - - # Verify correct endpoint was called + assert message == EmailMessage( + id="msg1", + from_addr="sender@example.com", + to_addr="test@temp.io", + cc=[], + subject="Test Subject", + body_text="Test body", + body_html="Test body
", + created_at=datetime.datetime( + 2023, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), + attachments=[], + ) mock_request.assert_called_once_with( method="GET", url="https://api.temp-mail.io/v1/messages/msg1", @@ -204,7 +205,6 @@ def test_get_message_success(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_delete_message_success(self, mock_request): - """Test successful message deletion.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {} @@ -212,11 +212,8 @@ def test_delete_message_success(self, mock_request): mock_request.return_value = mock_response client = TempMailClient("test-api-key") - result = client.delete_message("msg123") - - assert result is True + client.delete_message("msg123") - # Verify correct endpoint was called mock_request.assert_called_once_with( method="DELETE", url="https://api.temp-mail.io/v1/messages/msg123", @@ -227,7 +224,6 @@ def test_delete_message_success(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_delete_email_success(self, mock_request): - """Test successful email deletion.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {} @@ -235,11 +231,8 @@ def test_delete_email_success(self, mock_request): mock_request.return_value = mock_response client = TempMailClient("test-api-key") - result = client.delete_email("test@temp.io") + client.delete_email("test@temp.io") - assert result is True - - # Verify correct endpoint was called mock_request.assert_called_once_with( method="DELETE", url="https://api.temp-mail.io/v1/emails/test@temp.io", @@ -250,7 +243,6 @@ def test_delete_email_success(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_get_message_source_code_success(self, mock_request): - """Test successful message source code retrieval.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { @@ -265,7 +257,6 @@ def test_get_message_source_code_success(self, mock_request): assert "Received: from example.com" in source_code assert "Subject: Test Subject" in source_code - # Verify correct endpoint was called mock_request.assert_called_once_with( method="GET", url="https://api.temp-mail.io/v1/messages/msg1/source_code", @@ -276,7 +267,6 @@ def test_get_message_source_code_success(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_download_attachment_success(self, mock_request): - """Test successful attachment download.""" mock_response = Mock() mock_response.status_code = 200 mock_response.content = b"attachment content here" @@ -288,7 +278,6 @@ def test_download_attachment_success(self, mock_request): assert content == b"attachment content here" - # Verify correct endpoint was called mock_request.assert_called_once_with( method="GET", url="https://api.temp-mail.io/v1/attachments/attachment1", @@ -299,7 +288,6 @@ def test_download_attachment_success(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_authentication_error(self, mock_request): - """Test authentication error handling.""" mock_response = Mock() mock_response.status_code = 400 mock_response.json.return_value = { @@ -319,7 +307,6 @@ def test_authentication_error(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_rate_limit_error(self, mock_request): - """Test rate limit error handling.""" mock_response = Mock() mock_response.status_code = 429 mock_response.json.return_value = { @@ -342,7 +329,6 @@ def test_rate_limit_error(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_validation_error(self, mock_request): - """Test validation error handling.""" mock_response = Mock() mock_response.status_code = 400 mock_response.json.return_value = { @@ -362,7 +348,6 @@ def test_validation_error(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_api_error(self, mock_request): - """Test generic API error handling.""" mock_response = Mock() mock_response.status_code = 500 mock_response.json.return_value = { @@ -382,7 +367,6 @@ def test_api_error(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_get_rate_limit_success(self, mock_request): - """Test successful rate limit retrieval.""" mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { @@ -414,8 +398,6 @@ def test_get_rate_limit_success(self, mock_request): @patch("tempmail.client.requests.Session.request") def test_request_exception(self, mock_request): - """Test handling of request exceptions.""" - mock_request.side_effect = ConnectionError("Connection failed") client = TempMailClient("test-api-key") From 0e53f52f709685c58672483913f86d5e8b2d5ff8 Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Thu, 18 Sep 2025 00:06:19 +0400 Subject: [PATCH 23/24] Add attachment model --- .pre-commit-config.yaml | 2 +- README.md | 8 ++++---- tempmail/__init__.py | 2 ++ tempmail/client.py | 2 +- tempmail/models.py | 26 +++++++++++++++++--------- tests/test_client.py | 20 +++++++++++++++++--- 6 files changed, 42 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28c9e76..36dbdb0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ exclude: '.git' -default_stages: [ commit ] +default_stages: [ pre-commit ] repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/README.md b/README.md index 6cfa0ca..71a29d4 100644 --- a/README.md +++ b/README.md @@ -201,19 +201,19 @@ try: email = client.create_email() print(f"Created email: {email.email}") print(f"TTL: {email.ttl} seconds") - + # List available domains domains = client.list_domains() print(f"Available domains: {len(domains)}") - + # Check for messages (would be empty initially) messages = client.list_email_messages(email.email) print(f"Messages: {len(messages)}") - + # Check rate limit rate_limit = client.get_rate_limit() print(f"Requests remaining: {rate_limit.remaining}/{rate_limit.limit}") - + except AuthenticationError: print("Invalid API key") except RateLimitError: diff --git a/tempmail/__init__.py b/tempmail/__init__.py index 59edf41..1ec5063 100644 --- a/tempmail/__init__.py +++ b/tempmail/__init__.py @@ -10,6 +10,7 @@ Domain, EmailAddress, EmailMessage, + Attachment, ) from .exceptions import ( TempMailError, @@ -23,6 +24,7 @@ "RateLimit", "Domain", "EmailAddress", + "Attachment", "EmailMessage", "TempMailError", "AuthenticationError", diff --git a/tempmail/client.py b/tempmail/client.py index 77e093c..7785a23 100644 --- a/tempmail/client.py +++ b/tempmail/client.py @@ -148,7 +148,7 @@ def list_email_messages( data = self._make_request("GET", f"/v1/emails/{email}/messages") messages = [] - for msg_data in data.get("messages", []): + for msg_data in data["messages"]: messages.append(EmailMessage.from_json(msg_data)) return messages diff --git a/tempmail/models.py b/tempmail/models.py index a94dd38..3df2e66 100644 --- a/tempmail/models.py +++ b/tempmail/models.py @@ -3,7 +3,7 @@ import enum from dataclasses import dataclass from datetime import datetime -from typing import Optional, List, Dict, Any +from typing import List, Dict, Any @dataclass @@ -59,10 +59,18 @@ def from_json(cls, data: Dict[str, Any]) -> "EmailAddress": class Attachment: """Attachment information for an email message.""" - filename: str - content_type: str + id: str + name: str size: int # Size in bytes + @classmethod + def from_json(cls, data: Dict[str, Any]) -> "Attachment": + return cls( + id=data["id"], + name=data["name"], + size=data["size"], + ) + @dataclass class EmailMessage: @@ -74,9 +82,9 @@ class EmailMessage: subject: str body_text: str created_at: datetime - cc: Optional[List[str]] = None - body_html: Optional[str] = None - attachments: Optional[List[Dict[str, Any]]] = None + cc: List[str] + body_html: str + attachments: List[Attachment] @classmethod def from_json(cls, data: Dict[str, Any]) -> "EmailMessage": @@ -89,9 +97,9 @@ def from_json(cls, data: Dict[str, Any]) -> "EmailMessage": created_at=datetime.fromisoformat( data["created_at"].replace("Z", "+00:00") ), - cc=data.get("cc", []), - body_html=data.get("body_html"), - attachments=data.get("attachments", []), + cc=data["cc"], + body_html=data["body_html"], + attachments=[Attachment.from_json(v) for v in data["attachments"]], ) diff --git a/tests/test_client.py b/tests/test_client.py index c0a5000..35c2f01 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -14,7 +14,7 @@ TempMailError, ) from requests.exceptions import ConnectionError -from tempmail.models import DomainType, RateLimit +from tempmail.models import DomainType, RateLimit, Attachment class TestTempMailClient: @@ -116,7 +116,18 @@ def test_list_email_messages_success(self, mock_request): "body_text": "Test body", "body_html": "Test body
", "created_at": "2023-01-01T00:00:00Z", - "attachments": [], + "attachments": [ + { + "id": "att1", + "name": "file.txt", + "size": 1234, + }, + { + "id": "att2", + "name": "image.png", + "size": 4567, + }, + ], } ] } @@ -138,7 +149,10 @@ def test_list_email_messages_success(self, mock_request): created_at=datetime.datetime( 2023, 1, 1, 0, 0, tzinfo=datetime.timezone.utc ), - attachments=[], + attachments=[ + Attachment(id="att1", name="file.txt", size=1234), + Attachment(id="att2", name="image.png", size=4567), + ], ) @patch("tempmail.client.requests.Session.request") From 9a9468322a110b35f6a3856b2b8c2538e872caca Mon Sep 17 00:00:00 2001 From: lcd1232 <8745863+lcd1232@users.noreply.github.com> Date: Thu, 18 Sep 2025 00:09:52 +0400 Subject: [PATCH 24/24] Add badges --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 71a29d4..a87db72 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # Temp Mail Python Library +[](https://badge.fury.io/py/temp-mail-io) +[](https://pypi.org/project/temp-mail-io/) +[](https://github.com/temp-mail-io/temp-mail-python/actions/workflows/test.yml) +[](https://codecov.io/gh/temp-mail-io/temp-mail-python) +[](https://opensource.org/licenses/MIT) +[](https://pepy.tech/project/temp-mail-io) + Official Python library for the [Temp Mail API](https://temp-mail.io). Create temporary email addresses and receive emails programmatically. ## Installation