From 6908ae29f3890f91f20f803c511b39681e773dff Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 Date: Wed, 1 Oct 2025 07:08:53 +0200 Subject: [PATCH 01/49] Update ci --- .github/workflows/pre-commit.yml | 6 +++--- .github/workflows/publish-pypi.yml | 6 +++--- .github/workflows/run_tests.yml | 6 +++--- .pre-commit-config.yaml | 8 ++++++++ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 0f97d87..3ad3e01 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -8,8 +8,8 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 with: python-version: '3.10' - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 15a12ec..768e0ef 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -9,11 +9,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: ref: master - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' @@ -27,7 +27,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_api_key }} diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 4ee2935..7fe004f 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -8,12 +8,12 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c5c4721..1e61e55 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,6 +33,14 @@ repos: - id: rst-backticks + - repo: https://github.com/JelleZijlstra/autotyping + rev: 24.9.0 + hooks: + - id: autotyping + types: [python] + args: [--safe] + + - repo: meta hooks: - id: check-hooks-apply From a022d7216ff3d1a7bf8e5715340e792ea77a41e8 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 07:15:04 +0200 Subject: [PATCH 02/49] Update ci --- requirements.txt | 6 +++--- tox.ini | 20 ++++++-------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/requirements.txt b/requirements.txt index 84214e6..dcaf198 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -pytest >=7.2, < 7.3 -pytest-asyncio >=0.20.3, < 0.21 +pytest == 8.4.2 +pytest-asyncio == 1.2.0 # linter -pre-commit >= 3.0, < 3.1 +pre-commit == 4.3 diff --git a/tox.ini b/tox.ini index c2c40d4..f04c13a 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ envlist = py39 py310 py311 + py312 + py313 flake docs @@ -12,28 +14,18 @@ envlist = python = 3.8: py38 3.9: py39 - 3.10: py310, flake, docs + 3.10: py310 3.11: py311 + 3.12: py312, docs + 3.13: py313 [testenv] deps = - pytest - pytest-asyncio - asynctest -r{toxinidir}/requirements.txt commands = python -m pytest -[testenv:flake] -deps = - {[testenv]deps} - flake8 - # pydocstyle -commands = - flake8 -v - # pydocstyle - [testenv:docs] description = invoke sphinx-build to build the HTML docs @@ -46,7 +38,7 @@ commands = mkdir -p docs{/}_static sphinx-build -d "{envtmpdir}{/}doctree" docs "{toxworkdir}{/}docs_out" --color -b html -E -W -n --keep-going -allowlist_externals=mkdir +allowlist_externals = mkdir [pytest] From c0111a8dc5f4d0930d762459aa822ba687fdec93 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 07:24:44 +0200 Subject: [PATCH 03/49] Update ci --- docs/conf.py | 2 +- docs/requirements.txt | 10 +++++----- requirements.txt | 10 ++++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3d8b5c4..8206265 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'PyArtNet' -copyright = '2023, spacemanspiff2007' +copyright = '2025, spacemanspiff2007' author = 'spacemanspiff2007' diff --git a/docs/requirements.txt b/docs/requirements.txt index bd1e2df..997854d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,8 @@ # Packages required to build the documentation -sphinx >= 5.3, < 6 -sphinx-autodoc-typehints >= 1.22, < 2 -sphinx_rtd_theme == 1.1.1 -sphinx-exec-code == 0.8 +sphinx == 8.1.3 +sphinx-autodoc-typehints == 3.0.1 +sphinx_rtd_theme == 3.0.2 +sphinx-exec-code == 0.16 # monkeypatch -pytest >=7.2, < 7.3 +pytest == 8.4.2 diff --git a/requirements.txt b/requirements.txt index dcaf198..723666d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,11 @@ -pytest == 8.4.2 -pytest-asyncio == 1.2.0 +# Dependencies for old python versions +pre-commit == 3.5.0; python_version < '3.9' +pytest == 7.4.4; python_version < '3.10' +pytest-asyncio == 0.24; python_version < '3.9' + +pre-commit == 4.3.0; python_version >= '3.9' +pytest == 8.5.2; python_version >= '3.10' +pytest-asyncio == 1.2.0; python_version <= '3.9' # linter pre-commit == 4.3 From c78ea4c06d4826f668b7dc992e5ca9ae24a51b57 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 07:33:09 +0200 Subject: [PATCH 04/49] Update ci --- requirements.txt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 723666d..ed35c95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,10 @@ # Dependencies for old python versions -pre-commit == 3.5.0; python_version < '3.9' -pytest == 7.4.4; python_version < '3.10' -pytest-asyncio == 0.24; python_version < '3.9' +pytest == 7.4.4; python_version == '3.8' +pytest == 8.4.2; python_version >= '3.9' and python_version < '3.12' +pytest == 8.4.2; python_version >= '3.12' -pre-commit == 4.3.0; python_version >= '3.9' -pytest == 8.5.2; python_version >= '3.10' -pytest-asyncio == 1.2.0; python_version <= '3.9' +pytest-asyncio == 0.24; python_version < '3.9' +pytest-asyncio == 1.2.0; python_version >= '3.9' -# linter -pre-commit == 4.3 +pre-commit == 3.5.0; python_version < '3.9' +pre-commit == 4.3.0; python_version >= '3.9' From 2f4e012c1b2e796e678840aad42f5f762d34897d Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:12:38 +0200 Subject: [PATCH 05/49] try uv --- .github/workflows/run_tests.yml | 13 +- pyproject.toml | 57 ++ tox.ini | 11 +- uv.lock | 1407 +++++++++++++++++++++++++++++++ 4 files changed, 1473 insertions(+), 15 deletions(-) create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 7fe004f..57f50db 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -12,13 +12,10 @@ jobs: steps: - uses: actions/checkout@v5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install the latest version of uv and set the python version + uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox + activate-environment: true + - name: Test with python ${{ matrix.python-version }} + run: uv run --frozen pytest diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4cc45fa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ +[project] +name = "pyartnet" +version = "1.0.1" + +description = "Python wrappers for the Art-Net protocol to send DMX over Ethernet" +keywords = ["DMX", "Art-Net", "ArtNet", "sACN", "E1.31", "KiNet"] +readme = "readme.md" +dependencies = [] +requires-python = ">= 3.8" + +authors = [ + {name = "spaceman_spiff"}, +] +license = "GPL-3.0-or-later" + + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Operating System :: OS Independent", +] + + +[project.urls] +Documentation = 'https://pyartnet.readthedocs.io' +Repository = "https://github.com/spacemanspiff2007/PyArtNet'" +Issues = "https://github.com/spacemanspiff2007/PyArtNet/issues" + +[dependency-groups] +docs = [ + "sphinx", + "sphinx-autodoc-typehints", + "sphinx_rtd_theme", + "sphinx-exec-code", + "pytest" +] +tests = [ + "pytest", + "pytest-asyncio", +] + +dev = [ + {include-group = "tests"}, + "pre-commit" +] + + +[build-system] +requires = ["uv_build>=0.8.22,<0.9.0"] +build-backend = "uv_build" diff --git a/tox.ini b/tox.ini index f04c13a..1de3a5d 100644 --- a/tox.ini +++ b/tox.ini @@ -20,19 +20,16 @@ python = 3.13: py313 [testenv] -deps = - -r{toxinidir}/requirements.txt - +runner = uv-venv-lock-runner +extras = tests commands = python -m pytest [testenv:docs] +runner = uv-venv-lock-runner description = invoke sphinx-build to build the HTML docs - -deps = - {[testenv]deps} - -r{toxinidir}/docs/requirements.txt +extras = docs commands = mkdir -p docs{/}_static diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..48a4ea1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1407 @@ +version = 1 +revision = 3 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] + +[[package]] +name = "alabaster" +version = "0.7.13" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2", size = 11454, upload-time = "2023-01-13T06:42:53.797Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", size = 13857, upload-time = "2023-01-13T06:42:52.336Z" }, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[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 = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[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 = "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 = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "docutils" +version = "0.20.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/53/a5da4f2c5739cf66290fac1431ee52aff6851c7c8ffd8264f13affd7bcdd/docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b", size = 2058365, upload-time = "2023-05-16T23:39:19.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", size = 572666, upload-time = "2023-05-16T23:39:15.976Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[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 = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037, upload-time = "2024-09-17T19:02:01.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163, upload-time = "2024-09-17T19:02:00.268Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097, upload-time = "2024-09-14T23:50:32.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972, upload-time = "2024-09-14T23:50:30.747Z" }, +] + +[[package]] +name = "identify" +version = "2.6.14" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, +] + +[[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 = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[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 = "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.11'", + "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.11'", + "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 = "pre-commit" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "cfgv", marker = "python_full_version < '3.9'" }, + { name = "identify", version = "2.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "nodeenv", marker = "python_full_version < '3.9'" }, + { name = "pyyaml", marker = "python_full_version < '3.9'" }, + { name = "virtualenv", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b3/4ae08d21eb097162f5aad37f4585f8069a86402ed7f5362cc9ae097f9572/pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", size = 177079, upload-time = "2023-10-13T15:57:48.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/75/526915fedf462e05eeb1c75ceaf7e3f9cde7b5ce6f62740fe5f7f19a0050/pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660", size = 203698, upload-time = "2023-10-13T15:57:46.378Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "cfgv", marker = "python_full_version >= '3.9'" }, + { name = "identify", version = "2.6.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "nodeenv", marker = "python_full_version >= '3.9'" }, + { name = "pyyaml", marker = "python_full_version >= '3.9'" }, + { name = "virtualenv", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pyartnet" +version = "1.0.1" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pre-commit", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pre-commit", version = "4.3.0", 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-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +docs = [ + { 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 = "sphinx", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-typehints", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx-autodoc-typehints", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx-autodoc-typehints", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-exec-code" }, + { name = "sphinx-rtd-theme" }, +] +tests = [ + { 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-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] +docs = [ + { name = "pytest" }, + { name = "sphinx" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-exec-code" }, + { name = "sphinx-rtd-theme" }, +] +tests = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[[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.11'", + "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-asyncio" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { 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/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855, upload-time = "2024-08-22T08:03:18.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024, upload-time = "2024-08-22T08:03:15.536Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "pytest", version = "8.4.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/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/a2/09f67a3589cb4320fb5ce90d3fd4c9752636b8b6ad8f34b54d76c5a54693/PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", size = 186824, upload-time = "2025-09-29T20:27:35.918Z" }, + { url = "https://files.pythonhosted.org/packages/02/72/d972384252432d57f248767556ac083793292a4adf4e2d85dfe785ec2659/PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", size = 795069, upload-time = "2025-09-29T20:27:38.15Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3b/6c58ac0fa7c4e1b35e48024eb03d00817438310447f93ef4431673c24138/PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", size = 862585, upload-time = "2025-09-29T20:27:39.715Z" }, + { url = "https://files.pythonhosted.org/packages/25/a2/b725b61ac76a75583ae7104b3209f75ea44b13cfd026aa535ece22b7f22e/PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", size = 806018, upload-time = "2025-09-29T20:27:41.444Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/b2227677b2d1036d84f5ee95eb948e7af53d59fe3e4328784e4d290607e0/PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", size = 802822, upload-time = "2025-09-29T20:27:42.885Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/718a8ea22521e06ef19f91945766a892c5ceb1855df6adbde67d997ea7ed/PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", size = 143744, upload-time = "2025-09-29T20:27:44.487Z" }, + { url = "https://files.pythonhosted.org/packages/76/b2/2b69cee94c9eb215216fc05778675c393e3aa541131dc910df8e52c83776/PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", size = 160082, upload-time = "2025-09-29T20:27:46.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[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.11'", + "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 = "roman-numerals-py" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "alabaster", version = "0.7.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "babel", marker = "python_full_version < '3.9'" }, + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.20.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "imagesize", marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "jinja2", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pygments", marker = "python_full_version < '3.9'" }, + { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-applehelp", version = "1.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-devhelp", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-qthelp", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-serializinghtml", version = "1.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/01/688bdf9282241dca09fe6e3a1110eda399fa9b10d0672db609e37c2e7a39/sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f", size = 6828258, upload-time = "2023-08-02T02:06:09.375Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/17/325cf6a257d84751a48ae90752b3d8fe0be8f9535b6253add61c49d0d9bc/sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe", size = 3169543, upload-time = "2023-08-02T02:06:06.816Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "babel", marker = "python_full_version == '3.9.*'" }, + { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "imagesize", marker = "python_full_version == '3.9.*'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version == '3.9.*'" }, + { name = "packaging", marker = "python_full_version == '3.9.*'" }, + { name = "pygments", marker = "python_full_version == '3.9.*'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-applehelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-devhelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-qthelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-serializinghtml", version = "2.0.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/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "babel", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-applehelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-devhelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-qthelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-serializinghtml", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "babel", marker = "python_full_version >= '3.11'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "imagesize", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-applehelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-devhelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-qthelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-serializinghtml", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "sphinx", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/f0/b750f1ea593df9ba152e99929807530604d06fae887e5a38ae1e0a31358a/sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12", size = 38816, upload-time = "2024-04-10T17:53:06.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/5baffb0ef1b8fd72d0a5a3ab531e82c5e810df3530c8f61857c69026b7ac/sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149", size = 19533, upload-time = "2024-04-10T17:53:04.797Z" }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/03e7b917230dc057922130a79ba0240df1693bfd76727ea33fae84b39138/sphinx_autodoc_typehints-2.3.0.tar.gz", hash = "sha256:535c78ed2d6a1bad393ba9f3dfa2602cf424e2631ee207263e07874c38fde084", size = 40709, upload-time = "2024-08-29T16:25:48.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f3/e0a4ce49da4b6f4e4ce84b3c39a0677831884cb9d8a87ccbf1e9e56e53ac/sphinx_autodoc_typehints-2.3.0-py3-none-any.whl", hash = "sha256:3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67", size = 19836, upload-time = "2024-08-29T16:25:46.707Z" }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282, upload-time = "2025-01-16T18:25:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dc/dc46c5c7c566b7ec5e8f860f9c89533bf03c0e6aadc96fb9b337867e4460/sphinx_autodoc_typehints-3.0.1-py3-none-any.whl", hash = "sha256:4b64b676a14b5b79cefb6628a6dc8070e320d4963e8ff640a2f3e9390ae9045a", size = 20245, upload-time = "2025-01-16T18:25:27.394Z" }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724, upload-time = "2025-04-25T16:53:25.872Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563, upload-time = "2025-04-25T16:53:24.492Z" }, +] + +[[package]] +name = "sphinx-exec-code" +version = "0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/0d/a94b374aef31918bf88af058c10ebf556a199818e30a8fa0df6f952eabc7/sphinx_exec_code-0.16.tar.gz", hash = "sha256:51458982dc7a152802dfac9786af656c7fe35206203452d9f6322cac45534b32", size = 16274, upload-time = "2025-04-01T07:49:32.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/5c/a9d7d2407124512be434044e4504b2e62a7a1a0e5863ca384b37241c0ddf/sphinx_exec_code-0.16-py3-none-any.whl", hash = "sha256:b5cd4f8d2d4b929d522e14ccf30ac569f8864a9c241e91cb93fc8324abdd9c52", size = 16936, upload-time = "2025-04-01T07:49:31.312Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.20.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinx", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463, upload-time = "2024-11-13T11:06:04.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561, upload-time = "2024-11-13T11:06:02.094Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/32/df/45e827f4d7e7fcc84e853bcef1d836effd762d63ccb86f43ede4e98b478c/sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e", size = 24766, upload-time = "2023-01-23T09:41:54.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/c1/5e2cafbd03105ce50d8500f9b4e8a6e8d02e22d0475b574c3b3e9451a15f/sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", size = 120601, upload-time = "2023-01-23T09:41:52.364Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/33/dc28393f16385f722c893cb55539c641c9aaec8d1bc1c15b69ce0ac2dbb3/sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4", size = 17398, upload-time = "2020-02-29T04:14:43.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/09/5de5ed43a521387f18bdf5f5af31d099605c992fd25372b2b9b825ce48ee/sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", size = 84690, upload-time = "2020-02-29T04:14:40.765Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/47/64cff68ea3aa450c373301e5bebfbb9fce0a3e70aca245fcadd4af06cd75/sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", size = 27967, upload-time = "2023-01-31T17:29:20.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ee/a1f5e39046cbb5f8bc8fba87d1ddf1c6643fbc9194e58d26e606de4b9074/sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903", size = 99833, upload-time = "2023-01-31T17:29:18.489Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/8e/c4846e59f38a5f2b4a0e3b27af38f2fcf904d4bfd82095bf92de0b114ebd/sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", size = 21658, upload-time = "2020-02-29T04:19:10.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/14/05f9206cf4e9cfca1afb5fd224c7cd434dcc3a433d6d9e4e0264d29c6cdb/sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6", size = 90609, upload-time = "2020-02-29T04:19:08.451Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/72/835d6fadb9e5d02304cf39b18f93d227cd93abd3c41ebf58e6853eeb1455/sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952", size = 21019, upload-time = "2021-05-22T16:07:43.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/77/5464ec50dd0f1c1037e3c93249b040c8fc8078fdda97530eeb02424b6eea/sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", size = 94021, upload-time = "2021-05-22T16:07:41.627Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[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 = "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.11'", + "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.11'", + "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" }, +] + +[[package]] +name = "virtualenv" +version = "20.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock", version = "3.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, 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 = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, 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'" }, + { 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/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From e212acefc4a5f6f79cc2dcbea963534a22d08fd3 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:27:44 +0200 Subject: [PATCH 06/49] try uv --- .github/workflows/run_tests.yml | 2 ++ pyproject.toml | 6 ++++++ tox.ini | 5 ----- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 57f50db..bd67aad 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -17,5 +17,7 @@ jobs: with: python-version: ${{ matrix.python-version }} activate-environment: true + - name: Install Packages + run: uv sync --frozen --verbose - name: Test with python ${{ matrix.python-version }} run: uv run --frozen pytest diff --git a/pyproject.toml b/pyproject.toml index 4cc45fa..bc78933 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,3 +55,9 @@ dev = [ [build-system] requires = ["uv_build>=0.8.22,<0.9.0"] build-backend = "uv_build" + +[tool.pytest.ini_options] +asyncio_mode = "auto" +addopts = "-p no:cacheprovider" +testpaths = ["tests"] +pythonpath = ["."] diff --git a/tox.ini b/tox.ini index 1de3a5d..d3e5483 100644 --- a/tox.ini +++ b/tox.ini @@ -36,8 +36,3 @@ commands = sphinx-build -d "{envtmpdir}{/}doctree" docs "{toxworkdir}{/}docs_out" --color -b html -E -W -n --keep-going allowlist_externals = mkdir - - -[pytest] -asyncio_mode = auto -addopts = -p no:cacheprovider From 066df08ecf886f208560e0f17e08768939f405c8 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:37:47 +0200 Subject: [PATCH 07/49] try uv --- .github/workflows/run_tests.yml | 17 ++++++++++++++++- pyproject.toml | 1 + uv.lock | 16 +++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index bd67aad..0e0484e 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -18,6 +18,21 @@ jobs: python-version: ${{ matrix.python-version }} activate-environment: true - name: Install Packages - run: uv sync --frozen --verbose + run: uv sync --frozen - name: Test with python ${{ matrix.python-version }} run: uv run --frozen pytest + + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Install the latest version of uv and set the python version + uses: astral-sh/setup-uv@v6 + with: + python-version: '3.12' + activate-environment: true + - name: Install Packages + run: uv sync --frozen + - name: Test with python ${{ matrix.python-version }} + run: uv run --frozen sphinx-build docs build_docs --color -b html -E -W -n --keep-going diff --git a/pyproject.toml b/pyproject.toml index bc78933..923e43a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ tests = [ dev = [ {include-group = "tests"}, + {include-group = "docs"}, "pre-commit" ] diff --git a/uv.lock b/uv.lock index 48a4ea1..a151184 100644 --- a/uv.lock +++ b/uv.lock @@ -220,7 +220,7 @@ 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'" }, + { 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/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -634,6 +634,16 @@ dev = [ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinx", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-typehints", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx-autodoc-typehints", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx-autodoc-typehints", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-exec-code" }, + { name = "sphinx-rtd-theme" }, ] docs = [ { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -663,6 +673,10 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "sphinx" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-exec-code" }, + { name = "sphinx-rtd-theme" }, ] docs = [ { name = "pytest" }, From d182186298e6aa7965b6db56e2cdb5dc37e6b313 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:53:15 +0200 Subject: [PATCH 08/49] try uv --- docs/conf.py | 1 - docs/requirements.txt | 8 -------- pyproject.toml | 5 +++-- 3 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 docs/requirements.txt diff --git a/docs/conf.py b/docs/conf.py index 8206265..0d7dd34 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,7 +36,6 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] # -- Options for exec code ------------------------------------------------- diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 997854d..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Packages required to build the documentation -sphinx == 8.1.3 -sphinx-autodoc-typehints == 3.0.1 -sphinx_rtd_theme == 3.0.2 -sphinx-exec-code == 0.16 - -# monkeypatch -pytest == 8.4.2 diff --git a/pyproject.toml b/pyproject.toml index 923e43a..d113192 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,9 @@ docs = [ "sphinx-autodoc-typehints", "sphinx_rtd_theme", "sphinx-exec-code", - "pytest" + "pytest" # needed for monkeypatch ] + tests = [ "pytest", "pytest-asyncio", @@ -54,7 +55,7 @@ dev = [ [build-system] -requires = ["uv_build>=0.8.22,<0.9.0"] +requires = ["uv_build"] build-backend = "uv_build" [tool.pytest.ini_options] From 6249f32099a191dbe204f27638ae1e347b7b909f Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:44:10 +0200 Subject: [PATCH 09/49] try uv --- .github/workflows/run_tests.yml | 2 +- pyproject.toml | 2 ++ requirements.txt | 10 --------- tox.ini | 38 --------------------------------- 4 files changed, 3 insertions(+), 49 deletions(-) delete mode 100644 requirements.txt delete mode 100644 tox.ini diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 0e0484e..ce36e46 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -34,5 +34,5 @@ jobs: activate-environment: true - name: Install Packages run: uv sync --frozen - - name: Test with python ${{ matrix.python-version }} + - name: Build docs run: uv run --frozen sphinx-build docs build_docs --color -b html -E -W -n --keep-going diff --git a/pyproject.toml b/pyproject.toml index d113192..7134e58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ Documentation = 'https://pyartnet.readthedocs.io' Repository = "https://github.com/spacemanspiff2007/PyArtNet'" Issues = "https://github.com/spacemanspiff2007/PyArtNet/issues" + [dependency-groups] docs = [ "sphinx", @@ -58,6 +59,7 @@ dev = [ requires = ["uv_build"] build-backend = "uv_build" + [tool.pytest.ini_options] asyncio_mode = "auto" addopts = "-p no:cacheprovider" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ed35c95..0000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Dependencies for old python versions -pytest == 7.4.4; python_version == '3.8' -pytest == 8.4.2; python_version >= '3.9' and python_version < '3.12' -pytest == 8.4.2; python_version >= '3.12' - -pytest-asyncio == 0.24; python_version < '3.9' -pytest-asyncio == 1.2.0; python_version >= '3.9' - -pre-commit == 3.5.0; python_version < '3.9' -pre-commit == 4.3.0; python_version >= '3.9' diff --git a/tox.ini b/tox.ini deleted file mode 100644 index d3e5483..0000000 --- a/tox.ini +++ /dev/null @@ -1,38 +0,0 @@ -# content of: tox.ini , put in same dir as setup.py -[tox] -envlist = - py38 - py39 - py310 - py311 - py312 - py313 - flake - docs - -[gh-actions] -python = - 3.8: py38 - 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312, docs - 3.13: py313 - -[testenv] -runner = uv-venv-lock-runner -extras = tests -commands = - python -m pytest - - -[testenv:docs] -runner = uv-venv-lock-runner -description = invoke sphinx-build to build the HTML docs -extras = docs - -commands = - mkdir -p docs{/}_static - sphinx-build -d "{envtmpdir}{/}doctree" docs "{toxworkdir}{/}docs_out" --color -b html -E -W -n --keep-going - -allowlist_externals = mkdir From 2ccee730e52e5d2ebe7b9690fa30c2b51aae1a48 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:05:11 +0200 Subject: [PATCH 10/49] pre-commit --- .github/workflows/pre-commit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 3ad3e01..bf2a54d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -11,5 +11,5 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - uses: pre-commit/action@v3.0.1 From ffd98067788201ee2f5825a17b114f9e1997f861 Mon Sep 17 00:00:00 2001 From: Sebastian Andersson Date: Thu, 2 Oct 2025 06:41:05 +0200 Subject: [PATCH 11/49] Sync universes (#50) * Add universe sync support to sACN * Add universe sync support to Art-Net --- src/pyartnet/base/base_node.py | 10 ++++++ src/pyartnet/impl_artnet/node.py | 41 ++++++++++++++++++----- src/pyartnet/impl_sacn/node.py | 57 +++++++++++++++++++++++++++----- tests/test_impl/test_sacn.py | 25 +++++++++++++- 4 files changed, 116 insertions(+), 17 deletions(-) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 15fd2eb..7abdcd5 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -67,6 +67,12 @@ def _apply_output_correction(self): def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: TYPE_U): raise NotImplementedError() + def set_synchronous_mode(self, enabled: bool): + raise NotImplementedError() + + def _send_synchronization(self): + pass + def _send_data(self, data: Union[bytearray, bytes]) -> int: ret = self._socket.sendto(self._packet_base + data, self._dst) @@ -103,6 +109,8 @@ async def _process_values_task(self): self._process_jobs.remove(job) job.fade_complete() + self._send_synchronization() + await sleep(self._process_every) def start_refresh(self): @@ -128,6 +136,8 @@ async def _periodic_refresh_worker(self): for u in self._universes: u.send_data() + self._send_synchronization() + def get_universe(self, nr: int) -> TYPE_U: """Get universe by number diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 889891d..9866b13 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -35,23 +35,26 @@ def __init__(self, ip: str, port: int, *, packet = bytearray() packet.extend(map(ord, "Art-Net")) packet.append(0x00) # Null terminate Art-Net - packet.extend([0x00, 0x50]) # Opcode ArtDMX 0x5000 (Little endian) - packet.extend([0x00, 0x0e]) # Protocol version 14 self._packet_base = bytes(packet) + self._sync_enabled : bool = False + def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: 'pyartnet.impl_artnet.ArtNetUniverse'): # pre allocate the bytearray - _size = 6 + byte_size + _size = 10 + byte_size packet = bytearray(_size) - packet[0] = self._sequence_ctr.value # 1 | Sequence, - packet[1] = 0x00 # 1 | Physical input port (not used) - packet[2:4] = id.to_bytes(2, byteorder='little') # 2 | Universe + packet[0:2] =[0x00, 0x50] # Opcode ArtDMX 0x5000 (Little endian) + packet[2:4] = [0x00, 0x0e] # Protocol version 14 + + packet[4] = self._sequence_ctr.value # 1 | Sequence, + packet[5] = 0x00 # 1 | Physical input port (not used) + packet[6:8] = id.to_bytes(2, byteorder='little') # 2 | Universe - packet[4:6] = byte_size.to_bytes(2, 'big') # 2 | Number of channels Big Endian - packet[6: _size] = values # 0 - 512 | Channel values + packet[8:10] = byte_size.to_bytes(2, 'big') # 2 | Number of channels Big Endian + packet[10: _size] = values # 0 - 512 | Channel values self._send_data(packet) @@ -129,3 +132,25 @@ def __log_artnet_frame(self, p: Union[bytearray, bytes]): if show_description: log.debug(out_desc) log.debug(out) + + def set_synchronous_mode(self, enabled: bool): + self._sync_enabled = enabled + + def _send_synchronization(self): + if not self._sync_enabled: + return + + # pre allocate the bytearray + packet = bytearray(6) + + packet[0:2] = [0x00, 0x52] # Opcode ArtSync + packet[2:4] = [0x00, 0x0e] # Protocol version 14 + + packet[4] = 0 # Aux1 + packet[5] = 0 # Aux2 + + self._send_data(packet) + + # log complete packet + if log.isEnabledFor(logging.DEBUG): + self.__log_artnet_frame(self._packet_base + packet) diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index cec29c6..53bcba0 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -21,7 +21,9 @@ # Field constants VECTOR_ROOT_E131_DATA: Final = b'\x00\x00\x00\x04' +VECTOR_ROOT_E131_EXTENDED: Final = b'\x00\x00\x00\x08' VECTOR_E131_DATA_PACKET: Final = b'\x00\x00\x00\x02' +VECTOR_E131_EXTENDED_SYNCHRONIZATION: Final = b'\x00\x00\x00\x01' VECTOR_DMP_SET_PROPERTY: Final = 0x02 @@ -52,6 +54,7 @@ def __init__(self, ip: str, port: int, *, source_name_byte = source_name.encode('utf-8').ljust(64, b'\x00') if len(source_name_byte) > 64: raise ValueError('Source name too long!') + self._source_name_byte : bytes = source_name_byte # build base packet packet = bytearray() @@ -64,15 +67,11 @@ def __init__(self, ip: str, port: int, *, packet.extend(VECTOR_ROOT_E131_DATA) # | 4 | Vector packet.extend(cid) # | 16 | CID, a unique identifier - # Framing layer Part 1 - packet.extend([0x72, 0x57]) # | 2 | Flags and length - packet.extend(VECTOR_E131_DATA_PACKET) # | 4 | Vector - packet.extend(source_name_byte) # | 64 |Source Name - packet.append(100) # | 1 |Priority - packet.extend(int(50).to_bytes(2, 'big')) # | 2 | Synchronization universe - self._packet_base: bytearray = packet + self._synchronization_address : int = 0 + self._sync_sequence_number : int = 0 + def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: 'pyartnet.impl_sacn.universe.SacnUniverse'): @@ -81,6 +80,13 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, # DMX Start Code is not included in the byte size from the universe prop_count = byte_size + 1 + # Framing layer Part 1 + packet.extend((( 87 + prop_count) | 0x7000).to_bytes(2, 'big')) # Flags and Length + packet.extend(VECTOR_E131_DATA_PACKET) # | 4 | Vector + packet.extend(self._source_name_byte) # | 64 |Source Name + packet.append(100) # | 1 |Priority + packet.extend(int(self._synchronization_address).to_bytes(2, 'big')) # | 2 | Synchronization universe + # Framing layer Part 2 packet.append(universe._sequence_ctr.value) # | 1 | Sequence, packet.append(0x00) # | 1 | Options @@ -101,7 +107,6 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, # Update length for base packet base_packet = self._packet_base base_packet[16:18] = ((109 + prop_count) | 0x7000).to_bytes(2, 'big') # root layer - base_packet[38:40] = (( 87 + prop_count) | 0x7000).to_bytes(2, 'big') # framing layer self._send_data(packet) @@ -114,3 +119,39 @@ def _create_universe(self, nr: int) -> 'pyartnet.impl_sacn.SacnUniverse': if not 1 <= nr < 63_999: raise InvalidUniverseAddressError() return pyartnet.impl_sacn.SacnUniverse(self, nr) + + def set_synchronous_mode(self, enabled: bool, synchronization_address : int = 0): + if enabled: + assert(synchronization_address != 0) + self._synchronization_address = synchronization_address + else: + assert(synchronization_address == 0) + self._synchronization_address = 0 + + def _send_synchronization(self): + if (self._synchronization_address == 0): + return + + packet = bytearray() + + try: + base_packet = self._packet_base + base_packet[16:18] = ((33) | 0x7000).to_bytes(2, 'big') # root layer + base_packet[18:22] = VECTOR_ROOT_E131_EXTENDED + # Framing layer + packet.extend(((11) | 0x7000).to_bytes(2, 'big')) # | 2 | Flags and Length + packet.extend(VECTOR_E131_EXTENDED_SYNCHRONIZATION) # | 4 | Vector + self._sync_sequence_number += 1 + if (self._sync_sequence_number >= 255): + self._sync_sequence_number = 0 + packet.append(self._sync_sequence_number) # | 1 | Sequence Number + packet.extend(self._synchronization_address.to_bytes(2, 'big')) # | 2 | Synchronization universe + packet.extend([0, 0]) # | 2 | Reserved + + self._send_data(packet) + + if log.isEnabledFor(LVL_DEBUG): + # log complete packet + log.debug(f"Sending sACN Syncronization Packet to {self._ip}:{self._port}: {(base_packet + packet).hex()}") + finally: + self._packet_base[18:22] = VECTOR_ROOT_E131_DATA diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index 3aab3cc..391b3a7 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -16,7 +16,30 @@ async def test_sacn(patched_socket): data = '001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' \ '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' \ - '0000000064003200000001701502a100000001000b000102030405060708090a' + '0000000064000000000001701502a100000001000b000102030405060708090a' + + m = sacn._socket + m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) + + + await channel + +async def test_sacn_with_sync(patched_socket): + sacn = SacnNode( + 'ip', 9999999, + cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', + source_name="default source name") + universe = sacn.add_universe(1) + channel = universe.add_channel(1, 10) + channel.set_values(range(1, 11)) + + sacn.set_synchronous_mode(True, 1) + + universe.send_data() + + data = '001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' \ + '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' \ + '0000000064000100000001701502a100000001000b000102030405060708090a' m = sacn._socket m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) From 2524763b655d86c54f206d1eba6c27f7ccc139c8 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:00:59 +0200 Subject: [PATCH 12/49] small changes --- .github/workflows/pre-commit.yml | 4 +- .github/workflows/run_tests.yml | 2 +- src/pyartnet/base/base_node.py | 33 ++++++------ src/pyartnet/impl_artnet/node.py | 42 ++++++++++------ src/pyartnet/impl_kinet/node.py | 14 ++++-- src/pyartnet/impl_sacn/node.py | 86 +++++++++++++++++--------------- tests/conftest.py | 14 ++++-- tests/test_base_node.py | 14 ++---- tests/test_impl/test_impl.py | 16 +++++- 9 files changed, 128 insertions(+), 97 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index bf2a54d..e4f7f07 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,8 +1,6 @@ name: Pre-Commit -on: - pull_request: - branches: [main, master] +on: [push, pull_request] jobs: main: diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index ce36e46..b1b9ac9 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -1,6 +1,6 @@ name: Tests -on: [push] +on: [push, pull_request] jobs: test: diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 7abdcd5..6673c8d 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -21,7 +21,7 @@ class BaseNode(Generic[TYPE_U], OutputCorrection): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, - source_address: Optional[Tuple[str, int]] = None): + source_address: Optional[Tuple[str, int]] = None) -> None: super().__init__() # Destination @@ -60,7 +60,7 @@ def __init__(self, ip: str, port: int, *, self._universes: Tuple[TYPE_U, ...] = () self._universe_map: Dict[int, TYPE_U] = {} - def _apply_output_correction(self): + def _apply_output_correction(self) -> None: for u in self._universes: u._apply_output_correction() @@ -70,7 +70,7 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: T def set_synchronous_mode(self, enabled: bool): raise NotImplementedError() - def _send_synchronization(self): + def _send_synchronization(self) -> None: pass def _send_data(self, data: Union[bytearray, bytes]) -> int: @@ -80,7 +80,7 @@ def _send_data(self, data: Union[bytearray, bytes]) -> int: self._last_send = monotonic() return ret - async def _process_values_task(self): + async def _process_values_task(self) -> None: # wait a little, so we can schedule multiple tasks/updates, and they all start together await sleep(0.01) @@ -113,15 +113,15 @@ async def _process_values_task(self): await sleep(self._process_every) - def start_refresh(self): + def start_refresh(self) -> None: """Manually start the refresh task (if not already running)""" self._refresh_task.start() - def stop_refresh(self): + def stop_refresh(self) -> None: """Manually stop the refresh task""" self._refresh_task.cancel() - async def _periodic_refresh_worker(self): + async def _periodic_refresh_worker(self) -> None: while True: # sync the refresh messages next_refresh = monotonic() @@ -144,14 +144,13 @@ def get_universe(self, nr: int) -> TYPE_U: :param nr: universe nr :return: The universe """ - if not isinstance(nr, int) or not nr >= 0: - raise ValueError('BaseUniverse must be an int >= 0!') - nr = int(nr) + nr = self._validate_universe_nr(nr) try: return self._universe_map[nr] except KeyError: - raise UniverseNotFoundError(f'BaseUniverse {nr:d} not found!') from None + msg = f'BaseUniverse {nr:d} not found!' + raise UniverseNotFoundError(msg) from None def add_universe(self, nr: int = 0) -> TYPE_U: """Creates a new universe and adds it to the parent node @@ -159,12 +158,11 @@ def add_universe(self, nr: int = 0) -> TYPE_U: :param nr: universe nr :return: The universe """ - if not isinstance(nr, int) or not nr >= 0: - raise ValueError('BaseUniverse must be an int >= 0!') - nr = int(nr) + nr = self._validate_universe_nr(nr) if nr in self._universe_map: - raise DuplicateUniverseError(f'BaseUniverse {nr:d} does already exist!') + msg = f'BaseUniverse {nr:d} does already exist!' + raise DuplicateUniverseError(msg) # add to data self._universe_map[nr] = universe = self._create_universe(nr) @@ -175,6 +173,9 @@ def add_universe(self, nr: int = 0) -> TYPE_U: def _create_universe(self, nr: int) -> TYPE_U: raise NotImplementedError() + def _validate_universe_nr(self, nr: int) -> int: + raise NotImplementedError() + def __await__(self): while self._process_jobs: for job in self._process_jobs: @@ -183,5 +184,5 @@ def __await__(self): def __getitem__(self, nr: int) -> TYPE_U: return self.get_universe(nr) - def __len__(self): + def __len__(self) -> int: return len(self._universes) diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 9866b13..6ab84d5 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -22,7 +22,7 @@ def __init__(self, ip: str, port: int, *, # ArtNet specific fields sequence_counter: bool = True - ): + ) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, refresh_every=refresh_every, start_refresh_task=start_refresh_task, @@ -40,21 +40,21 @@ def __init__(self, ip: str, port: int, *, self._sync_enabled : bool = False def _send_universe(self, id: int, byte_size: int, values: bytearray, - universe: 'pyartnet.impl_artnet.ArtNetUniverse'): + universe: 'pyartnet.impl_artnet.ArtNetUniverse') -> None: # pre allocate the bytearray _size = 10 + byte_size packet = bytearray(_size) - packet[0:2] =[0x00, 0x50] # Opcode ArtDMX 0x5000 (Little endian) - packet[2:4] = [0x00, 0x0e] # Protocol version 14 + packet[0:2] = (0x00, 0x50) # 2 | Opcode ArtDMX 0x5000 (Little Endian) + packet[2:4] = (0x00, 0x0e) # 2 | Protocol version 14 (Little Endian) - packet[4] = self._sequence_ctr.value # 1 | Sequence, - packet[5] = 0x00 # 1 | Physical input port (not used) - packet[6:8] = id.to_bytes(2, byteorder='little') # 2 | Universe + packet[4] = self._sequence_ctr.value # 1 | Sequence, + packet[5] = 0x00 # 1 | Physical input port (not used) + packet[6:8] = id.to_bytes(2, 'little') # 2 | Universe (Little endian) - packet[8:10] = byte_size.to_bytes(2, 'big') # 2 | Number of channels Big Endian - packet[10: _size] = values # 0 - 512 | Channel values + packet[8:10] = byte_size.to_bytes(2, 'big') # 2 | Number of channels Big Endian + packet[10: _size] = values # 0 - 512 | Channel values self._send_data(packet) @@ -67,7 +67,14 @@ def _create_universe(self, nr: int) -> 'pyartnet.impl_artnet.ArtNetUniverse': raise InvalidUniverseAddressError() return pyartnet.impl_artnet.ArtNetUniverse(self, nr) - def __log_artnet_frame(self, p: Union[bytearray, bytes]): + def _validate_universe_nr(self, nr: int) -> int: + if not isinstance(nr, int): + raise TypeError() + if not 0 <= nr <= 32_768: + raise InvalidUniverseAddressError() + return int(nr) + + def __log_artnet_frame(self, p: Union[bytearray, bytes]) -> None: """Log Artnet Frame""" assert isinstance(p, (bytearray, bytes)) @@ -134,20 +141,25 @@ def __log_artnet_frame(self, p: Union[bytearray, bytes]): log.debug(out) def set_synchronous_mode(self, enabled: bool): + if self._refresh_every > 3.5: + msg = 'ArtNet synchronization requires refresh_every <= 3.5s' + raise ValueError(msg) + self._sync_enabled = enabled + return self - def _send_synchronization(self): + def _send_synchronization(self) -> None: if not self._sync_enabled: return # pre allocate the bytearray packet = bytearray(6) - packet[0:2] = [0x00, 0x52] # Opcode ArtSync - packet[2:4] = [0x00, 0x0e] # Protocol version 14 + packet[0:2] = (0x00, 0x52) # 2 | Opcode ArtSync 0x5200 (Little Endian) + packet[2:4] = (0x00, 0x0e) # 2 | Protocol Version 14 (Little Endian) - packet[4] = 0 # Aux1 - packet[5] = 0 # Aux2 + packet[4] = 0 # 1 | Aux1 + packet[5] = 0 # 1 | Aux2 self._send_data(packet) diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index c9be8da..4ec2cc6 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -19,7 +19,7 @@ class KiNetNode(BaseNode['pyartnet.impl_kinet.KiNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, - source_address: Optional[Tuple[str, int]] = None): + source_address: Optional[Tuple[str, int]] = None) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, refresh_every=refresh_every, start_refresh_task=start_refresh_task, @@ -31,7 +31,8 @@ def __init__(self, ip: str, port: int, *, packet.extend(s_pack(">IBBHI", 0, 0, 0, 0, 0xFFFFFFFF)) # sequence, port, padding, flags, timer self._packet_base = bytes(packet) - def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: 'pyartnet.impl_kinet.KiNetUniverse'): + def _send_universe(self, id: int, byte_size: int, + values: bytearray, universe: 'pyartnet.impl_kinet.KiNetUniverse') -> None: packet = bytearray() packet.append(byte_size) packet.extend(values) @@ -43,6 +44,11 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: ' log.debug(f"Sending KiNet frame to {self._ip}:{self._port}: {(self._packet_base + packet).hex()}") def _create_universe(self, nr: int) -> 'pyartnet.impl_kinet.KiNetUniverse': - if nr >= 32_768: + return pyartnet.impl_kinet.KiNetUniverse(self, self._validate_universe_nr(nr)) + + def _validate_universe_nr(self, nr: int) -> int: + if not isinstance(nr, int): + raise TypeError() + if not 0 <= nr <= 32_768: raise InvalidUniverseAddressError() - return pyartnet.impl_kinet.KiNetUniverse(self, nr) + return int(nr) diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 53bcba0..20b390f 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -5,7 +5,7 @@ from uuid import uuid4 import pyartnet.impl_sacn.universe -from pyartnet.base import BaseNode +from pyartnet.base import BaseNode, SequenceCounter from pyartnet.errors import InvalidCidError, InvalidUniverseAddressError # ----------------------------------------------------------------------------- @@ -35,7 +35,7 @@ def __init__(self, ip: str, port: int, *, # sACN E1.31 specific fields cid: Optional[bytes] = None, source_name: Optional[str] = None - ): + ) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, refresh_every=refresh_every, start_refresh_task=start_refresh_task, @@ -63,28 +63,28 @@ def __init__(self, ip: str, port: int, *, packet.extend(b'\x00\x10') # | 2 | Preamble Size packet.extend(b'\x00\x00') # | 2 | Post-amble Size packet.extend(ACN_PACKET_IDENTIFIER) # | 12 | Packet Identifier - packet.extend([0x72, 0x57]) # | 2 | Flags, Length + packet.extend((0x72, 0x57)) # | 2 | Flags, Length packet.extend(VECTOR_ROOT_E131_DATA) # | 4 | Vector packet.extend(cid) # | 16 | CID, a unique identifier self._packet_base: bytearray = packet self._synchronization_address : int = 0 - self._sync_sequence_number : int = 0 + self._sync_sequence_number: Final = SequenceCounter() def _send_universe(self, id: int, byte_size: int, values: bytearray, - universe: 'pyartnet.impl_sacn.universe.SacnUniverse'): + universe: 'pyartnet.impl_sacn.universe.SacnUniverse') -> None: packet = bytearray() # DMX Start Code is not included in the byte size from the universe prop_count = byte_size + 1 # Framing layer Part 1 - packet.extend((( 87 + prop_count) | 0x7000).to_bytes(2, 'big')) # Flags and Length - packet.extend(VECTOR_E131_DATA_PACKET) # | 4 | Vector - packet.extend(self._source_name_byte) # | 64 |Source Name - packet.append(100) # | 1 |Priority + packet.extend((( 87 + prop_count) | 0x7000).to_bytes(2, 'big')) # | 2 | Flags and Length + packet.extend(VECTOR_E131_DATA_PACKET) # | 4 | Vector + packet.extend(self._source_name_byte) # | 64 | Source Name + packet.append(100) # | 1 | Priority packet.extend(int(self._synchronization_address).to_bytes(2, 'big')) # | 2 | Synchronization universe # Framing layer Part 2 @@ -104,9 +104,10 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, packet.append(0x00) # | 1 | Property Values - DMX Start Code packet.extend(values) # | 0-512 | Property Values - DMX Data - # Update length for base packet + # Update length and package type for base packet base_packet = self._packet_base - base_packet[16:18] = ((109 + prop_count) | 0x7000).to_bytes(2, 'big') # root layer + base_packet[16:18] = ((109 + prop_count) | 0x7000).to_bytes(2, 'big') # | 2 | Flags, Length + base_packet[18:22] = VECTOR_ROOT_E131_DATA # | 4 | Vector self._send_data(packet) @@ -115,43 +116,46 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, log.debug(f"Sending sACN frame to {self._ip}:{self._port}: {(base_packet + packet).hex()}") def _create_universe(self, nr: int) -> 'pyartnet.impl_sacn.SacnUniverse': - # 6.2.7 E1.31 Data Packet: Universe - if not 1 <= nr < 63_999: + return pyartnet.impl_sacn.SacnUniverse(self, self._validate_universe_nr(nr)) + + def _validate_universe_nr(self, nr: int) -> int: + if not isinstance(nr, int): + raise TypeError() + # See spec 6.2.7 E1.31 Data Packet: Universe + if not 1 <= nr <= 63_999: raise InvalidUniverseAddressError() - return pyartnet.impl_sacn.SacnUniverse(self, nr) + return int(nr) + - def set_synchronous_mode(self, enabled: bool, synchronization_address : int = 0): + def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0): if enabled: - assert(synchronization_address != 0) - self._synchronization_address = synchronization_address + self._synchronization_address = self._validate_universe_nr(synchronization_address) else: - assert(synchronization_address == 0) + if synchronization_address != 0: + msg = 'synchronization_address must be 0 when disabling synchronous mode!' + raise ValueError(msg) self._synchronization_address = 0 - def _send_synchronization(self): - if (self._synchronization_address == 0): + def _send_synchronization(self) -> None: + if not self._synchronization_address: return packet = bytearray() - try: - base_packet = self._packet_base - base_packet[16:18] = ((33) | 0x7000).to_bytes(2, 'big') # root layer - base_packet[18:22] = VECTOR_ROOT_E131_EXTENDED - # Framing layer - packet.extend(((11) | 0x7000).to_bytes(2, 'big')) # | 2 | Flags and Length - packet.extend(VECTOR_E131_EXTENDED_SYNCHRONIZATION) # | 4 | Vector - self._sync_sequence_number += 1 - if (self._sync_sequence_number >= 255): - self._sync_sequence_number = 0 - packet.append(self._sync_sequence_number) # | 1 | Sequence Number - packet.extend(self._synchronization_address.to_bytes(2, 'big')) # | 2 | Synchronization universe - packet.extend([0, 0]) # | 2 | Reserved - - self._send_data(packet) - - if log.isEnabledFor(LVL_DEBUG): - # log complete packet - log.debug(f"Sending sACN Syncronization Packet to {self._ip}:{self._port}: {(base_packet + packet).hex()}") - finally: - self._packet_base[18:22] = VECTOR_ROOT_E131_DATA + # Framing layer + packet.extend((11 | 0x7000).to_bytes(2, 'big')) # | 2 | Flags and Length + packet.extend(VECTOR_E131_EXTENDED_SYNCHRONIZATION) # | 4 | Vector + packet.append(self._sync_sequence_number.value) # | 1 | Sequence Number + packet.extend(self._synchronization_address.to_bytes(2, 'big')) # | 2 | Synchronization universe + packet.extend([0, 0]) # | 2 | Reserved + + # Update length and package type for base packet + base_packet = self._packet_base + base_packet[16:18] = (33 | 0x7000).to_bytes(2, 'big') # | 2 | Flags, Length + base_packet[18:22] = VECTOR_ROOT_E131_EXTENDED # | 4 | Vector + + self._send_data(packet) + + if log.isEnabledFor(LVL_DEBUG): + # log complete packet + log.debug(f"Sending sACN Synchronization Packet to {self._ip}:{self._port}: {(base_packet + packet).hex()}") diff --git a/tests/conftest.py b/tests/conftest.py index dbe8688..757bccd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,24 +15,28 @@ class TestingNode(BaseNode): __test__ = False # prevent this from being collected by pytest - def __init__(self, ip: str, port: int): + def __init__(self, ip: str, port: int) -> None: super().__init__(ip, port, max_fps=1_000 // STEP_MS, start_refresh_task=False) self.data = [] - def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: 'pyartnet.base.BaseUniverse'): + def _send_universe(self, id: int, byte_size: int, + values: bytearray, universe: 'pyartnet.base.BaseUniverse') -> None: self.data.append(values.hex()) - async def sleep_steps(self, steps: int): + async def sleep_steps(self, steps: int) -> None: # use sleep because await sleep might actually take longer for _ in range(steps): await sleep(self._process_every) - async def wait_for_task_finish(self): + async def wait_for_task_finish(self) -> None: await self def _create_universe(self, nr: int) -> TYPE_U: return BaseUniverse(self, nr) + def _validate_universe_nr(self, nr: int) -> int: + return nr + @pytest.fixture(autouse=True) def patched_socket(monkeypatch): @@ -40,7 +44,7 @@ def patched_socket(monkeypatch): yield sock_sendto -def test_patched_socket(patched_socket): +def test_patched_socket(patched_socket) -> None: node = TestingNode('IP', 9999) assert node._socket.sendto is patched_socket diff --git a/tests/test_base_node.py b/tests/test_base_node.py index 72f6ca1..df4e0ab 100644 --- a/tests/test_base_node.py +++ b/tests/test_base_node.py @@ -8,13 +8,7 @@ from tests.conftest import STEP_MS, TestingNode -def test_universe_add_get(node: TestingNode): - for i in (1.3, -1): - with pytest.raises(ValueError, match='BaseUniverse must be an int >= 0!'): - node.add_universe(i) - - with pytest.raises(ValueError, match='BaseUniverse must be an int >= 0!'): - node.get_universe(i) +def test_universe_add_get(node: TestingNode) -> None: u = node.add_universe() assert len(node) == 1 @@ -35,14 +29,14 @@ def test_universe_add_get(node: TestingNode): ) -async def test_fade_await(node: TestingNode, universe: BaseUniverse, caplog): - async def check_no_wait_time_when_no_fade(): +async def test_fade_await(node: TestingNode, universe: BaseUniverse, caplog) -> None: + async def check_no_wait_time_when_no_fade() -> None: start = monotonic() for _ in range(1000): assert not await node assert monotonic() - start < 0.001 - async def check_wait_time_when_fade(steps: int): + async def check_wait_time_when_fade(steps: int) -> None: start = monotonic() await node assert monotonic() - start >= ((steps - 1) * STEP_MS) / 1000 diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index d2bfa4e..8f9c42d 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -6,11 +6,12 @@ from pyartnet import ArtNetNode, KiNetNode, SacnNode from pyartnet.base import BaseNode +from pyartnet.errors import InvalidUniverseAddressError from tests.conftest import TestingNode @pytest.mark.parametrize('c', (ArtNetNode, KiNetNode, SacnNode)) -def test_same_cls_signature(c): +def test_same_cls_signature(c) -> None: sig_base = inspect.signature(BaseNode) sig_obj = inspect.signature(c) @@ -20,7 +21,7 @@ def test_same_cls_signature(c): @pytest.mark.parametrize('cls', [ArtNetNode, SacnNode, KiNetNode]) -async def test_set_funcs(node: TestingNode, caplog, cls): +async def test_set_funcs(node: TestingNode, caplog, cls) -> None: caplog.set_level(logging.DEBUG) n = cls('ip', 9999) @@ -32,3 +33,14 @@ async def test_set_funcs(node: TestingNode, caplog, cls): c.set_fade([250], 700) await c + + +@pytest.mark.parametrize('cls', [ArtNetNode, SacnNode, KiNetNode]) +async def test_universe_validation(node: TestingNode, cls) -> None: + + n = cls('ip', 9999) + with pytest.raises(TypeError): + n.add_universe(1.3) + + with pytest.raises(InvalidUniverseAddressError): + n.add_universe(2 ** 16) From 29ea5669bda86db59ac5617d32d7c80bec37a629 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:10:49 +0200 Subject: [PATCH 13/49] . --- src/pyartnet/base/seq_counter.py | 6 ++++-- src/pyartnet/impl_sacn/node.py | 2 ++ src/pyartnet/py.typed | 0 tests/test_sequence_counter.py | 21 ++++++++------------- 4 files changed, 14 insertions(+), 15 deletions(-) create mode 100644 src/pyartnet/py.typed diff --git a/src/pyartnet/base/seq_counter.py b/src/pyartnet/base/seq_counter.py index e4e4aa8..37b7cbd 100644 --- a/src/pyartnet/base/seq_counter.py +++ b/src/pyartnet/base/seq_counter.py @@ -4,9 +4,11 @@ class SequenceCounter: __slots__ = ('_ctr', '_start', '_upper') - def __init__(self, start: int = 0, upper: int = 255): + def __init__(self, start: int = 0, upper: int = 255) -> None: + if start > upper: + raise ValueError() + self._ctr: int = start - assert start <= upper self._start: Final = start self._upper: Final = upper diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 20b390f..e41bdc4 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -70,6 +70,8 @@ def __init__(self, ip: str, port: int, *, self._packet_base: bytearray = packet self._synchronization_address : int = 0 + + # See spec 6.3.2 E1.31 Synchronization Packet: Sequence Number self._sync_sequence_number: Final = SequenceCounter() diff --git a/src/pyartnet/py.typed b/src/pyartnet/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_sequence_counter.py b/tests/test_sequence_counter.py index 44b15d6..20935e7 100644 --- a/tests/test_sequence_counter.py +++ b/tests/test_sequence_counter.py @@ -1,20 +1,15 @@ from pyartnet.base.seq_counter import SequenceCounter -def test_seq(): - s = SequenceCounter() - assert s.value == 0 - assert s.value == 1 - assert s.value == 2 +def test_seq() -> None: - s._ctr = 254 - assert s.value == 254 - assert s.value == 255 - assert s.value == 0 - assert s.value == 1 + s = SequenceCounter() + for _ in range(10): + for i in range(256): + assert s.value == i -def test_seq_artnet(): +def test_seq_artnet() -> None: s = SequenceCounter(1) assert s.value == 1 assert s.value == 2 @@ -25,14 +20,14 @@ def test_seq_artnet(): assert s.value == 1 -def test_seq_const(): +def test_seq_const() -> None: s = SequenceCounter(0, 0) assert s.value == 0 assert s.value == 0 assert s.value == 0 -def test_repr(): +def test_repr() -> None: s = SequenceCounter() assert repr(s) == '' assert repr(s) == '' From d9122ff2dcb033e8d2e9ce2171a0e69661cd1130 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:14:36 +0200 Subject: [PATCH 14/49] . --- .github/workflows/pre-commit.yml | 13 ------------- .github/workflows/run_tests.yml | 12 ++++++++++++ 2 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index e4f7f07..0000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Pre-Commit - -on: [push, pull_request] - -jobs: - main: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index b1b9ac9..c28898f 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -3,7 +3,18 @@ name: Tests on: [push, pull_request] jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: pre-commit/action@v3.0.1 + test: + needs: pre-commit runs-on: ubuntu-latest strategy: max-parallel: 4 @@ -24,6 +35,7 @@ jobs: docs: + needs: pre-commit runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 From 5ecbd291f09e98add46ded8d584d0ab9f1d4991c Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:46:01 +0200 Subject: [PATCH 15/49] . --- src/pyartnet/base/base_node.py | 12 +++-- src/pyartnet/impl_sacn/node.py | 87 +++++++++++++++++++++++++----- src/pyartnet/impl_sacn/universe.py | 5 +- tests/test_impl/test_sacn.py | 66 ++++++++++++++++------- 4 files changed, 132 insertions(+), 38 deletions(-) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 6673c8d..c59b27e 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -43,13 +43,13 @@ def __init__(self, ip: str, port: int, *, # refresh task self._refresh_every: float = max(0.1, refresh_every) - self._refresh_task: Final = ExceptionIgnoringTask(self._periodic_refresh_worker, f'Process task {name:s}') + self._refresh_task: Final = ExceptionIgnoringTask(self._periodic_refresh_worker, f'Refresh task {name:s}') if start_refresh_task: self._refresh_task.start() # fade task self._process_every: float = 1 / max(1, max_fps) - self._process_task: Final = SimpleBackgroundTask(self._process_values_task, f'Refresh task {name:s}') + self._process_task: Final = SimpleBackgroundTask(self._process_values_task, f'Process task {name:s}') self._process_jobs: List['pyartnet.base.ChannelBoundFade'] = [] # packet data @@ -73,9 +73,9 @@ def set_synchronous_mode(self, enabled: bool): def _send_synchronization(self) -> None: pass - def _send_data(self, data: Union[bytearray, bytes]) -> int: + def _send_data(self, data: Union[bytearray, bytes], dst: tuple[str, int] | str | None = None) -> int: - ret = self._socket.sendto(self._packet_base + data, self._dst) + ret = self._socket.sendto(self._packet_base + data, self._dst if dst is None else dst) self._last_send = monotonic() return ret @@ -109,7 +109,9 @@ async def _process_values_task(self) -> None: self._process_jobs.remove(job) job.fade_complete() - self._send_synchronization() + # send synchronization only if we actually sent something + if not idle_ct: + self._send_synchronization() await sleep(self._process_every) diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index e41bdc4..2170b25 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -1,5 +1,6 @@ # flake8: noqa: E262 import logging +from ipaddress import IPv6Address from logging import DEBUG as LVL_DEBUG from typing import Final, Optional, Tuple, Union from uuid import uuid4 @@ -56,6 +57,9 @@ def __init__(self, ip: str, port: int, *, raise ValueError('Source name too long!') self._source_name_byte : bytes = source_name_byte + # See spec 9.3 Allocation of Multicast Addresses + self._multicast: bool = False + # build base packet packet = bytearray() @@ -69,12 +73,15 @@ def __init__(self, ip: str, port: int, *, self._packet_base: bytearray = packet - self._synchronization_address : int = 0 - + # Synchronization Packet + # See Spec 6.2.4 E1.31 Data Packet: Synchronization Address + self._sync_address: int = 0 + # See spec 9.3 Allocation of Multicast Addresses + self._sync_dst: tuple[str, int] = self._dst # See spec 6.3.2 E1.31 Synchronization Packet: Sequence Number self._sync_sequence_number: Final = SequenceCounter() - + # noinspection PyProtectedMember def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: 'pyartnet.impl_sacn.universe.SacnUniverse') -> None: packet = bytearray() @@ -87,7 +94,7 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, packet.extend(VECTOR_E131_DATA_PACKET) # | 4 | Vector packet.extend(self._source_name_byte) # | 64 | Source Name packet.append(100) # | 1 | Priority - packet.extend(int(self._synchronization_address).to_bytes(2, 'big')) # | 2 | Synchronization universe + packet.extend(int(self._sync_address).to_bytes(2, 'big')) # | 2 | Synchronization universe # Framing layer Part 2 packet.append(universe._sequence_ctr.value) # | 1 | Sequence, @@ -111,11 +118,11 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, base_packet[16:18] = ((109 + prop_count) | 0x7000).to_bytes(2, 'big') # | 2 | Flags, Length base_packet[18:22] = VECTOR_ROOT_E131_DATA # | 4 | Vector - self._send_data(packet) + self._send_data(packet, universe._dst) if log.isEnabledFor(LVL_DEBUG): # log complete packet - log.debug(f"Sending sACN frame to {self._ip}:{self._port}: {(base_packet + packet).hex()}") + log.debug(f"Sending sACN frame to {_dst_str(universe._dst)}: {(base_packet + packet).hex()}") def _create_universe(self, nr: int) -> 'pyartnet.impl_sacn.SacnUniverse': return pyartnet.impl_sacn.SacnUniverse(self, self._validate_universe_nr(nr)) @@ -128,18 +135,62 @@ def _validate_universe_nr(self, nr: int) -> int: raise InvalidUniverseAddressError() return int(nr) + def _get_universe_ip_port(self, universe: int) -> tuple[str, int] | str: + if not self._multicast: + return self._dst + + u = self._validate_universe_nr(universe) + + universe_high = u // 255 + universe_low = u % 255 + + # IPv6 multicast address + if ':' in self._ip: + IPv6Address(self._ip) # validate IP + return f'FF18::83:00:{universe_high:d}:{universe_low:d}' + + # IPv4 multicast address + return f'239.255.{universe_high:d}.{universe_low:d}' + + def set_multicast_mode(self, enabled: bool): + """Either send packets to the node directly or through multicast. + :param enabled: If True multicast is enabled + """ + self._multicast = enabled - def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0): + # update all universe destinations + for universe in self._universes: + universe._dst = self._get_universe_ip_port(universe._universe) + + # update sync package destination + if self._sync_address: + self._sync_dst = self._get_universe_ip_port(self._sync_address) + + return self + + def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0) -> None: + """Enable or disable synchronous mode for this node. In synchronous mode multiple universes are sent to the + node and then a synchronization packet is sent to make the node output all universes at the same time. + This prevents tearing in multi universe panels. + + :param enabled: Enable or disable synchronous mode + :param synchronization_address: The universe address to use for synchronization packets. This must be the + same for all nodes that should be synchronized. + """ if enabled: - self._synchronization_address = self._validate_universe_nr(synchronization_address) + self._sync_address = sync_address = self._validate_universe_nr(synchronization_address) + self._sync_dst = self._get_universe_ip_port(sync_address) + else: if synchronization_address != 0: msg = 'synchronization_address must be 0 when disabling synchronous mode!' raise ValueError(msg) - self._synchronization_address = 0 + + self._sync_address = 0 + self._sync_dst = self._dst def _send_synchronization(self) -> None: - if not self._synchronization_address: + if not self._sync_address: return packet = bytearray() @@ -148,7 +199,7 @@ def _send_synchronization(self) -> None: packet.extend((11 | 0x7000).to_bytes(2, 'big')) # | 2 | Flags and Length packet.extend(VECTOR_E131_EXTENDED_SYNCHRONIZATION) # | 4 | Vector packet.append(self._sync_sequence_number.value) # | 1 | Sequence Number - packet.extend(self._synchronization_address.to_bytes(2, 'big')) # | 2 | Synchronization universe + packet.extend(self._sync_address.to_bytes(2, 'big')) # | 2 | Synchronization universe packet.extend([0, 0]) # | 2 | Reserved # Update length and package type for base packet @@ -156,8 +207,18 @@ def _send_synchronization(self) -> None: base_packet[16:18] = (33 | 0x7000).to_bytes(2, 'big') # | 2 | Flags, Length base_packet[18:22] = VECTOR_ROOT_E131_EXTENDED # | 4 | Vector - self._send_data(packet) + self._send_data(packet, self._sync_dst) if log.isEnabledFor(LVL_DEBUG): # log complete packet - log.debug(f"Sending sACN Synchronization Packet to {self._ip}:{self._port}: {(base_packet + packet).hex()}") + log.debug( + f"Sending sACN Synchronization Packet to {_dst_str(self._sync_dst):s}: " + f"{(base_packet + packet).hex()}" + ) + + +def _dst_str(dst: tuple[str, int] | str) -> str: + if isinstance(dst, str): + return dst + ip, port = dst + return f'{ip:s}:{port:d}' diff --git a/src/pyartnet/impl_sacn/universe.py b/src/pyartnet/impl_sacn/universe.py index eecf6cc..337fa16 100644 --- a/src/pyartnet/impl_sacn/universe.py +++ b/src/pyartnet/impl_sacn/universe.py @@ -7,8 +7,11 @@ class SacnUniverse(BaseUniverse): - def __init__(self, node: 'pyartnet.impl_sacn.SacnNode', universe: int = 0): + def __init__(self, node: 'pyartnet.impl_sacn.SacnNode', universe: int = 0) -> None: super().__init__(node, universe) # sACN has the sequence counter on the universe self._sequence_ctr: Final = SequenceCounter() + + # to support multicast + self._dst: tuple[str, int] | str = node._get_universe_ip_port(universe) diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index 391b3a7..bf25335 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -1,48 +1,76 @@ +import logging from binascii import a2b_hex +from unittest.mock import call + +import pytest from pyartnet import SacnNode -async def test_sacn(patched_socket): +async def test_sacn() -> None: sacn = SacnNode( 'ip', 9999999, cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', - source_name="default source name") - universe = sacn.add_universe(1) - channel = universe.add_channel(1, 10) - channel.set_values(range(1, 11)) + source_name="default source name", + start_refresh_task=True + ) - universe.send_data() + channel = sacn.add_universe(1).add_channel(1, 10) + channel.set_values(range(1, 11)) data = '001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' \ '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' \ '0000000064000000000001701502a100000001000b000102030405060708090a' + await channel + await sacn._process_task.task + m = sacn._socket m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) - await channel -async def test_sacn_with_sync(patched_socket): + +@pytest.mark.parametrize('multicast', [False, True]) +async def test_sacn_with_sync(caplog, multicast) -> None: + caplog.set_level(logging.DEBUG) + sacn = SacnNode( 'ip', 9999999, cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', - source_name="default source name") - universe = sacn.add_universe(1) - channel = universe.add_channel(1, 10) - channel.set_values(range(1, 11)) + source_name="default source name", + start_refresh_task=False + ) + sacn.set_synchronous_mode(True, 2) + sacn.set_multicast_mode(multicast) - sacn.set_synchronous_mode(True, 1) - - universe.send_data() + channel = sacn.add_universe(1).add_channel(1, 10) + channel.set_values(range(1, 11)) data = '001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' \ '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' \ - '0000000064000100000001701502a100000001000b000102030405060708090a' - - m = sacn._socket - m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) + '0000000064000200000001701502a100000001000b000102030405060708090a' + sync_data = '001000004153432d45312e31370000007021000000084168f52b1a7b2de11712e9ee383d2258700b000000010000020000' await channel + await sacn._process_task.task + + data_dst = ('ip', 9999999) if not multicast else '239.255.0.1' + sync_dst = ('ip', 9999999) if not multicast else '239.255.0.2' + data_msg = 'ip:9999999' if not multicast else '239.255.0.1' + sync_msg = 'ip:9999999' if not multicast else '239.255.0.2' + + m = sacn._socket + assert m.sendto.call_args_list == [ + call(bytearray(a2b_hex(data)), data_dst), + call(bytearray(a2b_hex(sync_data)), sync_dst), + ] + + assert caplog.record_tuples == [ + ('pyartnet.Universe', 10, 'Added channel "1/10": start: 1, stop: 10'), + ('pyartnet.Task', 10, 'Started Process task ip:9999999'), + ('pyartnet.SacnNode', 10, f'Sending sACN frame to {data_msg:s}: {data:s}'), + ('pyartnet.SacnNode', 10, f'Sending sACN Synchronization Packet to {sync_msg}: {sync_data:s}'), + ('pyartnet.Task', 10, 'Stopped Process task ip:9999999') + ] From ae402e2bb7f7f4d8670bf681d092bc6e2fcf2575 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:05:12 +0200 Subject: [PATCH 16/49] . --- .pre-commit-config.yaml | 36 +++++++------- .ruff.toml | 104 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 +- uv.lock | 28 +++++++++++ 4 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 .ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e61e55..232497c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,32 +1,36 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: + - id: check-ast + - id: check-builtin-literals + - id: check-docstring-first + - id: check-merge-conflict + - id: check-toml - id: check-yaml + - id: debug-statements - id: end-of-file-fixer exclude: \.(?:pdf|svg)$ - id: trailing-whitespace - - repo: https://github.com/pycqa/isort - rev: 5.12.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.3 hooks: - - id: isort - name: isort (python) + - id: ruff + name: ruff unused imports + # F401 [*] {name} imported but unused + args: [ "--select", "F401", "--extend-exclude", "__init__.py", "--fix"] + + - id: ruff + # I001 [*] Import block is un-sorted or un-formatted + # UP035 [*] Import from {target} instead: {names} + # Q000 [*] Double quote found but single quotes preferred + # Q001 [*] Double quote multiline found but single quotes preferred + args: [ "--select", "I001,UP035,Q000,Q001", "--fix"] - - repo: https://github.com/PyCQA/flake8 - rev: '6.0.0' - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear==23.1.20 - - flake8-comprehensions==3.10.1 - - flake8-pytest-style==1.6 - - flake8-noqa==1.3 - - pep8-naming==0.13.3 - - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..7e3c617 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,104 @@ +indent-width = 4 +line-length = 120 + +target-version = "py310" + +src = [ + "src", + "tests" +] + + +[lint] +select = ["ALL"] + +ignore = [ + "D", # https://docs.astral.sh/ruff/rules/#pydocstyle-d + "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "SLF", # https://docs.astral.sh/ruff/rules/#flake8-self-slf + + "RET501", # https://docs.astral.sh/ruff/rules/unnecessary-return-none/#unnecessary-return-none-ret501 + "TRY400", # https://docs.astral.sh/ruff/rules/error-instead-of-exception/ + + # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "A003", # Python builtin is shadowed by class attribute {name} from {row} + + # https://docs.astral.sh/ruff/rules/#pyflakes-f + "F401", # {name} imported but unused; consider using importlib.util.find_spec to test for availability + + # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + + # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "UP038", # Use X | Y in {} call instead of (X, Y) + + # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} + + # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble + "BLE001", # Do not catch blind exception: {name} + + # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + "RSE102", # Unnecessary parentheses on raised exception + + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "COM812", # Trailing comma missing + "COM819", # Trailing comma prohibited + + # https://docs.astral.sh/ruff/rules/#warning-w_1 + "PLW0603", # Using the global statement to update {name} is discouraged + + # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "G004", # Logging statement uses f-string + + # https://docs.astral.sh/ruff/rules/#refactor-r + "PLR1711", # Useless return statement at end of function + + # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "RUF005", # Consider {expression} instead of concatenation + + # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "PT007", # Wrong values type in @pytest.mark.parametrize expected {values} of {row} +] + + +[format] +quote-style = "single" + + +# https://docs.astral.sh/ruff/settings/#lintflake8-quotes +[lint.flake8-quotes] +inline-quotes = "single" +multiline-quotes = "single" + + +[lint.flake8-builtins] +builtins-ignorelist = ["id", "input"] + + +# https://docs.astral.sh/ruff/settings/#lintisort +[lint.isort] +lines-after-imports = 2 # https://docs.astral.sh/ruff/settings/#lint_isort_lines-after-imports + + +[lint.per-file-ignores] +"docs/conf.py" = [ + "INP001", # File `conf.py` is part of an implicit namespace package. Add an `__init__.py`. + "A001", # Variable `copyright` is shadowing a Python builtin + "PTH118", # `os.path.join()` should be replaced by `Path` with `/` operator + "PTH100", # `os.path.abspath()` should be replaced by `Path.resolve()` +] + +"setup.py" = ["PTH123"] + +"tests/*" = [ + "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + + # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "S101", # Use of assert detected + + # https://docs.astral.sh/ruff/rules/#refactor-r + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLR0913", # Too many arguments in function definition ({c_args} > {max_args}) +] diff --git a/pyproject.toml b/pyproject.toml index 7134e58..1ddf4c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,8 @@ tests = [ dev = [ {include-group = "tests"}, {include-group = "docs"}, - "pre-commit" + "pre-commit", + "ruff" ] diff --git a/uv.lock b/uv.lock index a151184..b2e3fa2 100644 --- a/uv.lock +++ b/uv.lock @@ -634,6 +634,7 @@ dev = [ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "ruff" }, { name = "sphinx", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, @@ -673,6 +674,7 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "ruff" }, { name = "sphinx" }, { name = "sphinx-autodoc-typehints" }, { name = "sphinx-exec-code" }, @@ -912,6 +914,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, ] +[[package]] +name = "ruff" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" }, + { url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" }, + { url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" }, + { url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" }, + { url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" }, +] + [[package]] name = "snowballstemmer" version = "3.0.1" From 80c9fb5ebd73632d2111b1fbe42a014fb12648ea Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:07:56 +0200 Subject: [PATCH 17/49] removed double quotes --- .ruff.toml | 2 +- setup.py | 57 ------------------------------- src/pyartnet/base/channel.py | 20 +++++++---- src/pyartnet/fades/fade_base.py | 4 +-- src/pyartnet/fades/fade_linear.py | 6 ++-- src/pyartnet/impl_artnet/node.py | 3 +- src/pyartnet/impl_kinet/node.py | 7 ++-- src/pyartnet/impl_sacn/node.py | 7 ++-- tests/test_impl/test_sacn.py | 4 +-- 9 files changed, 31 insertions(+), 79 deletions(-) delete mode 100644 setup.py diff --git a/.ruff.toml b/.ruff.toml index 7e3c617..36e1d9b 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,7 +1,7 @@ indent-width = 4 line-length = 120 -target-version = "py310" +target-version = "py38" src = [ "src", diff --git a/setup.py b/setup.py deleted file mode 100644 index de1a734..0000000 --- a/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -import typing -from pathlib import Path - -import setuptools # type: ignore - - -# Load version number without importing HABApp -def load_version() -> str: - version: typing.Dict[str, str] = {} - with open("src/pyartnet/__version__.py") as fp: - exec(fp.read(), version) - assert version['__version__'], version - return version['__version__'] - - -__version__ = load_version() - -print(f'Version: {__version__}') -print('') - -# When we run tox tests we don't have these files available so we skip them -readme = Path(__file__).with_name('readme.md') -long_description = '' -if readme.is_file(): - with readme.open("r", encoding='utf-8') as fh: - long_description = fh.read() - - -setuptools.setup( - name="pyartnet", - version=__version__, - author="spaceman_spiff", - # author_email="", - description="Python wrappers for the Art-Net protocol to send DMX over Ethernet", - keywords='DMX, Art-Net, ArtNet, sACN E1.31, E1.31, KiNet', - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/spacemanspiff2007/PyArtNet", - project_urls={ - 'Documentation': 'https://pyartnet.readthedocs.io', - 'GitHub': 'https://github.com/spacemanspiff2007/PyArtNet' - }, - package_dir={'': 'src'}, - package_data={'pyartnet': ['py.typed']}, - packages=setuptools.find_packages('src', exclude=['tests*']), - python_requires='>=3.8', - classifiers=[ - "Development Status :: 4 - Beta", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3 :: Only", - "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", - "Operating System :: OS Independent", - ], -) diff --git a/src/pyartnet/base/channel.py b/src/pyartnet/base/channel.py index 49aa05b..fc554a2 100644 --- a/src/pyartnet/base/channel.py +++ b/src/pyartnet/base/channel.py @@ -1,12 +1,17 @@ import logging import warnings from array import array +from collections.abc import Callable, Collection from logging import DEBUG as LVL_DEBUG from math import ceil -from typing import Any, Callable, Collection, Final, List, Literal, Optional, Type, Union - -from pyartnet.errors import ChannelOutOfUniverseError, ChannelValueOutOfBoundsError, \ - ChannelWidthError, ValueCountDoesNotMatchChannelWidthError +from typing import Any, Final, List, Literal, Optional, Type, Union + +from pyartnet.errors import ( + ChannelOutOfUniverseError, + ChannelValueOutOfBoundsError, + ChannelWidthError, + ValueCountDoesNotMatchChannelWidthError, +) from pyartnet.output_correction import linear from ..fades import FadeBase, LinearFade @@ -14,6 +19,7 @@ from .output_correction import OutputCorrection from .universe import BaseUniverse + log = logging.getLogger('pyartnet.Channel') @@ -28,7 +34,7 @@ class Channel(OutputCorrection): def __init__(self, universe: BaseUniverse, start: int, width: int, - byte_size: int = 1, byte_order: Literal['big', 'little'] = 'little'): + byte_size: int = 1, byte_order: Literal['big', 'little'] = 'little') -> None: super().__init__() # Validate Boundaries @@ -139,7 +145,7 @@ def to_buffer(self, buf: bytearray): def add_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, fade_class: Type[FadeBase] = LinearFade): warnings.warn( - f"{self.set_fade.__name__:s} is deprecated, use {self.set_fade.__name__:s} instead", DeprecationWarning) + f'{self.set_fade.__name__:s} is deprecated, use {self.set_fade.__name__:s} instead', DeprecationWarning) return self.set_fade(values, duration_ms, fade_class) # noinspection PyProtectedMember @@ -198,5 +204,5 @@ def __await__(self): yield from self._current_fade.event.wait().__await__() return True - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__:s} {self._start:d}/{self._width:d} {self._byte_size * 8:d}bit>' diff --git a/src/pyartnet/fades/fade_base.py b/src/pyartnet/fades/fade_base.py index bb3e6e6..1bed95b 100644 --- a/src/pyartnet/fades/fade_base.py +++ b/src/pyartnet/fades/fade_base.py @@ -1,7 +1,7 @@ class FadeBase: - def __init__(self): + def __init__(self) -> None: self.is_done = False def initialize(self, current: int, target: int, steps: int): @@ -9,7 +9,7 @@ def initialize(self, current: int, target: int, steps: int): def debug_initialize(self) -> str: """return debug string of the calculated values in initialize fade""" - return "" + return '' def calc_next_value(self) -> float: raise NotImplementedError() diff --git a/src/pyartnet/fades/fade_linear.py b/src/pyartnet/fades/fade_linear.py index bb6e0f4..c831141 100644 --- a/src/pyartnet/fades/fade_linear.py +++ b/src/pyartnet/fades/fade_linear.py @@ -3,16 +3,16 @@ class LinearFade(FadeBase): - def __init__(self): + def __init__(self) -> None: super().__init__() self.target: int = 0 # Target Value self.current: float = 0.0 # Current Value self.factor: float = 1.0 def debug_initialize(self) -> str: - return f"{self.current:03.0f} -> {self.target:03d} | step: {self.factor:+5.1f}" + return f'{self.current:03.0f} -> {self.target:03d} | step: {self.factor:+5.1f}' - def initialize(self, start: int, target: int, steps: int): + def initialize(self, start: int, target: int, steps: int) -> None: self.current = start self.target = target self.factor = (self.target - start) / steps diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 6ab84d5..51e1234 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -6,6 +6,7 @@ from pyartnet.base.seq_counter import SequenceCounter from pyartnet.errors import InvalidUniverseAddressError + # ----------------------------------------------------------------------------- # Documentation for ArtNet Protocol: # https://artisticlicence.com/support-and-resources/art-net-4/ @@ -33,7 +34,7 @@ def __init__(self, ip: str, port: int, *, # build base packet packet = bytearray() - packet.extend(map(ord, "Art-Net")) + packet.extend(map(ord, 'Art-Net')) packet.append(0x00) # Null terminate Art-Net self._packet_base = bytes(packet) diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index 4ec2cc6..0ff843e 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -7,6 +7,7 @@ from pyartnet.base import BaseNode from pyartnet.errors import InvalidUniverseAddressError + # ----------------------------------------------------------------------------- # Documentation for KiNet Protocol: # todo: find links @@ -27,8 +28,8 @@ def __init__(self, ip: str, port: int, *, # build base packet packet = bytearray() - packet.extend(s_pack(">IHH", 0x0401DC4A, 0x0100, 0x0101)) # Magic, version, type - packet.extend(s_pack(">IBBHI", 0, 0, 0, 0, 0xFFFFFFFF)) # sequence, port, padding, flags, timer + packet.extend(s_pack('>IHH', 0x0401DC4A, 0x0100, 0x0101)) # Magic, version, type + packet.extend(s_pack('>IBBHI', 0, 0, 0, 0, 0xFFFFFFFF)) # sequence, port, padding, flags, timer self._packet_base = bytes(packet) def _send_universe(self, id: int, byte_size: int, @@ -41,7 +42,7 @@ def _send_universe(self, id: int, byte_size: int, if log.isEnabledFor(LVL_DEBUG): # log complete packet - log.debug(f"Sending KiNet frame to {self._ip}:{self._port}: {(self._packet_base + packet).hex()}") + log.debug(f'Sending KiNet frame to {self._ip}:{self._port}: {(self._packet_base + packet).hex()}') def _create_universe(self, nr: int) -> 'pyartnet.impl_kinet.KiNetUniverse': return pyartnet.impl_kinet.KiNetUniverse(self, self._validate_universe_nr(nr)) diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 2170b25..4186317 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -9,6 +9,7 @@ from pyartnet.base import BaseNode, SequenceCounter from pyartnet.errors import InvalidCidError, InvalidUniverseAddressError + # ----------------------------------------------------------------------------- # Documentation for E1.31 Protocol: # https://tsp.esta.org/tsp/documents/published_docs.php @@ -122,7 +123,7 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, if log.isEnabledFor(LVL_DEBUG): # log complete packet - log.debug(f"Sending sACN frame to {_dst_str(universe._dst)}: {(base_packet + packet).hex()}") + log.debug(f'Sending sACN frame to {_dst_str(universe._dst)}: {(base_packet + packet).hex()}') def _create_universe(self, nr: int) -> 'pyartnet.impl_sacn.SacnUniverse': return pyartnet.impl_sacn.SacnUniverse(self, self._validate_universe_nr(nr)) @@ -212,8 +213,8 @@ def _send_synchronization(self) -> None: if log.isEnabledFor(LVL_DEBUG): # log complete packet log.debug( - f"Sending sACN Synchronization Packet to {_dst_str(self._sync_dst):s}: " - f"{(base_packet + packet).hex()}" + f'Sending sACN Synchronization Packet to {_dst_str(self._sync_dst):s}: ' + f'{(base_packet + packet).hex()}' ) diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index bf25335..bc6ed91 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -11,7 +11,7 @@ async def test_sacn() -> None: sacn = SacnNode( 'ip', 9999999, cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', - source_name="default source name", + source_name='default source name', start_refresh_task=True ) @@ -38,7 +38,7 @@ async def test_sacn_with_sync(caplog, multicast) -> None: sacn = SacnNode( 'ip', 9999999, cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', - source_name="default source name", + source_name='default source name', start_refresh_task=False ) sacn.set_synchronous_mode(True, 2) From 4c7ce9be4bdc5fda9139f96f1f3de398b0575137 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:09:37 +0200 Subject: [PATCH 18/49] sorted imports --- docs/conf.py | 1 + src/pyartnet/__init__.py | 2 ++ src/pyartnet/base/background_task.py | 11 ++++++----- src/pyartnet/base/base_node.py | 1 + src/pyartnet/base/channel_fade.py | 13 +++++++------ src/pyartnet/base/universe.py | 19 ++++++++++++------- tests/channel/test_boundaries.py | 13 ++++++++----- tests/channel/test_fade.py | 17 +++++++++-------- tests/channel/test_set_values.py | 5 +++-- tests/conftest.py | 3 ++- tests/test_base_node.py | 2 +- tests/test_impl/test_impl.py | 2 +- 12 files changed, 53 insertions(+), 36 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0d7dd34..cce6cfd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,6 +2,7 @@ import re import sys + RTD_BUILD = os.environ.get('READTHEDOCS') == 'True' # Configuration file for the Sphinx documentation builder. diff --git a/src/pyartnet/__init__.py b/src/pyartnet/__init__.py index c25b7b5..3b64f07 100644 --- a/src/pyartnet/__init__.py +++ b/src/pyartnet/__init__.py @@ -1,10 +1,12 @@ from . import errors, fades, output_correction from .__version__ import __version__ + # isort: split from .base import BaseUniverse, Channel + # isort: split from .impl_artnet import ArtNetNode diff --git a/src/pyartnet/base/background_task.py b/src/pyartnet/base/background_task.py index 9783fe0..d65a30a 100644 --- a/src/pyartnet/base/background_task.py +++ b/src/pyartnet/base/background_task.py @@ -1,13 +1,14 @@ import logging -from asyncio import create_task, sleep, Task +from asyncio import Task, create_task, sleep from time import monotonic from traceback import format_exc from typing import Any, Callable, Coroutine, Final, Optional, Set + log = logging.getLogger('pyartnet.Task') -def log_exception(e: Exception, name: str): +def log_exception(e: Exception, name: str) -> None: log.error(f'Error in worker for {name:s}:') for line in format_exc().splitlines(): log.error(line) @@ -22,7 +23,7 @@ def log_exception(e: Exception, name: str): class SimpleBackgroundTask: - def __init__(self, coro: Callable[[], Coroutine], name: str): + def __init__(self, coro: Callable[[], Coroutine], name: str) -> None: self.coro: Final = coro self.name: Final = name self.task: Optional[Task] = None @@ -42,7 +43,7 @@ def cancel(self): self.task.cancel() self.task = None - async def coro_wrap(self): + async def coro_wrap(self) -> None: log.debug(f'Started {self.name}') task = self.task assert task is not None @@ -58,7 +59,7 @@ async def coro_wrap(self): class ExceptionIgnoringTask(SimpleBackgroundTask): - async def coro_wrap(self): + async def coro_wrap(self) -> None: log.debug(f'Started {self.name}') task = self.task assert task is not None diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index c59b27e..5ff02af 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -10,6 +10,7 @@ from .background_task import ExceptionIgnoringTask, SimpleBackgroundTask from .output_correction import OutputCorrection + log = logging.getLogger('pyartnet.ArtNetNode') diff --git a/src/pyartnet/base/channel_fade.py b/src/pyartnet/base/channel_fade.py index 87c0c32..b2bdb17 100644 --- a/src/pyartnet/base/channel_fade.py +++ b/src/pyartnet/base/channel_fade.py @@ -1,6 +1,7 @@ import logging from asyncio import Event -from typing import Final, Iterable, List, Tuple, TYPE_CHECKING +from typing import TYPE_CHECKING, Final, Iterable, List, Tuple + if TYPE_CHECKING: import pyartnet @@ -11,7 +12,7 @@ # noinspection PyProtectedMember class ChannelBoundFade: - def __init__(self, channel: 'pyartnet.base.Channel', fades: Iterable['pyartnet.fades.FadeBase']): + def __init__(self, channel: 'pyartnet.base.Channel', fades: Iterable['pyartnet.fades.FadeBase']) -> None: super().__init__() self.channel: 'pyartnet.base.Channel' = channel @@ -21,7 +22,7 @@ def __init__(self, channel: 'pyartnet.base.Channel', fades: Iterable['pyartnet.f self.is_done = False self.event: Final = Event() - def process(self): + def process(self) -> None: finished = True for i, fade in enumerate(self.fades): if fade.is_done: @@ -35,7 +36,7 @@ def process(self): self.is_done = finished self.channel.set_values(self.values) - def cancel(self): + def cancel(self) -> None: # remove fade from channel c = self.channel self.channel = None # type: ignore[assignment] @@ -46,7 +47,7 @@ def cancel(self): # remove from parent node c._parent_node._process_jobs.remove(self) - def fade_complete(self): + def fade_complete(self) -> None: # remove fade from channel c = self.channel self.channel = None # type: ignore[assignment] @@ -57,7 +58,7 @@ def fade_complete(self): if c.callback_fade_finished is not None: c.callback_fade_finished(c) - def __repr__(self): + def __repr__(self) -> str: # Channel part if self.channel is not None: channel_part = f'channel={self.channel._start:d}/{self.channel._width:d}' diff --git a/src/pyartnet/base/universe.py b/src/pyartnet/base/universe.py index 5718ca9..6b17490 100644 --- a/src/pyartnet/base/universe.py +++ b/src/pyartnet/base/universe.py @@ -3,17 +3,22 @@ from typing import Dict, Final, Literal import pyartnet -from pyartnet.errors import ChannelExistsError, ChannelNotFoundError, \ - InvalidUniverseAddressError, OverlappingChannelError +from pyartnet.errors import ( + ChannelExistsError, + ChannelNotFoundError, + InvalidUniverseAddressError, + OverlappingChannelError, +) from .output_correction import OutputCorrection + log = logging.getLogger('pyartnet.Universe') # noinspection PyProtectedMember class BaseUniverse(OutputCorrection): - def __init__(self, node: 'pyartnet.base.BaseNode', universe: int = 0): + def __init__(self, node: 'pyartnet.base.BaseNode', universe: int = 0) -> None: super().__init__() if not 0 <= universe <= 32767: @@ -29,11 +34,11 @@ def __init__(self, node: 'pyartnet.base.BaseNode', universe: int = 0): self._channels: Dict[str, 'pyartnet.base.Channel'] = {} - def _apply_output_correction(self): + def _apply_output_correction(self) -> None: for c in self._channels.values(): c._apply_output_correction() - def channel_changed(self, channel: 'pyartnet.base.Channel'): + def channel_changed(self, channel: 'pyartnet.base.Channel') -> None: # update universe buffer channel.to_buffer(self._data) @@ -44,7 +49,7 @@ def channel_changed(self, channel: 'pyartnet.base.Channel'): # noinspection PyProtectedMember self._node._process_task.start() - def send_data(self): + def send_data(self) -> None: self._node._send_universe(self._universe, self._data_size, self._data, self) self._last_send = monotonic() self._data_changed = False @@ -126,7 +131,7 @@ def _resize_universe(self, min_size: int): # ----------------------------------------------------------- # emulate container - def __len__(self): + def __len__(self) -> int: return len(self._channels) def __getitem__(self, item: str) -> 'pyartnet.base.Channel': diff --git a/tests/channel/test_boundaries.py b/tests/channel/test_boundaries.py index efff26d..8f88107 100644 --- a/tests/channel/test_boundaries.py +++ b/tests/channel/test_boundaries.py @@ -3,11 +3,14 @@ import pytest from pyartnet.base.channel import Channel -from pyartnet.errors import ChannelOutOfUniverseError, \ - ChannelValueOutOfBoundsError, ValueCountDoesNotMatchChannelWidthError +from pyartnet.errors import ( + ChannelOutOfUniverseError, + ChannelValueOutOfBoundsError, + ValueCountDoesNotMatchChannelWidthError, +) -def test_channel_boundaries(): +def test_channel_boundaries() -> None: univ = Mock() with pytest.raises(ChannelOutOfUniverseError) as r: @@ -46,7 +49,7 @@ def get_node_universe_mock(): ('width', 'byte_size', 'invalid', 'valid'), ((1, 1, -1, 255), (1, 1, 256, 255), (3, 1, 256, 255), (1, 2, -1, 65535), (1, 2, 65536, 65535), (3, 2, 65536, 65535), )) -def test_set_invalid(width, byte_size, invalid, valid): +def test_set_invalid(width, byte_size, invalid, valid) -> None: node, universe = get_node_universe_mock() invalid_values = [0] * (width - 1) + [invalid] @@ -67,7 +70,7 @@ def test_set_invalid(width, byte_size, invalid, valid): c.set_fade(valid_values, 100) -async def test_set_missing(): +async def test_set_missing() -> None: node, universe = get_node_universe_mock() c = Channel(universe, 1, 1) diff --git a/tests/channel/test_fade.py b/tests/channel/test_fade.py index a66273a..d3f40a7 100644 --- a/tests/channel/test_fade.py +++ b/tests/channel/test_fade.py @@ -1,12 +1,13 @@ import asyncio from time import monotonic +from tests.conftest import STEP_MS, TestingNode + from pyartnet.base import BaseUniverse from pyartnet.base.channel import Channel -from tests.conftest import STEP_MS, TestingNode -async def test_channel_await(node: TestingNode, universe: BaseUniverse, caplog): +async def test_channel_await(node: TestingNode, universe: BaseUniverse, caplog) -> None: a = Channel(universe, 1, 1) assert a.get_values() == [0] @@ -19,7 +20,7 @@ async def test_channel_await(node: TestingNode, universe: BaseUniverse, caplog): assert stop - start >= 0.2 -async def test_single_step(node: TestingNode, universe: BaseUniverse, caplog): +async def test_single_step(node: TestingNode, universe: BaseUniverse, caplog) -> None: caplog.set_level(0) a = Channel(universe, 1, 1) @@ -41,7 +42,7 @@ async def test_single_step(node: TestingNode, universe: BaseUniverse, caplog): assert node.data == ['ff'] -async def test_single_fade(node: TestingNode, universe: BaseUniverse, caplog): +async def test_single_fade(node: TestingNode, universe: BaseUniverse, caplog) -> None: caplog.set_level(0) a = Channel(universe, 1, 1) @@ -63,7 +64,7 @@ async def test_single_fade(node: TestingNode, universe: BaseUniverse, caplog): assert node.data == ['01', '02'] -async def test_tripple_fade(node: TestingNode, universe: BaseUniverse, caplog): +async def test_tripple_fade(node: TestingNode, universe: BaseUniverse, caplog) -> None: caplog.set_level(0) a = Channel(universe, 1, 3) @@ -87,13 +88,13 @@ async def test_tripple_fade(node: TestingNode, universe: BaseUniverse, caplog): assert node.data == ['010203', '020406', '030609'] -async def test_fade_await(node: TestingNode, universe: BaseUniverse, caplog): +async def test_fade_await(node: TestingNode, universe: BaseUniverse, caplog) -> None: caplog.set_level(0) channel = Channel(universe, 1, 1) assert channel.get_values() == [0] - async def check_no_wait_time_when_no_fade(): + async def check_no_wait_time_when_no_fade() -> None: start = monotonic() for _ in range(1000): assert not await channel @@ -128,7 +129,7 @@ async def check_no_wait_time_when_no_fade(): await node.wait_for_task_finish() -async def test_up_down_fade(node: TestingNode, universe: BaseUniverse, caplog): +async def test_up_down_fade(node: TestingNode, universe: BaseUniverse, caplog) -> None: caplog.set_level(0) a = Channel(universe, 1, 1) diff --git a/tests/channel/test_set_values.py b/tests/channel/test_set_values.py index a70d0a3..ebd2af3 100644 --- a/tests/channel/test_set_values.py +++ b/tests/channel/test_set_values.py @@ -1,9 +1,10 @@ +from tests.conftest import TestingNode + from pyartnet.base import BaseUniverse from pyartnet.base.channel import Channel -from tests.conftest import TestingNode -async def test_channel_set_values(node: TestingNode, universe: BaseUniverse, caplog): +async def test_channel_set_values(node: TestingNode, universe: BaseUniverse, caplog) -> None: a = Channel(universe, 1, 1) assert a.get_values() == [0] diff --git a/tests/conftest.py b/tests/conftest.py index 757bccd..f9457d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,11 +3,12 @@ from typing import List import pytest +from tests.helper import MockedSocket import pyartnet.base.base_node from pyartnet.base import BaseNode, BaseUniverse from pyartnet.base.base_node import TYPE_U -from tests.helper import MockedSocket + STEP_MS = 15 diff --git a/tests/test_base_node.py b/tests/test_base_node.py index df4e0ab..41786cd 100644 --- a/tests/test_base_node.py +++ b/tests/test_base_node.py @@ -1,11 +1,11 @@ from time import monotonic import pytest +from tests.conftest import STEP_MS, TestingNode from pyartnet.base import BaseUniverse from pyartnet.base.channel import Channel from pyartnet.errors import DuplicateUniverseError -from tests.conftest import STEP_MS, TestingNode def test_universe_add_get(node: TestingNode) -> None: diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index 8f9c42d..a6f70cc 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -3,11 +3,11 @@ from asyncio import sleep import pytest +from tests.conftest import TestingNode from pyartnet import ArtNetNode, KiNetNode, SacnNode from pyartnet.base import BaseNode from pyartnet.errors import InvalidUniverseAddressError -from tests.conftest import TestingNode @pytest.mark.parametrize('c', (ArtNetNode, KiNetNode, SacnNode)) From 78fa5e6e78c75eac0faff13885d67737fb73c2ca Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:20:59 +0200 Subject: [PATCH 19/49] . --- .ruff.toml | 5 +++-- src/pyartnet/base/base_node.py | 12 +++++++----- src/pyartnet/impl_artnet/node.py | 6 ++++-- src/pyartnet/impl_kinet/node.py | 6 ++++-- src/pyartnet/impl_sacn/node.py | 7 ++++--- tests/conftest.py | 5 +++-- tests/test_impl/__init__.py | 0 tests/test_impl/test_sacn.py | 12 ++++++------ 8 files changed, 31 insertions(+), 22 deletions(-) create mode 100644 tests/test_impl/__init__.py diff --git a/.ruff.toml b/.ruff.toml index 36e1d9b..c4e3be8 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -90,8 +90,6 @@ lines-after-imports = 2 # https://docs.astral.sh/ruff/settings/#lint_isort_l "PTH100", # `os.path.abspath()` should be replaced by `Path.resolve()` ] -"setup.py" = ["PTH123"] - "tests/*" = [ "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann @@ -101,4 +99,7 @@ lines-after-imports = 2 # https://docs.astral.sh/ruff/settings/#lint_isort_l # https://docs.astral.sh/ruff/rules/#refactor-r "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLR0913", # Too many arguments in function definition ({c_args} > {max_args}) + + # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt + "FBT003", # Boolean positional value in function call ] diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 5ff02af..7aabbdc 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import logging import socket from asyncio import sleep from time import monotonic -from typing import Dict, Final, Generic, List, Optional, Tuple, TypeVar, Union +from typing import Final, Generic, Optional, TypeVar, Union import pyartnet @@ -22,7 +24,7 @@ class BaseNode(Generic[TYPE_U], OutputCorrection): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, - source_address: Optional[Tuple[str, int]] = None) -> None: + source_address: Optional[tuple[str, int]] = None) -> None: super().__init__() # Destination @@ -51,15 +53,15 @@ def __init__(self, ip: str, port: int, *, # fade task self._process_every: float = 1 / max(1, max_fps) self._process_task: Final = SimpleBackgroundTask(self._process_values_task, f'Process task {name:s}') - self._process_jobs: List['pyartnet.base.ChannelBoundFade'] = [] + self._process_jobs: list['pyartnet.base.ChannelBoundFade'] = [] # packet data self._packet_base: Union[bytearray, bytes] = bytearray() self._last_send: float = 0 # containing universes - self._universes: Tuple[TYPE_U, ...] = () - self._universe_map: Dict[int, TYPE_U] = {} + self._universes: tuple[TYPE_U, ...] = () + self._universe_map: dict[int, TYPE_U] = {} def _apply_output_correction(self) -> None: for u in self._universes: diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 51e1234..d6708dc 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import logging -from typing import Final, Optional, Tuple, Union +from typing import Final, Optional, Union import pyartnet from pyartnet.base import BaseNode @@ -19,7 +21,7 @@ class ArtNetNode(BaseNode['pyartnet.impl_artnet.ArtNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, - source_address: Optional[Tuple[str, int]] = None, + source_address: Optional[tuple[str, int]] = None, # ArtNet specific fields sequence_counter: bool = True diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index 0ff843e..add969e 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import logging from logging import DEBUG as LVL_DEBUG from struct import pack as s_pack -from typing import Optional, Tuple, Union +from typing import Optional, Union import pyartnet from pyartnet.base import BaseNode @@ -20,7 +22,7 @@ class KiNetNode(BaseNode['pyartnet.impl_kinet.KiNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, - source_address: Optional[Tuple[str, int]] = None) -> None: + source_address: Optional[tuple[str, int]] = None) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, refresh_every=refresh_every, start_refresh_task=start_refresh_task, diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 4186317..d68d4f0 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -1,8 +1,9 @@ -# flake8: noqa: E262 +from __future__ import annotations + import logging from ipaddress import IPv6Address from logging import DEBUG as LVL_DEBUG -from typing import Final, Optional, Tuple, Union +from typing import Final, Optional, Union from uuid import uuid4 import pyartnet.impl_sacn.universe @@ -33,7 +34,7 @@ class SacnNode(BaseNode['pyartnet.impl_sacn.SacnUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, - source_address: Optional[Tuple[str, int]] = None, + source_address: Optional[tuple[str, int]] = None, # sACN E1.31 specific fields cid: Optional[bytes] = None, source_name: Optional[str] = None diff --git a/tests/conftest.py b/tests/conftest.py index f9457d9..15fc6d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import logging from asyncio import sleep -from typing import List import pytest from tests.helper import MockedSocket @@ -67,7 +68,7 @@ def ensure_no_errors(caplog): yield None - log_records: List[logging.LogRecord] = [] + log_records: list[logging.LogRecord] = [] name_indent = 0 level_indent = 0 diff --git a/tests/test_impl/__init__.py b/tests/test_impl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index bc6ed91..7e58820 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -18,9 +18,9 @@ async def test_sacn() -> None: channel = sacn.add_universe(1).add_channel(1, 10) channel.set_values(range(1, 11)) - data = '001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' \ - '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' \ - '0000000064000000000001701502a100000001000b000102030405060708090a' + data = ('001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' + '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' + '0000000064000000000001701502a100000001000b000102030405060708090a') await channel await sacn._process_task.task @@ -47,9 +47,9 @@ async def test_sacn_with_sync(caplog, multicast) -> None: channel = sacn.add_universe(1).add_channel(1, 10) channel.set_values(range(1, 11)) - data = '001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' \ - '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' \ - '0000000064000200000001701502a100000001000b000102030405060708090a' + data = ('001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' + '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' + '0000000064000200000001701502a100000001000b000102030405060708090a') sync_data = '001000004153432d45312e31370000007021000000084168f52b1a7b2de11712e9ee383d2258700b000000010000020000' From 830834a77b292d0cbcf92e780de0e6ab957d8195 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:23:39 +0200 Subject: [PATCH 20/49] . --- src/pyartnet/base/base_node.py | 8 ++++---- src/pyartnet/impl_artnet/node.py | 6 +++--- src/pyartnet/impl_kinet/node.py | 4 ++-- src/pyartnet/impl_sacn/node.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 7aabbdc..cc8967a 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -4,7 +4,7 @@ import socket from asyncio import sleep from time import monotonic -from typing import Final, Generic, Optional, TypeVar, Union +from typing import Final, Generic, Optional, TypeVar import pyartnet @@ -23,7 +23,7 @@ class BaseNode(Generic[TYPE_U], OutputCorrection): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, + refresh_every: int | float | None = 2, start_refresh_task: bool = True, source_address: Optional[tuple[str, int]] = None) -> None: super().__init__() @@ -56,7 +56,7 @@ def __init__(self, ip: str, port: int, *, self._process_jobs: list['pyartnet.base.ChannelBoundFade'] = [] # packet data - self._packet_base: Union[bytearray, bytes] = bytearray() + self._packet_base: bytearray | bytes = bytearray() self._last_send: float = 0 # containing universes @@ -76,7 +76,7 @@ def set_synchronous_mode(self, enabled: bool): def _send_synchronization(self) -> None: pass - def _send_data(self, data: Union[bytearray, bytes], dst: tuple[str, int] | str | None = None) -> int: + def _send_data(self, data: bytearray | bytes, dst: tuple[str, int] | str | None = None) -> int: ret = self._socket.sendto(self._packet_base + data, self._dst if dst is None else dst) diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index d6708dc..c6d3766 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import Final, Optional, Union +from typing import Final, Optional import pyartnet from pyartnet.base import BaseNode @@ -20,7 +20,7 @@ class ArtNetNode(BaseNode['pyartnet.impl_artnet.ArtNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, + refresh_every: int | float | None = 2, start_refresh_task: bool = True, source_address: Optional[tuple[str, int]] = None, # ArtNet specific fields @@ -77,7 +77,7 @@ def _validate_universe_nr(self, nr: int) -> int: raise InvalidUniverseAddressError() return int(nr) - def __log_artnet_frame(self, p: Union[bytearray, bytes]) -> None: + def __log_artnet_frame(self, p: bytearray | bytes) -> None: """Log Artnet Frame""" assert isinstance(p, (bytearray, bytes)) diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index add969e..096aaae 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -3,7 +3,7 @@ import logging from logging import DEBUG as LVL_DEBUG from struct import pack as s_pack -from typing import Optional, Union +from typing import Optional import pyartnet from pyartnet.base import BaseNode @@ -21,7 +21,7 @@ class KiNetNode(BaseNode['pyartnet.impl_kinet.KiNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, + refresh_every: int | float | None = 2, start_refresh_task: bool = True, source_address: Optional[tuple[str, int]] = None) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index d68d4f0..7114b9d 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -3,7 +3,7 @@ import logging from ipaddress import IPv6Address from logging import DEBUG as LVL_DEBUG -from typing import Final, Optional, Union +from typing import Final, Optional from uuid import uuid4 import pyartnet.impl_sacn.universe @@ -33,7 +33,7 @@ class SacnNode(BaseNode['pyartnet.impl_sacn.SacnUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: Union[int, float, None] = 2, start_refresh_task: bool = True, + refresh_every: int | float | None = 2, start_refresh_task: bool = True, source_address: Optional[tuple[str, int]] = None, # sACN E1.31 specific fields From 2cf3353563887f79bfaeb32cc2cb82aed9725cb1 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:58:23 +0200 Subject: [PATCH 21/49] . --- src/pyartnet/base/background_task.py | 6 ++++-- src/pyartnet/base/base_node.py | 4 ++-- src/pyartnet/base/channel.py | 8 +++++--- src/pyartnet/base/channel_fade.py | 2 ++ src/pyartnet/base/output_correction.py | 10 ++++++---- src/pyartnet/base/universe.py | 2 ++ src/pyartnet/impl_artnet/node.py | 4 ++-- src/pyartnet/impl_kinet/node.py | 3 +-- src/pyartnet/impl_sacn/node.py | 6 +++--- tests/channel/test_buffer.py | 12 +++++++----- 10 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/pyartnet/base/background_task.py b/src/pyartnet/base/background_task.py index d65a30a..09a89ff 100644 --- a/src/pyartnet/base/background_task.py +++ b/src/pyartnet/base/background_task.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import logging from asyncio import Task, create_task, sleep from time import monotonic from traceback import format_exc -from typing import Any, Callable, Coroutine, Final, Optional, Set +from typing import Any, Callable, Coroutine, Final, Set log = logging.getLogger('pyartnet.Task') @@ -26,7 +28,7 @@ class SimpleBackgroundTask: def __init__(self, coro: Callable[[], Coroutine], name: str) -> None: self.coro: Final = coro self.name: Final = name - self.task: Optional[Task] = None + self.task: Task | None = None def start(self): if self.task is not None: diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index cc8967a..8b4f183 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -4,7 +4,7 @@ import socket from asyncio import sleep from time import monotonic -from typing import Final, Generic, Optional, TypeVar +from typing import Final, Generic, TypeVar import pyartnet @@ -24,7 +24,7 @@ class BaseNode(Generic[TYPE_U], OutputCorrection): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: int | float | None = 2, start_refresh_task: bool = True, - source_address: Optional[tuple[str, int]] = None) -> None: + source_address: tuple[str, int] | None = None) -> None: super().__init__() # Destination diff --git a/src/pyartnet/base/channel.py b/src/pyartnet/base/channel.py index fc554a2..10afeb8 100644 --- a/src/pyartnet/base/channel.py +++ b/src/pyartnet/base/channel.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import logging import warnings from array import array from collections.abc import Callable, Collection from logging import DEBUG as LVL_DEBUG from math import ceil -from typing import Any, Final, List, Literal, Optional, Type, Union +from typing import Any, Final, List, Literal, Type, Union from pyartnet.errors import ( ChannelOutOfUniverseError, @@ -78,13 +80,13 @@ def __init__(self, universe: BaseUniverse, self._correction_current: Callable[[float, int], float] = linear # Fade - self._current_fade: Optional[ChannelBoundFade] = None + self._current_fade: ChannelBoundFade | None = None # --------------------------------------------------------------------- # Values that can be set by the user # --------------------------------------------------------------------- # Callbacks - self.callback_fade_finished: Optional[Callable[[Channel], Any]] = None + self.callback_fade_finished: Callable[[Channel], Any] | None = None def _apply_output_correction(self): # default correction is linear diff --git a/src/pyartnet/base/channel_fade.py b/src/pyartnet/base/channel_fade.py index b2bdb17..f1ab64d 100644 --- a/src/pyartnet/base/channel_fade.py +++ b/src/pyartnet/base/channel_fade.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from asyncio import Event from typing import TYPE_CHECKING, Final, Iterable, List, Tuple diff --git a/src/pyartnet/base/output_correction.py b/src/pyartnet/base/output_correction.py index 0e3e7bc..b7d57e6 100644 --- a/src/pyartnet/base/output_correction.py +++ b/src/pyartnet/base/output_correction.py @@ -1,12 +1,14 @@ -from typing import Callable, Optional +from __future__ import annotations + +from typing import Callable class OutputCorrection: - def __init__(self): + def __init__(self) -> None: super().__init__() - self._correction_output: Optional[Callable[[float, int], float]] = None + self._correction_output: Callable[[float, int], float] | None = None - def set_output_correction(self, func: Optional[Callable[[float, int], float]]) -> None: + def set_output_correction(self, func: Callable[[float, int], float] | None) -> None: """Set the output correction function. :param func: None to disable output correction or the function which will be used to transform the values diff --git a/src/pyartnet/base/universe.py b/src/pyartnet/base/universe.py index 6b17490..ee67086 100644 --- a/src/pyartnet/base/universe.py +++ b/src/pyartnet/base/universe.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from time import monotonic from typing import Dict, Final, Literal diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index c6d3766..8817a99 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import Final, Optional +from typing import Final import pyartnet from pyartnet.base import BaseNode @@ -21,7 +21,7 @@ class ArtNetNode(BaseNode['pyartnet.impl_artnet.ArtNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: int | float | None = 2, start_refresh_task: bool = True, - source_address: Optional[tuple[str, int]] = None, + source_address: tuple[str, int] | None = None, # ArtNet specific fields sequence_counter: bool = True diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index 096aaae..b2f2038 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -3,7 +3,6 @@ import logging from logging import DEBUG as LVL_DEBUG from struct import pack as s_pack -from typing import Optional import pyartnet from pyartnet.base import BaseNode @@ -22,7 +21,7 @@ class KiNetNode(BaseNode['pyartnet.impl_kinet.KiNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: int | float | None = 2, start_refresh_task: bool = True, - source_address: Optional[tuple[str, int]] = None) -> None: + source_address: tuple[str, int] | None = None) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, refresh_every=refresh_every, start_refresh_task=start_refresh_task, diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 7114b9d..486737e 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -3,7 +3,7 @@ import logging from ipaddress import IPv6Address from logging import DEBUG as LVL_DEBUG -from typing import Final, Optional +from typing import Final from uuid import uuid4 import pyartnet.impl_sacn.universe @@ -34,10 +34,10 @@ class SacnNode(BaseNode['pyartnet.impl_sacn.SacnUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: int | float | None = 2, start_refresh_task: bool = True, - source_address: Optional[tuple[str, int]] = None, + source_address: tuple[str, int] | None = None, # sACN E1.31 specific fields - cid: Optional[bytes] = None, source_name: Optional[str] = None + cid: bytes | None = None, source_name: str | None = None ) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, diff --git a/tests/channel/test_buffer.py b/tests/channel/test_buffer.py index 3462f58..152576b 100644 --- a/tests/channel/test_buffer.py +++ b/tests/channel/test_buffer.py @@ -1,10 +1,12 @@ -from typing import Iterable, Optional +from __future__ import annotations + +from typing import Iterable from unittest.mock import Mock from pyartnet.base.channel import Channel -def to_buf(c: Channel, v: Iterable[int], buf: Optional[bytearray] = None) -> bytearray: +def to_buf(c: Channel, v: Iterable[int], buf: bytearray | None = None) -> bytearray: c.set_values(v) assert c.get_values() == list(v) @@ -14,7 +16,7 @@ def to_buf(c: Channel, v: Iterable[int], buf: Optional[bytearray] = None) -> byt return buf -def test_channel_1b_values_single(): +def test_channel_1b_values_single() -> None: universe = Mock() universe.output_correction = None @@ -34,7 +36,7 @@ def test_channel_1b_values_single(): assert buf == b'\xf0\x00\xff\x00\x0f' -def test_channel_1b_values_multiple(): +def test_channel_1b_values_multiple() -> None: universe = Mock() universe.output_correction = None @@ -46,7 +48,7 @@ def test_channel_1b_values_multiple(): assert to_buf(c, [128, 0, 255]) == b'\x00\x00\x80\x00\xff' -def test_channel_2b_values_single(): +def test_channel_2b_values_single() -> None: universe = Mock() universe.output_correction = None From 13ba65d33601450ce7a958b725925a9d3d830d29 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:10:04 +0200 Subject: [PATCH 22/49] . --- src/pyartnet/base/background_task.py | 4 +-- src/pyartnet/base/base_node.py | 29 ++++++++++-------- src/pyartnet/base/channel.py | 45 ++++++++++++++++------------ src/pyartnet/base/channel_fade.py | 10 +++---- src/pyartnet/base/universe.py | 28 +++++++++-------- src/pyartnet/impl_artnet/node.py | 12 ++++---- src/pyartnet/impl_kinet/node.py | 4 +-- src/pyartnet/impl_sacn/node.py | 10 ++++--- src/pyartnet/impl_sacn/universe.py | 7 +++-- tests/conftest.py | 12 +++++--- 10 files changed, 92 insertions(+), 69 deletions(-) diff --git a/src/pyartnet/base/background_task.py b/src/pyartnet/base/background_task.py index 09a89ff..0a011a3 100644 --- a/src/pyartnet/base/background_task.py +++ b/src/pyartnet/base/background_task.py @@ -4,7 +4,7 @@ from asyncio import Task, create_task, sleep from time import monotonic from traceback import format_exc -from typing import Any, Callable, Coroutine, Final, Set +from typing import Any, Callable, Coroutine, Final log = logging.getLogger('pyartnet.Task') @@ -16,7 +16,7 @@ def log_exception(e: Exception, name: str) -> None: log.error(line) -_BACKGROUND_TASKS: Set[Task] = set() +_BACKGROUND_TASKS: set[Task] = set() # use variables, so it's easy to e.g. implement thread safe scheduling CREATE_TASK = create_task diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 8b4f183..7fa7b03 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -4,23 +4,26 @@ import socket from asyncio import sleep from time import monotonic -from typing import Final, Generic, TypeVar +from typing import TYPE_CHECKING, Final, Generic, TypeVar -import pyartnet +from pyartnet.errors import DuplicateUniverseError, UniverseNotFoundError -from ..errors import DuplicateUniverseError, UniverseNotFoundError from .background_task import ExceptionIgnoringTask, SimpleBackgroundTask from .output_correction import OutputCorrection +if TYPE_CHECKING: + import pyartnet + + log = logging.getLogger('pyartnet.ArtNetNode') -TYPE_U = TypeVar('TYPE_U', bound='pyartnet.base.BaseUniverse') +UNIVERSE_TYPE = TypeVar('UNIVERSE_TYPE', bound='pyartnet.base.BaseUniverse') # noinspection PyProtectedMember -class BaseNode(Generic[TYPE_U], OutputCorrection): +class BaseNode(OutputCorrection, Generic[UNIVERSE_TYPE]): def __init__(self, ip: str, port: int, *, max_fps: int = 25, refresh_every: int | float | None = 2, start_refresh_task: bool = True, @@ -53,21 +56,21 @@ def __init__(self, ip: str, port: int, *, # fade task self._process_every: float = 1 / max(1, max_fps) self._process_task: Final = SimpleBackgroundTask(self._process_values_task, f'Process task {name:s}') - self._process_jobs: list['pyartnet.base.ChannelBoundFade'] = [] + self._process_jobs: list[pyartnet.base.ChannelBoundFade] = [] # packet data self._packet_base: bytearray | bytes = bytearray() self._last_send: float = 0 # containing universes - self._universes: tuple[TYPE_U, ...] = () - self._universe_map: dict[int, TYPE_U] = {} + self._universes: tuple[UNIVERSE_TYPE, ...] = () + self._universe_map: dict[int, UNIVERSE_TYPE] = {} def _apply_output_correction(self) -> None: for u in self._universes: u._apply_output_correction() - def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: TYPE_U): + def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: UNIVERSE_TYPE): raise NotImplementedError() def set_synchronous_mode(self, enabled: bool): @@ -143,7 +146,7 @@ async def _periodic_refresh_worker(self) -> None: self._send_synchronization() - def get_universe(self, nr: int) -> TYPE_U: + def get_universe(self, nr: int) -> UNIVERSE_TYPE: """Get universe by number :param nr: universe nr @@ -157,7 +160,7 @@ def get_universe(self, nr: int) -> TYPE_U: msg = f'BaseUniverse {nr:d} not found!' raise UniverseNotFoundError(msg) from None - def add_universe(self, nr: int = 0) -> TYPE_U: + def add_universe(self, nr: int = 0) -> UNIVERSE_TYPE: """Creates a new universe and adds it to the parent node :param nr: universe nr @@ -175,7 +178,7 @@ def add_universe(self, nr: int = 0) -> TYPE_U: return universe - def _create_universe(self, nr: int) -> TYPE_U: + def _create_universe(self, nr: int) -> UNIVERSE_TYPE: raise NotImplementedError() def _validate_universe_nr(self, nr: int) -> int: @@ -186,7 +189,7 @@ def __await__(self): for job in self._process_jobs: yield from job.channel.__await__() - def __getitem__(self, nr: int) -> TYPE_U: + def __getitem__(self, nr: int) -> UNIVERSE_TYPE: return self.get_universe(nr) def __len__(self) -> int: diff --git a/src/pyartnet/base/channel.py b/src/pyartnet/base/channel.py index 10afeb8..2ec9bb9 100644 --- a/src/pyartnet/base/channel.py +++ b/src/pyartnet/base/channel.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Collection from logging import DEBUG as LVL_DEBUG from math import ceil -from typing import Any, Final, List, Literal, Type, Union +from typing import TYPE_CHECKING, Any, Final, Literal, Union from pyartnet.errors import ( ChannelOutOfUniverseError, @@ -14,12 +14,15 @@ ChannelWidthError, ValueCountDoesNotMatchChannelWidthError, ) +from pyartnet.fades import FadeBase, LinearFade from pyartnet.output_correction import linear -from ..fades import FadeBase, LinearFade from .channel_fade import ChannelBoundFade from .output_correction import OutputCorrection -from .universe import BaseUniverse + + +if TYPE_CHECKING: + from .universe import BaseUniverse log = logging.getLogger('pyartnet.Channel') @@ -41,15 +44,16 @@ def __init__(self, universe: BaseUniverse, # Validate Boundaries if byte_size not in ARRAY_TYPE: - raise ValueError(f'Value size must be {", ".join(map(str, ARRAY_TYPE))}') + msg = f'Value size must be {", ".join(map(str, ARRAY_TYPE))}' + raise ValueError(msg) if start < 1 or start > 512: - raise ChannelOutOfUniverseError( - f'Start position of channel out of universe (1..512): {start}') + msg = f'Start position of channel out of universe (1..512): {start}' + raise ChannelOutOfUniverseError(msg) if width <= 0 or not isinstance(width, int): - raise ChannelWidthError( - f'Channel width must be int > 0: {width} ({type(width)})') + msg = f'Channel width must be int > 0: {width} ({type(width)})' + raise ChannelWidthError(msg) total_byte_width: Final = width * byte_size @@ -58,10 +62,11 @@ def __init__(self, universe: BaseUniverse, self._stop: Final = start + total_byte_width - 1 if self._stop > 512: - raise ChannelOutOfUniverseError( + msg = ( f'End position of channel out of universe (1..512): ' f'start: {self._start} width: {self._width} * {byte_size}bytes -> {self._stop}' ) + raise ChannelOutOfUniverseError(msg) # value representation self._byte_size: Final = byte_size @@ -98,7 +103,7 @@ def _apply_output_correction(self): self._correction_current = obj._correction_output return None - def get_values(self) -> List[int]: + def get_values(self) -> list[int]: """Get the current (uncorrected) channel values :return: list of channel values @@ -112,8 +117,9 @@ def set_values(self, values: Collection[Union[int, float]]): """ # get output correction function if len(values) != self._width: + msg = f'Not enough fade values specified, expected {self._width} but got {len(values)}!' raise ValueCountDoesNotMatchChannelWidthError( - f'Not enough fade values specified, expected {self._width} but got {len(values)}!') + msg) correction = self._correction_current value_max = self._value_max @@ -122,7 +128,8 @@ def set_values(self, values: Collection[Union[int, float]]): for i, val in enumerate(values): raw_new = round(val) if not 0 <= raw_new <= value_max: - raise ChannelValueOutOfBoundsError(f'Channel value out of bounds! 0 <= {val} <= {value_max:d}') + msg = f'Channel value out of bounds! 0 <= {val} <= {value_max:d}' + raise ChannelValueOutOfBoundsError(msg) self._values_raw[i] = raw_new act_new = round(correction(val, value_max)) if correction is not linear else raw_new @@ -145,14 +152,14 @@ def to_buffer(self, buf: bytearray): return self def add_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, - fade_class: Type[FadeBase] = LinearFade): + fade_class: type[FadeBase] = LinearFade): warnings.warn( f'{self.set_fade.__name__:s} is deprecated, use {self.set_fade.__name__:s} instead', DeprecationWarning) return self.set_fade(values, duration_ms, fade_class) # noinspection PyProtectedMember def set_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, - fade_class: Type[FadeBase] = LinearFade): + fade_class: type[FadeBase] = LinearFade): """Add and schedule a new fade for the channel :param values: Target values for the fade @@ -161,8 +168,8 @@ def set_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, """ # check that we passed all values if len(values) != self._width: - raise ValueCountDoesNotMatchChannelWidthError( - f'Not enough fade values specified, expected {self._width} but got {len(values)}!') + msg = f'Not enough fade values specified, expected {self._width} but got {len(values)}!' + raise ValueCountDoesNotMatchChannelWidthError(msg) if self._current_fade is not None: self._current_fade.cancel() @@ -174,15 +181,15 @@ def set_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, fade_steps: int = ceil(duration_ms / step_time_ms) # build fades - fades: List[FadeBase] = [] + fades: list[FadeBase] = [] for i, target in enumerate(values): # default is linear k = fade_class() if not isinstance(target, FadeBase) else target fades.append(k) if not 0 <= target <= self._value_max: - raise ChannelValueOutOfBoundsError( - f'Target value out of bounds! 0 <= {target} <= {self._value_max}') + msg = f'Target value out of bounds! 0 <= {target} <= {self._value_max}' + raise ChannelValueOutOfBoundsError(msg) k.initialize(self._values_raw[i], target, fade_steps) diff --git a/src/pyartnet/base/channel_fade.py b/src/pyartnet/base/channel_fade.py index f1ab64d..5914506 100644 --- a/src/pyartnet/base/channel_fade.py +++ b/src/pyartnet/base/channel_fade.py @@ -2,7 +2,7 @@ import logging from asyncio import Event -from typing import TYPE_CHECKING, Final, Iterable, List, Tuple +from typing import TYPE_CHECKING, Final, Iterable if TYPE_CHECKING: @@ -14,12 +14,12 @@ # noinspection PyProtectedMember class ChannelBoundFade: - def __init__(self, channel: 'pyartnet.base.Channel', fades: Iterable['pyartnet.fades.FadeBase']) -> None: + def __init__(self, channel: pyartnet.base.Channel, fades: Iterable[pyartnet.fades.FadeBase]) -> None: super().__init__() - self.channel: 'pyartnet.base.Channel' = channel + self.channel: pyartnet.base.Channel = channel - self.fades: Tuple['pyartnet.fades.FadeBase', ...] = tuple(fades) - self.values: List[float] = [0 for _ in fades] + self.fades: tuple[pyartnet.fades.FadeBase, ...] = tuple(fades) + self.values: list[float] = [0 for _ in fades] self.is_done = False self.event: Final = Event() diff --git a/src/pyartnet/base/universe.py b/src/pyartnet/base/universe.py index ee67086..9dd9fa1 100644 --- a/src/pyartnet/base/universe.py +++ b/src/pyartnet/base/universe.py @@ -2,7 +2,7 @@ import logging from time import monotonic -from typing import Dict, Final, Literal +from typing import Final, Literal import pyartnet from pyartnet.errors import ( @@ -20,7 +20,7 @@ # noinspection PyProtectedMember class BaseUniverse(OutputCorrection): - def __init__(self, node: 'pyartnet.base.BaseNode', universe: int = 0) -> None: + def __init__(self, node: pyartnet.base.BaseNode, universe: int = 0) -> None: super().__init__() if not 0 <= universe <= 32767: @@ -34,13 +34,13 @@ def __init__(self, node: 'pyartnet.base.BaseNode', universe: int = 0) -> None: self._data_changed = True self._last_send: float = 0 - self._channels: Dict[str, 'pyartnet.base.Channel'] = {} + self._channels: dict[str, pyartnet.base.Channel] = {} def _apply_output_correction(self) -> None: for c in self._channels.values(): c._apply_output_correction() - def channel_changed(self, channel: 'pyartnet.base.Channel') -> None: + def channel_changed(self, channel: pyartnet.base.Channel) -> None: # update universe buffer channel.to_buffer(self._data) @@ -56,23 +56,25 @@ def send_data(self) -> None: self._last_send = monotonic() self._data_changed = False - def get_channel(self, channel_name: str) -> 'pyartnet.base.Channel': + def get_channel(self, channel_name: str) -> pyartnet.base.Channel: """Return a channel by name or raise an exception :param channel_name: name of the channel """ if not isinstance(channel_name, str): - raise TypeError('Channel name must be str') + msg = 'Channel name must be str' + raise TypeError(msg) try: return self._channels[channel_name] except KeyError: - raise ChannelNotFoundError(f'Channel "{channel_name}" not found in the universe!') from None + msg = f'Channel "{channel_name}" not found in the universe!' + raise ChannelNotFoundError(msg) from None def add_channel(self, start: int, width: int, channel_name: str = '', - byte_size: int = 1, byte_order: Literal['big', 'little'] = 'little') -> 'pyartnet.base.Channel': + byte_size: int = 1, byte_order: Literal['big', 'little'] = 'little') -> pyartnet.base.Channel: """Add a new channel to the universe. This will automatically resize the universe accordingly. :param start: start position in the universe @@ -90,7 +92,8 @@ def add_channel(self, # Make sure we don't accidentally overwrite the channel if channel_name in self._channels: - raise ChannelExistsError(f'Channel "{channel_name}" does already exist in the universe!') + msg = f'Channel "{channel_name}" does already exist in the universe!' + raise ChannelExistsError(msg) # Make sure channels are not overlapping because they will overwrite each other # and this leads to unintended behavior @@ -99,7 +102,8 @@ def add_channel(self, continue for i in range(_c._start, _c._stop + 1): if start <= i <= chan._stop: - raise OverlappingChannelError(f'New channel {channel_name} is overlapping with channel {_n:s}!') + msg = f'New channel {channel_name} is overlapping with channel {_n:s}!' + raise OverlappingChannelError(msg) self._resize_universe(chan._stop) @@ -110,7 +114,7 @@ def add_channel(self, chan._apply_output_correction() return chan - def _resize_universe(self, min_size: int): + def _resize_universe(self, min_size: int) -> None: new_size = max(min_size, 2) for c in self._channels.values(): @@ -136,5 +140,5 @@ def _resize_universe(self, min_size: int): def __len__(self) -> int: return len(self._channels) - def __getitem__(self, item: str) -> 'pyartnet.base.Channel': + def __getitem__(self, item: str) -> pyartnet.base.Channel: return self.get_channel(item) diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 8817a99..7ed4a20 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -16,6 +16,8 @@ log = logging.getLogger('pyartnet.ArtNetNode') +ARTNET_MAX_UNIVERSE: Final = 32_768 + class ArtNetNode(BaseNode['pyartnet.impl_artnet.ArtNetUniverse']): def __init__(self, ip: str, port: int, *, @@ -43,7 +45,7 @@ def __init__(self, ip: str, port: int, *, self._sync_enabled : bool = False def _send_universe(self, id: int, byte_size: int, values: bytearray, - universe: 'pyartnet.impl_artnet.ArtNetUniverse') -> None: + universe: pyartnet.impl_artnet.ArtNetUniverse) -> None: # pre allocate the bytearray _size = 10 + byte_size @@ -65,15 +67,13 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, if log.isEnabledFor(logging.DEBUG): self.__log_artnet_frame(self._packet_base + packet) - def _create_universe(self, nr: int) -> 'pyartnet.impl_artnet.ArtNetUniverse': - if nr >= 32_768: - raise InvalidUniverseAddressError() - return pyartnet.impl_artnet.ArtNetUniverse(self, nr) + def _create_universe(self, nr: int) -> pyartnet.impl_artnet.ArtNetUniverse: + return pyartnet.impl_artnet.ArtNetUniverse(self, self._validate_universe_nr(nr)) def _validate_universe_nr(self, nr: int) -> int: if not isinstance(nr, int): raise TypeError() - if not 0 <= nr <= 32_768: + if not 0 <= nr <= ARTNET_MAX_UNIVERSE: raise InvalidUniverseAddressError() return int(nr) diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index b2f2038..a1ade35 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -34,7 +34,7 @@ def __init__(self, ip: str, port: int, *, self._packet_base = bytes(packet) def _send_universe(self, id: int, byte_size: int, - values: bytearray, universe: 'pyartnet.impl_kinet.KiNetUniverse') -> None: + values: bytearray, universe: pyartnet.impl_kinet.KiNetUniverse) -> None: packet = bytearray() packet.append(byte_size) packet.extend(values) @@ -45,7 +45,7 @@ def _send_universe(self, id: int, byte_size: int, # log complete packet log.debug(f'Sending KiNet frame to {self._ip}:{self._port}: {(self._packet_base + packet).hex()}') - def _create_universe(self, nr: int) -> 'pyartnet.impl_kinet.KiNetUniverse': + def _create_universe(self, nr: int) -> pyartnet.impl_kinet.KiNetUniverse: return pyartnet.impl_kinet.KiNetUniverse(self, self._validate_universe_nr(nr)) def _validate_universe_nr(self, nr: int) -> int: diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 486737e..76ecb22 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -47,7 +47,8 @@ def __init__(self, ip: str, port: int, *, # CID Field if cid is not None: if not isinstance(cid, bytes) or len(cid) != 16: - raise InvalidCidError('CID must be 16bytes!') + msg = 'CID must be 16bytes!' + raise InvalidCidError(msg) else: cid = uuid4().bytes @@ -56,7 +57,8 @@ def __init__(self, ip: str, port: int, *, source_name = 'PyArtNet' source_name_byte = source_name.encode('utf-8').ljust(64, b'\x00') if len(source_name_byte) > 64: - raise ValueError('Source name too long!') + msg = 'Source name too long!' + raise ValueError(msg) self._source_name_byte : bytes = source_name_byte # See spec 9.3 Allocation of Multicast Addresses @@ -85,7 +87,7 @@ def __init__(self, ip: str, port: int, *, # noinspection PyProtectedMember def _send_universe(self, id: int, byte_size: int, values: bytearray, - universe: 'pyartnet.impl_sacn.universe.SacnUniverse') -> None: + universe: pyartnet.impl_sacn.universe.SacnUniverse) -> None: packet = bytearray() # DMX Start Code is not included in the byte size from the universe @@ -126,7 +128,7 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, # log complete packet log.debug(f'Sending sACN frame to {_dst_str(universe._dst)}: {(base_packet + packet).hex()}') - def _create_universe(self, nr: int) -> 'pyartnet.impl_sacn.SacnUniverse': + def _create_universe(self, nr: int) -> pyartnet.impl_sacn.SacnUniverse: return pyartnet.impl_sacn.SacnUniverse(self, self._validate_universe_nr(nr)) def _validate_universe_nr(self, nr: int) -> int: diff --git a/src/pyartnet/impl_sacn/universe.py b/src/pyartnet/impl_sacn/universe.py index 337fa16..0bdefff 100644 --- a/src/pyartnet/impl_sacn/universe.py +++ b/src/pyartnet/impl_sacn/universe.py @@ -1,10 +1,13 @@ -from typing import Final +from typing import TYPE_CHECKING, Final -import pyartnet from pyartnet.base import BaseUniverse from pyartnet.base.seq_counter import SequenceCounter +if TYPE_CHECKING: + import pyartnet + + class SacnUniverse(BaseUniverse): def __init__(self, node: 'pyartnet.impl_sacn.SacnNode', universe: int = 0) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 15fc6d0..f9bafa8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,13 +2,17 @@ import logging from asyncio import sleep +from typing import TYPE_CHECKING import pytest from tests.helper import MockedSocket -import pyartnet.base.base_node from pyartnet.base import BaseNode, BaseUniverse -from pyartnet.base.base_node import TYPE_U + + +if TYPE_CHECKING: + import pyartnet.base.base_node + from pyartnet.base.base_node import UNIVERSE_TYPE STEP_MS = 15 @@ -22,7 +26,7 @@ def __init__(self, ip: str, port: int) -> None: self.data = [] def _send_universe(self, id: int, byte_size: int, - values: bytearray, universe: 'pyartnet.base.BaseUniverse') -> None: + values: bytearray, universe: pyartnet.base.BaseUniverse) -> None: self.data.append(values.hex()) async def sleep_steps(self, steps: int) -> None: @@ -33,7 +37,7 @@ async def sleep_steps(self, steps: int) -> None: async def wait_for_task_finish(self) -> None: await self - def _create_universe(self, nr: int) -> TYPE_U: + def _create_universe(self, nr: int) -> UNIVERSE_TYPE: return BaseUniverse(self, nr) def _validate_universe_nr(self, nr: int) -> int: From 1e24b268d92b05b296728d3a960ede1ce9220f37 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:21:38 +0200 Subject: [PATCH 23/49] . --- pyproject.toml | 4 +++- src/pyartnet/base/base_node.py | 8 +++++--- src/pyartnet/impl_artnet/node.py | 15 ++++++++++----- src/pyartnet/impl_kinet/node.py | 7 ++++++- src/pyartnet/impl_sacn/node.py | 12 ++++++++++-- uv.lock | 5 +++++ 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1ddf4c2..91c2326 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,9 @@ version = "1.0.1" description = "Python wrappers for the Art-Net protocol to send DMX over Ethernet" keywords = ["DMX", "Art-Net", "ArtNet", "sACN", "E1.31", "KiNet"] readme = "readme.md" -dependencies = [] +dependencies = [ + "typing-extensions" +] requires-python = ">= 3.8" authors = [ diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 7fa7b03..fc78315 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -6,6 +6,8 @@ from time import monotonic from typing import TYPE_CHECKING, Final, Generic, TypeVar +from typing_extensions import Self + from pyartnet.errors import DuplicateUniverseError, UniverseNotFoundError from .background_task import ExceptionIgnoringTask, SimpleBackgroundTask @@ -26,7 +28,7 @@ class BaseNode(OutputCorrection, Generic[UNIVERSE_TYPE]): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: int | float | None = 2, start_refresh_task: bool = True, + refresh_every: float | None = 2, start_refresh_task: bool = True, source_address: tuple[str, int] | None = None) -> None: super().__init__() @@ -70,10 +72,10 @@ def _apply_output_correction(self) -> None: for u in self._universes: u._apply_output_correction() - def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: UNIVERSE_TYPE): + def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: UNIVERSE_TYPE) -> None: raise NotImplementedError() - def set_synchronous_mode(self, enabled: bool): + def set_synchronous_mode(self, enabled: bool) -> Self: raise NotImplementedError() def _send_synchronization(self) -> None: diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 7ed4a20..7dc09d7 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -3,6 +3,8 @@ import logging from typing import Final +from typing_extensions import Self, override + import pyartnet from pyartnet.base import BaseNode from pyartnet.base.seq_counter import SequenceCounter @@ -16,13 +18,11 @@ log = logging.getLogger('pyartnet.ArtNetNode') -ARTNET_MAX_UNIVERSE: Final = 32_768 - class ArtNetNode(BaseNode['pyartnet.impl_artnet.ArtNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: int | float | None = 2, start_refresh_task: bool = True, + refresh_every: float | None = 2, start_refresh_task: bool = True, source_address: tuple[str, int] | None = None, # ArtNet specific fields @@ -44,6 +44,7 @@ def __init__(self, ip: str, port: int, *, self._sync_enabled : bool = False + @override def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: pyartnet.impl_artnet.ArtNetUniverse) -> None: @@ -67,13 +68,15 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, if log.isEnabledFor(logging.DEBUG): self.__log_artnet_frame(self._packet_base + packet) + @override def _create_universe(self, nr: int) -> pyartnet.impl_artnet.ArtNetUniverse: return pyartnet.impl_artnet.ArtNetUniverse(self, self._validate_universe_nr(nr)) + @override def _validate_universe_nr(self, nr: int) -> int: if not isinstance(nr, int): raise TypeError() - if not 0 <= nr <= ARTNET_MAX_UNIVERSE: + if not 0 <= nr <= 32_768: raise InvalidUniverseAddressError() return int(nr) @@ -143,7 +146,8 @@ def __log_artnet_frame(self, p: bytearray | bytes) -> None: log.debug(out_desc) log.debug(out) - def set_synchronous_mode(self, enabled: bool): + @override + def set_synchronous_mode(self, enabled: bool) -> Self: if self._refresh_every > 3.5: msg = 'ArtNet synchronization requires refresh_every <= 3.5s' raise ValueError(msg) @@ -151,6 +155,7 @@ def set_synchronous_mode(self, enabled: bool): self._sync_enabled = enabled return self + @override def _send_synchronization(self) -> None: if not self._sync_enabled: return diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index a1ade35..0d4191c 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -4,6 +4,8 @@ from logging import DEBUG as LVL_DEBUG from struct import pack as s_pack +from typing_extensions import override + import pyartnet from pyartnet.base import BaseNode from pyartnet.errors import InvalidUniverseAddressError @@ -20,7 +22,7 @@ class KiNetNode(BaseNode['pyartnet.impl_kinet.KiNetUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: int | float | None = 2, start_refresh_task: bool = True, + refresh_every: float | None = 2, start_refresh_task: bool = True, source_address: tuple[str, int] | None = None) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, @@ -33,6 +35,7 @@ def __init__(self, ip: str, port: int, *, packet.extend(s_pack('>IBBHI', 0, 0, 0, 0, 0xFFFFFFFF)) # sequence, port, padding, flags, timer self._packet_base = bytes(packet) + @override def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: pyartnet.impl_kinet.KiNetUniverse) -> None: packet = bytearray() @@ -45,9 +48,11 @@ def _send_universe(self, id: int, byte_size: int, # log complete packet log.debug(f'Sending KiNet frame to {self._ip}:{self._port}: {(self._packet_base + packet).hex()}') + @override def _create_universe(self, nr: int) -> pyartnet.impl_kinet.KiNetUniverse: return pyartnet.impl_kinet.KiNetUniverse(self, self._validate_universe_nr(nr)) + @override def _validate_universe_nr(self, nr: int) -> int: if not isinstance(nr, int): raise TypeError() diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 76ecb22..51b3885 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -6,6 +6,8 @@ from typing import Final from uuid import uuid4 +from typing_extensions import Self, override + import pyartnet.impl_sacn.universe from pyartnet.base import BaseNode, SequenceCounter from pyartnet.errors import InvalidCidError, InvalidUniverseAddressError @@ -33,7 +35,7 @@ class SacnNode(BaseNode['pyartnet.impl_sacn.SacnUniverse']): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: int | float | None = 2, start_refresh_task: bool = True, + refresh_every: float | None = 2, start_refresh_task: bool = True, source_address: tuple[str, int] | None = None, # sACN E1.31 specific fields @@ -86,6 +88,7 @@ def __init__(self, ip: str, port: int, *, self._sync_sequence_number: Final = SequenceCounter() # noinspection PyProtectedMember + @override def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: pyartnet.impl_sacn.universe.SacnUniverse) -> None: packet = bytearray() @@ -128,9 +131,11 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, # log complete packet log.debug(f'Sending sACN frame to {_dst_str(universe._dst)}: {(base_packet + packet).hex()}') + @override def _create_universe(self, nr: int) -> pyartnet.impl_sacn.SacnUniverse: return pyartnet.impl_sacn.SacnUniverse(self, self._validate_universe_nr(nr)) + @override def _validate_universe_nr(self, nr: int) -> int: if not isinstance(nr, int): raise TypeError() @@ -156,7 +161,8 @@ def _get_universe_ip_port(self, universe: int) -> tuple[str, int] | str: # IPv4 multicast address return f'239.255.{universe_high:d}.{universe_low:d}' - def set_multicast_mode(self, enabled: bool): + @override + def set_multicast_mode(self, enabled: bool) -> Self: """Either send packets to the node directly or through multicast. :param enabled: If True multicast is enabled """ @@ -172,6 +178,7 @@ def set_multicast_mode(self, enabled: bool): return self + @override def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0) -> None: """Enable or disable synchronous mode for this node. In synchronous mode multiple universes are sent to the node and then a synchronization packet is sent to make the node output all universes at the same time. @@ -193,6 +200,7 @@ def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0) self._sync_address = 0 self._sync_dst = self._dst + @override def _send_synchronization(self) -> None: if not self._sync_address: return diff --git a/uv.lock b/uv.lock index b2e3fa2..5d61ced 100644 --- a/uv.lock +++ b/uv.lock @@ -625,6 +625,10 @@ wheels = [ name = "pyartnet" version = "1.0.1" source = { editable = "." } +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'" }, +] [package.dev-dependencies] dev = [ @@ -668,6 +672,7 @@ tests = [ ] [package.metadata] +requires-dist = [{ name = "typing-extensions" }] [package.metadata.requires-dev] dev = [ From c31c694766e4146f1a7989dac7b6da7404caba42 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:41:21 +0200 Subject: [PATCH 24/49] . --- .ruff.toml | 11 ++++++++++ src/pyartnet/base/background_task.py | 5 +++-- src/pyartnet/base/channel.py | 33 ++++++++++++++++------------ src/pyartnet/fades/fade_base.py | 2 +- src/pyartnet/fades/fade_linear.py | 5 +++++ src/pyartnet/output_correction.py | 2 +- tests/channel/test_boundaries.py | 4 ++-- tests/conftest.py | 7 +++--- 8 files changed, 45 insertions(+), 24 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index c4e3be8..215d08b 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -102,4 +102,15 @@ lines-after-imports = 2 # https://docs.astral.sh/ruff/settings/#lint_isort_l # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt "FBT003", # Boolean positional value in function call + + # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg + "ARG001", # Unused function argument {name} + "ARG002", # Unused method argument {name} + "ARG003", # Unused class method argument: {name} + "ARG004", # Unused static method argument: {name} + "ARG005", # Unused lambda argument: {name} +] + +"src/pyartnet/*" = [ + "PLR2004" # Magic value used in comparison, consider replacing `0.01` with a constant variable ] diff --git a/src/pyartnet/base/background_task.py b/src/pyartnet/base/background_task.py index 0a011a3..b15cb91 100644 --- a/src/pyartnet/base/background_task.py +++ b/src/pyartnet/base/background_task.py @@ -30,15 +30,16 @@ def __init__(self, coro: Callable[[], Coroutine], name: str) -> None: self.name: Final = name self.task: Task | None = None - def start(self): + def start(self) -> None: if self.task is not None: return None self.task = task = CREATE_TASK(self.coro_wrap(), name=self.name) _BACKGROUND_TASKS.add(task) task.add_done_callback(_BACKGROUND_TASKS.discard) + return None - def cancel(self): + def cancel(self) -> None: if self.task is None: return None diff --git a/src/pyartnet/base/channel.py b/src/pyartnet/base/channel.py index 2ec9bb9..2852b32 100644 --- a/src/pyartnet/base/channel.py +++ b/src/pyartnet/base/channel.py @@ -3,11 +3,14 @@ import logging import warnings from array import array -from collections.abc import Callable, Collection from logging import DEBUG as LVL_DEBUG from math import ceil -from typing import TYPE_CHECKING, Any, Final, Literal, Union +from typing import TYPE_CHECKING, Any, Final, Generator, Literal +from typing_extensions import Self + +from pyartnet.base.channel_fade import ChannelBoundFade +from pyartnet.base.output_correction import OutputCorrection from pyartnet.errors import ( ChannelOutOfUniverseError, ChannelValueOutOfBoundsError, @@ -17,11 +20,10 @@ from pyartnet.fades import FadeBase, LinearFade from pyartnet.output_correction import linear -from .channel_fade import ChannelBoundFade -from .output_correction import OutputCorrection - if TYPE_CHECKING: + from collections.abc import Callable, Collection + from .universe import BaseUniverse @@ -93,7 +95,7 @@ def __init__(self, universe: BaseUniverse, # Callbacks self.callback_fade_finished: Callable[[Channel], Any] | None = None - def _apply_output_correction(self): + def _apply_output_correction(self) -> None: # default correction is linear self._correction_current = linear @@ -102,6 +104,7 @@ def _apply_output_correction(self): if obj._correction_output is not None: self._correction_current = obj._correction_output return None + return None def get_values(self) -> list[int]: """Get the current (uncorrected) channel values @@ -110,7 +113,7 @@ def get_values(self) -> list[int]: """ return self._values_raw.tolist() - def set_values(self, values: Collection[Union[int, float]]): + def set_values(self, values: Collection[int | float]) -> Self: """Set values for a channel without a fade :param values: Iterable of values with the same size as the channel width @@ -141,7 +144,7 @@ def set_values(self, values: Collection[Union[int, float]]): self._parent_universe.channel_changed(self) return self - def to_buffer(self, buf: bytearray): + def to_buffer(self, buf: bytearray) -> Self: byte_order = self._byte_order byte_size = self._byte_size @@ -151,15 +154,17 @@ def to_buffer(self, buf: bytearray): start += byte_size return self - def add_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, - fade_class: type[FadeBase] = LinearFade): + def add_fade(self, values: Collection[int | FadeBase], duration_ms: int, + fade_class: type[FadeBase] = LinearFade) -> Self: warnings.warn( - f'{self.set_fade.__name__:s} is deprecated, use {self.set_fade.__name__:s} instead', DeprecationWarning) + f'{self.set_fade.__name__:s} is deprecated, use {self.set_fade.__name__:s} instead', + DeprecationWarning, stacklevel=2 + ) return self.set_fade(values, duration_ms, fade_class) # noinspection PyProtectedMember - def set_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, - fade_class: type[FadeBase] = LinearFade): + def set_fade(self, values: Collection[int | FadeBase], duration_ms: int, + fade_class: type[FadeBase] = LinearFade) -> Self: """Add and schedule a new fade for the channel :param values: Target values for the fade @@ -207,7 +212,7 @@ def set_fade(self, values: Collection[Union[int, FadeBase]], duration_ms: int, log.debug(f'CH {self._start + i}: {fade.debug_initialize():s}') return self - def __await__(self): + def __await__(self) -> Generator[None, None, bool]: if self._current_fade is None: return False yield from self._current_fade.event.wait().__await__() diff --git a/src/pyartnet/fades/fade_base.py b/src/pyartnet/fades/fade_base.py index 1bed95b..13d380f 100644 --- a/src/pyartnet/fades/fade_base.py +++ b/src/pyartnet/fades/fade_base.py @@ -4,7 +4,7 @@ class FadeBase: def __init__(self) -> None: self.is_done = False - def initialize(self, current: int, target: int, steps: int): + def initialize(self, current: int, target: int, steps: int) -> None: raise NotImplementedError() def debug_initialize(self) -> str: diff --git a/src/pyartnet/fades/fade_linear.py b/src/pyartnet/fades/fade_linear.py index c831141..ba2b3c2 100644 --- a/src/pyartnet/fades/fade_linear.py +++ b/src/pyartnet/fades/fade_linear.py @@ -1,3 +1,5 @@ +from typing_extensions import override + from .fade_base import FadeBase @@ -9,14 +11,17 @@ def __init__(self) -> None: self.current: float = 0.0 # Current Value self.factor: float = 1.0 + @override def debug_initialize(self) -> str: return f'{self.current:03.0f} -> {self.target:03d} | step: {self.factor:+5.1f}' + @override def initialize(self, start: int, target: int, steps: int) -> None: self.current = start self.target = target self.factor = (self.target - start) / steps + @override def calc_next_value(self) -> float: self.current += self.factor diff --git a/src/pyartnet/output_correction.py b/src/pyartnet/output_correction.py index 7181cb2..a70b55f 100644 --- a/src/pyartnet/output_correction.py +++ b/src/pyartnet/output_correction.py @@ -1,4 +1,4 @@ -def linear(val: float, max_val: int = 0xFF) -> float: +def linear(val: float, max_val: int = 0xFF) -> float: # noqa: ARG001 """linear output correction""" return val diff --git a/tests/channel/test_boundaries.py b/tests/channel/test_boundaries.py index 8f88107..967ee8d 100644 --- a/tests/channel/test_boundaries.py +++ b/tests/channel/test_boundaries.py @@ -50,7 +50,7 @@ def get_node_universe_mock(): ((1, 1, -1, 255), (1, 1, 256, 255), (3, 1, 256, 255), (1, 2, -1, 65535), (1, 2, 65536, 65535), (3, 2, 65536, 65535), )) def test_set_invalid(width, byte_size, invalid, valid) -> None: - node, universe = get_node_universe_mock() + _, universe = get_node_universe_mock() invalid_values = [0] * (width - 1) + [invalid] valid_values = [0] * (width - 1) + [valid] @@ -71,7 +71,7 @@ def test_set_invalid(width, byte_size, invalid, valid) -> None: async def test_set_missing() -> None: - node, universe = get_node_universe_mock() + _, universe = get_node_universe_mock() c = Channel(universe, 1, 1) with pytest.raises(ValueCountDoesNotMatchChannelWidthError) as e: diff --git a/tests/conftest.py b/tests/conftest.py index f9bafa8..dd2f853 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,13 +55,12 @@ def test_patched_socket(patched_socket) -> None: assert node._socket.sendto is patched_socket -@pytest.fixture() +@pytest.fixture def node(): - node = TestingNode('IP', 9999) - return node + return TestingNode('IP', 9999) -@pytest.fixture() +@pytest.fixture def universe(node: BaseNode): return node.add_universe() From 0a67f16f535be2e207f1209c49801886b22675f2 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:51:06 +0200 Subject: [PATCH 25/49] . --- tests/channel/test_channel.py | 2 +- tests/helper.py | 17 +++++++++++++---- tests/test_channel_fade.py | 2 +- tests/test_output_correction.py | 2 +- tests/test_universe.py | 6 +++--- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/channel/test_channel.py b/tests/channel/test_channel.py index 73c0e57..efa99d5 100644 --- a/tests/channel/test_channel.py +++ b/tests/channel/test_channel.py @@ -1,7 +1,7 @@ from pyartnet.base import BaseUniverse -def test_values_add_channel(universe: BaseUniverse): +def test_values_add_channel(universe: BaseUniverse) -> None: u = universe.add_channel(1, 2, byte_size=3, byte_order='big') assert u._start == 1 assert u._width == 2 diff --git a/tests/helper.py b/tests/helper.py index 8f878d0..713d158 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,13 +1,21 @@ +from __future__ import annotations + import socket +from typing import TYPE_CHECKING from unittest.mock import Mock -from pytest import MonkeyPatch # noqa: PT013 +from pytest import MonkeyPatch import pyartnet +if TYPE_CHECKING: + from types import TracebackType + + + class MockedSocket: - def __init__(self): + def __init__(self) -> None: self.mp = MonkeyPatch() def mock(self): @@ -22,11 +30,12 @@ def mock(self): self.mp.setattr(pyartnet.base.base_node, 'socket', m) return m_sendto - def undo(self): + def undo(self) -> None: self.mp.undo() def __enter__(self): return self.mock() - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: type[BaseException] | None, + exc_val: BaseException | None, exc_tb: TracebackType | None) -> None: self.undo() diff --git a/tests/test_channel_fade.py b/tests/test_channel_fade.py index 473ba13..fa3cce4 100644 --- a/tests/test_channel_fade.py +++ b/tests/test_channel_fade.py @@ -4,7 +4,7 @@ from pyartnet.base.channel_fade import ChannelBoundFade -def test_repr(): +def test_repr() -> None: universe = Mock() universe.output_correction = None diff --git a/tests/test_output_correction.py b/tests/test_output_correction.py index e37e7a0..2af5e1d 100644 --- a/tests/test_output_correction.py +++ b/tests/test_output_correction.py @@ -6,6 +6,6 @@ @pytest.mark.parametrize('max_val', [ pytest.param(k, id=f'{k:X}') for k in (0xFF, 0xFFFF, 0xFFFFFF, 0xFFFFFFFF, 0xFFFFFFFFFF)]) @pytest.mark.parametrize('corr', [quadratic, quadruple, cubic]) -def test_correction(corr, max_val): +def test_correction(corr, max_val) -> None: assert corr(0, max_val=max_val) == 0 assert corr(max_val, max_val=max_val) == max_val diff --git a/tests/test_universe.py b/tests/test_universe.py index 863d3a8..b83af17 100644 --- a/tests/test_universe.py +++ b/tests/test_universe.py @@ -5,7 +5,7 @@ from pyartnet.errors import ChannelNotFoundError -def test_exceptions(universe: BaseUniverse): +def test_exceptions(universe: BaseUniverse) -> None: universe.add_channel(1, 1) with pytest.raises(errors.ChannelExistsError) as e: @@ -35,7 +35,7 @@ def test_exceptions(universe: BaseUniverse): universe.add_channel(8, 20) -def test_universe_resize(universe: BaseUniverse): +def test_universe_resize(universe: BaseUniverse) -> None: assert universe._data_size == 0 assert universe._data == b'' @@ -57,7 +57,7 @@ def test_universe_resize(universe: BaseUniverse): assert universe._data == b'\x00\x00\x00\x00' -def test_access(universe: BaseUniverse): +def test_access(universe: BaseUniverse) -> None: with pytest.raises(ChannelNotFoundError) as e: universe.get_channel('1') From 11e7949f766e38ae8d8523bc0b827c96ed67121a Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:55:09 +0200 Subject: [PATCH 26/49] . --- docs/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index cce6cfd..f9284fc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -70,3 +70,6 @@ nitpick_ignore_regex.append( (re.compile(r'py:data|py:class'), re.compile(r'typing\..+')) ) + nitpick_ignore_regex.append( + (re.compile(r'py:class'), re.compile(r'collections\.abc\..+')) + ) From 8f992c70f082381ced171cd1a5e13455a8eec48a Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:58:27 +0200 Subject: [PATCH 27/49] . --- readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.md b/readme.md index 41628da..e5e1847 100644 --- a/readme.md +++ b/readme.md @@ -14,6 +14,10 @@ Docs and examples can be found [here](https://pyartnet.readthedocs.io/en/latest/ # Changelog +#### 1.1.0 (2025-XX-XX) +- Added support for transmitting multiple universes in sync +- used UV +- ruff and typing fixes #### 1.0.1 (2023-02-20) - Fixed an issue where consecutive fades would not start from the correct value From 1739204d65aa648f8881394023a6ffea50939b3c Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 15 Oct 2025 07:23:27 +0200 Subject: [PATCH 28/49] . --- docs/pyartnet.rst | 1 + pyproject.toml | 1 + src/pyartnet/impl_artnet/node.py | 15 ++++++++- tests/test_impl/test_artnet.py | 56 ++++++++++++++++++++++++++++++++ tests/test_impl/test_sacn.py | 3 ++ 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 tests/test_impl/test_artnet.py diff --git a/docs/pyartnet.rst b/docs/pyartnet.rst index f631e8f..7ae7593 100644 --- a/docs/pyartnet.rst +++ b/docs/pyartnet.rst @@ -156,6 +156,7 @@ Example async def main(): # hide: stop + from pyartnet import ArtNetNode, output_correction # create node/universe/channel diff --git a/pyproject.toml b/pyproject.toml index 91c2326..4503f70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Operating System :: OS Independent", diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 7dc09d7..5eb8d8f 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -97,8 +97,15 @@ def __log_artnet_frame(self, p: bytearray | bytes) -> None: host_fmt = ' ' * (36 + len(self._ip)) out_desc = '{:s} {:2s} {:2s} {:4s} {:4s}'.format(host_fmt, 'Sq', '', 'Univ', ' Len') - _max_channel = p[16] << 8 | p[17] pre = bytearray(p[:12]).hex().upper() + + # low byte first: 5200 -> 0052 + a = p[8:10] + if p[8:10] == b'\x00\x52': + log.debug(f'Sync to {self._ip:s}: {pre} {p[12]:02x} {p[13]:02x}') + return None + + _max_channel = p[16] << 8 | p[17] out = f'Packet to {self._ip:s}: {pre} {p[12]:02x} {p[13]:02x} {p[13]:02x}{p[14]:02x} {_max_channel:04x}' # check what to print @@ -148,6 +155,12 @@ def __log_artnet_frame(self, p: bytearray | bytes) -> None: @override def set_synchronous_mode(self, enabled: bool) -> Self: + """Enable or disable synchronous mode for this node. In synchronous mode multiple universes are sent to the + node and then a synchronization packet is sent to make the node output all universes at the same time. + This prevents tearing in multi universe panels. + + :param enabled: Enable or disable synchronous mode + """ if self._refresh_every > 3.5: msg = 'ArtNet synchronization requires refresh_every <= 3.5s' raise ValueError(msg) diff --git a/tests/test_impl/test_artnet.py b/tests/test_impl/test_artnet.py new file mode 100644 index 0000000..8d5437b --- /dev/null +++ b/tests/test_impl/test_artnet.py @@ -0,0 +1,56 @@ + +import asyncio +import logging +from binascii import a2b_hex +from unittest.mock import call + +from pyartnet import ArtNetNode + + +async def test_artnet() -> None: + arnet = ArtNetNode('ip', 9999999, start_refresh_task=True) + channel = arnet.add_universe(1).add_channel(1, 10) + channel.set_values(range(1, 11)) + + data = '4172742d4e6574000050000e01000100000a0102030405060708090a' + + await channel + await arnet._process_task.task + await asyncio.sleep(0.3) + + m = arnet._socket + m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) + + await channel + + +async def test_artnet_with_sync(caplog) -> None: + caplog.set_level(logging.DEBUG) + + artnet = ArtNetNode('ip', 9999999, start_refresh_task=False) + artnet.set_synchronous_mode(True) + + channel = artnet.add_universe(1).add_channel(1, 10) + channel.set_values(range(1, 11)) + + data = '4172742d4e6574000050000e01000100000a0102030405060708090a' + sync_data = '4172742d4e6574000052000e0000' + + await channel + await artnet._process_task.task + await asyncio.sleep(0.3) + + m = artnet._socket + assert m.sendto.call_args_list == [ + call(bytearray(a2b_hex(data)), ('ip', 9999999)), + call(bytearray(a2b_hex(sync_data)), ('ip', 9999999)), + ] + + assert caplog.record_tuples == [ + ('pyartnet.Universe', 10, 'Added channel "1/10": start: 1, stop: 10'), + ('pyartnet.Task', 10, 'Started Process task ip:9999999'), + ('pyartnet.ArtNetNode', 10, ' Sq Univ Len 1 2 3 4 5 6 7 8 9 10 '), # noqa: E501 + ('pyartnet.ArtNetNode', 10, 'Packet to ip: 4172742D4E6574000050000E 01 00 0001 000a 001 002 003 004 005 006 007 008 009 010'), # noqa: E501 + ('pyartnet.ArtNetNode', 10, 'Sync to ip: 4172742D4E6574000052000E 00 00'), + ('pyartnet.Task', 10, 'Stopped Process task ip:9999999'), + ] diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index 7e58820..5150841 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -1,3 +1,4 @@ +import asyncio import logging from binascii import a2b_hex from unittest.mock import call @@ -24,6 +25,7 @@ async def test_sacn() -> None: await channel await sacn._process_task.task + await asyncio.sleep(0.3) m = sacn._socket m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) @@ -55,6 +57,7 @@ async def test_sacn_with_sync(caplog, multicast) -> None: await channel await sacn._process_task.task + await asyncio.sleep(0.3) data_dst = ('ip', 9999999) if not multicast else '239.255.0.1' sync_dst = ('ip', 9999999) if not multicast else '239.255.0.2' From cec3bcf0dd21e705db2e90ef125b969bf4040609 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 20 Oct 2025 10:43:54 +0200 Subject: [PATCH 29/49] fix IPv6 multicast --- .ruff.toml | 3 --- src/pyartnet/impl_sacn/node.py | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index 215d08b..4c7af51 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -30,9 +30,6 @@ ignore = [ # https://docs.astral.sh/ruff/rules/#flake8-bandit-s "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes - # https://docs.astral.sh/ruff/rules/#pyupgrade-up - "UP038", # Use X | Y in {} call instead of (X, Y) - # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 51b3885..c54fa6f 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -150,16 +150,13 @@ def _get_universe_ip_port(self, universe: int) -> tuple[str, int] | str: u = self._validate_universe_nr(universe) - universe_high = u // 255 - universe_low = u % 255 - # IPv6 multicast address if ':' in self._ip: IPv6Address(self._ip) # validate IP - return f'FF18::83:00:{universe_high:d}:{universe_low:d}' + return f'FF18::8300:{u:04X}' # IPv4 multicast address - return f'239.255.{universe_high:d}.{universe_low:d}' + return f'239.255.{u // 255:d}.{u % 255:d}' @override def set_multicast_mode(self, enabled: bool) -> Self: From 37453bb4e0ab33d37a8404aee90a5036bb6efa62 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 21 Oct 2025 07:12:16 +0200 Subject: [PATCH 30/49] fix multicast --- src/pyartnet/base/base_node.py | 2 +- src/pyartnet/base/channel.py | 17 +++++++++++------ src/pyartnet/impl_artnet/node.py | 6 ++++-- src/pyartnet/impl_kinet/node.py | 7 +++++-- src/pyartnet/impl_sacn/node.py | 30 +++++++++++++++++------------- src/pyartnet/impl_sacn/universe.py | 2 +- tests/test_impl/test_impl.py | 10 ++++++++-- tests/test_impl/test_sacn.py | 8 ++++---- 8 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index fc78315..5a630b1 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -28,7 +28,7 @@ class BaseNode(OutputCorrection, Generic[UNIVERSE_TYPE]): def __init__(self, ip: str, port: int, *, max_fps: int = 25, - refresh_every: float | None = 2, start_refresh_task: bool = True, + refresh_every: float = 2, start_refresh_task: bool = True, source_address: tuple[str, int] | None = None) -> None: super().__init__() diff --git a/src/pyartnet/base/channel.py b/src/pyartnet/base/channel.py index 2852b32..1a4eb73 100644 --- a/src/pyartnet/base/channel.py +++ b/src/pyartnet/base/channel.py @@ -73,8 +73,8 @@ def __init__(self, universe: BaseUniverse, # value representation self._byte_size: Final = byte_size self._byte_order: Final = byte_order - self._value_max: Final = 256 ** self._byte_size - 1 - self._buf_start: Final = self._start - 1 + self._value_max: Final[int] = 256 ** self._byte_size - 1 + self._buf_start: Final[int] = self._start - 1 null_vals = [0 for _ in range(self._width)] self._values_raw: array[int] = array(ARRAY_TYPE[self._byte_size], null_vals) # uncorrected values @@ -188,15 +188,20 @@ def set_fade(self, values: Collection[int | FadeBase], duration_ms: int, # build fades fades: list[FadeBase] = [] for i, target in enumerate(values): - # default is linear - k = fade_class() if not isinstance(target, FadeBase) else target - fades.append(k) + + # Is a fade initialized by the user + if isinstance(target, FadeBase): + fades.append(target) + continue if not 0 <= target <= self._value_max: msg = f'Target value out of bounds! 0 <= {target} <= {self._value_max}' raise ChannelValueOutOfBoundsError(msg) - k.initialize(self._values_raw[i], target, fade_steps) + # default is linear + _fade = fade_class() + _fade.initialize(self._values_raw[i], target, fade_steps) + fades.append(_fade) # Add to scheduling self._current_fade = ChannelBoundFade(self, fades) diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 5eb8d8f..82408a5 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -16,13 +16,15 @@ # https://artisticlicence.com/support-and-resources/art-net-4/ # ----------------------------------------------------------------------------- +ARTNET_PORT: Final = 6454 + log = logging.getLogger('pyartnet.ArtNetNode') class ArtNetNode(BaseNode['pyartnet.impl_artnet.ArtNetUniverse']): - def __init__(self, ip: str, port: int, *, + def __init__(self, ip: str, port: int = ARTNET_PORT, *, max_fps: int = 25, - refresh_every: float | None = 2, start_refresh_task: bool = True, + refresh_every: float = 2, start_refresh_task: bool = True, source_address: tuple[str, int] | None = None, # ArtNet specific fields diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index 0d4191c..6d2b097 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -3,6 +3,7 @@ import logging from logging import DEBUG as LVL_DEBUG from struct import pack as s_pack +from typing import Final from typing_extensions import override @@ -16,13 +17,15 @@ # todo: find links # ----------------------------------------------------------------------------- +KINET_PORT: Final = 6038 + log = logging.getLogger('pyartnet.KiNetNode') class KiNetNode(BaseNode['pyartnet.impl_kinet.KiNetUniverse']): - def __init__(self, ip: str, port: int, *, + def __init__(self, ip: str, port: int = KINET_PORT, *, max_fps: int = 25, - refresh_every: float | None = 2, start_refresh_task: bool = True, + refresh_every: float = 2, start_refresh_task: bool = True, source_address: tuple[str, int] | None = None) -> None: super().__init__(ip=ip, port=port, max_fps=max_fps, diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index c54fa6f..21a55e3 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -31,11 +31,14 @@ VECTOR_E131_EXTENDED_SYNCHRONIZATION: Final = b'\x00\x00\x00\x01' VECTOR_DMP_SET_PROPERTY: Final = 0x02 +# Defined Parameters (Appendix A) +ACN_SDT_MULTICAST_PORT: Final = 5568 + class SacnNode(BaseNode['pyartnet.impl_sacn.SacnUniverse']): - def __init__(self, ip: str, port: int, *, + def __init__(self, ip: str, port: int = ACN_SDT_MULTICAST_PORT, *, max_fps: int = 25, - refresh_every: float | None = 2, start_refresh_task: bool = True, + refresh_every: float = 2, start_refresh_task: bool = True, source_address: tuple[str, int] | None = None, # sACN E1.31 specific fields @@ -144,7 +147,7 @@ def _validate_universe_nr(self, nr: int) -> int: raise InvalidUniverseAddressError() return int(nr) - def _get_universe_ip_port(self, universe: int) -> tuple[str, int] | str: + def _get_universe_ip_port(self, universe: int) -> tuple[str, int]: if not self._multicast: return self._dst @@ -153,12 +156,11 @@ def _get_universe_ip_port(self, universe: int) -> tuple[str, int] | str: # IPv6 multicast address if ':' in self._ip: IPv6Address(self._ip) # validate IP - return f'FF18::8300:{u:04X}' + return f'FF18::8300:{u:04X}', ACN_SDT_MULTICAST_PORT # IPv4 multicast address - return f'239.255.{u // 255:d}.{u % 255:d}' + return f'239.255.{u // 255:d}.{u % 255:d}', ACN_SDT_MULTICAST_PORT - @override def set_multicast_mode(self, enabled: bool) -> Self: """Either send packets to the node directly or through multicast. :param enabled: If True multicast is enabled @@ -176,7 +178,7 @@ def set_multicast_mode(self, enabled: bool) -> Self: return self @override - def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0) -> None: + def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0) -> None: # type: ignore [override] """Enable or disable synchronous mode for this node. In synchronous mode multiple universes are sent to the node and then a synchronization packet is sent to make the node output all universes at the same time. This prevents tearing in multi universe panels. @@ -202,14 +204,16 @@ def _send_synchronization(self) -> None: if not self._sync_address: return - packet = bytearray() + packet = bytearray(11) # Framing layer - packet.extend((11 | 0x7000).to_bytes(2, 'big')) # | 2 | Flags and Length - packet.extend(VECTOR_E131_EXTENDED_SYNCHRONIZATION) # | 4 | Vector - packet.append(self._sync_sequence_number.value) # | 1 | Sequence Number - packet.extend(self._sync_address.to_bytes(2, 'big')) # | 2 | Synchronization universe - packet.extend([0, 0]) # | 2 | Reserved + packet[0:2] = (11 | 0x7000).to_bytes(2, 'big') # | 2 | Flags and Length + packet[2:6] = VECTOR_E131_EXTENDED_SYNCHRONIZATION # | 4 | Vector + packet[6] = self._sync_sequence_number.value # | 1 | Sequence Number + packet[7:9] = self._sync_address.to_bytes(2, 'big') # | 2 | Synchronization universe + # packet[9:11] = [0, 0] # | 2 | Reserved + # +----+---------- + # = 11 # Update length and package type for base packet base_packet = self._packet_base diff --git a/src/pyartnet/impl_sacn/universe.py b/src/pyartnet/impl_sacn/universe.py index 0bdefff..cf90830 100644 --- a/src/pyartnet/impl_sacn/universe.py +++ b/src/pyartnet/impl_sacn/universe.py @@ -17,4 +17,4 @@ def __init__(self, node: 'pyartnet.impl_sacn.SacnNode', universe: int = 0) -> No self._sequence_ctr: Final = SequenceCounter() # to support multicast - self._dst: tuple[str, int] | str = node._get_universe_ip_port(universe) + self._dst: tuple[str, int] = node._get_universe_ip_port(universe) diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index a6f70cc..267848a 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -15,9 +15,15 @@ def test_same_cls_signature(c) -> None: sig_base = inspect.signature(BaseNode) sig_obj = inspect.signature(c) - for name, parameter in sig_base.parameters.items(): + for name, base_parameter in sig_base.parameters.items(): assert name in sig_obj.parameters - assert sig_obj.parameters[name] == parameter + obj_parameter = sig_obj.parameters[name] + + # some ports have a default which we ignore here + if name == 'port': + obj_parameter = obj_parameter.replace(default=inspect.Parameter.empty) + + assert obj_parameter == base_parameter @pytest.mark.parametrize('cls', [ArtNetNode, SacnNode, KiNetNode]) diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index 5150841..ff691ee 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -59,10 +59,10 @@ async def test_sacn_with_sync(caplog, multicast) -> None: await sacn._process_task.task await asyncio.sleep(0.3) - data_dst = ('ip', 9999999) if not multicast else '239.255.0.1' - sync_dst = ('ip', 9999999) if not multicast else '239.255.0.2' - data_msg = 'ip:9999999' if not multicast else '239.255.0.1' - sync_msg = 'ip:9999999' if not multicast else '239.255.0.2' + data_dst = ('ip', 9999999) if not multicast else ('239.255.0.1', 5568) + sync_dst = ('ip', 9999999) if not multicast else ('239.255.0.2', 5568) + data_msg = 'ip:9999999' if not multicast else '239.255.0.1:5568' + sync_msg = 'ip:9999999' if not multicast else '239.255.0.2:5568' m = sacn._socket assert m.sendto.call_args_list == [ From 683b53202206e1e90158881345572393b918a7d5 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 21 Oct 2025 07:37:29 +0200 Subject: [PATCH 31/49] . --- .github/workflows/run_tests.yml | 2 +- src/pyartnet/base/background_task.py | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index c28898f..98c85d1 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -19,7 +19,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v5 diff --git a/src/pyartnet/base/background_task.py b/src/pyartnet/base/background_task.py index b15cb91..d22797e 100644 --- a/src/pyartnet/base/background_task.py +++ b/src/pyartnet/base/background_task.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from asyncio import Task, create_task, sleep +from asyncio import Task, create_task, current_task, sleep from time import monotonic from traceback import format_exc from typing import Any, Callable, Coroutine, Final @@ -40,16 +40,17 @@ def start(self) -> None: return None def cancel(self) -> None: - if self.task is None: + if (task := self.task) is None: return None - self.task.cancel() self.task = None + task.cancel() + return None async def coro_wrap(self) -> None: log.debug(f'Started {self.name}') task = self.task - assert task is not None + assert task is current_task() try: await self.coro() @@ -65,7 +66,7 @@ class ExceptionIgnoringTask(SimpleBackgroundTask): async def coro_wrap(self) -> None: log.debug(f'Started {self.name}') task = self.task - assert task is not None + assert task is current_task() wait = 0 @@ -80,10 +81,7 @@ async def coro_wrap(self) -> None: # simple sleep logic with an increasing timeout time_to_exception = monotonic() - start - if time_to_exception < 16 or time_to_exception < wait: - wait = max(2, wait * 2) - else: - wait = 0 + wait = max(2, wait * 2) if time_to_exception < 16 or time_to_exception < wait else 0 log.debug(f'Retry in {wait:d} seconds') finally: From aecc293c71f6ea7063716488c560a9d6fc9a5bb5 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:50:34 +0200 Subject: [PATCH 32/49] . --- src/pyartnet/base/_network.py | 186 +++++++++++ src/pyartnet/base/base_node.py | 4 - src/pyartnet/impl_kinet/node.py | 3 +- tests/test_base/__init__.py | 0 tests/{ => test_base}/test_base_node.py | 0 tests/test_base/test_network.py | 36 ++ uv.lock | 418 ++++++++++++++---------- 7 files changed, 475 insertions(+), 172 deletions(-) create mode 100644 src/pyartnet/base/_network.py create mode 100644 tests/test_base/__init__.py rename tests/{ => test_base}/test_base_node.py (100%) create mode 100644 tests/test_base/test_network.py diff --git a/src/pyartnet/base/_network.py b/src/pyartnet/base/_network.py new file mode 100644 index 0000000..d5d0b24 --- /dev/null +++ b/src/pyartnet/base/_network.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import socket +from asyncio import get_running_loop +from ipaddress import AddressValueError, IPv4Address, IPv6Address +from socket import AF_INET, AF_INET6, AF_UNSPEC, SOCK_DGRAM +from typing import Final, Literal + +from typing_extensions import Self + + +USE_IP_VERSION: Final = Literal['auto', 'v4', 'v6'] + + +def validate_port(port: int, *, allow_0: bool = False) -> int: + if not isinstance(port, int): + msg = 'port must be an integer' + raise TypeError(msg) + + lower = 0 if allow_0 else 1 + if not lower < port < 65536: + msg = f'port must be between {lower:d} and 65536' + raise ValueError(msg) + + return port + + +def validate_string(host: str) -> str: + if not isinstance(host, str): + msg = 'hostname must be a string' + raise TypeError(msg) + if not host: + msg = 'hostname cannot be empty' + raise ValueError(msg) + return host + + +async def resolve_hostname(host: str, port: int | None = None, + mode: USE_IP_VERSION = 'auto') -> tuple[tuple[socket.AddressFamily, str], ...]: + try: + family = {'auto': AF_UNSPEC, 'v4': AF_INET, 'v6': AF_INET6}[mode] + except KeyError: + msg = f'Invalid mode: "{mode:s}"' + raise ValueError(msg) from None + + try: + info = await get_running_loop().getaddrinfo(host, port, type=SOCK_DGRAM, family=family) + except socket.gaierror as e: + msg = f'Cannot resolve hostname "{host:s}"! {e.errno}: {e.strerror}' + raise ValueError(msg) from None + + return tuple( + (v[0], v[4][0]) for v in info if v[0] in (AF_INET, AF_INET6) + ) + + +async def validate_source_ip(source_ip: str, source_port: int) -> IPv4Address | IPv6Address: + available = await resolve_hostname(socket.gethostname(), source_port) + + for family, ip in available: + if ip == source_ip: + if family == AF_INET: + return IPv4Address(source_ip) + return IPv6Address(source_ip) + + available_ips = [k[1] for k in sorted(available)] + msg = f'Source IP "{source_ip}" is not available on this system! Available: {", ".join(available_ips)}' + raise ValueError(msg) + + +async def get_ip(host: str, port: int, ip_version: USE_IP_VERSION) -> IPv4Address | IPv6Address: + # check if it's a valid ip + for cls in (IPv4Address, IPv6Address): + try: + return cls(host) + except AddressValueError: # noqa: PERF203 + pass + + # must be hostname - try to resolve it + info = await resolve_hostname(host, port, mode=ip_version) + family, resolved_ip = info[0] + + if family == AF_INET: + return IPv4Address(resolved_ip) + return IPv6Address(resolved_ip) + + +class NetworkInfoBase: + def __init__(self, *, ip_v6: bool = False) -> None: + self.ip_v6: Final = ip_v6 + + def create_socket(self) -> socket.socket: + # create nonblocking UDP socket + sock: Final = socket.socket(AF_INET6 if self.ip_v6 else AF_INET, SOCK_DGRAM) + sock.setblocking(False) + + return sock + + def validate_ip(self, ip: str) -> str: + if self.ip_v6: + IPv6Address(ip) + else: + IPv4Address(ip) + return ip + + +class UnicastNetworkInfo(NetworkInfoBase): + def __init__(self, dst: tuple[str, int], src: tuple[str, int] | None = None, *, ip_v6: bool = False) -> None: + super().__init__(ip_v6=ip_v6) + self.dst: Final = dst + self.src: Final = src + + def __repr__(self) -> str: + ip, port = self.dst + src = f'{self.src[0]:s}:{self.src[1]:d}' if self.src is not None else 'None' + return f'{self.__class__.__name__:s}(dst={ip:s}:{port:d}, source={src:s})' + + def create_socket(self) -> socket.socket: + sock: Final = super().create_socket() + + # option to set source port/ip + if (src := self.src) is not None: + # set source port/ip + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(src) + + return sock + + @classmethod + async def create(cls, hostname: str, port: int, source_ip: str | None = None, source_port: int = 0, *, + ip_version: USE_IP_VERSION = 'auto') -> Self: + + validate_string(hostname) + validate_port(port) + + dst_ip = await get_ip(hostname, port, ip_version) + + source: tuple[str, int] | None = None + if source_ip is not None: + validate_string(source_ip) + validate_port(source_port, allow_0=True) + + await validate_source_ip(source_ip, source_port) + source = (source_ip, source_port) + + # destination and source IP version must match + try: + dst_ip.__class__(source_ip) + except AddressValueError: + msg = f'Source IP "{source_ip}" is not a valid IPv{dst_ip.version}!' + raise ValueError(msg) from None + + return cls(dst=(hostname, port), src=source, ip_v6=dst_ip.version == 6) + + +class MulticastNetworkInfo(NetworkInfoBase): + def __init__(self, src: tuple[str, int], *, ip_v6: bool = False) -> None: + super().__init__(ip_v6=ip_v6) + self.src: Final = src + + def __repr__(self) -> str: + return f'{self.__class__.__name__:s}(source={self.src[0]:s} ipv6={self.ip_v6})' + + def create_socket(self) -> socket.socket: + sock: Final = super().create_socket() + + # set source port/ip + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(self.src) + + # setup socket for multicast + sock.setsockopt( + socket.IPPROTO_IP, + socket.IPV6_MULTICAST_IF if self.ip_v6 else socket.IP_MULTICAST_IF, + socket.inet_pton(AF_INET6 if self.ip_v6 else AF_INET, self.src[0]) + ) + + return sock + + @classmethod + async def create(cls, interface_ip: str, interface_port: int = 0) -> Self: + validate_string(interface_ip) + validate_port(interface_port, allow_0=True) + + dst_ip = await validate_source_ip(interface_ip, interface_port) + return cls(src=(interface_ip, interface_port), ip_v6=dst_ip.version == 6) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 5a630b1..030c8d0 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -1,6 +1,5 @@ from __future__ import annotations -import logging import socket from asyncio import sleep from time import monotonic @@ -18,9 +17,6 @@ import pyartnet -log = logging.getLogger('pyartnet.ArtNetNode') - - UNIVERSE_TYPE = TypeVar('UNIVERSE_TYPE', bound='pyartnet.base.BaseUniverse') diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index 6d2b097..863f1af 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -13,8 +13,7 @@ # ----------------------------------------------------------------------------- -# Documentation for KiNet Protocol: -# todo: find links +# Documentation for KiNet Protocol is unclear # ----------------------------------------------------------------------------- KINET_PORT: Final = 6038 diff --git a/tests/test_base/__init__.py b/tests/test_base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_base_node.py b/tests/test_base/test_base_node.py similarity index 100% rename from tests/test_base_node.py rename to tests/test_base/test_base_node.py diff --git a/tests/test_base/test_network.py b/tests/test_base/test_network.py new file mode 100644 index 0000000..c7296b9 --- /dev/null +++ b/tests/test_base/test_network.py @@ -0,0 +1,36 @@ +import pytest + +from pyartnet.base._network import get_ip, validate_source_ip + + +async def test_hostname() -> None: + with pytest.raises(ValueError) as e: # noqa: PT011 + await validate_source_ip('does_not_exist', 0) + + assert str(e.value).startswith('Source IP "does_not_exist" is not available on this system!') + + +async def test_get_ip() -> None: + # ip address v4 + address = '127.0.0.1' + obj = await get_ip(address, 0, ip_version='v6') + assert str(obj) == address + assert obj.version == 4 + + # ip address v6 + address = '::1' + obj = await get_ip(address, 0, ip_version='v4') + assert str(obj) == address + assert obj.version == 6 + + # hostname gets resolved + obj = await get_ip('localhost', 0, ip_version='v4') + assert str(obj) == '127.0.0.1' + assert obj.version == 4 + + obj = await get_ip('localhost', 0, ip_version='v6') + assert str(obj) == '::1' + assert obj.version == 6 + + obj = await get_ip('localhost', 0, ip_version='auto') + assert str(obj) in ('::1', '127.0.0.1') diff --git a/uv.lock b/uv.lock index 5d61ced..1148b7f 100644 --- a/uv.lock +++ b/uv.lock @@ -68,11 +68,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.8.3" +version = "2025.10.5" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, ] [[package]] @@ -86,88 +86,122 @@ wheels = [ [[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" }, +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4e/3926a1c11f0433791985727965263f788af00db3482d89a7545ca5ecc921/charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", size = 198599, upload-time = "2025-10-14T04:41:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7c/b92d1d1dcffc34592e71ea19c882b6709e43d20fa498042dea8b815638d7/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", size = 143090, upload-time = "2025-10-14T04:41:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/84/ce/61a28d3bb77281eb24107b937a497f3c43089326d27832a63dcedaab0478/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", size = 139490, upload-time = "2025-10-14T04:41:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/c9e59a91b2061c6f8bb98a150670cb16d4cd7c4ba7d11ad0cdf789155f41/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", size = 155334, upload-time = "2025-10-14T04:41:56.724Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/f17ae176a80f22ff823456af91ba3bc59df308154ff53aef0d39eb3d3419/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", size = 152823, upload-time = "2025-10-14T04:41:58.236Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fa/cf5bb2409a385f78750e78c8d2e24780964976acdaaed65dbd6083ae5b40/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", size = 147618, upload-time = "2025-10-14T04:41:59.409Z" }, + { url = "https://files.pythonhosted.org/packages/9b/63/579784a65bc7de2d4518d40bb8f1870900163e86f17f21fd1384318c459d/charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", size = 145516, upload-time = "2025-10-14T04:42:00.579Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a9/94ec6266cd394e8f93a4d69cca651d61bf6ac58d2a0422163b30c698f2c7/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", size = 145266, upload-time = "2025-10-14T04:42:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/d6626eb97764b58c2779fa7928fa7d1a49adb8ce687c2dbba4db003c1939/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", size = 139559, upload-time = "2025-10-14T04:42:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/09/01/ddbe6b01313ba191dbb0a43c7563bc770f2448c18127f9ea4b119c44dff0/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", size = 156653, upload-time = "2025-10-14T04:42:04.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d05543378bea89296e9af4510b44c704626e191da447235c8fdedfc5b7b2/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", size = 145644, upload-time = "2025-10-14T04:42:05.211Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/2866c4377998ef8a1f6802f6431e774a4c8ebe75b0a6e569ceec55c9cbfb/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", size = 153964, upload-time = "2025-10-14T04:42:06.341Z" }, + { url = "https://files.pythonhosted.org/packages/4a/66/66c72468a737b4cbd7851ba2c522fe35c600575fbeac944460b4fd4a06fe/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", size = 148777, upload-time = "2025-10-14T04:42:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/d0d56677fdddbffa8ca00ec411f67bb8c947f9876374ddc9d160d4f2c4b3/charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", size = 98687, upload-time = "2025-10-14T04:42:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/00/64/c3bc303d1b586480b1c8e6e1e2191a6d6dd40255244e5cf16763dcec52e6/charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", size = 106115, upload-time = "2025-10-14T04:42:09.793Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] @@ -220,7 +254,7 @@ 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.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.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 = [ @@ -244,8 +278,6 @@ name = "filelock" version = "3.19.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } @@ -253,6 +285,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, ] +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + [[package]] name = "identify" version = "2.6.1" @@ -267,25 +312,25 @@ wheels = [ [[package]] name = "identify" -version = "2.6.14" +version = "2.6.15" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.11" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] @@ -331,11 +376,28 @@ wheels = [ name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] 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 = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -546,8 +608,6 @@ name = "platformdirs" version = "4.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.11'", - "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" } @@ -555,6 +615,19 @@ 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 = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -611,7 +684,7 @@ resolution-markers = [ ] dependencies = [ { name = "cfgv", marker = "python_full_version >= '3.9'" }, - { name = "identify", version = "2.6.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "identify", version = "2.6.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "nodeenv", marker = "python_full_version >= '3.9'" }, { name = "pyyaml", marker = "python_full_version >= '3.9'" }, { name = "virtualenv", marker = "python_full_version >= '3.9'" }, @@ -646,7 +719,7 @@ dev = [ { name = "sphinx-autodoc-typehints", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "sphinx-autodoc-typehints", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx-autodoc-typehints", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-typehints", version = "3.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-exec-code" }, { name = "sphinx-rtd-theme" }, ] @@ -660,7 +733,7 @@ docs = [ { name = "sphinx-autodoc-typehints", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "sphinx-autodoc-typehints", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx-autodoc-typehints", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-typehints", version = "3.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-exec-code" }, { name = "sphinx-rtd-theme" }, ] @@ -716,7 +789,7 @@ resolution-markers = [ 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 = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, 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'" }, @@ -738,7 +811,8 @@ resolution-markers = [ 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 = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { 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'" }, @@ -921,28 +995,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.13.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" }, - { url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" }, - { url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" }, - { url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" }, - { url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" }, - { url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" }, - { url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" }, - { url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" }, - { url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" }, - { url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" }, - { url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" }, - { url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" }, +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" }, + { url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" }, + { url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" }, + { url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" }, + { url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" }, + { url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" }, + { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" }, ] [[package]] @@ -1126,7 +1200,7 @@ wheels = [ [[package]] name = "sphinx-autodoc-typehints" -version = "3.2.0" +version = "3.5.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", @@ -1134,9 +1208,9 @@ resolution-markers = [ dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724, upload-time = "2025-04-25T16:53:25.872Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/4f/4fd5583678bb7dc8afa69e9b309e6a99ee8d79ad3a4728f4e52fd7cb37c7/sphinx_autodoc_typehints-3.5.2.tar.gz", hash = "sha256:5fcd4a3eb7aa89424c1e2e32bedca66edc38367569c9169a80f4b3e934171fdb", size = 37839, upload-time = "2025-10-16T00:50:15.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563, upload-time = "2025-04-25T16:53:24.492Z" }, + { url = "https://files.pythonhosted.org/packages/05/f2/9657c98a66973b7c35bfd48ba65d1922860de9598fbb535cd96e3f58a908/sphinx_autodoc_typehints-3.5.2-py3-none-any.whl", hash = "sha256:0accd043619f53c86705958e323b419e41667917045ac9215d7be1b493648d8c", size = 21184, upload-time = "2025-10-16T00:50:13.973Z" }, ] [[package]] @@ -1322,41 +1396,51 @@ wheels = [ [[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" }, +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] [[package]] @@ -1413,20 +1497,22 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.34.0" +version = "20.35.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock", version = "3.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "filelock", version = "3.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, 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 = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { 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.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" }, ] [[package]] From 29916ee811ddc0b96b2032b36f134484e93583a0 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:34:24 +0100 Subject: [PATCH 33/49] implement network interface --- src/pyartnet/base/base_node.py | 52 ++++++---------- src/pyartnet/base/{_network.py => network.py} | 10 +-- src/pyartnet/impl_artnet/node.py | 23 ++++--- src/pyartnet/impl_kinet/node.py | 18 +++--- src/pyartnet/impl_sacn/node.py | 61 +++++++------------ tests/conftest.py | 9 +-- tests/helper.py | 23 +++++-- tests/test_base/test_base_node.py | 25 ++++++++ tests/test_base/test_network.py | 2 +- tests/test_impl/test_artnet.py | 9 +-- tests/test_impl/test_impl.py | 13 ++-- tests/test_impl/test_sacn.py | 18 ++++-- 12 files changed, 142 insertions(+), 121 deletions(-) rename src/pyartnet/base/{_network.py => network.py} (96%) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 030c8d0..94e3d0e 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -1,17 +1,16 @@ from __future__ import annotations -import socket from asyncio import sleep from time import monotonic from typing import TYPE_CHECKING, Final, Generic, TypeVar from typing_extensions import Self +from pyartnet.base.background_task import ExceptionIgnoringTask, SimpleBackgroundTask +from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget +from pyartnet.base.output_correction import OutputCorrection from pyartnet.errors import DuplicateUniverseError, UniverseNotFoundError -from .background_task import ExceptionIgnoringTask, SimpleBackgroundTask -from .output_correction import OutputCorrection - if TYPE_CHECKING: import pyartnet @@ -22,48 +21,40 @@ # noinspection PyProtectedMember class BaseNode(OutputCorrection, Generic[UNIVERSE_TYPE]): - def __init__(self, ip: str, port: int, *, + def __init__(self, network: UnicastNetworkTarget | MulticastNetworkTarget, *, + name: str | None = None, max_fps: int = 25, - refresh_every: float = 2, start_refresh_task: bool = True, - source_address: tuple[str, int] | None = None) -> None: + refresh_every: float = 2, start_refresh_task: bool = True) -> None: super().__init__() - # Destination - self._ip: Final = ip - self._port: Final = port - self._dst: Final = (self._ip, self._port) - - # socket setup - self._socket: Final = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP - self._socket.setblocking(False) # nonblocking for true asyncio - - # option to set source port/ip - if source_address is not None: - self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._socket.bind(source_address) - - # Name used for the Tasks (e.g. in error msg) - name: Final = f'{self._ip:s}:{self._port}' + self._network: Final = network + self._socket: Final = network.create_socket() + self._name: Final = name if name is not None else f'{self.__class__.__name__}-{id(self):x}' # refresh task self._refresh_every: float = max(0.1, refresh_every) - self._refresh_task: Final = ExceptionIgnoringTask(self._periodic_refresh_worker, f'Refresh task {name:s}') + self._refresh_task: Final = ExceptionIgnoringTask(self._periodic_refresh_worker, f'Refresh task {self._name:s}') if start_refresh_task: self._refresh_task.start() # fade task self._process_every: float = 1 / max(1, max_fps) - self._process_task: Final = SimpleBackgroundTask(self._process_values_task, f'Process task {name:s}') + self._process_task: Final = SimpleBackgroundTask(self._process_values_task, f'Process task {self._name:s}') self._process_jobs: list[pyartnet.base.ChannelBoundFade] = [] # packet data self._packet_base: bytearray | bytes = bytearray() - self._last_send: float = 0 # containing universes self._universes: tuple[UNIVERSE_TYPE, ...] = () self._universe_map: dict[int, UNIVERSE_TYPE] = {} + def __repr__(self) -> str: + universe_str = '-' if not self._universes else ','.join(str(u._universe) for u in self._universes) + network = str(self._network).replace('NetworkTarget', '') + return (f'<{self.__class__.__name__:s} name={self._name:s} network={network!s} ' + f'universe{"s" if len(self._universes) != 1 else ""}={universe_str:s}>') + def _apply_output_correction(self) -> None: for u in self._universes: u._apply_output_correction() @@ -77,12 +68,9 @@ def set_synchronous_mode(self, enabled: bool) -> Self: def _send_synchronization(self) -> None: pass - def _send_data(self, data: bytearray | bytes, dst: tuple[str, int] | str | None = None) -> int: - - ret = self._socket.sendto(self._packet_base + data, self._dst if dst is None else dst) - - self._last_send = monotonic() - return ret + def _send_data(self, data: bytearray | bytes, dst: tuple[str, int] | str | None = None) -> None: + self._socket.sendto(self._packet_base + data, dst) + return None async def _process_values_task(self) -> None: # wait a little, so we can schedule multiple tasks/updates, and they all start together diff --git a/src/pyartnet/base/_network.py b/src/pyartnet/base/network.py similarity index 96% rename from src/pyartnet/base/_network.py rename to src/pyartnet/base/network.py index d5d0b24..0914721 100644 --- a/src/pyartnet/base/_network.py +++ b/src/pyartnet/base/network.py @@ -4,7 +4,7 @@ from asyncio import get_running_loop from ipaddress import AddressValueError, IPv4Address, IPv6Address from socket import AF_INET, AF_INET6, AF_UNSPEC, SOCK_DGRAM -from typing import Final, Literal +from typing import Final, Literal, override from typing_extensions import Self @@ -85,7 +85,7 @@ async def get_ip(host: str, port: int, ip_version: USE_IP_VERSION) -> IPv4Addres return IPv6Address(resolved_ip) -class NetworkInfoBase: +class NetworkTargetBase: def __init__(self, *, ip_v6: bool = False) -> None: self.ip_v6: Final = ip_v6 @@ -104,7 +104,7 @@ def validate_ip(self, ip: str) -> str: return ip -class UnicastNetworkInfo(NetworkInfoBase): +class UnicastNetworkTarget(NetworkTargetBase): def __init__(self, dst: tuple[str, int], src: tuple[str, int] | None = None, *, ip_v6: bool = False) -> None: super().__init__(ip_v6=ip_v6) self.dst: Final = dst @@ -115,6 +115,7 @@ def __repr__(self) -> str: src = f'{self.src[0]:s}:{self.src[1]:d}' if self.src is not None else 'None' return f'{self.__class__.__name__:s}(dst={ip:s}:{port:d}, source={src:s})' + @override def create_socket(self) -> socket.socket: sock: Final = super().create_socket() @@ -153,7 +154,7 @@ async def create(cls, hostname: str, port: int, source_ip: str | None = None, so return cls(dst=(hostname, port), src=source, ip_v6=dst_ip.version == 6) -class MulticastNetworkInfo(NetworkInfoBase): +class MulticastNetworkTarget(NetworkTargetBase): def __init__(self, src: tuple[str, int], *, ip_v6: bool = False) -> None: super().__init__(ip_v6=ip_v6) self.src: Final = src @@ -161,6 +162,7 @@ def __init__(self, src: tuple[str, int], *, ip_v6: bool = False) -> None: def __repr__(self) -> str: return f'{self.__class__.__name__:s}(source={self.src[0]:s} ipv6={self.ip_v6})' + @override def create_socket(self) -> socket.socket: sock: Final = super().create_socket() diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 82408a5..4093796 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -7,6 +7,7 @@ import pyartnet from pyartnet.base import BaseNode +from pyartnet.base.network import UnicastNetworkTarget from pyartnet.base.seq_counter import SequenceCounter from pyartnet.errors import InvalidUniverseAddressError @@ -22,18 +23,19 @@ class ArtNetNode(BaseNode['pyartnet.impl_artnet.ArtNetUniverse']): - def __init__(self, ip: str, port: int = ARTNET_PORT, *, + def __init__(self, network: UnicastNetworkTarget, *, + name: str | None = None, max_fps: int = 25, refresh_every: float = 2, start_refresh_task: bool = True, - source_address: tuple[str, int] | None = None, # ArtNet specific fields sequence_counter: bool = True ) -> None: - super().__init__(ip=ip, port=port, - max_fps=max_fps, - refresh_every=refresh_every, start_refresh_task=start_refresh_task, - source_address=source_address) + super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every, + start_refresh_task=start_refresh_task) + + self._dst: Final = network.dst + self._ip: Final = self._dst[0] # ArtNet specific fields self._sequence_ctr: Final = SequenceCounter(1) if sequence_counter else SequenceCounter(0, 0) @@ -64,7 +66,7 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, packet[8:10] = byte_size.to_bytes(2, 'big') # 2 | Number of channels Big Endian packet[10: _size] = values # 0 - 512 | Channel values - self._send_data(packet) + self._send_data(packet, self._dst) # log complete packet if log.isEnabledFor(logging.DEBUG): @@ -84,7 +86,8 @@ def _validate_universe_nr(self, nr: int) -> int: def __log_artnet_frame(self, p: bytearray | bytes) -> None: """Log Artnet Frame""" - assert isinstance(p, (bytearray, bytes)) + if not isinstance(p, (bytearray, bytes)): + raise TypeError() # runs the first time if not hasattr(self, '_log_ctr'): @@ -102,7 +105,6 @@ def __log_artnet_frame(self, p: bytearray | bytes) -> None: pre = bytearray(p[:12]).hex().upper() # low byte first: 5200 -> 0052 - a = p[8:10] if p[8:10] == b'\x00\x52': log.debug(f'Sync to {self._ip:s}: {pre} {p[12]:02x} {p[13]:02x}') return None @@ -154,6 +156,7 @@ def __log_artnet_frame(self, p: bytearray | bytes) -> None: if show_description: log.debug(out_desc) log.debug(out) + return None @override def set_synchronous_mode(self, enabled: bool) -> Self: @@ -184,7 +187,7 @@ def _send_synchronization(self) -> None: packet[4] = 0 # 1 | Aux1 packet[5] = 0 # 1 | Aux2 - self._send_data(packet) + self._send_data(packet, self._dst) # log complete packet if log.isEnabledFor(logging.DEBUG): diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index 863f1af..ecaf0e8 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -9,6 +9,7 @@ import pyartnet from pyartnet.base import BaseNode +from pyartnet.base.network import UnicastNetworkTarget from pyartnet.errors import InvalidUniverseAddressError @@ -22,14 +23,14 @@ class KiNetNode(BaseNode['pyartnet.impl_kinet.KiNetUniverse']): - def __init__(self, ip: str, port: int = KINET_PORT, *, + def __init__(self, network: UnicastNetworkTarget, *, + name: str | None = None, max_fps: int = 25, - refresh_every: float = 2, start_refresh_task: bool = True, - source_address: tuple[str, int] | None = None) -> None: - super().__init__(ip=ip, port=port, - max_fps=max_fps, - refresh_every=refresh_every, start_refresh_task=start_refresh_task, - source_address=source_address) + refresh_every: float = 2, start_refresh_task: bool = True) -> None: + super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every, + start_refresh_task=start_refresh_task) + + self._dst: Final = network.dst # build base packet packet = bytearray() @@ -48,7 +49,8 @@ def _send_universe(self, id: int, byte_size: int, if log.isEnabledFor(LVL_DEBUG): # log complete packet - log.debug(f'Sending KiNet frame to {self._ip}:{self._port}: {(self._packet_base + packet).hex()}') + ip, port = self._dst + log.debug(f'Sending KiNet frame to {ip}:{port}: {(self._packet_base + packet).hex()}') @override def _create_universe(self, nr: int) -> pyartnet.impl_kinet.KiNetUniverse: diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 21a55e3..5dce233 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -1,15 +1,15 @@ from __future__ import annotations import logging -from ipaddress import IPv6Address from logging import DEBUG as LVL_DEBUG from typing import Final from uuid import uuid4 -from typing_extensions import Self, override +from typing_extensions import override import pyartnet.impl_sacn.universe from pyartnet.base import BaseNode, SequenceCounter +from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget from pyartnet.errors import InvalidCidError, InvalidUniverseAddressError @@ -36,18 +36,16 @@ class SacnNode(BaseNode['pyartnet.impl_sacn.SacnUniverse']): - def __init__(self, ip: str, port: int = ACN_SDT_MULTICAST_PORT, *, + def __init__(self, network: UnicastNetworkTarget | MulticastNetworkTarget, *, + name: str | None = None, max_fps: int = 25, refresh_every: float = 2, start_refresh_task: bool = True, - source_address: tuple[str, int] | None = None, # sACN E1.31 specific fields cid: bytes | None = None, source_name: str | None = None ) -> None: - super().__init__(ip=ip, port=port, - max_fps=max_fps, - refresh_every=refresh_every, start_refresh_task=start_refresh_task, - source_address=source_address) + super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every, + start_refresh_task=start_refresh_task) # CID Field if cid is not None: @@ -61,7 +59,7 @@ def __init__(self, ip: str, port: int = ACN_SDT_MULTICAST_PORT, *, if source_name is None: source_name = 'PyArtNet' source_name_byte = source_name.encode('utf-8').ljust(64, b'\x00') - if len(source_name_byte) > 64: + if len(source_name_byte) != 64: msg = 'Source name too long!' raise ValueError(msg) self._source_name_byte : bytes = source_name_byte @@ -86,7 +84,7 @@ def __init__(self, ip: str, port: int = ACN_SDT_MULTICAST_PORT, *, # See Spec 6.2.4 E1.31 Data Packet: Synchronization Address self._sync_address: int = 0 # See spec 9.3 Allocation of Multicast Addresses - self._sync_dst: tuple[str, int] = self._dst + self._sync_dst: tuple[str, int] = ('NOT_SET', 0) # See spec 6.3.2 E1.31 Synchronization Packet: Sequence Number self._sync_sequence_number: Final = SequenceCounter() @@ -148,34 +146,17 @@ def _validate_universe_nr(self, nr: int) -> int: return int(nr) def _get_universe_ip_port(self, universe: int) -> tuple[str, int]: - if not self._multicast: - return self._dst + if isinstance(network := self._network, UnicastNetworkTarget): + return network.dst u = self._validate_universe_nr(universe) # IPv6 multicast address - if ':' in self._ip: - IPv6Address(self._ip) # validate IP - return f'FF18::8300:{u:04X}', ACN_SDT_MULTICAST_PORT + if network.ip_v6: + return network.validate_ip(f'FF18::8300:{u:04X}'), ACN_SDT_MULTICAST_PORT # IPv4 multicast address - return f'239.255.{u // 255:d}.{u % 255:d}', ACN_SDT_MULTICAST_PORT - - def set_multicast_mode(self, enabled: bool) -> Self: - """Either send packets to the node directly or through multicast. - :param enabled: If True multicast is enabled - """ - self._multicast = enabled - - # update all universe destinations - for universe in self._universes: - universe._dst = self._get_universe_ip_port(universe._universe) - - # update sync package destination - if self._sync_address: - self._sync_dst = self._get_universe_ip_port(self._sync_address) - - return self + return network.validate_ip(f'239.255.{u // 255:d}.{u % 255:d}'), ACN_SDT_MULTICAST_PORT @override def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0) -> None: # type: ignore [override] @@ -188,16 +169,18 @@ def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0) same for all nodes that should be synchronized. """ if enabled: - self._sync_address = sync_address = self._validate_universe_nr(synchronization_address) + sync_address = self._validate_universe_nr(synchronization_address) self._sync_dst = self._get_universe_ip_port(sync_address) + self._sync_address = sync_address + return None - else: - if synchronization_address != 0: - msg = 'synchronization_address must be 0 when disabling synchronous mode!' - raise ValueError(msg) + if synchronization_address != 0: + msg = 'synchronization_address must be 0 when disabling synchronous mode!' + raise ValueError(msg) + + self._sync_address = 0 + return None - self._sync_address = 0 - self._sync_dst = self._dst @override def _send_synchronization(self) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index dd2f853..a80be54 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ from tests.helper import MockedSocket from pyartnet.base import BaseNode, BaseUniverse +from pyartnet.base.network import NetworkTargetBase, UnicastNetworkTarget if TYPE_CHECKING: @@ -21,8 +22,8 @@ class TestingNode(BaseNode): __test__ = False # prevent this from being collected by pytest - def __init__(self, ip: str, port: int) -> None: - super().__init__(ip, port, max_fps=1_000 // STEP_MS, start_refresh_task=False) + def __init__(self, network: NetworkTargetBase) -> None: + super().__init__(network, max_fps=1_000 // STEP_MS, start_refresh_task=False) self.data = [] def _send_universe(self, id: int, byte_size: int, @@ -51,13 +52,13 @@ def patched_socket(monkeypatch): def test_patched_socket(patched_socket) -> None: - node = TestingNode('IP', 9999) + node = TestingNode(UnicastNetworkTarget(dst=('IP', 9999))) assert node._socket.sendto is patched_socket @pytest.fixture def node(): - return TestingNode('IP', 9999) + return TestingNode(UnicastNetworkTarget(dst=('IP', 9999))) @pytest.fixture diff --git a/tests/helper.py b/tests/helper.py index 713d158..33b3817 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -6,7 +6,7 @@ from pytest import MonkeyPatch -import pyartnet +import pyartnet.base.network as network_module if TYPE_CHECKING: @@ -19,15 +19,26 @@ def __init__(self) -> None: self.mp = MonkeyPatch() def mock(self): - m_socket_obj = Mock(['sendto', 'setblocking'], name='socket_obj') + m_socket_obj = Mock(['sendto', 'setblocking', 'setsockopt', 'bind'], name='socket_obj') m_socket_obj.sendto = m_sendto = Mock(name='socket_obj.sendto') - m = Mock(['socket', 'AF_INET', 'SOCK_DGRAM'], name='Mock socket package') + module_names = [ + name for name in dir(socket) + if name.startswith(('AF_', 'SOCK_', 'SOL_', 'IPPROTO_', 'IP_', 'SO_',)) or + name in ('socket', 'gethostname', 'inet_pton') + ] + + m = Mock(module_names, name='Mock socket package' + ) + m.gethostname = socket.gethostname m.socket = Mock([], return_value=m_socket_obj, name='Mock socket obj') - m.AF_INET = socket.AF_INET - m.SOCK_DGRAM = socket.AF_INET - self.mp.setattr(pyartnet.base.base_node, 'socket', m) + # Copy constants + for name in dir(socket): + if name.startswith(('AF_', 'SOCK_', 'SOL_', 'IPPROTO_', 'IP_', 'SO_')): + setattr(m, name, getattr(socket, name)) + + self.mp.setattr(network_module, 'socket', m) return m_sendto def undo(self) -> None: diff --git a/tests/test_base/test_base_node.py b/tests/test_base/test_base_node.py index 41786cd..2691962 100644 --- a/tests/test_base/test_base_node.py +++ b/tests/test_base/test_base_node.py @@ -1,3 +1,4 @@ +import re from time import monotonic import pytest @@ -5,9 +6,33 @@ from pyartnet.base import BaseUniverse from pyartnet.base.channel import Channel +from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget from pyartnet.errors import DuplicateUniverseError +def test_repr(node: TestingNode) -> None: + + re_id = re.compile(r'(name=TestingNode-)[0-f]+') + + def _repr(obj: object) -> str: + return re_id.sub(r'\g<1>123456', str(obj)) + + # Unicast + node = TestingNode(UnicastNetworkTarget(dst=('IP', 9999))) + assert _repr(node) == '' + + node.add_universe(9) + assert _repr(node) == '' + + node.add_universe(2) + assert (_repr(node) == + '') + + # Multicast + node = TestingNode(MulticastNetworkTarget(src=('IP', 99999))) + assert _repr(node) == '' + + def test_universe_add_get(node: TestingNode) -> None: u = node.add_universe() diff --git a/tests/test_base/test_network.py b/tests/test_base/test_network.py index c7296b9..e2b7c22 100644 --- a/tests/test_base/test_network.py +++ b/tests/test_base/test_network.py @@ -1,6 +1,6 @@ import pytest -from pyartnet.base._network import get_ip, validate_source_ip +from pyartnet.base.network import get_ip, validate_source_ip async def test_hostname() -> None: diff --git a/tests/test_impl/test_artnet.py b/tests/test_impl/test_artnet.py index 8d5437b..37084ca 100644 --- a/tests/test_impl/test_artnet.py +++ b/tests/test_impl/test_artnet.py @@ -5,10 +5,11 @@ from unittest.mock import call from pyartnet import ArtNetNode +from pyartnet.base.network import UnicastNetworkTarget async def test_artnet() -> None: - arnet = ArtNetNode('ip', 9999999, start_refresh_task=True) + arnet = ArtNetNode(UnicastNetworkTarget(('ip', 9999999)), start_refresh_task=True) channel = arnet.add_universe(1).add_channel(1, 10) channel.set_values(range(1, 11)) @@ -27,7 +28,7 @@ async def test_artnet() -> None: async def test_artnet_with_sync(caplog) -> None: caplog.set_level(logging.DEBUG) - artnet = ArtNetNode('ip', 9999999, start_refresh_task=False) + artnet = ArtNetNode(UnicastNetworkTarget(('ip', 9999999)), name='device1', start_refresh_task=False) artnet.set_synchronous_mode(True) channel = artnet.add_universe(1).add_channel(1, 10) @@ -48,9 +49,9 @@ async def test_artnet_with_sync(caplog) -> None: assert caplog.record_tuples == [ ('pyartnet.Universe', 10, 'Added channel "1/10": start: 1, stop: 10'), - ('pyartnet.Task', 10, 'Started Process task ip:9999999'), + ('pyartnet.Task', 10, 'Started Process task device1'), ('pyartnet.ArtNetNode', 10, ' Sq Univ Len 1 2 3 4 5 6 7 8 9 10 '), # noqa: E501 ('pyartnet.ArtNetNode', 10, 'Packet to ip: 4172742D4E6574000050000E 01 00 0001 000a 001 002 003 004 005 006 007 008 009 010'), # noqa: E501 ('pyartnet.ArtNetNode', 10, 'Sync to ip: 4172742D4E6574000052000E 00 00'), - ('pyartnet.Task', 10, 'Stopped Process task ip:9999999'), + ('pyartnet.Task', 10, 'Stopped Process task device1'), ] diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index 267848a..0eaa1e5 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -7,6 +7,7 @@ from pyartnet import ArtNetNode, KiNetNode, SacnNode from pyartnet.base import BaseNode +from pyartnet.base.network import UnicastNetworkTarget from pyartnet.errors import InvalidUniverseAddressError @@ -17,12 +18,10 @@ def test_same_cls_signature(c) -> None: for name, base_parameter in sig_base.parameters.items(): assert name in sig_obj.parameters + # network parameter can be different + if name == 'network': + continue obj_parameter = sig_obj.parameters[name] - - # some ports have a default which we ignore here - if name == 'port': - obj_parameter = obj_parameter.replace(default=inspect.Parameter.empty) - assert obj_parameter == base_parameter @@ -30,7 +29,7 @@ def test_same_cls_signature(c) -> None: async def test_set_funcs(node: TestingNode, caplog, cls) -> None: caplog.set_level(logging.DEBUG) - n = cls('ip', 9999) + n = cls(UnicastNetworkTarget(('ip', 9999999), ip_v6=False)) u = n.add_universe(1) c = u.add_channel(1, 1) @@ -44,7 +43,7 @@ async def test_set_funcs(node: TestingNode, caplog, cls) -> None: @pytest.mark.parametrize('cls', [ArtNetNode, SacnNode, KiNetNode]) async def test_universe_validation(node: TestingNode, cls) -> None: - n = cls('ip', 9999) + n = cls(UnicastNetworkTarget(('ip', 9999999), ip_v6=False)) with pytest.raises(TypeError): n.add_universe(1.3) diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index ff691ee..63548db 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -6,11 +6,12 @@ import pytest from pyartnet import SacnNode +from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget async def test_sacn() -> None: sacn = SacnNode( - 'ip', 9999999, + UnicastNetworkTarget(('ip', 9999999), ip_v6=False), cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', source_name='default source name', start_refresh_task=True @@ -37,14 +38,19 @@ async def test_sacn() -> None: async def test_sacn_with_sync(caplog, multicast) -> None: caplog.set_level(logging.DEBUG) + if multicast: + network = MulticastNetworkTarget(('ip', 9999999), ip_v6=False) + else: + network = UnicastNetworkTarget(('ip', 9999999), ip_v6=False) + sacn = SacnNode( - 'ip', 9999999, + network, cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', source_name='default source name', - start_refresh_task=False + start_refresh_task=False, + name='device1' ) sacn.set_synchronous_mode(True, 2) - sacn.set_multicast_mode(multicast) channel = sacn.add_universe(1).add_channel(1, 10) channel.set_values(range(1, 11)) @@ -72,8 +78,8 @@ async def test_sacn_with_sync(caplog, multicast) -> None: assert caplog.record_tuples == [ ('pyartnet.Universe', 10, 'Added channel "1/10": start: 1, stop: 10'), - ('pyartnet.Task', 10, 'Started Process task ip:9999999'), + ('pyartnet.Task', 10, 'Started Process task device1'), ('pyartnet.SacnNode', 10, f'Sending sACN frame to {data_msg:s}: {data:s}'), ('pyartnet.SacnNode', 10, f'Sending sACN Synchronization Packet to {sync_msg}: {sync_data:s}'), - ('pyartnet.Task', 10, 'Stopped Process task ip:9999999') + ('pyartnet.Task', 10, 'Stopped Process task device1') ] From 0b82e23ad9aab6a46e18830229fd5f07d108dc88 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:52:09 +0100 Subject: [PATCH 34/49] implement class factory --- docs/pyartnet.rst | 89 +++++++++++++++------------- src/pyartnet/base/background_task.py | 14 ++++- src/pyartnet/base/base_node.py | 31 +++++++--- src/pyartnet/impl_artnet/node.py | 26 +++++++- src/pyartnet/impl_kinet/node.py | 28 +++++++-- src/pyartnet/impl_sacn/node.py | 44 ++++++++++++-- tests/conftest.py | 2 +- tests/helper.py | 2 +- tests/test_base/test_base_node.py | 10 ++++ tests/test_impl/test_artnet.py | 68 +++++++++++---------- tests/test_impl/test_impl.py | 24 ++++++++ 11 files changed, 245 insertions(+), 93 deletions(-) diff --git a/docs/pyartnet.rst b/docs/pyartnet.rst index 7ae7593..7b55672 100644 --- a/docs/pyartnet.rst +++ b/docs/pyartnet.rst @@ -22,27 +22,28 @@ Getting Started from pyartnet import ArtNetNode async def main(): - # Run this code in your async function - node = ArtNetNode('IP', 6454) - # Create universe 0 - universe = node.add_universe(0) + node = await ArtNetNode.create('IP', 6454) + async with node: - # Add a channel to the universe which consists of 3 values - # Default size of a value is 8Bit (0..255) so this would fill - # the DMX values 1..3 of the universe - channel = universe.add_channel(start=1, width=3) + # Create universe 0 + universe = node.add_universe(0) - # Fade channel to 255,0,0 in 5s - # The fade will automatically run in the background - channel.add_fade([255,0,0], 1000) + # Add a channel to the universe which consists of 3 values + # Default size of a value is 8Bit (0..255) so this would fill + # the DMX values 1..3 of the universe + channel = universe.add_channel(start=1, width=3) - # this can be used to wait till the fade is complete - await channel + # Fade channel to 255,0,0 in 5s + # The fade will automatically run in the background + channel.add_fade([255,0,0], 1000) - # hide: start - node.stop_refresh() - # hide: stop + # this can be used to wait till the fade is complete + await channel + + # hide: start + node.stop_refresh() + # hide: stop asyncio.run(main()) @@ -70,23 +71,25 @@ If no channel name is specified during creation the default name will be built w # hide: stop # create node/universe - node = ArtNetNode('IP', 6454) - universe = node.add_universe(0) + node = await ArtNetNode.create('IP', 6454) + async with node: + + universe = node.add_universe(0) - # create the channel - channel = universe.add_channel(start=1, width=3) + # create the channel + channel = universe.add_channel(start=1, width=3) - # after creation this would also work (default name) - channel = universe['1/3'] - channel = universe.get_channel('1/3') + # after creation this would also work (default name) + channel = universe['1/3'] + channel = universe.get_channel('1/3') - # it's possible to name the channel during creation - universe.add_channel(start=4, width=3, channel_name='Dimmer1') + # it's possible to name the channel during creation + universe.add_channel(start=4, width=3, channel_name='Dimmer1') - # access is then by name - channel = universe['Dimmer1'] - channel = universe.get_channel('Dimmer1') + # access is then by name + channel = universe['Dimmer1'] + channel = universe.get_channel('Dimmer1') # hide: start asyncio.run(main()) @@ -111,11 +114,12 @@ Channel properties can be set when creating the channel through :meth:`BaseUnive # hide: stop # create node/universe - node = ArtNetNode('IP', 6454) - universe = node.add_universe(0) + node = await ArtNetNode.create('IP', 6454) + async with node: + universe = node.add_universe(0) - # create a 16bit channel - channel = universe.add_channel(start=1, width=3, byte_size=2) + # create a 16bit channel + channel = universe.add_channel(start=1, width=3, byte_size=2) # hide: start asyncio.run(main()) @@ -160,19 +164,20 @@ Example from pyartnet import ArtNetNode, output_correction # create node/universe/channel - node = ArtNetNode('IP', 6454) - universe = node.add_universe(0) - channel = universe.add_channel(start=1, width=3) + node = await ArtNetNode.create('IP', 6454) + async with node: + universe = node.add_universe(0) + channel = universe.add_channel(start=1, width=3) - # set quadratic correction for the whole universe to quadratic - universe.set_output_correction(output_correction.quadratic) + # set quadratic correction for the whole universe to quadratic + universe.set_output_correction(output_correction.quadratic) - # Explicitly set output for this channel to linear - channel.set_output_correction(output_correction.linear) + # Explicitly set output for this channel to linear + channel.set_output_correction(output_correction.linear) - # Remove output correction for the channel. - # The channel will now use the correction from the universe again - channel.set_output_correction(None) + # Remove output correction for the channel. + # The channel will now use the correction from the universe again + channel.set_output_correction(None) # hide: start diff --git a/src/pyartnet/base/background_task.py b/src/pyartnet/base/background_task.py index d22797e..797792e 100644 --- a/src/pyartnet/base/background_task.py +++ b/src/pyartnet/base/background_task.py @@ -1,7 +1,8 @@ from __future__ import annotations import logging -from asyncio import Task, create_task, current_task, sleep +from asyncio import CancelledError, Task, create_task, current_task, sleep +from contextlib import suppress from time import monotonic from traceback import format_exc from typing import Any, Callable, Coroutine, Final @@ -47,6 +48,17 @@ def cancel(self) -> None: task.cancel() return None + async def cancel_wait(self) -> None: + if (task := self.task) is None: + return None + + self.task = None + task.cancel() + + with suppress(CancelledError): + await task + return None + async def coro_wrap(self) -> None: log.debug(f'Started {self.name}') task = self.task diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 94e3d0e..e757ff3 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -7,13 +7,15 @@ from typing_extensions import Self from pyartnet.base.background_task import ExceptionIgnoringTask, SimpleBackgroundTask -from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget from pyartnet.base.output_correction import OutputCorrection from pyartnet.errors import DuplicateUniverseError, UniverseNotFoundError if TYPE_CHECKING: + from types import TracebackType + import pyartnet + from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget UNIVERSE_TYPE = TypeVar('UNIVERSE_TYPE', bound='pyartnet.base.BaseUniverse') @@ -24,7 +26,7 @@ class BaseNode(OutputCorrection, Generic[UNIVERSE_TYPE]): def __init__(self, network: UnicastNetworkTarget | MulticastNetworkTarget, *, name: str | None = None, max_fps: int = 25, - refresh_every: float = 2, start_refresh_task: bool = True) -> None: + refresh_every: float = 2) -> None: super().__init__() self._network: Final = network @@ -34,8 +36,6 @@ def __init__(self, network: UnicastNetworkTarget | MulticastNetworkTarget, *, # refresh task self._refresh_every: float = max(0.1, refresh_every) self._refresh_task: Final = ExceptionIgnoringTask(self._periodic_refresh_worker, f'Refresh task {self._name:s}') - if start_refresh_task: - self._refresh_task.start() # fade task self._process_every: float = 1 / max(1, max_fps) @@ -55,6 +55,10 @@ def __repr__(self) -> str: return (f'<{self.__class__.__name__:s} name={self._name:s} network={network!s} ' f'universe{"s" if len(self._universes) != 1 else ""}={universe_str:s}>') + @property + def name(self) -> str: + return self._name + def _apply_output_correction(self) -> None: for u in self._universes: u._apply_output_correction() @@ -107,13 +111,13 @@ async def _process_values_task(self) -> None: await sleep(self._process_every) - def start_refresh(self) -> None: + async def start_refresh(self) -> None: """Manually start the refresh task (if not already running)""" self._refresh_task.start() - def stop_refresh(self) -> None: + async def stop_refresh(self) -> None: """Manually stop the refresh task""" - self._refresh_task.cancel() + return await self._refresh_task.cancel_wait() async def _periodic_refresh_worker(self) -> None: while True: @@ -180,3 +184,16 @@ def __getitem__(self, nr: int) -> UNIVERSE_TYPE: def __len__(self) -> int: return len(self._universes) + + async def __aenter__(self) -> Self: + self._refresh_task.start() + + return self + + async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, + exc_tb: TracebackType | None) -> None: + + self._socket.close() + await self._process_task.cancel_wait() + await self._refresh_task.cancel_wait() + return None diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 4093796..407e09d 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -7,7 +7,7 @@ import pyartnet from pyartnet.base import BaseNode -from pyartnet.base.network import UnicastNetworkTarget +from pyartnet.base.network import USE_IP_VERSION, UnicastNetworkTarget from pyartnet.base.seq_counter import SequenceCounter from pyartnet.errors import InvalidUniverseAddressError @@ -31,8 +31,7 @@ def __init__(self, network: UnicastNetworkTarget, *, # ArtNet specific fields sequence_counter: bool = True ) -> None: - super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every, - start_refresh_task=start_refresh_task) + super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every) self._dst: Final = network.dst self._ip: Final = self._dst[0] @@ -48,6 +47,27 @@ def __init__(self, network: UnicastNetworkTarget, *, self._sync_enabled : bool = False + @classmethod + async def create(cls, hostname: str, port: int = ARTNET_PORT, *, + source_ip: str | None = None, source_port: int = 0, ip_version: USE_IP_VERSION = 'auto', + name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: + """Creates a new node. The packages will be sent directly to the node (unicast). + + :param hostname: ip or hostname of the device + :param port: port of device + :param source_ip: ip of the network interface that shall be used to send data + :param source_port: source port + :param ip_version: which ip version to use if hostname is a hostname and not an ip address + :param name: a custom name of the node + :param max_fps: maximum frames per second to send + :param refresh_every: refresh interval in seconds + """ + + network = await UnicastNetworkTarget.create( + hostname, port, source_ip=source_ip, source_port=source_port, ip_version=ip_version + ) + return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) + @override def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: pyartnet.impl_artnet.ArtNetUniverse) -> None: diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index ecaf0e8..3c3806a 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -5,11 +5,11 @@ from struct import pack as s_pack from typing import Final -from typing_extensions import override +from typing_extensions import Self, override import pyartnet from pyartnet.base import BaseNode -from pyartnet.base.network import UnicastNetworkTarget +from pyartnet.base.network import USE_IP_VERSION, UnicastNetworkTarget from pyartnet.errors import InvalidUniverseAddressError @@ -27,8 +27,7 @@ def __init__(self, network: UnicastNetworkTarget, *, name: str | None = None, max_fps: int = 25, refresh_every: float = 2, start_refresh_task: bool = True) -> None: - super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every, - start_refresh_task=start_refresh_task) + super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every) self._dst: Final = network.dst @@ -38,6 +37,27 @@ def __init__(self, network: UnicastNetworkTarget, *, packet.extend(s_pack('>IBBHI', 0, 0, 0, 0, 0xFFFFFFFF)) # sequence, port, padding, flags, timer self._packet_base = bytes(packet) + @classmethod + async def create(cls, hostname: str, port: int = KINET_PORT, *, + source_ip: str | None = None, source_port: int = 0, ip_version: USE_IP_VERSION = 'auto', + name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: + """Creates a new node. The packages will be sent directly to the node (unicast). + + :param hostname: ip or hostname of the device + :param port: port of device + :param source_ip: ip of the network interface that shall be used to send data + :param source_port: source port + :param ip_version: which ip version to use if hostname is a hostname and not an ip address + :param name: a custom name of the node + :param max_fps: maximum frames per second to send + :param refresh_every: refresh interval in seconds + """ + + network = await UnicastNetworkTarget.create( + hostname, port, source_ip=source_ip, source_port=source_port, ip_version=ip_version + ) + return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) + @override def _send_universe(self, id: int, byte_size: int, values: bytearray, universe: pyartnet.impl_kinet.KiNetUniverse) -> None: diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 5dce233..fd438f0 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -5,11 +5,11 @@ from typing import Final from uuid import uuid4 -from typing_extensions import override +from typing_extensions import Self, override import pyartnet.impl_sacn.universe from pyartnet.base import BaseNode, SequenceCounter -from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget +from pyartnet.base.network import USE_IP_VERSION, MulticastNetworkTarget, UnicastNetworkTarget from pyartnet.errors import InvalidCidError, InvalidUniverseAddressError @@ -44,8 +44,7 @@ def __init__(self, network: UnicastNetworkTarget | MulticastNetworkTarget, *, # sACN E1.31 specific fields cid: bytes | None = None, source_name: str | None = None ) -> None: - super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every, - start_refresh_task=start_refresh_task) + super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every) # CID Field if cid is not None: @@ -132,6 +131,43 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, # log complete packet log.debug(f'Sending sACN frame to {_dst_str(universe._dst)}: {(base_packet + packet).hex()}') + @classmethod + async def create(cls, hostname: str, port: int = ACN_SDT_MULTICAST_PORT, *, + source_ip: str | None = None, source_port: int = 0, ip_version: USE_IP_VERSION = 'auto', + name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: + """Creates a new node. The packages will be sent directly to the node (unicast). + + :param hostname: ip or hostname of the device + :param port: port of device + :param source_ip: ip of the network interface that shall be used to send data + :param source_port: source port + :param ip_version: which ip version to use if hostname is a hostname and not an ip address + :param name: a custom name of the node + :param max_fps: maximum frames per second to send + :param refresh_every: refresh interval in seconds + """ + + network = await UnicastNetworkTarget.create( + hostname, port, source_ip=source_ip, source_port=source_port, ip_version=ip_version + ) + return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) + + @classmethod + async def create_multicast(cls, interface_ip: str, interface_port: int = 0, *, + name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: + """Creates a new node. The packages will be sent as multicast. + + :param interface_ip: interface ip of the network interface that shall be used to send data + :param interface_port: source port + :param name: a custom name of the node + :param max_fps: maximum frames per second to send + :param refresh_every: refresh interval in seconds + """ + + network = await MulticastNetworkTarget.create(interface_ip, interface_port) + return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) + + @override def _create_universe(self, nr: int) -> pyartnet.impl_sacn.SacnUniverse: return pyartnet.impl_sacn.SacnUniverse(self, self._validate_universe_nr(nr)) diff --git a/tests/conftest.py b/tests/conftest.py index a80be54..6345221 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ class TestingNode(BaseNode): __test__ = False # prevent this from being collected by pytest def __init__(self, network: NetworkTargetBase) -> None: - super().__init__(network, max_fps=1_000 // STEP_MS, start_refresh_task=False) + super().__init__(network, max_fps=1_000 // STEP_MS) self.data = [] def _send_universe(self, id: int, byte_size: int, diff --git a/tests/helper.py b/tests/helper.py index 33b3817..bce656e 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -19,7 +19,7 @@ def __init__(self) -> None: self.mp = MonkeyPatch() def mock(self): - m_socket_obj = Mock(['sendto', 'setblocking', 'setsockopt', 'bind'], name='socket_obj') + m_socket_obj = Mock(['sendto', 'setblocking', 'setsockopt', 'bind', 'close'], name='socket_obj') m_socket_obj.sendto = m_sendto = Mock(name='socket_obj.sendto') module_names = [ diff --git a/tests/test_base/test_base_node.py b/tests/test_base/test_base_node.py index 2691962..c59a021 100644 --- a/tests/test_base/test_base_node.py +++ b/tests/test_base/test_base_node.py @@ -97,3 +97,13 @@ async def check_wait_time_when_fade(steps: int) -> None: await check_no_wait_time_when_no_fade() await node.wait_for_task_finish() + + +async def test_context(node: TestingNode) -> None: + + node._socket.close.assert_not_called() + + async with node: + assert node._refresh_task.task is not None + + node._socket.close.assert_called() diff --git a/tests/test_impl/test_artnet.py b/tests/test_impl/test_artnet.py index 37084ca..3d2a373 100644 --- a/tests/test_impl/test_artnet.py +++ b/tests/test_impl/test_artnet.py @@ -9,49 +9,57 @@ async def test_artnet() -> None: - arnet = ArtNetNode(UnicastNetworkTarget(('ip', 9999999)), start_refresh_task=True) - channel = arnet.add_universe(1).add_channel(1, 10) - channel.set_values(range(1, 11)) + async with ArtNetNode(UnicastNetworkTarget(('ip', 9999999))) as artnet: + channel = artnet.add_universe(1).add_channel(1, 10) + channel.set_values(range(1, 11)) - data = '4172742d4e6574000050000e01000100000a0102030405060708090a' + data = '4172742d4e6574000050000e01000100000a0102030405060708090a' - await channel - await arnet._process_task.task - await asyncio.sleep(0.3) + await channel + await artnet._process_task.task + await asyncio.sleep(0.3) - m = arnet._socket - m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) + m = artnet._socket + m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) - await channel + await channel async def test_artnet_with_sync(caplog) -> None: caplog.set_level(logging.DEBUG) - artnet = ArtNetNode(UnicastNetworkTarget(('ip', 9999999)), name='device1', start_refresh_task=False) - artnet.set_synchronous_mode(True) + async with ArtNetNode(UnicastNetworkTarget(('ip', 9999999)), name='device1') as artnet: + artnet.set_synchronous_mode(True) - channel = artnet.add_universe(1).add_channel(1, 10) - channel.set_values(range(1, 11)) + channel = artnet.add_universe(1).add_channel(1, 10) + channel.set_values(range(1, 11)) - data = '4172742d4e6574000050000e01000100000a0102030405060708090a' - sync_data = '4172742d4e6574000052000e0000' + data = '4172742d4e6574000050000e01000100000a0102030405060708090a' + sync_data = '4172742d4e6574000052000e0000' - await channel - await artnet._process_task.task - await asyncio.sleep(0.3) + await channel + await artnet._process_task.task + await asyncio.sleep(0.3) - m = artnet._socket - assert m.sendto.call_args_list == [ - call(bytearray(a2b_hex(data)), ('ip', 9999999)), - call(bytearray(a2b_hex(sync_data)), ('ip', 9999999)), - ] + m = artnet._socket + assert m.sendto.call_args_list == [ + call(bytearray(a2b_hex(data)), ('ip', 9999999)), + call(bytearray(a2b_hex(sync_data)), ('ip', 9999999)), + ] + + assert caplog.record_tuples == [ + ('pyartnet.Universe', 10, 'Added channel "1/10": start: 1, stop: 10'), + ('pyartnet.Task', 10, 'Started Refresh task device1'), + ('pyartnet.Task', 10, 'Started Process task device1'), + ('pyartnet.ArtNetNode', 10, ' Sq Univ Len 1 2 3 4 5 6 7 8 9 10 '), # noqa: E501 + ('pyartnet.ArtNetNode', 10, 'Packet to ip: 4172742D4E6574000050000E 01 00 0001 000a 001 002 003 004 005 006 007 008 009 010'), # noqa: E501 + ('pyartnet.ArtNetNode', 10, 'Sync to ip: 4172742D4E6574000052000E 00 00'), + ('pyartnet.Task', 10, 'Stopped Process task device1'), + ] + + caplog.clear() + # context manager cancels the refresh task, too assert caplog.record_tuples == [ - ('pyartnet.Universe', 10, 'Added channel "1/10": start: 1, stop: 10'), - ('pyartnet.Task', 10, 'Started Process task device1'), - ('pyartnet.ArtNetNode', 10, ' Sq Univ Len 1 2 3 4 5 6 7 8 9 10 '), # noqa: E501 - ('pyartnet.ArtNetNode', 10, 'Packet to ip: 4172742D4E6574000050000E 01 00 0001 000a 001 002 003 004 005 006 007 008 009 010'), # noqa: E501 - ('pyartnet.ArtNetNode', 10, 'Sync to ip: 4172742D4E6574000052000E 00 00'), - ('pyartnet.Task', 10, 'Stopped Process task device1'), + ('pyartnet.Task', 10, 'Stopped Refresh task device1'), ] diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index 0eaa1e5..493c6ab 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -25,6 +25,30 @@ def test_same_cls_signature(c) -> None: assert obj_parameter == base_parameter +def test_same_unicast_signature() -> None: + classes = (ArtNetNode, KiNetNode, SacnNode) + + cls_base = classes[0] + sig_base = inspect.signature(cls_base.create) + + for cls in classes[1:]: + + assert cls.__doc__ == cls_base.__doc__ + + sig_obj = inspect.signature(cls.create) + + for name, base_parameter in sig_base.parameters.items(): + assert name in sig_obj.parameters + obj_parameter = sig_obj.parameters[name] + + # some ports have a default which we ignore here + if name == 'port': + obj_parameter = obj_parameter.replace(default=inspect.Parameter.empty) + base_parameter = base_parameter.replace(default=inspect.Parameter.empty) + + assert obj_parameter == base_parameter + + @pytest.mark.parametrize('cls', [ArtNetNode, SacnNode, KiNetNode]) async def test_set_funcs(node: TestingNode, caplog, cls) -> None: caplog.set_level(logging.DEBUG) From eb34586877116c816426524838a1680e4268c937 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:54:36 +0100 Subject: [PATCH 35/49] . --- src/pyartnet/base/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyartnet/base/network.py b/src/pyartnet/base/network.py index 0914721..88b1098 100644 --- a/src/pyartnet/base/network.py +++ b/src/pyartnet/base/network.py @@ -4,9 +4,9 @@ from asyncio import get_running_loop from ipaddress import AddressValueError, IPv4Address, IPv6Address from socket import AF_INET, AF_INET6, AF_UNSPEC, SOCK_DGRAM -from typing import Final, Literal, override +from typing import Final, Literal -from typing_extensions import Self +from typing_extensions import Self, override USE_IP_VERSION: Final = Literal['auto', 'v4', 'v6'] From 35df31b9de8ccafe19744ce255ad5f745f2e39da Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:02:51 +0100 Subject: [PATCH 36/49] . --- docs/pyartnet.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/pyartnet.rst b/docs/pyartnet.rst index 7b55672..106ee12 100644 --- a/docs/pyartnet.rst +++ b/docs/pyartnet.rst @@ -16,6 +16,12 @@ Getting Started # hide: start from helper import MockedSocket MockedSocket().mock() + + import pyartnet.base.network as network_module + from ipaddress import IPv4Address + async def get_ip(*args, **kwargs): + return IPv4Address('127.0.0.1') + network_module.get_ip = get_ip # hide: stop import asyncio @@ -64,6 +70,12 @@ If no channel name is specified during creation the default name will be built w from helper import MockedSocket MockedSocket().mock() + import pyartnet.base.network as network_module + from ipaddress import IPv4Address + async def get_ip(*args, **kwargs): + return IPv4Address('127.0.0.1') + network_module.get_ip = get_ip + import asyncio from pyartnet import ArtNetNode @@ -107,6 +119,12 @@ Channel properties can be set when creating the channel through :meth:`BaseUnive from helper import MockedSocket MockedSocket().mock() + import pyartnet.base.network as network_module + from ipaddress import IPv4Address + async def get_ip(*args, **kwargs): + return IPv4Address('127.0.0.1') + network_module.get_ip = get_ip + import asyncio from pyartnet import ArtNetNode @@ -156,6 +174,12 @@ Example from helper import MockedSocket MockedSocket().mock() + import pyartnet.base.network as network_module + from ipaddress import IPv4Address + async def get_ip(*args, **kwargs): + return IPv4Address('127.0.0.1') + network_module.get_ip = get_ip + import asyncio async def main(): From bd7706d0aa8c3f5f307f26f993ca009401ca62da Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:21:34 +0100 Subject: [PATCH 37/49] . --- docs/pyartnet.rst | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/pyartnet.rst b/docs/pyartnet.rst index 106ee12..ff313e3 100644 --- a/docs/pyartnet.rst +++ b/docs/pyartnet.rst @@ -29,8 +29,7 @@ Getting Started async def main(): - node = await ArtNetNode.create('IP', 6454) - async with node: + async with await ArtNetNode.create('IP', 6454) as node: # Create universe 0 universe = node.add_universe(0) @@ -83,8 +82,7 @@ If no channel name is specified during creation the default name will be built w # hide: stop # create node/universe - node = await ArtNetNode.create('IP', 6454) - async with node: + async with await ArtNetNode.create('IP', 6454) as node: universe = node.add_universe(0) @@ -132,8 +130,7 @@ Channel properties can be set when creating the channel through :meth:`BaseUnive # hide: stop # create node/universe - node = await ArtNetNode.create('IP', 6454) - async with node: + async with await ArtNetNode.create('IP', 6454) as node: universe = node.add_universe(0) # create a 16bit channel @@ -188,8 +185,7 @@ Example from pyartnet import ArtNetNode, output_correction # create node/universe/channel - node = await ArtNetNode.create('IP', 6454) - async with node: + async with await ArtNetNode.create('IP', 6454) as node: universe = node.add_universe(0) channel = universe.add_channel(start=1, width=3) From 26c97cec1f498fda7a2425cecbab53c1f8f734f3 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:21:19 +0100 Subject: [PATCH 38/49] . --- docs/pyartnet.rst | 8 +- src/pyartnet/base/base_node.py | 20 ++++- src/pyartnet/base/network.py | 142 ++++++++++++++---------------- src/pyartnet/impl_artnet/node.py | 13 ++- src/pyartnet/impl_kinet/node.py | 15 ++-- src/pyartnet/impl_sacn/node.py | 35 ++++---- tests/conftest.py | 19 +++- tests/helper.py | 15 ++-- tests/test_base/test_base_node.py | 6 +- tests/test_base/test_network.py | 18 ++-- tests/test_impl/test_artnet.py | 7 +- tests/test_impl/test_impl.py | 19 ++-- tests/test_impl/test_sacn.py | 99 +++++++++++---------- 13 files changed, 216 insertions(+), 200 deletions(-) diff --git a/docs/pyartnet.rst b/docs/pyartnet.rst index ff313e3..40fa9ee 100644 --- a/docs/pyartnet.rst +++ b/docs/pyartnet.rst @@ -29,7 +29,7 @@ Getting Started async def main(): - async with await ArtNetNode.create('IP', 6454) as node: + async with ArtNetNode.create('IP', 6454) as node: # Create universe 0 universe = node.add_universe(0) @@ -82,7 +82,7 @@ If no channel name is specified during creation the default name will be built w # hide: stop # create node/universe - async with await ArtNetNode.create('IP', 6454) as node: + async with ArtNetNode.create('IP', 6454) as node: universe = node.add_universe(0) @@ -130,7 +130,7 @@ Channel properties can be set when creating the channel through :meth:`BaseUnive # hide: stop # create node/universe - async with await ArtNetNode.create('IP', 6454) as node: + async with ArtNetNode.create('IP', 6454) as node: universe = node.add_universe(0) # create a 16bit channel @@ -185,7 +185,7 @@ Example from pyartnet import ArtNetNode, output_correction # create node/universe/channel - async with await ArtNetNode.create('IP', 6454) as node: + async with ArtNetNode.create('IP', 6454) as node: universe = node.add_universe(0) channel = universe.add_channel(start=1, width=3) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index e757ff3..73e8c03 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -1,6 +1,7 @@ from __future__ import annotations from asyncio import sleep +from socket import socket from time import monotonic from typing import TYPE_CHECKING, Final, Generic, TypeVar @@ -30,7 +31,7 @@ def __init__(self, network: UnicastNetworkTarget | MulticastNetworkTarget, *, super().__init__() self._network: Final = network - self._socket: Final = network.create_socket() + self._socket: socket | None = None self._name: Final = name if name is not None else f'{self.__class__.__name__}-{id(self):x}' # refresh task @@ -73,7 +74,11 @@ def _send_synchronization(self) -> None: pass def _send_data(self, data: bytearray | bytes, dst: tuple[str, int] | str | None = None) -> None: - self._socket.sendto(self._packet_base + data, dst) + if (sock := self._socket) is None: + msg = 'Socket is closed!' + raise RuntimeError(msg) + + sock.sendto(self._packet_base + data, dst) return None async def _process_values_task(self) -> None: @@ -186,14 +191,21 @@ def __len__(self) -> int: return len(self._universes) async def __aenter__(self) -> Self: - self._refresh_task.start() + if self._socket is not None: + return self + ip_v6 = await self._network.is_ip_v6() + self._socket = self._network.create_socket(ip_v6=ip_v6) + + self._refresh_task.start() return self async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None: + if (sock := self._socket) is not None: + self.socket = None + sock.close() - self._socket.close() await self._process_task.cancel_wait() await self._refresh_task.cancel_wait() return None diff --git a/src/pyartnet/base/network.py b/src/pyartnet/base/network.py index 88b1098..f72632e 100644 --- a/src/pyartnet/base/network.py +++ b/src/pyartnet/base/network.py @@ -9,7 +9,7 @@ from typing_extensions import Self, override -USE_IP_VERSION: Final = Literal['auto', 'v4', 'v6'] +RESOLVE_TO_IP_TYPE: Final = Literal['auto', 'v4', 'v6'] def validate_port(port: int, *, allow_0: bool = False) -> int: @@ -36,7 +36,7 @@ def validate_string(host: str) -> str: async def resolve_hostname(host: str, port: int | None = None, - mode: USE_IP_VERSION = 'auto') -> tuple[tuple[socket.AddressFamily, str], ...]: + mode: RESOLVE_TO_IP_TYPE = 'auto') -> list[IPv4Address | IPv6Address]: try: family = {'auto': AF_UNSPEC, 'v4': AF_INET, 'v6': AF_INET6}[mode] except KeyError: @@ -44,69 +44,47 @@ async def resolve_hostname(host: str, port: int | None = None, raise ValueError(msg) from None try: - info = await get_running_loop().getaddrinfo(host, port, type=SOCK_DGRAM, family=family) + addr_info = await get_running_loop().getaddrinfo(host, port, type=SOCK_DGRAM, family=family) except socket.gaierror as e: msg = f'Cannot resolve hostname "{host:s}"! {e.errno}: {e.strerror}' raise ValueError(msg) from None - return tuple( - (v[0], v[4][0]) for v in info if v[0] in (AF_INET, AF_INET6) - ) + ret: list[IPv4Address | IPv6Address] = [] + for family, _, _, _, sockaddr in addr_info: + if family == AF_INET: + ret.append(IPv4Address(sockaddr[0])) + elif family == AF_INET6: + ret.append(IPv6Address(sockaddr[0])) + return ret -async def validate_source_ip(source_ip: str, source_port: int) -> IPv4Address | IPv6Address: - available = await resolve_hostname(socket.gethostname(), source_port) - for family, ip in available: - if ip == source_ip: - if family == AF_INET: - return IPv4Address(source_ip) - return IPv6Address(source_ip) +def validate_ip_address(host: str) -> IPv4Address | IPv6Address: + validate_string(host) - available_ips = [k[1] for k in sorted(available)] - msg = f'Source IP "{source_ip}" is not available on this system! Available: {", ".join(available_ips)}' - raise ValueError(msg) - - -async def get_ip(host: str, port: int, ip_version: USE_IP_VERSION) -> IPv4Address | IPv6Address: - # check if it's a valid ip - for cls in (IPv4Address, IPv6Address): - try: - return cls(host) - except AddressValueError: # noqa: PERF203 - pass - - # must be hostname - try to resolve it - info = await resolve_hostname(host, port, mode=ip_version) - family, resolved_ip = info[0] - - if family == AF_INET: - return IPv4Address(resolved_ip) - return IPv6Address(resolved_ip) + try: + return IPv4Address(host) + except AddressValueError: + pass + return IPv6Address(host) class NetworkTargetBase: - def __init__(self, *, ip_v6: bool = False) -> None: - self.ip_v6: Final = ip_v6 - def create_socket(self) -> socket.socket: + def create_socket(self, *, ip_v6: bool) -> socket.socket: # create nonblocking UDP socket - sock: Final = socket.socket(AF_INET6 if self.ip_v6 else AF_INET, SOCK_DGRAM) + sock: Final = socket.socket(AF_INET6 if ip_v6 else AF_INET, SOCK_DGRAM) sock.setblocking(False) return sock - def validate_ip(self, ip: str) -> str: - if self.ip_v6: - IPv6Address(ip) - else: - IPv4Address(ip) - return ip + async def is_ip_v6(self) -> bool: + raise NotImplementedError() class UnicastNetworkTarget(NetworkTargetBase): - def __init__(self, dst: tuple[str, int], src: tuple[str, int] | None = None, *, ip_v6: bool = False) -> None: - super().__init__(ip_v6=ip_v6) + def __init__(self, dst: tuple[str, int], src: tuple[str, int] | None = None) -> None: + super().__init__() self.dst: Final = dst self.src: Final = src @@ -116,8 +94,8 @@ def __repr__(self) -> str: return f'{self.__class__.__name__:s}(dst={ip:s}:{port:d}, source={src:s})' @override - def create_socket(self) -> socket.socket: - sock: Final = super().create_socket() + def create_socket(self, *, ip_v6: bool) -> socket.socket: + sock: Final = super().create_socket(ip_v6=ip_v6) # option to set source port/ip if (src := self.src) is not None: @@ -128,43 +106,55 @@ def create_socket(self) -> socket.socket: return sock @classmethod - async def create(cls, hostname: str, port: int, source_ip: str | None = None, source_port: int = 0, *, - ip_version: USE_IP_VERSION = 'auto') -> Self: - - validate_string(hostname) + def create(cls, host: str, port: int, source_ip: str | None = None, source_port: int = 0) -> Self: + validate_string(host) validate_port(port) - dst_ip = await get_ip(hostname, port, ip_version) - source: tuple[str, int] | None = None if source_ip is not None: - validate_string(source_ip) + validate_ip_address(source_ip) validate_port(source_port, allow_0=True) - - await validate_source_ip(source_ip, source_port) source = (source_ip, source_port) - # destination and source IP version must match - try: - dst_ip.__class__(source_ip) - except AddressValueError: - msg = f'Source IP "{source_ip}" is not a valid IPv{dst_ip.version}!' - raise ValueError(msg) from None + return cls(dst=(host, port), src=source) - return cls(dst=(hostname, port), src=source, ip_v6=dst_ip.version == 6) + @override + async def is_ip_v6(self) -> bool: + try: + dst_ip = validate_ip_address(self.dst[0]) + except AddressValueError: + pass + else: + if self.dst is not None: + # destination and source IP version must match + try: + dst_ip.__class__(self.dst[0]) + except AddressValueError: + msg = f'Source IP "{self.dst[0]}" is not a valid IPv{dst_ip.version}!' + raise ValueError(msg) from None + + return dst_ip.version == 6 + + # source ip can be used to set the mode for resolution + mode: RESOLVE_TO_IP_TYPE = 'auto' + if self.src is not None: + mode = 'v6' if validate_ip_address(self.src[0]).version == 6 else 'v4' + + info = await resolve_hostname(self.dst[0], self.dst[1], mode=mode) + return info[0].version == 6 class MulticastNetworkTarget(NetworkTargetBase): - def __init__(self, src: tuple[str, int], *, ip_v6: bool = False) -> None: - super().__init__(ip_v6=ip_v6) + def __init__(self, src: tuple[str, int]) -> None: + super().__init__() self.src: Final = src def __repr__(self) -> str: - return f'{self.__class__.__name__:s}(source={self.src[0]:s} ipv6={self.ip_v6})' + return f'{self.__class__.__name__:s}(source={self.src[0]:s})' @override - def create_socket(self) -> socket.socket: - sock: Final = super().create_socket() + def create_socket(self, *, ip_v6: bool) -> socket.socket: + sock: Final = super().create_socket(ip_v6=ip_v6) # set source port/ip sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -173,16 +163,18 @@ def create_socket(self) -> socket.socket: # setup socket for multicast sock.setsockopt( socket.IPPROTO_IP, - socket.IPV6_MULTICAST_IF if self.ip_v6 else socket.IP_MULTICAST_IF, - socket.inet_pton(AF_INET6 if self.ip_v6 else AF_INET, self.src[0]) + socket.IPV6_MULTICAST_IF if ip_v6 else socket.IP_MULTICAST_IF, + socket.inet_pton(AF_INET6 if ip_v6 else AF_INET, self.src[0]) ) return sock @classmethod - async def create(cls, interface_ip: str, interface_port: int = 0) -> Self: - validate_string(interface_ip) - validate_port(interface_port, allow_0=True) + def create(cls, source_ip: str, source_port: int = 0) -> Self: + validate_ip_address(source_ip) + validate_port(source_port, allow_0=True) + return cls(src=(source_ip, source_port)) - dst_ip = await validate_source_ip(interface_ip, interface_port) - return cls(src=(interface_ip, interface_port), ip_v6=dst_ip.version == 6) + @override + async def is_ip_v6(self) -> bool: + return validate_ip_address(self.src[0]).version == 6 diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 407e09d..5a5857f 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -7,7 +7,7 @@ import pyartnet from pyartnet.base import BaseNode -from pyartnet.base.network import USE_IP_VERSION, UnicastNetworkTarget +from pyartnet.base.network import UnicastNetworkTarget from pyartnet.base.seq_counter import SequenceCounter from pyartnet.errors import InvalidUniverseAddressError @@ -48,24 +48,21 @@ def __init__(self, network: UnicastNetworkTarget, *, self._sync_enabled : bool = False @classmethod - async def create(cls, hostname: str, port: int = ARTNET_PORT, *, - source_ip: str | None = None, source_port: int = 0, ip_version: USE_IP_VERSION = 'auto', + def create(cls, host: str, port: int = ARTNET_PORT, *, + source_ip: str | None = None, source_port: int = 0, name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: """Creates a new node. The packages will be sent directly to the node (unicast). - :param hostname: ip or hostname of the device + :param host: ip or hostname of the device :param port: port of device :param source_ip: ip of the network interface that shall be used to send data :param source_port: source port - :param ip_version: which ip version to use if hostname is a hostname and not an ip address :param name: a custom name of the node :param max_fps: maximum frames per second to send :param refresh_every: refresh interval in seconds """ - network = await UnicastNetworkTarget.create( - hostname, port, source_ip=source_ip, source_port=source_port, ip_version=ip_version - ) + network = UnicastNetworkTarget.create(host, port, source_ip=source_ip, source_port=source_port) return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) @override diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index 3c3806a..e6ef53a 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -9,7 +9,7 @@ import pyartnet from pyartnet.base import BaseNode -from pyartnet.base.network import USE_IP_VERSION, UnicastNetworkTarget +from pyartnet.base.network import UnicastNetworkTarget from pyartnet.errors import InvalidUniverseAddressError @@ -38,24 +38,21 @@ def __init__(self, network: UnicastNetworkTarget, *, self._packet_base = bytes(packet) @classmethod - async def create(cls, hostname: str, port: int = KINET_PORT, *, - source_ip: str | None = None, source_port: int = 0, ip_version: USE_IP_VERSION = 'auto', - name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: + def create(cls, host: str, port: int = KINET_PORT, *, + source_ip: str | None = None, source_port: int = 0, + name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: """Creates a new node. The packages will be sent directly to the node (unicast). - :param hostname: ip or hostname of the device + :param host: ip or hostname of the device :param port: port of device :param source_ip: ip of the network interface that shall be used to send data :param source_port: source port - :param ip_version: which ip version to use if hostname is a hostname and not an ip address :param name: a custom name of the node :param max_fps: maximum frames per second to send :param refresh_every: refresh interval in seconds """ - network = await UnicastNetworkTarget.create( - hostname, port, source_ip=source_ip, source_port=source_port, ip_version=ip_version - ) + network = UnicastNetworkTarget.create(host, port, source_ip=source_ip, source_port=source_port) return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) @override diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index fd438f0..0c343be 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -1,7 +1,9 @@ from __future__ import annotations import logging +from ipaddress import IPv4Address, IPv6Address from logging import DEBUG as LVL_DEBUG +from socket import AF_INET6 from typing import Final from uuid import uuid4 @@ -9,7 +11,7 @@ import pyartnet.impl_sacn.universe from pyartnet.base import BaseNode, SequenceCounter -from pyartnet.base.network import USE_IP_VERSION, MulticastNetworkTarget, UnicastNetworkTarget +from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget from pyartnet.errors import InvalidCidError, InvalidUniverseAddressError @@ -132,39 +134,36 @@ def _send_universe(self, id: int, byte_size: int, values: bytearray, log.debug(f'Sending sACN frame to {_dst_str(universe._dst)}: {(base_packet + packet).hex()}') @classmethod - async def create(cls, hostname: str, port: int = ACN_SDT_MULTICAST_PORT, *, - source_ip: str | None = None, source_port: int = 0, ip_version: USE_IP_VERSION = 'auto', + def create(cls, host: str, port: int = ACN_SDT_MULTICAST_PORT, *, + source_ip: str | None = None, source_port: int = 0, name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: """Creates a new node. The packages will be sent directly to the node (unicast). - :param hostname: ip or hostname of the device + :param host: ip or hostname of the device :param port: port of device :param source_ip: ip of the network interface that shall be used to send data :param source_port: source port - :param ip_version: which ip version to use if hostname is a hostname and not an ip address :param name: a custom name of the node :param max_fps: maximum frames per second to send :param refresh_every: refresh interval in seconds """ - network = await UnicastNetworkTarget.create( - hostname, port, source_ip=source_ip, source_port=source_port, ip_version=ip_version - ) + network = UnicastNetworkTarget.create(host, port, source_ip=source_ip, source_port=source_port) return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) @classmethod - async def create_multicast(cls, interface_ip: str, interface_port: int = 0, *, + async def create_multicast(cls, source_ip: str, source_port: int = 0, *, name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: """Creates a new node. The packages will be sent as multicast. - :param interface_ip: interface ip of the network interface that shall be used to send data - :param interface_port: source port + :param source_ip: interface ip of the network interface that shall be used to send data + :param source_port: source port :param name: a custom name of the node :param max_fps: maximum frames per second to send :param refresh_every: refresh interval in seconds """ - network = await MulticastNetworkTarget.create(interface_ip, interface_port) + network = MulticastNetworkTarget.create(source_ip, source_port) return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) @@ -187,12 +186,16 @@ def _get_universe_ip_port(self, universe: int) -> tuple[str, int]: u = self._validate_universe_nr(universe) - # IPv6 multicast address - if network.ip_v6: - return network.validate_ip(f'FF18::8300:{u:04X}'), ACN_SDT_MULTICAST_PORT + if self._socket.family == AF_INET6: + # IPv6 multicast address + address = f'FF18::8300:{u:04X}' + IPv6Address(address) + return address, ACN_SDT_MULTICAST_PORT # IPv4 multicast address - return network.validate_ip(f'239.255.{u // 255:d}.{u % 255:d}'), ACN_SDT_MULTICAST_PORT + address = f'239.255.{u // 255:d}.{u % 255:d}' + IPv4Address(address) + return address, ACN_SDT_MULTICAST_PORT @override def set_synchronous_mode(self, enabled: bool, synchronization_address: int = 0) -> None: # type: ignore [override] diff --git a/tests/conftest.py b/tests/conftest.py index 6345221..04d7496 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,9 +6,10 @@ import pytest from tests.helper import MockedSocket +from typing_extensions import override from pyartnet.base import BaseNode, BaseUniverse -from pyartnet.base.network import NetworkTargetBase, UnicastNetworkTarget +from pyartnet.base.network import MulticastNetworkTarget, NetworkTargetBase, UnicastNetworkTarget if TYPE_CHECKING: @@ -19,6 +20,18 @@ STEP_MS = 15 +class TestingUnicastNetworkTarget(UnicastNetworkTarget): + @override + async def is_ip_v6(self) -> bool: + return False + + +class TestingMulticastNetworkTarget(MulticastNetworkTarget): + @override + async def is_ip_v6(self) -> bool: + return False + + class TestingNode(BaseNode): __test__ = False # prevent this from being collected by pytest @@ -52,13 +65,13 @@ def patched_socket(monkeypatch): def test_patched_socket(patched_socket) -> None: - node = TestingNode(UnicastNetworkTarget(dst=('IP', 9999))) + node = TestingNode(TestingUnicastNetworkTarget(dst=('IP', 9999))) assert node._socket.sendto is patched_socket @pytest.fixture def node(): - return TestingNode(UnicastNetworkTarget(dst=('IP', 9999))) + return TestingNode(TestingUnicastNetworkTarget(dst=('IP', 9999))) @pytest.fixture diff --git a/tests/helper.py b/tests/helper.py index bce656e..62a981a 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -19,24 +19,23 @@ def __init__(self) -> None: self.mp = MonkeyPatch() def mock(self): - m_socket_obj = Mock(['sendto', 'setblocking', 'setsockopt', 'bind', 'close'], name='socket_obj') + m_socket_obj = Mock(['sendto', 'setblocking', 'setsockopt', 'bind', 'close', 'family'], name='socket_obj') m_socket_obj.sendto = m_sendto = Mock(name='socket_obj.sendto') + m_socket_obj.family = socket.AF_INET - module_names = [ + constant_names = [ name for name in dir(socket) if name.startswith(('AF_', 'SOCK_', 'SOL_', 'IPPROTO_', 'IP_', 'SO_',)) or - name in ('socket', 'gethostname', 'inet_pton') + name in ('herror', 'gaierror') ] - m = Mock(module_names, name='Mock socket package' - ) + m = Mock(['socket', 'gethostname', 'inet_pton', *constant_names], name='Mock socket package') m.gethostname = socket.gethostname m.socket = Mock([], return_value=m_socket_obj, name='Mock socket obj') # Copy constants - for name in dir(socket): - if name.startswith(('AF_', 'SOCK_', 'SOL_', 'IPPROTO_', 'IP_', 'SO_')): - setattr(m, name, getattr(socket, name)) + for name in constant_names: + setattr(m, name, getattr(socket, name)) self.mp.setattr(network_module, 'socket', m) return m_sendto diff --git a/tests/test_base/test_base_node.py b/tests/test_base/test_base_node.py index c59a021..e35a7a2 100644 --- a/tests/test_base/test_base_node.py +++ b/tests/test_base/test_base_node.py @@ -30,7 +30,7 @@ def _repr(obj: object) -> str: # Multicast node = TestingNode(MulticastNetworkTarget(src=('IP', 99999))) - assert _repr(node) == '' + assert _repr(node) == '' def test_universe_add_get(node: TestingNode) -> None: @@ -100,10 +100,10 @@ async def check_wait_time_when_fade(steps: int) -> None: async def test_context(node: TestingNode) -> None: - - node._socket.close.assert_not_called() + assert node._socket is None async with node: + node._socket.close.assert_not_called() assert node._refresh_task.task is not None node._socket.close.assert_called() diff --git a/tests/test_base/test_network.py b/tests/test_base/test_network.py index e2b7c22..dc53229 100644 --- a/tests/test_base/test_network.py +++ b/tests/test_base/test_network.py @@ -1,36 +1,36 @@ import pytest -from pyartnet.base.network import get_ip, validate_source_ip +from pyartnet.base.network import resolve_hostname, validate_ip_address async def test_hostname() -> None: with pytest.raises(ValueError) as e: # noqa: PT011 - await validate_source_ip('does_not_exist', 0) + await resolve_hostname('does_not_exist', 0) - assert str(e.value).startswith('Source IP "does_not_exist" is not available on this system!') + assert str(e.value).startswith('Cannot resolve hostname "does_not_exist"! 11001: getaddrinfo failed') async def test_get_ip() -> None: # ip address v4 address = '127.0.0.1' - obj = await get_ip(address, 0, ip_version='v6') + obj = validate_ip_address(address) assert str(obj) == address assert obj.version == 4 # ip address v6 address = '::1' - obj = await get_ip(address, 0, ip_version='v4') + obj = validate_ip_address(address) assert str(obj) == address assert obj.version == 6 # hostname gets resolved - obj = await get_ip('localhost', 0, ip_version='v4') + (obj, ) = await resolve_hostname('localhost', 0, mode='v4') assert str(obj) == '127.0.0.1' assert obj.version == 4 - obj = await get_ip('localhost', 0, ip_version='v6') + (obj, ) = await resolve_hostname('localhost', 0, mode='v6') assert str(obj) == '::1' assert obj.version == 6 - obj = await get_ip('localhost', 0, ip_version='auto') - assert str(obj) in ('::1', '127.0.0.1') + obj = await resolve_hostname('localhost', 0, mode='auto') + assert str(obj[0]) in ('::1', '127.0.0.1') diff --git a/tests/test_impl/test_artnet.py b/tests/test_impl/test_artnet.py index 3d2a373..fc35be9 100644 --- a/tests/test_impl/test_artnet.py +++ b/tests/test_impl/test_artnet.py @@ -4,12 +4,13 @@ from binascii import a2b_hex from unittest.mock import call +from tests.conftest import TestingUnicastNetworkTarget + from pyartnet import ArtNetNode -from pyartnet.base.network import UnicastNetworkTarget async def test_artnet() -> None: - async with ArtNetNode(UnicastNetworkTarget(('ip', 9999999))) as artnet: + async with ArtNetNode(TestingUnicastNetworkTarget(('ip', 9999999))) as artnet: channel = artnet.add_universe(1).add_channel(1, 10) channel.set_values(range(1, 11)) @@ -28,7 +29,7 @@ async def test_artnet() -> None: async def test_artnet_with_sync(caplog) -> None: caplog.set_level(logging.DEBUG) - async with ArtNetNode(UnicastNetworkTarget(('ip', 9999999)), name='device1') as artnet: + async with ArtNetNode(TestingUnicastNetworkTarget(('ip', 9999999)), name='device1') as artnet: artnet.set_synchronous_mode(True) channel = artnet.add_universe(1).add_channel(1, 10) diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index 493c6ab..2ca7428 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -3,11 +3,10 @@ from asyncio import sleep import pytest -from tests.conftest import TestingNode +from tests.conftest import TestingNode, TestingUnicastNetworkTarget from pyartnet import ArtNetNode, KiNetNode, SacnNode from pyartnet.base import BaseNode -from pyartnet.base.network import UnicastNetworkTarget from pyartnet.errors import InvalidUniverseAddressError @@ -53,21 +52,21 @@ def test_same_unicast_signature() -> None: async def test_set_funcs(node: TestingNode, caplog, cls) -> None: caplog.set_level(logging.DEBUG) - n = cls(UnicastNetworkTarget(('ip', 9999999), ip_v6=False)) - u = n.add_universe(1) - c = u.add_channel(1, 1) + async with cls(TestingUnicastNetworkTarget(('ip', 9999999))) as n: + u = n.add_universe(1) + c = u.add_channel(1, 1) - c.set_values([5]) - await sleep(0.1) + c.set_values([5]) + await sleep(0.1) - c.set_fade([250], 700) - await c + c.set_fade([250], 700) + await c @pytest.mark.parametrize('cls', [ArtNetNode, SacnNode, KiNetNode]) async def test_universe_validation(node: TestingNode, cls) -> None: - n = cls(UnicastNetworkTarget(('ip', 9999999), ip_v6=False)) + n = cls(TestingUnicastNetworkTarget(('ip', 9999999))) with pytest.raises(TypeError): n.add_universe(1.3) diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index 63548db..f07281f 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -4,34 +4,35 @@ from unittest.mock import call import pytest +from tests.conftest import TestingMulticastNetworkTarget, TestingUnicastNetworkTarget from pyartnet import SacnNode -from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget async def test_sacn() -> None: sacn = SacnNode( - UnicastNetworkTarget(('ip', 9999999), ip_v6=False), + TestingUnicastNetworkTarget(('ip', 9999999)), cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', source_name='default source name', start_refresh_task=True ) + async with sacn: - channel = sacn.add_universe(1).add_channel(1, 10) - channel.set_values(range(1, 11)) + channel = sacn.add_universe(1).add_channel(1, 10) + channel.set_values(range(1, 11)) - data = ('001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' - '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' - '0000000064000000000001701502a100000001000b000102030405060708090a') + data = ('001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' + '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' + '0000000064000000000001701502a100000001000b000102030405060708090a') - await channel - await sacn._process_task.task - await asyncio.sleep(0.3) + await channel + await sacn._process_task.task + await asyncio.sleep(0.3) - m = sacn._socket - m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) + m = sacn._socket + m.sendto.assert_called_once_with(bytearray(a2b_hex(data)), ('ip', 9999999)) - await channel + await channel @pytest.mark.parametrize('multicast', [False, True]) @@ -39,9 +40,9 @@ async def test_sacn_with_sync(caplog, multicast) -> None: caplog.set_level(logging.DEBUG) if multicast: - network = MulticastNetworkTarget(('ip', 9999999), ip_v6=False) + network = TestingMulticastNetworkTarget(('ip', 9999999)) else: - network = UnicastNetworkTarget(('ip', 9999999), ip_v6=False) + network = TestingUnicastNetworkTarget(('ip', 9999999)) sacn = SacnNode( network, @@ -50,36 +51,38 @@ async def test_sacn_with_sync(caplog, multicast) -> None: start_refresh_task=False, name='device1' ) - sacn.set_synchronous_mode(True, 2) - - channel = sacn.add_universe(1).add_channel(1, 10) - channel.set_values(range(1, 11)) - - data = ('001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' - '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' - '0000000064000200000001701502a100000001000b000102030405060708090a') - - sync_data = '001000004153432d45312e31370000007021000000084168f52b1a7b2de11712e9ee383d2258700b000000010000020000' - - await channel - await sacn._process_task.task - await asyncio.sleep(0.3) - - data_dst = ('ip', 9999999) if not multicast else ('239.255.0.1', 5568) - sync_dst = ('ip', 9999999) if not multicast else ('239.255.0.2', 5568) - data_msg = 'ip:9999999' if not multicast else '239.255.0.1:5568' - sync_msg = 'ip:9999999' if not multicast else '239.255.0.2:5568' - - m = sacn._socket - assert m.sendto.call_args_list == [ - call(bytearray(a2b_hex(data)), data_dst), - call(bytearray(a2b_hex(sync_data)), sync_dst), - ] - - assert caplog.record_tuples == [ - ('pyartnet.Universe', 10, 'Added channel "1/10": start: 1, stop: 10'), - ('pyartnet.Task', 10, 'Started Process task device1'), - ('pyartnet.SacnNode', 10, f'Sending sACN frame to {data_msg:s}: {data:s}'), - ('pyartnet.SacnNode', 10, f'Sending sACN Synchronization Packet to {sync_msg}: {sync_data:s}'), - ('pyartnet.Task', 10, 'Stopped Process task device1') - ] + async with sacn: + sacn.set_synchronous_mode(True, 2) + + channel = sacn.add_universe(1).add_channel(1, 10) + channel.set_values(range(1, 11)) + + data = ('001000004153432d45312e31370000007078000000044168f52b1a7b2de11712e9ee383d225870620000000264656661756c7420' + '736f75726365206e616d650000000000000000000000000000000000000000000000000000000000000000000000000000000000' + '0000000064000200000001701502a100000001000b000102030405060708090a') + + sync_data = '001000004153432d45312e31370000007021000000084168f52b1a7b2de11712e9ee383d2258700b000000010000020000' + + await channel + await sacn._process_task.task + await asyncio.sleep(0.3) + + data_dst = ('ip', 9999999) if not multicast else ('239.255.0.1', 5568) + sync_dst = ('ip', 9999999) if not multicast else ('239.255.0.2', 5568) + data_msg = 'ip:9999999' if not multicast else '239.255.0.1:5568' + sync_msg = 'ip:9999999' if not multicast else '239.255.0.2:5568' + + m = sacn._socket + assert m.sendto.call_args_list == [ + call(bytearray(a2b_hex(data)), data_dst), + call(bytearray(a2b_hex(sync_data)), sync_dst), + ] + + assert caplog.record_tuples == [ + ('pyartnet.Universe', 10, 'Added channel "1/10": start: 1, stop: 10'), + ('pyartnet.Task', 10, 'Started Refresh task device1'), + ('pyartnet.Task', 10, 'Started Process task device1'), + ('pyartnet.SacnNode', 10, f'Sending sACN frame to {data_msg:s}: {data:s}'), + ('pyartnet.SacnNode', 10, f'Sending sACN Synchronization Packet to {sync_msg}: {sync_data:s}'), + ('pyartnet.Task', 10, 'Stopped Process task device1') + ] From 8c6a55668dbe0139ca812dc980d65217a3b4857b Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:24:58 +0100 Subject: [PATCH 39/49] . --- docs/pyartnet.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/pyartnet.rst b/docs/pyartnet.rst index 40fa9ee..67ac45a 100644 --- a/docs/pyartnet.rst +++ b/docs/pyartnet.rst @@ -19,9 +19,9 @@ Getting Started import pyartnet.base.network as network_module from ipaddress import IPv4Address - async def get_ip(*args, **kwargs): - return IPv4Address('127.0.0.1') - network_module.get_ip = get_ip + async def resolve_hostname(*args, **kwargs): + return [IPv4Address('127.0.0.1')] + network_module.resolve_hostname = resolve_hostname # hide: stop import asyncio @@ -71,9 +71,9 @@ If no channel name is specified during creation the default name will be built w import pyartnet.base.network as network_module from ipaddress import IPv4Address - async def get_ip(*args, **kwargs): - return IPv4Address('127.0.0.1') - network_module.get_ip = get_ip + async def resolve_hostname(*args, **kwargs): + return [IPv4Address('127.0.0.1')] + network_module.resolve_hostname = resolve_hostname import asyncio from pyartnet import ArtNetNode @@ -119,9 +119,9 @@ Channel properties can be set when creating the channel through :meth:`BaseUnive import pyartnet.base.network as network_module from ipaddress import IPv4Address - async def get_ip(*args, **kwargs): - return IPv4Address('127.0.0.1') - network_module.get_ip = get_ip + async def resolve_hostname(*args, **kwargs): + return [IPv4Address('127.0.0.1')] + network_module.resolve_hostname = resolve_hostname import asyncio from pyartnet import ArtNetNode @@ -173,9 +173,9 @@ Example import pyartnet.base.network as network_module from ipaddress import IPv4Address - async def get_ip(*args, **kwargs): - return IPv4Address('127.0.0.1') - network_module.get_ip = get_ip + async def resolve_hostname(*args, **kwargs): + return [IPv4Address('127.0.0.1')] + network_module.resolve_hostname = resolve_hostname import asyncio From b705298c648ef485707879b0a2ab6877442251d9 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:30:05 +0100 Subject: [PATCH 40/49] . --- tests/conftest.py | 17 ++--------------- tests/helper.py | 15 ++++++++++++++- tests/test_base/test_network.py | 5 ++++- tests/test_impl/test_artnet.py | 2 +- tests/test_impl/test_impl.py | 3 ++- tests/test_impl/test_sacn.py | 2 +- 6 files changed, 24 insertions(+), 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 04d7496..951947d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,33 +5,20 @@ from typing import TYPE_CHECKING import pytest -from tests.helper import MockedSocket -from typing_extensions import override +from tests.helper import MockedSocket, TestingUnicastNetworkTarget from pyartnet.base import BaseNode, BaseUniverse -from pyartnet.base.network import MulticastNetworkTarget, NetworkTargetBase, UnicastNetworkTarget if TYPE_CHECKING: import pyartnet.base.base_node from pyartnet.base.base_node import UNIVERSE_TYPE + from pyartnet.base.network import NetworkTargetBase STEP_MS = 15 -class TestingUnicastNetworkTarget(UnicastNetworkTarget): - @override - async def is_ip_v6(self) -> bool: - return False - - -class TestingMulticastNetworkTarget(MulticastNetworkTarget): - @override - async def is_ip_v6(self) -> bool: - return False - - class TestingNode(BaseNode): __test__ = False # prevent this from being collected by pytest diff --git a/tests/helper.py b/tests/helper.py index 62a981a..78e7e8a 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,12 +1,13 @@ from __future__ import annotations import socket -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, override from unittest.mock import Mock from pytest import MonkeyPatch import pyartnet.base.network as network_module +from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget if TYPE_CHECKING: @@ -49,3 +50,15 @@ def __enter__(self): def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None: self.undo() + + +class TestingUnicastNetworkTarget(UnicastNetworkTarget): + @override + async def is_ip_v6(self) -> bool: + return False + + +class TestingMulticastNetworkTarget(MulticastNetworkTarget): + @override + async def is_ip_v6(self) -> bool: + return False diff --git a/tests/test_base/test_network.py b/tests/test_base/test_network.py index dc53229..8e24f66 100644 --- a/tests/test_base/test_network.py +++ b/tests/test_base/test_network.py @@ -7,7 +7,10 @@ async def test_hostname() -> None: with pytest.raises(ValueError) as e: # noqa: PT011 await resolve_hostname('does_not_exist', 0) - assert str(e.value).startswith('Cannot resolve hostname "does_not_exist"! 11001: getaddrinfo failed') + assert str(e.value) in ( + 'Cannot resolve hostname "does_not_exist"! 11001: getaddrinfo failed' + 'Cannot resolve hostname "does_not_exist"! -3: Temporary failure in name resolution' + ) async def test_get_ip() -> None: diff --git a/tests/test_impl/test_artnet.py b/tests/test_impl/test_artnet.py index fc35be9..4f77c6a 100644 --- a/tests/test_impl/test_artnet.py +++ b/tests/test_impl/test_artnet.py @@ -4,7 +4,7 @@ from binascii import a2b_hex from unittest.mock import call -from tests.conftest import TestingUnicastNetworkTarget +from tests.helper import TestingUnicastNetworkTarget from pyartnet import ArtNetNode diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index 2ca7428..50835ce 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -3,7 +3,8 @@ from asyncio import sleep import pytest -from tests.conftest import TestingNode, TestingUnicastNetworkTarget +from tests.conftest import TestingNode +from tests.helper import TestingUnicastNetworkTarget from pyartnet import ArtNetNode, KiNetNode, SacnNode from pyartnet.base import BaseNode diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index f07281f..71083b7 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -4,7 +4,7 @@ from unittest.mock import call import pytest -from tests.conftest import TestingMulticastNetworkTarget, TestingUnicastNetworkTarget +from tests.helper import TestingMulticastNetworkTarget, TestingUnicastNetworkTarget from pyartnet import SacnNode From 8575bb19c6dc9ca48412e2ccd1de7cfd25f5f606 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:31:58 +0100 Subject: [PATCH 41/49] . --- tests/helper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/helper.py b/tests/helper.py index 78e7e8a..b0e7384 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,10 +1,11 @@ from __future__ import annotations import socket -from typing import TYPE_CHECKING, override +from typing import TYPE_CHECKING from unittest.mock import Mock from pytest import MonkeyPatch +from typing_extensions import override import pyartnet.base.network as network_module from pyartnet.base.network import MulticastNetworkTarget, UnicastNetworkTarget From 7581d33ed7cc49eaa23abcc27bdf4a6383308ab2 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:35:20 +0100 Subject: [PATCH 42/49] . --- tests/conftest.py | 6 +++--- tests/helper.py | 4 ++-- tests/test_base/test_network.py | 16 ++++++++-------- tests/test_impl/test_artnet.py | 6 +++--- tests/test_impl/test_impl.py | 6 +++--- tests/test_impl/test_sacn.py | 8 ++++---- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 951947d..5fae33e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING import pytest -from tests.helper import MockedSocket, TestingUnicastNetworkTarget +from tests.helper import MockedSocket, UnicastNetworkTestingTarget from pyartnet.base import BaseNode, BaseUniverse @@ -52,13 +52,13 @@ def patched_socket(monkeypatch): def test_patched_socket(patched_socket) -> None: - node = TestingNode(TestingUnicastNetworkTarget(dst=('IP', 9999))) + node = TestingNode(UnicastNetworkTestingTarget(dst=('IP', 9999))) assert node._socket.sendto is patched_socket @pytest.fixture def node(): - return TestingNode(TestingUnicastNetworkTarget(dst=('IP', 9999))) + return TestingNode(UnicastNetworkTestingTarget(dst=('IP', 9999))) @pytest.fixture diff --git a/tests/helper.py b/tests/helper.py index b0e7384..7a44ae0 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -53,13 +53,13 @@ def __exit__(self, exc_type: type[BaseException] | None, self.undo() -class TestingUnicastNetworkTarget(UnicastNetworkTarget): +class UnicastNetworkTestingTarget(UnicastNetworkTarget): @override async def is_ip_v6(self) -> bool: return False -class TestingMulticastNetworkTarget(MulticastNetworkTarget): +class MulticastTestingNetworkTarget(MulticastNetworkTarget): @override async def is_ip_v6(self) -> bool: return False diff --git a/tests/test_base/test_network.py b/tests/test_base/test_network.py index 8e24f66..1809edd 100644 --- a/tests/test_base/test_network.py +++ b/tests/test_base/test_network.py @@ -27,13 +27,13 @@ async def test_get_ip() -> None: assert obj.version == 6 # hostname gets resolved - (obj, ) = await resolve_hostname('localhost', 0, mode='v4') - assert str(obj) == '127.0.0.1' - assert obj.version == 4 + objs = await resolve_hostname('localhost', 0, mode='v4') + assert str(objs[0]) == '127.0.0.1' + assert objs[0].version == 4 - (obj, ) = await resolve_hostname('localhost', 0, mode='v6') - assert str(obj) == '::1' - assert obj.version == 6 + objs = await resolve_hostname('localhost', 0, mode='v6') + assert str(objs[0]) == '::1' + assert objs[0].version == 6 - obj = await resolve_hostname('localhost', 0, mode='auto') - assert str(obj[0]) in ('::1', '127.0.0.1') + objs = await resolve_hostname('localhost', 0, mode='auto') + assert str(objs[0]) in ('::1', '127.0.0.1') diff --git a/tests/test_impl/test_artnet.py b/tests/test_impl/test_artnet.py index 4f77c6a..2ffa697 100644 --- a/tests/test_impl/test_artnet.py +++ b/tests/test_impl/test_artnet.py @@ -4,13 +4,13 @@ from binascii import a2b_hex from unittest.mock import call -from tests.helper import TestingUnicastNetworkTarget +from tests.helper import UnicastNetworkTestingTarget from pyartnet import ArtNetNode async def test_artnet() -> None: - async with ArtNetNode(TestingUnicastNetworkTarget(('ip', 9999999))) as artnet: + async with ArtNetNode(UnicastNetworkTestingTarget(('ip', 9999999))) as artnet: channel = artnet.add_universe(1).add_channel(1, 10) channel.set_values(range(1, 11)) @@ -29,7 +29,7 @@ async def test_artnet() -> None: async def test_artnet_with_sync(caplog) -> None: caplog.set_level(logging.DEBUG) - async with ArtNetNode(TestingUnicastNetworkTarget(('ip', 9999999)), name='device1') as artnet: + async with ArtNetNode(UnicastNetworkTestingTarget(('ip', 9999999)), name='device1') as artnet: artnet.set_synchronous_mode(True) channel = artnet.add_universe(1).add_channel(1, 10) diff --git a/tests/test_impl/test_impl.py b/tests/test_impl/test_impl.py index 50835ce..7ad69cf 100644 --- a/tests/test_impl/test_impl.py +++ b/tests/test_impl/test_impl.py @@ -4,7 +4,7 @@ import pytest from tests.conftest import TestingNode -from tests.helper import TestingUnicastNetworkTarget +from tests.helper import UnicastNetworkTestingTarget from pyartnet import ArtNetNode, KiNetNode, SacnNode from pyartnet.base import BaseNode @@ -53,7 +53,7 @@ def test_same_unicast_signature() -> None: async def test_set_funcs(node: TestingNode, caplog, cls) -> None: caplog.set_level(logging.DEBUG) - async with cls(TestingUnicastNetworkTarget(('ip', 9999999))) as n: + async with cls(UnicastNetworkTestingTarget(('ip', 9999999))) as n: u = n.add_universe(1) c = u.add_channel(1, 1) @@ -67,7 +67,7 @@ async def test_set_funcs(node: TestingNode, caplog, cls) -> None: @pytest.mark.parametrize('cls', [ArtNetNode, SacnNode, KiNetNode]) async def test_universe_validation(node: TestingNode, cls) -> None: - n = cls(TestingUnicastNetworkTarget(('ip', 9999999))) + n = cls(UnicastNetworkTestingTarget(('ip', 9999999))) with pytest.raises(TypeError): n.add_universe(1.3) diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index 71083b7..44de8a1 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -4,14 +4,14 @@ from unittest.mock import call import pytest -from tests.helper import TestingMulticastNetworkTarget, TestingUnicastNetworkTarget +from tests.helper import MulticastTestingNetworkTarget, UnicastNetworkTestingTarget from pyartnet import SacnNode async def test_sacn() -> None: sacn = SacnNode( - TestingUnicastNetworkTarget(('ip', 9999999)), + UnicastNetworkTestingTarget(('ip', 9999999)), cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', source_name='default source name', start_refresh_task=True @@ -40,9 +40,9 @@ async def test_sacn_with_sync(caplog, multicast) -> None: caplog.set_level(logging.DEBUG) if multicast: - network = TestingMulticastNetworkTarget(('ip', 9999999)) + network = MulticastTestingNetworkTarget(('ip', 9999999)) else: - network = TestingUnicastNetworkTarget(('ip', 9999999)) + network = UnicastNetworkTestingTarget(('ip', 9999999)) sacn = SacnNode( network, From 5e7aab2f6e19fbee89ece614b6abf746561feef9 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:54:08 +0100 Subject: [PATCH 43/49] . --- src/pyartnet/base/network.py | 4 ++-- src/pyartnet/impl_sacn/node.py | 2 +- tests/test_base/test_network.py | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/pyartnet/base/network.py b/src/pyartnet/base/network.py index f72632e..e196d8f 100644 --- a/src/pyartnet/base/network.py +++ b/src/pyartnet/base/network.py @@ -18,8 +18,8 @@ def validate_port(port: int, *, allow_0: bool = False) -> int: raise TypeError(msg) lower = 0 if allow_0 else 1 - if not lower < port < 65536: - msg = f'port must be between {lower:d} and 65536' + if not lower <= port <= 65535: + msg = f'port must be between {lower:d} and 65535' raise ValueError(msg) return port diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 0c343be..5850ee8 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -152,7 +152,7 @@ def create(cls, host: str, port: int = ACN_SDT_MULTICAST_PORT, *, return cls(network, name=name, max_fps=max_fps, refresh_every=refresh_every) @classmethod - async def create_multicast(cls, source_ip: str, source_port: int = 0, *, + def create_multicast(cls, source_ip: str, source_port: int = 0, *, name: str | None = None, max_fps: int = 25, refresh_every: float = 2) -> Self: """Creates a new node. The packages will be sent as multicast. diff --git a/tests/test_base/test_network.py b/tests/test_base/test_network.py index 1809edd..4c379c7 100644 --- a/tests/test_base/test_network.py +++ b/tests/test_base/test_network.py @@ -1,6 +1,6 @@ import pytest -from pyartnet.base.network import resolve_hostname, validate_ip_address +from pyartnet.base.network import resolve_hostname, validate_ip_address, validate_port async def test_hostname() -> None: @@ -13,6 +13,19 @@ async def test_hostname() -> None: ) +def test_validate_port() -> None: + with pytest.raises(ValueError) as e: + validate_port(0) + assert str(e.value) == 'port must be between 1 and 65535' + + with pytest.raises(ValueError) as e: + validate_port(65536) + assert str(e.value) == 'port must be between 1 and 65535' + + validate_port(0, allow_0=True) + validate_port(65535) + + async def test_get_ip() -> None: # ip address v4 address = '127.0.0.1' From b45f418c9941867569a54c7db3dd57fca0ea5987 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Fri, 31 Oct 2025 05:49:51 +0100 Subject: [PATCH 44/49] . --- src/pyartnet/base/base_node.py | 2 +- src/pyartnet/impl_sacn/node.py | 12 ++++++++---- tests/test_base/test_network.py | 4 +++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 73e8c03..eba1644 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -75,7 +75,7 @@ def _send_synchronization(self) -> None: def _send_data(self, data: bytearray | bytes, dst: tuple[str, int] | str | None = None) -> None: if (sock := self._socket) is None: - msg = 'Socket is closed!' + msg = 'Socket closed! Did you forget to use "async with"?' raise RuntimeError(msg) sock.sendto(self._packet_base + data, dst) diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 5850ee8..5283a6a 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -184,16 +184,20 @@ def _get_universe_ip_port(self, universe: int) -> tuple[str, int]: if isinstance(network := self._network, UnicastNetworkTarget): return network.dst - u = self._validate_universe_nr(universe) + self._validate_universe_nr(universe) - if self._socket.family == AF_INET6: + if (sock := self._socket) is None: + msg = 'Socket closed! Did you forget to use "async with"?' + raise RuntimeError(msg) + + if sock.family == AF_INET6: # IPv6 multicast address - address = f'FF18::8300:{u:04X}' + address = f'FF18::8300:{universe:04X}' IPv6Address(address) return address, ACN_SDT_MULTICAST_PORT # IPv4 multicast address - address = f'239.255.{u // 255:d}.{u % 255:d}' + address = f'239.255.{universe // 255:d}.{universe % 255:d}' IPv4Address(address) return address, ACN_SDT_MULTICAST_PORT diff --git a/tests/test_base/test_network.py b/tests/test_base/test_network.py index 4c379c7..e24f994 100644 --- a/tests/test_base/test_network.py +++ b/tests/test_base/test_network.py @@ -18,11 +18,13 @@ def test_validate_port() -> None: validate_port(0) assert str(e.value) == 'port must be between 1 and 65535' + validate_port(1) + validate_port(0, allow_0=True) + with pytest.raises(ValueError) as e: validate_port(65536) assert str(e.value) == 'port must be between 1 and 65535' - validate_port(0, allow_0=True) validate_port(65535) From 8074f5b30e1840bf7889facba22b67d2547b3917 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Fri, 31 Oct 2025 06:46:35 +0100 Subject: [PATCH 45/49] detect ip version earlier --- src/pyartnet/base/base_node.py | 4 +- src/pyartnet/base/network.py | 69 +++++++++++++++++++++------------- tests/helper.py | 10 +++-- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index eba1644..9c25f8f 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -194,8 +194,8 @@ async def __aenter__(self) -> Self: if self._socket is not None: return self - ip_v6 = await self._network.is_ip_v6() - self._socket = self._network.create_socket(ip_v6=ip_v6) + await self._network.resolve_hostname() + self._socket = self._network.create_socket() self._refresh_task.start() return self diff --git a/src/pyartnet/base/network.py b/src/pyartnet/base/network.py index e196d8f..eef9ca1 100644 --- a/src/pyartnet/base/network.py +++ b/src/pyartnet/base/network.py @@ -70,21 +70,30 @@ def validate_ip_address(host: str) -> IPv4Address | IPv6Address: class NetworkTargetBase: + def __init__(self, *, ip_v6: bool | None = None) -> None: + self._ip_v6: bool | None = ip_v6 - def create_socket(self, *, ip_v6: bool) -> socket.socket: + def create_socket(self) -> socket.socket: # create nonblocking UDP socket - sock: Final = socket.socket(AF_INET6 if ip_v6 else AF_INET, SOCK_DGRAM) + sock: Final = socket.socket(AF_INET6 if self.ip_v6 else AF_INET, SOCK_DGRAM) sock.setblocking(False) return sock - async def is_ip_v6(self) -> bool: + @property + def ip_v6(self) -> bool: + if self._ip_v6 is None: + msg = 'Host not yet resolved!' + raise RuntimeError(msg) + return self._ip_v6 + + async def resolve_hostname(self) -> None: raise NotImplementedError() class UnicastNetworkTarget(NetworkTargetBase): - def __init__(self, dst: tuple[str, int], src: tuple[str, int] | None = None) -> None: - super().__init__() + def __init__(self, dst: tuple[str, int], src: tuple[str, int] | None = None, *, ip_v6: bool | None = None) -> None: + super().__init__(ip_v6=ip_v6) self.dst: Final = dst self.src: Final = src @@ -94,8 +103,8 @@ def __repr__(self) -> str: return f'{self.__class__.__name__:s}(dst={ip:s}:{port:d}, source={src:s})' @override - def create_socket(self, *, ip_v6: bool) -> socket.socket: - sock: Final = super().create_socket(ip_v6=ip_v6) + def create_socket(self) -> socket.socket: + sock: Final = super().create_socket() # option to set source port/ip if (src := self.src) is not None: @@ -116,24 +125,29 @@ def create(cls, host: str, port: int, source_ip: str | None = None, source_port: validate_port(source_port, allow_0=True) source = (source_ip, source_port) - return cls(dst=(host, port), src=source) - - @override - async def is_ip_v6(self) -> bool: + # if host is an IP address, determine IP version now + ip_v6: bool | None = None try: - dst_ip = validate_ip_address(self.dst[0]) + dst_ip = validate_ip_address(host) except AddressValueError: pass else: - if self.dst is not None: + if source_ip is not None: # destination and source IP version must match try: - dst_ip.__class__(self.dst[0]) + dst_ip.__class__(source_ip) except AddressValueError: - msg = f'Source IP "{self.dst[0]}" is not a valid IPv{dst_ip.version}!' + msg = f'Source IP "{source_ip}" is not a valid IPv{dst_ip.version}!' raise ValueError(msg) from None - return dst_ip.version == 6 + ip_v6 = dst_ip.version == 6 + + return cls(dst=(host, port), src=source, ip_v6=ip_v6) + + @override + async def resolve_hostname(self) -> None: + if self._ip_v6 is not None: + return None # source ip can be used to set the mode for resolution mode: RESOLVE_TO_IP_TYPE = 'auto' @@ -141,20 +155,21 @@ async def is_ip_v6(self) -> bool: mode = 'v6' if validate_ip_address(self.src[0]).version == 6 else 'v4' info = await resolve_hostname(self.dst[0], self.dst[1], mode=mode) - return info[0].version == 6 + self._ip_v6 = info[0].version == 6 + return None class MulticastNetworkTarget(NetworkTargetBase): - def __init__(self, src: tuple[str, int]) -> None: - super().__init__() + def __init__(self, src: tuple[str, int], *, ip_v6: bool | None = None) -> None: + super().__init__(ip_v6=ip_v6) self.src: Final = src def __repr__(self) -> str: return f'{self.__class__.__name__:s}(source={self.src[0]:s})' @override - def create_socket(self, *, ip_v6: bool) -> socket.socket: - sock: Final = super().create_socket(ip_v6=ip_v6) + def create_socket(self) -> socket.socket: + sock: Final = super().create_socket() # set source port/ip sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -163,18 +178,18 @@ def create_socket(self, *, ip_v6: bool) -> socket.socket: # setup socket for multicast sock.setsockopt( socket.IPPROTO_IP, - socket.IPV6_MULTICAST_IF if ip_v6 else socket.IP_MULTICAST_IF, - socket.inet_pton(AF_INET6 if ip_v6 else AF_INET, self.src[0]) + socket.IPV6_MULTICAST_IF if self.ip_v6 else socket.IP_MULTICAST_IF, + socket.inet_pton(AF_INET6 if self.ip_v6 else AF_INET, self.src[0]) ) return sock @classmethod def create(cls, source_ip: str, source_port: int = 0) -> Self: - validate_ip_address(source_ip) + ip_obj = validate_ip_address(source_ip) validate_port(source_port, allow_0=True) - return cls(src=(source_ip, source_port)) + return cls(src=(source_ip, source_port), ip_v6=ip_obj.version == 6) @override - async def is_ip_v6(self) -> bool: - return validate_ip_address(self.src[0]).version == 6 + async def resolve_hostname(self) -> None: + return None diff --git a/tests/helper.py b/tests/helper.py index 7a44ae0..d880ff9 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -55,11 +55,13 @@ def __exit__(self, exc_type: type[BaseException] | None, class UnicastNetworkTestingTarget(UnicastNetworkTarget): @override - async def is_ip_v6(self) -> bool: - return False + async def resolve_hostname(self) -> None: + self._ip_v6 = False + return None class MulticastTestingNetworkTarget(MulticastNetworkTarget): @override - async def is_ip_v6(self) -> bool: - return False + async def resolve_hostname(self) -> None: + self._ip_v6 = False + return None From 31ffee879297a85b23c6fd06d8b1d46ac46204f7 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 3 Nov 2025 06:35:27 +0100 Subject: [PATCH 46/49] . --- src/pyartnet/base/base_node.py | 4 ++-- src/pyartnet/base/network.py | 4 ++-- src/pyartnet/impl_artnet/node.py | 2 +- src/pyartnet/impl_kinet/node.py | 2 +- src/pyartnet/impl_sacn/node.py | 2 +- tests/test_impl/test_sacn.py | 2 -- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pyartnet/base/base_node.py b/src/pyartnet/base/base_node.py index 9c25f8f..dbde9d2 100644 --- a/src/pyartnet/base/base_node.py +++ b/src/pyartnet/base/base_node.py @@ -1,7 +1,6 @@ from __future__ import annotations from asyncio import sleep -from socket import socket from time import monotonic from typing import TYPE_CHECKING, Final, Generic, TypeVar @@ -13,6 +12,7 @@ if TYPE_CHECKING: + from socket import socket from types import TracebackType import pyartnet @@ -78,7 +78,7 @@ def _send_data(self, data: bytearray | bytes, dst: tuple[str, int] | str | None msg = 'Socket closed! Did you forget to use "async with"?' raise RuntimeError(msg) - sock.sendto(self._packet_base + data, dst) + sock.sendto(self._packet_base + data, dst) #type: ignore[arg-type] return None async def _process_values_task(self) -> None: diff --git a/src/pyartnet/base/network.py b/src/pyartnet/base/network.py index eef9ca1..7ec28be 100644 --- a/src/pyartnet/base/network.py +++ b/src/pyartnet/base/network.py @@ -6,10 +6,10 @@ from socket import AF_INET, AF_INET6, AF_UNSPEC, SOCK_DGRAM from typing import Final, Literal -from typing_extensions import Self, override +from typing_extensions import Self, TypeAlias, override -RESOLVE_TO_IP_TYPE: Final = Literal['auto', 'v4', 'v6'] +RESOLVE_TO_IP_TYPE: TypeAlias = Literal['auto', 'v4', 'v6'] def validate_port(port: int, *, allow_0: bool = False) -> int: diff --git a/src/pyartnet/impl_artnet/node.py b/src/pyartnet/impl_artnet/node.py index 5a5857f..e6bd17b 100644 --- a/src/pyartnet/impl_artnet/node.py +++ b/src/pyartnet/impl_artnet/node.py @@ -26,7 +26,7 @@ class ArtNetNode(BaseNode['pyartnet.impl_artnet.ArtNetUniverse']): def __init__(self, network: UnicastNetworkTarget, *, name: str | None = None, max_fps: int = 25, - refresh_every: float = 2, start_refresh_task: bool = True, + refresh_every: float = 2, # ArtNet specific fields sequence_counter: bool = True diff --git a/src/pyartnet/impl_kinet/node.py b/src/pyartnet/impl_kinet/node.py index e6ef53a..1f28db8 100644 --- a/src/pyartnet/impl_kinet/node.py +++ b/src/pyartnet/impl_kinet/node.py @@ -26,7 +26,7 @@ class KiNetNode(BaseNode['pyartnet.impl_kinet.KiNetUniverse']): def __init__(self, network: UnicastNetworkTarget, *, name: str | None = None, max_fps: int = 25, - refresh_every: float = 2, start_refresh_task: bool = True) -> None: + refresh_every: float = 2) -> None: super().__init__(network, name=name, max_fps=max_fps, refresh_every=refresh_every) self._dst: Final = network.dst diff --git a/src/pyartnet/impl_sacn/node.py b/src/pyartnet/impl_sacn/node.py index 5283a6a..5309fc4 100644 --- a/src/pyartnet/impl_sacn/node.py +++ b/src/pyartnet/impl_sacn/node.py @@ -41,7 +41,7 @@ class SacnNode(BaseNode['pyartnet.impl_sacn.SacnUniverse']): def __init__(self, network: UnicastNetworkTarget | MulticastNetworkTarget, *, name: str | None = None, max_fps: int = 25, - refresh_every: float = 2, start_refresh_task: bool = True, + refresh_every: float = 2, # sACN E1.31 specific fields cid: bytes | None = None, source_name: str | None = None diff --git a/tests/test_impl/test_sacn.py b/tests/test_impl/test_sacn.py index 44de8a1..cf8fb3e 100644 --- a/tests/test_impl/test_sacn.py +++ b/tests/test_impl/test_sacn.py @@ -14,7 +14,6 @@ async def test_sacn() -> None: UnicastNetworkTestingTarget(('ip', 9999999)), cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', source_name='default source name', - start_refresh_task=True ) async with sacn: @@ -48,7 +47,6 @@ async def test_sacn_with_sync(caplog, multicast) -> None: network, cid=b'\x41\x68\xf5\x2b\x1a\x7b\x2d\xe1\x17\x12\xe9\xee\x38\x3d\x22\x58', source_name='default source name', - start_refresh_task=False, name='device1' ) async with sacn: From 11e5c642212f8ac51f274d88e7230e8f93c849c1 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 3 Nov 2025 06:44:29 +0100 Subject: [PATCH 47/49] . --- .pre-commit-config.yaml | 2 +- uv.lock | 50 ++++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 232497c..531635c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.3 hooks: - id: ruff name: ruff unused imports diff --git a/uv.lock b/uv.lock index 1148b7f..4ba5ab5 100644 --- a/uv.lock +++ b/uv.lock @@ -995,28 +995,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" }, - { url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" }, - { url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" }, - { url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" }, - { url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" }, - { url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" }, - { url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" }, - { url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" }, - { url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" }, - { url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" }, - { url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" }, - { url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" }, - { url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" }, - { url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" }, - { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" }, +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, ] [[package]] @@ -1497,7 +1497,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.3" +version = "20.35.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -1510,9 +1510,9 @@ 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.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" }, + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] [[package]] From 7ed9be8c6e9c2ae2406f5849bdce5860305d5713 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 3 Nov 2025 07:07:22 +0100 Subject: [PATCH 48/49] . --- .github/workflows/publish-pypi.yml | 6 +++--- .github/workflows/run_tests.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 768e0ef..dfa5f87 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -12,10 +12,10 @@ jobs: - uses: actions/checkout@v5 with: ref: master - - name: Set up Python 3.10 - uses: actions/setup-python@v5 + - name: Set up Python 3.12 + uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.12' - name: Install setuptools run: | diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 98c85d1..ae65587 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.12' - uses: pre-commit/action@v3.0.1 @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Install the latest version of uv and set the python version - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: python-version: ${{ matrix.python-version }} activate-environment: true @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Install the latest version of uv and set the python version - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: python-version: '3.12' activate-environment: true From b2c275719b137685a95b74d0e3b8cc5d1fd2e333 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Tue, 4 Nov 2025 05:48:14 +0100 Subject: [PATCH 49/49] 2.0 --- readme.md | 11 +++++++++-- src/pyartnet/__version__.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index e5e1847..cf9a22c 100644 --- a/readme.md +++ b/readme.md @@ -14,10 +14,17 @@ Docs and examples can be found [here](https://pyartnet.readthedocs.io/en/latest/ # Changelog -#### 1.1.0 (2025-XX-XX) +#### 2.0 (2025-11-04) +- **Breaking change**: + Nodes now need to be run through an async context manager, e.g.: + ```python + async with ArtNetNode.create('IP') as node: + ... + ``` - Added support for transmitting multiple universes in sync -- used UV +- Added support for transmitting SACN through the broadcast address - ruff and typing fixes +- used UV #### 1.0.1 (2023-02-20) - Fixed an issue where consecutive fades would not start from the correct value diff --git a/src/pyartnet/__version__.py b/src/pyartnet/__version__.py index cd7ca49..3b3dacb 100644 --- a/src/pyartnet/__version__.py +++ b/src/pyartnet/__version__.py @@ -1 +1 @@ -__version__ = '1.0.1' +__version__ = '2.0'