diff --git a/Dockerfile b/Dockerfile index 5e3a4db..ba7be8e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN pip install uv ADD ./mavis/reporting /app/mavis/reporting ADD README.md /app/ -RUN uv sync --frozen --all-extras +RUN uv sync --frozen RUN npm install FROM builder diff --git a/mavis/reporting/helpers/auth_helper.py b/mavis/reporting/helpers/auth_helper.py index c3e5be9..78b2a49 100644 --- a/mavis/reporting/helpers/auth_helper.py +++ b/mavis/reporting/helpers/auth_helper.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta, timezone import jwt -import urllib +from urllib.parse import quote +from typing import Any from flask import ( request, @@ -8,6 +9,7 @@ redirect, current_app, ) +from flask.sessions import SessionMixin from functools import wraps @@ -34,7 +36,7 @@ def decorated_function(*args, **kwargs): log_user_in(verification_data, session) return redirect( - url_helper.url_without_param(request.full_path, "code") + str(url_helper.url_without_param(request.full_path, "code")) ) except KeyError: pass @@ -44,9 +46,9 @@ def decorated_function(*args, **kwargs): return_url = url_helper.externalise_current_url(current_app, request) target_url = mavis_helper.mavis_url( current_app, - "/start?redirect_uri=" + urllib.parse.quote(return_url), + "/start?redirect_uri=" + quote(return_url), ) - return redirect(target_url) + return redirect(str(target_url)) return f(*args, **kwargs) @@ -85,7 +87,7 @@ def encode_jwt(payload, current_app=current_app): return jwt.encode(payload, secret, algorithm="HS512") -def log_user_in(data, session=session): +def log_user_in(data: dict[str, Any], session: SessionMixin): jwt_data = data["jwt_data"] session["last_visit"] = datetime.now().astimezone(timezone.utc) diff --git a/mavis/reporting/helpers/mavis_helper.py b/mavis/reporting/helpers/mavis_helper.py index b5a0261..4073159 100644 --- a/mavis/reporting/helpers/mavis_helper.py +++ b/mavis/reporting/helpers/mavis_helper.py @@ -1,7 +1,7 @@ from http import HTTPStatus import requests import urllib.parse -import werkzeug +from werkzeug.exceptions import Unauthorized from flask import redirect @@ -49,7 +49,7 @@ def api_call(current_app, session, path, params={}): response = get_request(url, headers=headers) if response.status_code in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]: session.clear() - raise (werkzeug.exceptions.Unauthorized) + raise Unauthorized() return response @@ -60,7 +60,7 @@ def login_and_return_after(current_app, return_url): "/start?redirect_uri=" + urllib.parse.quote_plus(return_url), ) current_app.logger.warning("REDIRECTING TO %s", target_url) - return redirect(target_url) + return redirect(str(target_url)) def get_request(url, headers={}): diff --git a/mavis/reporting/helpers/url_helper.py b/mavis/reporting/helpers/url_helper.py index 591cd9b..03c2168 100644 --- a/mavis/reporting/helpers/url_helper.py +++ b/mavis/reporting/helpers/url_helper.py @@ -1,7 +1,7 @@ from urllib.parse import urlparse, parse_qs, parse_qsl, urlencode, urlunparse, urljoin -def url_without_param(url, param): +def url_without_param(url: str, param: str) -> str: parsed_url = urlparse(url) query_params_as_dict = parse_qs(parsed_url.query) if param in query_params_as_dict: @@ -16,7 +16,7 @@ def url_without_param(url, param): return url -def externalise_current_url(current_app, request): +def externalise_current_url(current_app, request) -> str: return urljoin( current_app.config["ROOT_URL"] or request.host_url, request.full_path ) diff --git a/mavis/reporting/views.py b/mavis/reporting/views.py index c16d881..e480323 100644 --- a/mavis/reporting/views.py +++ b/mavis/reporting/views.py @@ -51,7 +51,7 @@ def download(): @main.errorhandler(404) -def page_not_found(): +def page_not_found(_error): return render_template("errors/404.html"), 404 diff --git a/mise.toml b/mise.toml index c2c9730..9c992fa 100644 --- a/mise.toml +++ b/mise.toml @@ -17,7 +17,7 @@ run = "npm install" [tasks."uv:sync"] description = "Sync Python dependencies with uv" -run = "uv sync --all-extras" +run = "uv sync --group dev" [tasks."build:favicons"] description = "Copy NHS UK frontend favicons" @@ -66,6 +66,11 @@ description = "Run code linting with autofix" depends = ["uv:sync"] run = "uv run ruff check --fix ." +[tasks.typecheck] +description = "Run type checking with pyright" +depends = ["uv:sync"] +run = "uv run pyright" + [tasks."dev:scss"] description = "Watch and build SCSS files" depends = ["npm:install"] @@ -121,4 +126,4 @@ run = "mise docker:run" [tasks.ci] description = "Run CI checks in parallel" -depends = ["gitleaks", "lint", "test"] +depends = ["gitleaks", "lint", "test", "typecheck"] diff --git a/pyproject.toml b/pyproject.toml index 08ea5b7..c3098b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "pyjwt>=2.10.1", ] -[project.optional-dependencies] +[dependency-groups] dev = [ "ruff==0.12.12", "pytest>=8.4.1", @@ -22,8 +22,12 @@ dev = [ "coverage-badge>=1.1.2", "boto3==1.40.24", "pyyaml>=6.0.2", + "pyright>=1.1.405", ] +[tool.ruff] +output-format = "github" + [tool.ruff.lint] select = ["ARG", "PL", "E", "W", "F"] @@ -34,3 +38,7 @@ only-include = ["mavis", "scripts"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.pyright] +venvPath = "." +venv = ".venv" diff --git a/tests/helpers/test_auth_helper.py b/tests/helpers/test_auth_helper.py index ac45186..85829ac 100644 --- a/tests/helpers/test_auth_helper.py +++ b/tests/helpers/test_auth_helper.py @@ -1,7 +1,8 @@ from os import urandom - +from typing import cast from datetime import datetime, timedelta, timezone +from flask.sessions import SessionMixin from mavis.reporting.helpers import auth_helper @@ -57,7 +58,7 @@ def test_that_log_user_in_sets_last_visit_to_now(app): with app.app_context(): configure_app(app) mock_session = {} - auth_helper.log_user_in(mock_user_info(), mock_session) + auth_helper.log_user_in(mock_user_info(), cast(SessionMixin, mock_session)) assert mock_session["last_visit"] is not None assert datetime.now().astimezone(timezone.utc) - mock_session[ "last_visit" @@ -68,7 +69,7 @@ def test_that_log_user_in_copies_cis2_info_from_the_given_data(app): mock_session = {} with app.app_context(): configure_app(app) - auth_helper.log_user_in(mock_user_info(), mock_session) + auth_helper.log_user_in(mock_user_info(), cast(SessionMixin, mock_session)) assert mock_session["cis2_info"] == mock_user_info()["jwt_data"]["cis2_info"] @@ -78,7 +79,7 @@ def test_that_log_user_in_copies_user_from_the_given_data(app): with app.app_context(): configure_app(app) - auth_helper.log_user_in(fake_data, mock_session) + auth_helper.log_user_in(fake_data, cast(SessionMixin, mock_session)) assert mock_session["user"] == fake_data["jwt_data"]["user"] @@ -89,7 +90,7 @@ def test_that_log_user_in_sets_minimal_jwt( fake_data = mock_user_info() with app.app_context(): configure_app(app) - auth_helper.log_user_in(fake_data, mock_session) + auth_helper.log_user_in(fake_data, cast(SessionMixin, mock_session)) assert mock_session["jwt"] is not None jwt_payload = auth_helper.decode_jwt(mock_session["jwt"]) diff --git a/tests/helpers/test_mavis_helper.py b/tests/helpers/test_mavis_helper.py index 391f77a..a2ffb26 100644 --- a/tests/helpers/test_mavis_helper.py +++ b/tests/helpers/test_mavis_helper.py @@ -1,7 +1,7 @@ import json from http import HTTPStatus import pytest -import werkzeug +from werkzeug.exceptions import Unauthorized from flask import current_app from unittest.mock import patch @@ -60,7 +60,7 @@ def __init__(self, **kwargs): self.json_obj = kwargs.get("json_obj", None) def json(self): - return self.json_obj or json.loads(self.text) + return self.json_obj or json.loads(self.text or "{}") def test_that_verify_auth_code_is_called_correctly( @@ -146,7 +146,7 @@ def test_that_an_unauthorized_api_call_raises_an_exception_and_clears_the_sessio "mavis.reporting.helpers.mavis_helper.get_request", return_value=mock_response, ): - with pytest.raises(werkzeug.exceptions.Unauthorized): + with pytest.raises(Unauthorized): mavis_helper.api_call( app, mock_session, diff --git a/uv.lock b/uv.lock index aafe3e5..9f83346 100644 --- a/uv.lock +++ b/uv.lock @@ -13,30 +13,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.40.23" +version = "1.40.24" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/24/bf634f3e5ff8dd5b4e38d838492fb14d3f6aa4b1a78ab8ca31b7054a9947/boto3-1.40.23.tar.gz", hash = "sha256:ca5e2f767a7b759b0bb5c7e5c665effa1512799e763823e68d04e7c10b1399d5", size = 111564, upload-time = "2025-09-03T19:27:57.996Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/41/b1a375d02481c0c276acec2f1624ee6561f912007e9bdf89d794620d69cb/boto3-1.40.24.tar.gz", hash = "sha256:cc147ad13e8edf7ec69cbb4df8fe60f187f8b2c9ab8befa0fd1fbcfa4fc80b1f", size = 111584, upload-time = "2025-09-04T19:22:08.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/53/5fdaa30851c34e8a14113dfeef402d899aab2c3f111781ab1965ae3d2978/boto3-1.40.23-py3-none-any.whl", hash = "sha256:9826fb6abcdda2f016a939b81e94d1a9e1147ae897a523f366cf5a1558936356", size = 139323, upload-time = "2025-09-03T19:27:56.603Z" }, + { url = "https://files.pythonhosted.org/packages/72/97/0edd43090a5e7d2abbb651d8efbe3b68ac1f98b07076c2c14c38ef5d62f6/boto3-1.40.24-py3-none-any.whl", hash = "sha256:24a19e275d33e918afc22a78c6a1e20c14d02cc00e2f786b05e2a4a32191457e", size = 139323, upload-time = "2025-09-04T19:22:06.433Z" }, ] [[package]] name = "botocore" -version = "1.40.23" +version = "1.40.24" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/1b/3815d258e70e6eea26648b93ba6fe41d32f7646e600b48c97d7f89e0157c/botocore-1.40.23.tar.gz", hash = "sha256:de07cceaf9b142c183e165d303a0eee289c34d63f63e6b7a640406d6bacfb646", size = 14327014, upload-time = "2025-09-03T19:27:48.463Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/aa/229503199f4d1533db327b69509487b9e73604c735506e92be266d744b61/botocore-1.40.24.tar.gz", hash = "sha256:af2b49e52950a12229440d7c297aaad0a7b75fd1c4f8700b164948b207a08cf0", size = 14328213, upload-time = "2025-09-04T19:21:56.488Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/e6/bffb1b0fec021a6f8eee941adca53d843a03fcdd69c23f2b0f8a58e4d033/botocore-1.40.23-py3-none-any.whl", hash = "sha256:487cced8f9346f7d1038e9158c56b6ecf6d839dd43e345bc730053c8cf321ed3", size = 13999485, upload-time = "2025-09-03T19:27:46.118Z" }, + { url = "https://files.pythonhosted.org/packages/0c/79/19e515fdba3d6ced97344fc2d6cc81454f77fad049724f2126b7ab83f332/botocore-1.40.24-py3-none-any.whl", hash = "sha256:d566840f2291bb5df1c0903ad385c61c865927d562d41dcf6468c9cee4cc313a", size = 14001934, upload-time = "2025-09-04T19:21:52.667Z" }, ] [[package]] @@ -256,11 +256,12 @@ dependencies = [ { name = "requests" }, ] -[package.optional-dependencies] +[package.dev-dependencies] dev = [ { name = "boto3" }, { name = "coverage" }, { name = "coverage-badge" }, + { name = "pyright" }, { name = "pytest" }, { name = "pyyaml" }, { name = "ruff" }, @@ -268,20 +269,24 @@ dev = [ [package.metadata] requires-dist = [ - { name = "boto3", marker = "extra == 'dev'", specifier = "==1.40.23" }, - { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.10.6" }, - { name = "coverage-badge", marker = "extra == 'dev'", specifier = ">=1.1.2" }, { name = "flask", specifier = "==3.1.2" }, { name = "gunicorn", specifier = ">=23.0.0" }, { name = "nhsuk-frontend-jinja", specifier = "==0.4.1" }, { name = "py-healthcheck", specifier = ">=1.10.1" }, { name = "pyjwt", specifier = ">=2.10.1" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.1" }, - { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.2" }, { name = "requests", specifier = ">=2.32.5" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.12.11" }, ] -provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "boto3", specifier = "==1.40.24" }, + { name = "coverage", specifier = ">=7.10.6" }, + { name = "coverage-badge", specifier = ">=1.1.2" }, + { name = "pyright", specifier = ">=1.1.405" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "ruff", specifier = "==0.12.12" }, +] [[package]] name = "markupsafe" @@ -323,6 +328,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/8b/dfc0cfc3db073e2a32719528e83a89eb841820bd32f778b6742a448d5c43/nhsuk_frontend_jinja-0.4.1-py3-none-any.whl", hash = "sha256:ce34d28f43f9fb87012177731f2e463ab8fb44c7b847555342089096b8b1334c", size = 49181, upload-time = "2025-09-02T10:40:55.065Z" }, ] +[[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" @@ -371,6 +385,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] +[[package]] +name = "pyright" +version = "1.1.405" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/6c/ba4bbee22e76af700ea593a1d8701e3225080956753bee9750dcc25e2649/pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763", size = 4068319, upload-time = "2025-09-04T03:37:06.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1a/524f832e1ff1962a22a1accc775ca7b143ba2e9f5924bb6749dce566784a/pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a", size = 5905038, upload-time = "2025-09-04T03:37:04.913Z" }, +] + [[package]] name = "pytest" version = "8.4.1" @@ -433,28 +460,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" }, - { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" }, - { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" }, - { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" }, - { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" }, - { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" }, - { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" }, - { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" }, +version = "0.12.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" }, + { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, + { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, ] [[package]] @@ -487,6 +514,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +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.5.0"