From 8754161bf423fccdef0cbe8693e54c831893635d Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Fri, 30 Jan 2026 00:36:31 -0500 Subject: [PATCH] Cleanup testing and small parts of vetting details --- bats_ai/core/tests/conftest.py | 39 +---- bats_ai/core/tests/factories.py | 15 +- bats_ai/core/tests/test_admin.py | 34 ++-- bats_ai/core/tests/test_auth.py | 15 ++ bats_ai/core/tests/test_vetting_details.py | 175 +++++++++++---------- bats_ai/core/views/vetting_details.py | 31 ++-- tox.ini | 3 + uv.lock | 96 +++++------ 8 files changed, 196 insertions(+), 212 deletions(-) create mode 100644 bats_ai/core/tests/test_auth.py diff --git a/bats_ai/core/tests/conftest.py b/bats_ai/core/tests/conftest.py index d1c5e8fe..7bdf30bf 100644 --- a/bats_ai/core/tests/conftest.py +++ b/bats_ai/core/tests/conftest.py @@ -1,10 +1,8 @@ -from django.contrib.auth.models import User from django.test import Client +from ninja.testing import TestClient import pytest -from bats_ai.core.models import VettingDetails - -from .factories import SuperuserFactory, UserFactory, VettingDetailsFactory +from bats_ai.api import api @pytest.fixture @@ -13,34 +11,5 @@ def client() -> Client: @pytest.fixture -def user() -> User: - return UserFactory() - - -@pytest.fixture -def superuser() -> User: - return SuperuserFactory() - - -@pytest.fixture -def authenticated_client(user: User) -> Client: - client = Client() - client.force_login(user=user) - return client - - -@pytest.fixture -def authorized_client(superuser: User) -> Client: - client = Client() - client.force_login(user=superuser) - return client - - -@pytest.fixture -def vetting_details(user: User) -> VettingDetails: - return VettingDetailsFactory(user=user) - - -@pytest.fixture -def random_user_vetting_details() -> VettingDetails: - return VettingDetailsFactory(user=UserFactory()) +def api_client() -> TestClient: + return TestClient(api) diff --git a/bats_ai/core/tests/factories.py b/bats_ai/core/tests/factories.py index 708fbacf..fceedd67 100644 --- a/bats_ai/core/tests/factories.py +++ b/bats_ai/core/tests/factories.py @@ -14,23 +14,10 @@ class Meta: last_name = factory.Faker('last_name') -class SuperuserFactory(factory.django.DjangoModelFactory[User]): - class Meta: - model = User - django_get_or_create = ('email',) - - username = factory.SelfAttribute('email') - email = factory.Faker('safe_email') - first_name = factory.Faker('first_name') - last_name = factory.Faker('last_name') +class SuperuserFactory(UserFactory): is_superuser = True is_staff = True - @classmethod - def _create(cls, model_class, *args, **kwargs): - manager = cls._get_manager(model_class) - return manager.create_superuser(*args, **kwargs) - class VettingDetailsFactory(factory.django.DjangoModelFactory[VettingDetails]): diff --git a/bats_ai/core/tests/test_admin.py b/bats_ai/core/tests/test_admin.py index 2ba1d16b..d388dbc0 100644 --- a/bats_ai/core/tests/test_admin.py +++ b/bats_ai/core/tests/test_admin.py @@ -1,18 +1,24 @@ +from ninja.testing import TestClient import pytest +from bats_ai.core.tests.factories import SuperuserFactory, UserFactory + + +@pytest.mark.django_db +def test_check_is_admin_authenticated(api_client: TestClient): + user = UserFactory.create() + + resp = api_client.get('configuration/is_admin/', user=user) + + assert resp.status_code == 200 + assert resp.data['is_admin'] is False + -@pytest.mark.parametrize( - 'client_fixture,status_code,is_admin', - [ - ('client', 401, None), - ('authenticated_client', 200, False), - ('authorized_client', 200, True), - ], -) @pytest.mark.django_db -def test_is_admin(client_fixture, status_code, is_admin, request): - api_client = request.getfixturevalue(client_fixture) - resp = api_client.get('/api/v1/configuration/is_admin/') - assert resp.status_code == status_code - if is_admin is not None: - assert resp.json()['is_admin'] == is_admin +def test_check_is_admin_superuser(api_client: TestClient): + user = SuperuserFactory.create() + + resp = api_client.get('configuration/is_admin/', user=user) + + assert resp.status_code == 200 + assert resp.data['is_admin'] is True diff --git a/bats_ai/core/tests/test_auth.py b/bats_ai/core/tests/test_auth.py new file mode 100644 index 00000000..e328059c --- /dev/null +++ b/bats_ai/core/tests/test_auth.py @@ -0,0 +1,15 @@ +from django.test import Client +import pytest + + +@pytest.mark.parametrize( + 'url_suffix', + [ + 'configuration/is_admin/', + ], +) +@pytest.mark.django_db +def test_auth_anonymous_deny(url_suffix: str, client: Client): + resp = client.get(f'/api/v1/{url_suffix}') + + assert resp.status_code == 401 diff --git a/bats_ai/core/tests/test_vetting_details.py b/bats_ai/core/tests/test_vetting_details.py index e5537267..2c961ac7 100644 --- a/bats_ai/core/tests/test_vetting_details.py +++ b/bats_ai/core/tests/test_vetting_details.py @@ -1,114 +1,131 @@ +from ninja.testing import TestClient import pytest -from .factories import UserFactory, VettingDetailsFactory +from bats_ai.core.models import VettingDetails + +from .factories import SuperuserFactory, UserFactory, VettingDetailsFactory -@pytest.mark.parametrize( - 'client_fixture,status_code', - [ - ('client', 401), - ('authenticated_client', 200), - ('authorized_client', 200), - ], -) @pytest.mark.django_db -def test_get_vetting_details(client_fixture, status_code, user, vetting_details, request): - api_client = request.getfixturevalue(client_fixture) - resp = api_client.get(f'/api/v1/vetting/user/{user.id}') - assert resp.status_code == status_code - if status_code == 200: - assert resp.json()['reference_materials'] == vetting_details.reference_materials +def test_get_vetting_details(api_client: TestClient): + vetting_details = VettingDetailsFactory.create() + + resp = api_client.get(f'vetting/user/{vetting_details.user.id}', user=vetting_details.user) + + assert resp.status_code == 200 + assert resp.data['reference_materials'] == vetting_details.reference_materials @pytest.mark.django_db -def test_get_vetting_details_other_user(authenticated_client): - other_user = UserFactory() - VettingDetailsFactory(user=other_user) - resp = authenticated_client.get(f'/api/v1/vetting/user/{other_user.id}') +def test_get_vetting_details_other_user(api_client: TestClient): + vetting_details = VettingDetailsFactory.create() + other_user = UserFactory.create() + + resp = api_client.get(f'vetting/user/{vetting_details.user.id}', user=other_user) + assert resp.status_code == 404 @pytest.mark.django_db -def test_create_vetting_details(client): - test_text = 'foo' - data = {'reference_materials': test_text} - test_user = UserFactory() - client.force_login(user=test_user) - resp = client.post( - f'/api/v1/vetting/user/{test_user.id}', data=data, content_type='application/json' +def test_create_vetting_details(api_client: TestClient): + user = UserFactory.create() + + resp = api_client.post( + f'vetting/user/{user.id}', + json={'reference_materials': 'foo'}, + user=user, ) + assert resp.status_code == 200 - assert resp.json()['user_id'] == test_user.id + assert resp.data['user_id'] == user.id + assert resp.data['reference_materials'] == 'foo' + assert VettingDetails.objects.filter(user=user, reference_materials='foo').exists() -@pytest.mark.parametrize( - 'client_fixture,status_code', - [ - ('authenticated_client', 404), - ('authorized_client', 200), - ], -) @pytest.mark.django_db -def test_create_vetting_details_other_user(client_fixture, status_code, request): - api_client = request.getfixturevalue(client_fixture) - test_text = 'foo' - data = {'reference_materials': test_text} - other_user = UserFactory() +def test_create_vetting_details_other_user(api_client: TestClient): + user = UserFactory.create() + other_user = UserFactory.create() + resp = api_client.post( - f'/api/v1/vetting/user/{other_user.id}', data=data, content_type='application/json' + f'vetting/user/{user.id}', + json={'reference_materials': 'foo'}, + user=other_user, ) - assert resp.status_code == status_code - if status_code == 200: - assert resp.json()['reference_materials'] == test_text + + assert resp.status_code == 404 @pytest.mark.django_db -def test_update_vetting_details(client): - test_text = 'bar' - data = {'reference_materials': 'bar'} - test_user = UserFactory() - VettingDetailsFactory(user=test_user, reference_materials='foo') - client.force_login(test_user) - - initial_resp = client.get(f'/api/v1/vetting/user/{test_user.id}') - assert initial_resp.status_code == 200 - - resp = client.post( - f'/api/v1/vetting/user/{test_user.id}', data=data, content_type='application/json' +def test_create_vetting_details_other_superuser(api_client: TestClient): + user = UserFactory.create() + other_superuser = SuperuserFactory.create() + + resp = api_client.post( + f'vetting/user/{user.id}', + json={'reference_materials': 'foo'}, + user=other_superuser, ) + assert resp.status_code == 200 + assert resp.data['reference_materials'] == 'foo' + assert VettingDetails.objects.filter(user=user, reference_materials='foo').exists() - new_details_response = client.get(f'/api/v1/vetting/user/{test_user.id}') - assert new_details_response.status_code == 200 - assert new_details_response.json()['reference_materials'] == test_text + +@pytest.mark.django_db +def test_update_vetting_details(api_client: TestClient): + vetting_details = VettingDetailsFactory.create() + + resp = api_client.post( + f'vetting/user/{vetting_details.user.id}', + json={'reference_materials': 'foo'}, + user=vetting_details.user, + ) + + assert resp.status_code == 200 + assert resp.data['reference_materials'] == 'foo' + vetting_details.refresh_from_db() + assert vetting_details.reference_materials == 'foo' -@pytest.mark.parametrize( - 'client_fixture,status_code', - [ - ('authenticated_client', 404), - ('authorized_client', 200), - ], -) @pytest.mark.django_db -def test_update_vetting_details_other_user( - client_fixture, status_code, random_user_vetting_details, request -): - api_client = request.getfixturevalue(client_fixture) +def test_update_vetting_details_other_user(api_client: TestClient): + vetting_details = VettingDetailsFactory.create() + other_user = UserFactory.create() + resp = api_client.post( - f'/api/v1/vetting/user/{random_user_vetting_details.user.id}', - data={'reference_materials': 'foo'}, - content_type='application/json', + f'vetting/user/{vetting_details.user.id}', + json={'reference_materials': 'foo'}, + user=other_user, ) - assert resp.status_code == status_code + assert resp.status_code == 404 @pytest.mark.django_db -def test_update_vetting_details_length_constraint(authorized_client, random_user_vetting_details): - data = {'reference_materials': 'a' * 2001} - resp = authorized_client.post( - f'/api/v1/vetting/user/{random_user_vetting_details.user.id}', - data=data, - content_type='application/json', +def test_update_vetting_details_other_superuser(api_client: TestClient): + vetting_details = VettingDetailsFactory.create() + other_superuser = SuperuserFactory.create() + + resp = api_client.post( + f'vetting/user/{vetting_details.user.id}', + json={'reference_materials': 'foo'}, + user=other_superuser, ) + + assert resp.status_code == 200 + assert resp.data['reference_materials'] == 'foo' + vetting_details.refresh_from_db() + assert vetting_details.reference_materials == 'foo' + + +@pytest.mark.django_db +def test_update_vetting_details_length_constraint(api_client: TestClient): + vetting_details = VettingDetailsFactory.create() + + resp = api_client.post( + f'vetting/user/{vetting_details.user.id}', + json={'reference_materials': 'a' * 2001}, + user=vetting_details.user, + ) + assert resp.status_code == 400 diff --git a/bats_ai/core/views/vetting_details.py b/bats_ai/core/views/vetting_details.py index 3896e17a..0fbe06b4 100644 --- a/bats_ai/core/views/vetting_details.py +++ b/bats_ai/core/views/vetting_details.py @@ -1,4 +1,5 @@ from django.http import Http404, HttpRequest, HttpResponseBadRequest +from django.shortcuts import get_object_or_404 from ninja import Schema from ninja.pagination import RouterPaginated @@ -12,10 +13,6 @@ class VettingDetailsSchema(Schema): user_id: int reference_materials: str - @classmethod - def from_orm(cls, obj): - return cls(id=obj.id, reference_materials=obj.reference_materials, user_id=obj.user_id) - class UpdateVettingDetailsSchema(Schema): reference_materials: str @@ -23,16 +20,11 @@ class UpdateVettingDetailsSchema(Schema): @router.get('/user/{user_id}', response=VettingDetailsSchema) def get_vetting_details_for_user(request: HttpRequest, user_id: int): - details = VettingDetails.objects.filter(user_id=user_id).first() - - if not details: - raise Http404() - - if details.user != request.user and not request.user.is_staff: + if not (user_id == request.user.pk or request.user.is_superuser): # Don't leak user IDs, prefer to return a 404 over a 403 raise Http404 - return details + return get_object_or_404(VettingDetails, user_id=user_id) @router.post('/user/{user_id}', response=VettingDetailsSchema) @@ -41,7 +33,7 @@ def update_or_create_vetting_details_for_user( payload: UpdateVettingDetailsSchema, user_id: int, ): - if not (request.user.pk == user_id or request.user.is_staff): + if not (user_id == request.user.pk or request.user.is_superuser): raise Http404 if len(payload.reference_materials) > 2000: @@ -49,13 +41,8 @@ def update_or_create_vetting_details_for_user( 'reference_materials exceeds maximum length of 2000 characters' ) - details = VettingDetails.objects.filter(user_id=user_id).first() - - if not details: - details = VettingDetails(user=request.user, reference_materials=payload.reference_materials) - else: - details.reference_materials = payload.reference_materials - - details.save() - - return details + vetting_details, _created = VettingDetails.objects.update_or_create( + user_id=user_id, + defaults={'reference_materials': payload.reference_materials}, + ) + return vetting_details diff --git a/tox.ini b/tox.ini index 248ce8e5..e9a7f680 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,9 @@ commands = mypy {posargs} [testenv:test] +setenv = + # Workaround until release of https://github.com/vitalik/django-ninja/pull/1622 + NINJA_SKIP_REGISTRY = true dependency_groups = test commands = diff --git a/uv.lock b/uv.lock index aa14a6f9..9dc80f70 100644 --- a/uv.lock +++ b/uv.lock @@ -518,11 +518,11 @@ wheels = [ [[package]] name = "cachetools" -version = "6.1.0" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload-time = "2025-06-16T18:51:03.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload-time = "2025-06-16T18:51:01.514Z" }, + { url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" }, ] [[package]] @@ -1423,11 +1423,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.18.0" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -2763,11 +2763,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] @@ -3276,15 +3276,15 @@ wheels = [ [[package]] name = "pyproject-api" -version = "1.9.1" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" }, + { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, ] [[package]] @@ -3298,7 +3298,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.1" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -3309,9 +3309,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -3328,14 +3328,14 @@ wheels = [ [[package]] name = "pytest-mock" -version = "3.14.1" +version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] @@ -4220,7 +4220,7 @@ wheels = [ [[package]] name = "tox" -version = "4.27.0" +version = "4.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -4235,23 +4235,23 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/b7/19c01717747076f63c54d871ada081cd711a7c9a7572f2225675c3858b94/tox-4.27.0.tar.gz", hash = "sha256:b97d5ecc0c0d5755bcc5348387fef793e1bfa68eb33746412f4c60881d7f5f57", size = 198351, upload-time = "2025-06-17T15:17:50.585Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/9b/5909f40b281ebd37c2f83de5087b9cb8a9a64c33745f334be0aeaedadbbc/tox-4.34.1.tar.gz", hash = "sha256:ef1e82974c2f5ea02954d590ee0b967fad500c3879b264ea19efb9a554f3cc60", size = 205306, upload-time = "2026-01-09T17:42:59.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/3a/30889167f41ecaffb957ec4409e1cbc1d5d558a5bbbdfb734a5b9911930f/tox-4.27.0-py3-none-any.whl", hash = "sha256:2b8a7fb986b82aa2c830c0615082a490d134e0626dbc9189986da46a313c4f20", size = 173441, upload-time = "2025-06-17T15:17:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fe6629e277ce615e53d0a0b65dc23c88b15a402bb7dbf771f17bbd18f1c4/tox-4.34.1-py3-none-any.whl", hash = "sha256:5610d69708bab578d618959b023f8d7d5d3386ed14a2392aeebf9c583615af60", size = 176812, upload-time = "2026-01-09T17:42:58.629Z" }, ] [[package]] name = "tox-uv" -version = "1.26.1" +version = "1.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tox" }, { name = "uv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/00/98e564731fc361cc2f1e39c58d2feb0b4c9f9a7cb06f0c769cdeb9a98004/tox_uv-1.26.1.tar.gz", hash = "sha256:241cc530b4a80436c4487977c8303d9aace398c6561d5e7d8845606fa7d482ab", size = 21849, upload-time = "2025-06-23T20:17:54.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/90/06752775b8cfadba8856190f5beae9f552547e0f287e0246677972107375/tox_uv-1.29.0.tar.gz", hash = "sha256:30fa9e6ad507df49d3c6a2f88894256bcf90f18e240a00764da6ecab1db24895", size = 23427, upload-time = "2025-10-09T20:40:27.384Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/0b/e47c1bb2bc9e20b22a6913ea2162b7bb5729d38924fa2c1d4eaf95d3b36f/tox_uv-1.26.1-py3-none-any.whl", hash = "sha256:edc25b254e5cdbb13fc5d23d6d05b511dee562ab72b0e99da4a874a78018c38e", size = 16661, upload-time = "2025-06-23T20:17:52.492Z" }, + { url = "https://files.pythonhosted.org/packages/5c/17/221d62937c4130b044bb437caac4181e7e13d5536bbede65264db1f0ac9f/tox_uv-1.29.0-py3-none-any.whl", hash = "sha256:b1d251286edeeb4bc4af1e24c8acfdd9404700143c2199ccdbb4ea195f7de6cc", size = 17254, upload-time = "2025-10-09T20:40:25.885Z" }, ] [[package]] @@ -4325,27 +4325,27 @@ wheels = [ [[package]] name = "uv" -version = "0.7.19" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/52/50f8be2cc8c9dc89319e9cbc72656a676742ab59c2d9f78e5bf94898f960/uv-0.7.19.tar.gz", hash = "sha256:c99b4ee986d2ca3a597dfe91baeb86ce5ccc7cd4292a9f5eb108d1ae45ec2705", size = 3355519, upload-time = "2025-07-02T21:42:20.966Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/f4/41d97a3345aebb94ba84d53c7eaef72d5436aac6c89f73956b87604eb1e1/uv-0.7.19-py3-none-linux_armv6l.whl", hash = "sha256:9b1b8908c47509526b6531c4d350c84b0e03a0923a2cb405c3cc53fbc73b1d3e", size = 17587804, upload-time = "2025-07-02T21:41:32.254Z" }, - { url = "https://files.pythonhosted.org/packages/96/51/a260f73b615ea6953128182c5e03473e6a3321d047af1aa7acba496f7b2f/uv-0.7.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bd596055c178d5022de4d3c5a3d02a982ad747be83b270893ac3d97d4ab4358", size = 17679828, upload-time = "2025-07-02T21:41:35.809Z" }, - { url = "https://files.pythonhosted.org/packages/8c/59/4ba64e727b5b570e07e04671c70eda334e91c8375aa2d38cdfda24a64fa0/uv-0.7.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f303b80d840298f668ce6a3157e2b0b58402fd4e293a478278234efde8724e75", size = 16367731, upload-time = "2025-07-02T21:41:38.677Z" }, - { url = "https://files.pythonhosted.org/packages/19/b5/ec4dd36640f2019b0c4cbec7ca182509289d988ba2e8587ca627e0c016b2/uv-0.7.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:692f6e80b4d98b2fbf93b1a17ed00b60ac058b04507e8f32d6fc5205eb2934c7", size = 16933649, upload-time = "2025-07-02T21:41:41.156Z" }, - { url = "https://files.pythonhosted.org/packages/d7/d5/7dc3382c732aa42257ab03738a5595d3b15890ffcce1972c86dd6845c673/uv-0.7.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:576bf4d53385c89e049662c358e8582e7f728af1e55f5eca95d496188cf5a406", size = 17285587, upload-time = "2025-07-02T21:41:46.079Z" }, - { url = "https://files.pythonhosted.org/packages/46/01/25f78f929d457178fad8f167048d012bbdf4dd4e74372e54fbafa5fccd7b/uv-0.7.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8546bbb5b852a249bb8b4e895eaa1e8ea9de3a0e26954a0413aa402e388868f5", size = 17994092, upload-time = "2025-07-02T21:41:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/04/fe/f983fc90d98bfb2941d58b35a59a7411f6632e719883431786aa18bad5f9/uv-0.7.19-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7e1fff9b4552b532264cb3dd39f802aa98cb974a490714cef71ab848269b7e41", size = 19196540, upload-time = "2025-07-02T21:41:50.909Z" }, - { url = "https://files.pythonhosted.org/packages/91/a9/56bd9de82f2d66db246506196546a8346653e03b118c5488054e7f3fa9f5/uv-0.7.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83416ca351aea36155ec07fce6ac9513e043389646a45a1ad567a532ef101dd", size = 18957639, upload-time = "2025-07-02T21:41:53.224Z" }, - { url = "https://files.pythonhosted.org/packages/04/d0/6093c3818eaf485de85c821f440191a7fd45ce56297493fb6e01baf5fdf3/uv-0.7.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cb6fa07564d04e751dac1f0a0a641b9795838e8c98b6389135690b61a20753c", size = 18502456, upload-time = "2025-07-02T21:41:58.229Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5e/bd0594f69dcdc633ffafd500538f137169a8b588f628a4f6abd5dc198426/uv-0.7.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dee2c73fe29e8f119ac074ebb3b2aa4390272e5ab3a5f00f75ca16caf120d64", size = 18391983, upload-time = "2025-07-02T21:42:00.63Z" }, - { url = "https://files.pythonhosted.org/packages/7b/12/f51c559e4bcf6065fc43491cff1780108a207eca13511ffcd73a5c8dbf8b/uv-0.7.19-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:52b7d64a97b18196cccbbbd8036ad649a72b7b1a7fd4b22297219c55a733127c", size = 17169522, upload-time = "2025-07-02T21:42:02.983Z" }, - { url = "https://files.pythonhosted.org/packages/de/15/75d8cf9f809e911a1492bad3988f3aa29319ac2b312d48fea017c48006a3/uv-0.7.19-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:987059dee02b8e455829f5844dbcbe202cdedf04108942382dadcc29fa140d6a", size = 17257476, upload-time = "2025-07-02T21:42:06.067Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5e/214d127a4c323e031998b81b7a144c2c72c4056fcc25117515799962b0ee/uv-0.7.19-py3-none-musllinux_1_1_i686.whl", hash = "sha256:e6bffad5d26a2c23ccd5a544ac3fd285427b1f8704cf7b3fdc8ec7954a7f6cad", size = 17569440, upload-time = "2025-07-02T21:42:08.378Z" }, - { url = "https://files.pythonhosted.org/packages/46/f8/280823b29ca2a19fcdb728b46ef27f969c5b8a2dc952e556d67c7c6f9293/uv-0.7.19-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cb6c0d61294af5b6cabd6831aa795abf3134f96736a973c046acc9f5a3501f0d", size = 18531849, upload-time = "2025-07-02T21:42:11.073Z" }, - { url = "https://files.pythonhosted.org/packages/73/28/690c02e4f63a6fb46cc9f5670a6e208dd6fef1b33f328e0916738f5ddc2f/uv-0.7.19-py3-none-win32.whl", hash = "sha256:e59efa9b0449b49acca0ca817666cc2d4a03bd619c77867bea57b133f224e5f3", size = 17581671, upload-time = "2025-07-02T21:42:13.351Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d7/9658133273c393bdf5127b87b00abeec04f90bc0e2004d4ea180502f24e8/uv-0.7.19-py3-none-win_amd64.whl", hash = "sha256:b971035a69bf1c28424896894c181d8b65624f43b95858a3b34a33dba04a5a2a", size = 19320233, upload-time = "2025-07-02T21:42:16.025Z" }, - { url = "https://files.pythonhosted.org/packages/e9/50/59b4141026491b625110161fcc0e383b4eb9a81937d4608c614ab990a789/uv-0.7.19-py3-none-win_arm64.whl", hash = "sha256:729befc8b4d05b9a86192af09228472c058c9ec071dd42d84190f10507b7c6e0", size = 17943387, upload-time = "2025-07-02T21:42:18.804Z" }, +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/36/f7fe4de0ad81234ac43938fe39c6ba84595c6b3a1868d786a4d7ad19e670/uv-0.10.0.tar.gz", hash = "sha256:ad01dd614a4bb8eb732da31ade41447026427397c5ad171cc98bd59579ef57ea", size = 3854103, upload-time = "2026-02-05T20:57:55.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/69/33fb64aee6ba138b1aaf957e20778e94a8c23732e41cdf68e6176aa2cf4e/uv-0.10.0-py3-none-linux_armv6l.whl", hash = "sha256:38dc0ccbda6377eb94095688c38e5001b8b40dfce14b9654949c1f0b6aa889df", size = 21984662, upload-time = "2026-02-05T20:57:19.076Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5a/e3ff8a98cfbabc5c2d09bf304d2d9d2d7b2e7d60744241ac5ed762015e5c/uv-0.10.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a165582c1447691109d49d09dccb065d2a23852ff42bf77824ff169909aa85da", size = 21057249, upload-time = "2026-02-05T20:56:48.921Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/ec8f24f8d0f19c4fda0718d917bb78b9e6f02a4e1963b401f1c4f4614a54/uv-0.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aefea608971f4f23ac3dac2006afb8eb2b2c1a2514f5fee1fac18e6c45fd70c4", size = 19827174, upload-time = "2026-02-05T20:57:10.581Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/09b38b93208906728f591f66185a425be3acdb97c448460137d0e6ecb30a/uv-0.10.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d4b621bcc5d0139502789dc299bae8bf55356d07b95cb4e57e50e2afcc5f43e1", size = 21629522, upload-time = "2026-02-05T20:57:29.959Z" }, + { url = "https://files.pythonhosted.org/packages/89/f3/48d92c90e869331306979efaa29a44c3e7e8376ae343edc729df0d534dfb/uv-0.10.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:b4bea728a6b64826d0091f95f28de06dd2dc786384b3d336a90297f123b4da0e", size = 21614812, upload-time = "2026-02-05T20:56:58.103Z" }, + { url = "https://files.pythonhosted.org/packages/ff/43/d0dedfcd4fe6e36cabdbeeb43425cd788604db9d48425e7b659d0f7ba112/uv-0.10.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc0cc2a4bcf9efbff9a57e2aed21c2d4b5a7ec2cc0096e0c33d7b53da17f6a3b", size = 21577072, upload-time = "2026-02-05T20:57:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/b8c9320fd8d86f356e37505a02aa2978ed28f9c63b59f15933e98bce97e5/uv-0.10.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:070ca2f0e8c67ca9a8f70ce403c956b7ed9d51e0c2e9dbbcc4efa5e0a2483f79", size = 22829664, upload-time = "2026-02-05T20:57:22.689Z" }, + { url = "https://files.pythonhosted.org/packages/56/9c/2c36b30b05c74b2af0e663e0e68f1d10b91a02a145e19b6774c121120c0b/uv-0.10.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8070c66149c06f9b39092a06f593a2241345ea2b1d42badc6f884c2cc089a1b1", size = 23705815, upload-time = "2026-02-05T20:57:37.604Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/8c7fdb14ab72e26ca872e07306e496a6b8cf42353f9bf6251b015be7f535/uv-0.10.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3db1d5390b3a624de672d7b0f9c9d8197693f3b2d3d9c4d9e34686dcbc34197a", size = 22890313, upload-time = "2026-02-05T20:57:26.35Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/5c152350b1a6d0af019801f91a1bdeac854c33deb36275f6c934f0113cb5/uv-0.10.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b46db718763bf742e986ebbc7a30ca33648957a0dcad34382970b992f5e900", size = 22769440, upload-time = "2026-02-05T20:56:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/980e5399c6f4943b81754be9b7deb87bd56430e035c507984e17267d6a97/uv-0.10.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:eb95d28590edd73b8fdd80c27d699c45c52f8305170c6a90b830caf7f36670a4", size = 21695296, upload-time = "2026-02-05T20:57:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e7/f44ad40275be2087b3910df4678ed62cf0c82eeb3375c4a35037a79747db/uv-0.10.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5871eef5046a81df3f1636a3d2b4ccac749c23c7f4d3a4bae5496cb2876a1814", size = 22424291, upload-time = "2026-02-05T20:57:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/c2/81/31c0c0a8673140756e71a1112bf8f0fcbb48a4cf4587a7937f5bd55256b6/uv-0.10.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:1af0ec125a07edb434dfaa98969f6184c1313dbec2860c3c5ce2d533b257132a", size = 22109479, upload-time = "2026-02-05T20:57:02.258Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/2eb51bc233bad3d13ad64a0c280fd4d1ebebf5c2939b3900a46670fa2b91/uv-0.10.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:45909b9a734250da05b10101e0a067e01ffa2d94bbb07de4b501e3cee4ae0ff3", size = 22972087, upload-time = "2026-02-05T20:57:52.847Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f7/49987207b87b5c21e1f0e81c52892813e8cdf7e318b6373d6585773ebcdd/uv-0.10.0-py3-none-win32.whl", hash = "sha256:d5498851b1f07aa9c9af75578b2029a11743cb933d741f84dcbb43109a968c29", size = 20896746, upload-time = "2026-02-05T20:57:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/80/b2/1370049596c6ff7fa1fe22fccf86a093982eac81017b8c8aff541d7263b2/uv-0.10.0-py3-none-win_amd64.whl", hash = "sha256:edd469425cd62bcd8c8cc0226c5f9043a94e37ed869da8268c80fdbfd3e5015e", size = 23433041, upload-time = "2026-02-05T20:57:41.41Z" }, + { url = "https://files.pythonhosted.org/packages/e3/76/1034c46244feafec2c274ac52b094f35d47c94cdb11461c24cf4be8a0c0c/uv-0.10.0-py3-none-win_arm64.whl", hash = "sha256:e90c509749b3422eebb54057434b7119892330d133b9690a88f8a6b0f3116be3", size = 21880261, upload-time = "2026-02-05T20:57:14.724Z" }, ] [[package]] @@ -4359,16 +4359,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.31.2" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]]