diff --git a/src/backend/api/v1/auth/tests/test_auth.py b/src/backend/api/v1/auth/tests/test_auth.py index 6244be6..f125bcc 100644 --- a/src/backend/api/v1/auth/tests/test_auth.py +++ b/src/backend/api/v1/auth/tests/test_auth.py @@ -1,4 +1,5 @@ import json +from typing import override from django.test import Client, TestCase from django.urls import reverse @@ -7,13 +8,14 @@ from apps.users.auth.jwt import ( create_token_pair, ) from apps.users.models import User, UserRole -from apps.users.tests._helpers import _auth_header, _make_user +from apps.users.tests.helpers import auth_header, make_user class AuthAPITest(TestCase): + @override def setUp(self) -> None: self.client = Client() - self.user: User = _make_user( + self.user: User = make_user( username="api_auth", email="api_auth@x.com", password="testpass123", @@ -92,7 +94,7 @@ class AuthAPITest(TestCase): def test_me_authenticated(self) -> None: resp = self.client.get( reverse("api-1:me"), - HTTP_AUTHORIZATION=_auth_header(self.user), + HTTP_AUTHORIZATION=auth_header(self.user), ) self.assertEqual(resp.status_code, 200) data = resp.json() diff --git a/src/backend/api/v1/handlers.py b/src/backend/api/v1/handlers.py index 5eb196f..d06675a 100644 --- a/src/backend/api/v1/handlers.py +++ b/src/backend/api/v1/handlers.py @@ -47,7 +47,7 @@ def handle_validation_error( { "field": field, "issue": error.get("msg", "Unknown error"), - "rejectedValue": error.get("input"), + "rejected_value": error.get("input"), } ) @@ -77,7 +77,7 @@ def handle_django_validation_error( { "field": field, "issue": str(error.message), - "rejectedValue": None, + "rejected_value": None, } for error in errors ) @@ -86,7 +86,7 @@ def handle_django_validation_error( { "field": "non_field_error", "issue": str(error.message), - "rejectedValue": None, + "rejected_value": None, } for error in exc.error_list ) diff --git a/src/backend/api/v1/reviews/schemas.py b/src/backend/api/v1/reviews/schemas.py index 36ad23e..e9be43a 100644 --- a/src/backend/api/v1/reviews/schemas.py +++ b/src/backend/api/v1/reviews/schemas.py @@ -9,8 +9,6 @@ from apps.users.models import User class ApproverOut(ModelSchema): - first_name: str = Field("", alias="firstName") - last_name: str = Field("", alias="lastName") class Meta: model = User @@ -55,12 +53,10 @@ class ApproverGroupOut(ModelSchema): class ApproverGroupCreateIn(Schema): experimenter_id: str = Field( ..., - alias="experimenterId", description="UUID of the experimenter user this group belongs to.", ) approver_ids: list[str] = Field( default_factory=list, - alias="approverIds", description=( "List of user UUIDs to add as approvers. " "Each user must have the 'approver' role." @@ -68,7 +64,6 @@ class ApproverGroupCreateIn(Schema): ) min_approvals: int = Field( 1, - alias="minApprovals", ge=1, description=( "Number of distinct approvals required. " @@ -80,7 +75,6 @@ class ApproverGroupCreateIn(Schema): class ApproverGroupUpdateIn(Schema): approver_ids: list[str] | None = Field( None, - alias="approverIds", description=( "If provided, replaces the current set of approvers. " "Each user must have the 'approver' role." @@ -88,7 +82,6 @@ class ApproverGroupUpdateIn(Schema): ) min_approvals: int | None = Field( None, - alias="minApprovals", ge=1, description="New minimum approval threshold.", ) @@ -102,7 +95,6 @@ class ApproverGroupListOut(Schema): class ApproverGroupAddApproverIn(Schema): approver_id: str = Field( ..., - alias="approverId", description="UUID of the user to add as an approver.", ) @@ -110,7 +102,6 @@ class ApproverGroupAddApproverIn(Schema): class ApproverGroupRemoveApproverIn(Schema): approver_id: str = Field( ..., - alias="approverId", description="UUID of the approver to remove.", ) @@ -133,13 +124,11 @@ class ReviewSettingsOut(ModelSchema): class ReviewSettingsUpdateIn(Schema): default_min_approvals: int | None = Field( None, - alias="defaultMinApprovals", ge=1, description="New default minimum approval threshold.", ) allow_any_approver: bool | None = Field( None, - alias="allowAnyApprover", description="New fallback policy for approver eligibility.", ) diff --git a/src/backend/api/v1/reviews/tests/test_reviews_api.py b/src/backend/api/v1/reviews/tests/test_reviews_api.py index efea0e3..4c2f0d1 100644 --- a/src/backend/api/v1/reviews/tests/test_reviews_api.py +++ b/src/backend/api/v1/reviews/tests/test_reviews_api.py @@ -1,34 +1,35 @@ import json import uuid +from typing import override from django.test import Client, TestCase from django.urls import reverse from apps.reviews.models import ApproverGroup from apps.reviews.services import approver_group_create, review_settings_update -from apps.reviews.tests._helpers import ( - _get, - _make_admin, - _make_approver, - _make_experimenter, - _make_viewer, +from apps.reviews.tests.helpers import ( + make_admin, + make_approver, + make_experimenter, + make_viewer, ) from apps.users.models import User -from apps.users.tests._helpers import _auth_header +from apps.users.tests.helpers import auth_header class ApproverGroupAPITest(TestCase): + @override def setUp(self) -> None: self.client = Client() - self.admin: User = _make_admin("_api") - self.viewer: User = _make_viewer("_api") - self.experimenter: User = _make_experimenter("_api") - self.approver1: User = _make_approver("_api1") - self.approver2: User = _make_approver("_api2") - self.approver3: User = _make_approver("_api3") - self.admin_auth: str = _auth_header(self.admin) - self.viewer_auth: str = _auth_header(self.viewer) - self.exp_auth: str = _auth_header(self.experimenter) + self.admin: User = make_admin("_api") + self.viewer: User = make_viewer("_api") + self.experimenter: User = make_experimenter("_api") + self.approver1: User = make_approver("_api1") + self.approver2: User = make_approver("_api2") + self.approver3: User = make_approver("_api3") + self.admin_auth: str = auth_header(self.admin) + self.viewer_auth: str = auth_header(self.viewer) + self.exp_auth: str = auth_header(self.experimenter) def test_list_groups_admin(self) -> None: approver_group_create( @@ -57,7 +58,7 @@ class ApproverGroupAPITest(TestCase): self.assertEqual(resp.status_code, 401) def test_list_groups_pagination(self) -> None: - exp2: User = _make_experimenter("_api2") + exp2: User = make_experimenter("_api2") approver_group_create(experimenter=self.experimenter) approver_group_create(experimenter=exp2) resp = self.client.get( @@ -74,12 +75,12 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(self.experimenter.pk), - "approverIds": [ + "experimenter_id": str(self.experimenter.pk), + "approver_ids": [ str(self.approver1.pk), str(self.approver2.pk), ], - "minApprovals": 2, + "min_approvals": 2, } ), content_type="application/json", @@ -87,7 +88,7 @@ class ApproverGroupAPITest(TestCase): ) self.assertEqual(resp.status_code, 201) data = resp.json() - self.assertEqual(_get(data, "minApprovals", "min_approvals"), 2) + self.assertEqual(data["min_approvals"], 2) self.assertEqual(len(data["approvers"]), 2) self.assertEqual(data["experimenter"]["id"], str(self.experimenter.pk)) @@ -96,8 +97,8 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(self.experimenter.pk), - "minApprovals": 1, + "experimenter_id": str(self.experimenter.pk), + "min_approvals": 1, } ), content_type="application/json", @@ -112,8 +113,8 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(self.experimenter.pk), - "minApprovals": 1, + "experimenter_id": str(self.experimenter.pk), + "min_approvals": 1, } ), content_type="application/json", @@ -126,8 +127,8 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(self.experimenter.pk), - "minApprovals": 1, + "experimenter_id": str(self.experimenter.pk), + "min_approvals": 1, } ), content_type="application/json", @@ -140,8 +141,8 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(uuid.uuid4()), - "minApprovals": 1, + "experimenter_id": str(uuid.uuid4()), + "min_approvals": 1, } ), content_type="application/json", @@ -154,8 +155,8 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(self.viewer.pk), - "minApprovals": 1, + "experimenter_id": str(self.viewer.pk), + "min_approvals": 1, } ), content_type="application/json", @@ -168,8 +169,8 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(self.experimenter.pk), - "minApprovals": 1, + "experimenter_id": str(self.experimenter.pk), + "min_approvals": 1, } ), content_type="application/json", @@ -179,8 +180,8 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(self.experimenter.pk), - "minApprovals": 1, + "experimenter_id": str(self.experimenter.pk), + "min_approvals": 1, } ), content_type="application/json", @@ -193,9 +194,9 @@ class ApproverGroupAPITest(TestCase): reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(self.experimenter.pk), - "approverIds": [str(self.viewer.pk)], - "minApprovals": 1, + "experimenter_id": str(self.experimenter.pk), + "approver_ids": [str(self.viewer.pk)], + "min_approvals": 1, } ), content_type="application/json", @@ -203,7 +204,7 @@ class ApproverGroupAPITest(TestCase): ) self.assertIn(resp.status_code, [422, 400]) - def test_get_group_admin(self) -> None: + def testget_group_admin(self) -> None: group: ApproverGroup = approver_group_create( experimenter=self.experimenter, approver_ids=[str(self.approver1.pk)], @@ -220,7 +221,7 @@ class ApproverGroupAPITest(TestCase): self.assertEqual(data["id"], str(group.pk)) self.assertEqual(len(data["approvers"]), 1) - def test_get_group_not_found(self) -> None: + def testget_group_not_found(self) -> None: resp = self.client.get( reverse( "api-1:get_approver_group", @@ -230,7 +231,7 @@ class ApproverGroupAPITest(TestCase): ) self.assertEqual(resp.status_code, 404) - def test_get_group_viewer_denied(self) -> None: + def testget_group_viewer_denied(self) -> None: group: ApproverGroup = approver_group_create( experimenter=self.experimenter ) @@ -256,11 +257,11 @@ class ApproverGroupAPITest(TestCase): ), data=json.dumps( { - "approverIds": [ + "approver_ids": [ str(self.approver1.pk), str(self.approver2.pk), ], - "minApprovals": 2, + "min_approvals": 2, } ), content_type="application/json", @@ -268,7 +269,7 @@ class ApproverGroupAPITest(TestCase): ) self.assertEqual(resp.status_code, 200) data = resp.json() - self.assertEqual(_get(data, "minApprovals", "min_approvals"), 2) + self.assertEqual(data["min_approvals"], 2) self.assertEqual(len(data["approvers"]), 2) def test_update_group_partial_min_approvals(self) -> None: @@ -285,13 +286,13 @@ class ApproverGroupAPITest(TestCase): "api-1:update_approver_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"minApprovals": 2}), + data=json.dumps({"min_approvals": 2}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) self.assertEqual(resp.status_code, 200) data = resp.json() - self.assertEqual(_get(data, "minApprovals", "min_approvals"), 2) + self.assertEqual(data["min_approvals"], 2) def test_update_group_not_found(self) -> None: resp = self.client.patch( @@ -299,7 +300,7 @@ class ApproverGroupAPITest(TestCase): "api-1:update_approver_group", kwargs={"group_id": str(uuid.uuid4())}, ), - data=json.dumps({"minApprovals": 1}), + data=json.dumps({"min_approvals": 1}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -314,7 +315,7 @@ class ApproverGroupAPITest(TestCase): "api-1:update_approver_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"minApprovals": 1}), + data=json.dumps({"min_approvals": 1}), content_type="application/json", HTTP_AUTHORIZATION=self.viewer_auth, ) @@ -368,7 +369,7 @@ class ApproverGroupAPITest(TestCase): "api-1:add_approver_to_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(self.approver2.pk)}), + data=json.dumps({"approver_id": str(self.approver2.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -381,7 +382,7 @@ class ApproverGroupAPITest(TestCase): "api-1:add_approver_to_group", kwargs={"group_id": str(uuid.uuid4())}, ), - data=json.dumps({"approverId": str(self.approver1.pk)}), + data=json.dumps({"approver_id": str(self.approver1.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -396,7 +397,7 @@ class ApproverGroupAPITest(TestCase): "api-1:add_approver_to_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(uuid.uuid4())}), + data=json.dumps({"approver_id": str(uuid.uuid4())}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -411,7 +412,7 @@ class ApproverGroupAPITest(TestCase): "api-1:add_approver_to_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(self.viewer.pk)}), + data=json.dumps({"approver_id": str(self.viewer.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -427,7 +428,7 @@ class ApproverGroupAPITest(TestCase): "api-1:add_approver_to_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(self.approver1.pk)}), + data=json.dumps({"approver_id": str(self.approver1.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -442,7 +443,7 @@ class ApproverGroupAPITest(TestCase): "api-1:add_approver_to_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(self.approver1.pk)}), + data=json.dumps({"approver_id": str(self.approver1.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.viewer_auth, ) @@ -462,7 +463,7 @@ class ApproverGroupAPITest(TestCase): "api-1:remove_approver_from_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(self.approver2.pk)}), + data=json.dumps({"approver_id": str(self.approver2.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -480,7 +481,7 @@ class ApproverGroupAPITest(TestCase): "api-1:remove_approver_from_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(self.approver1.pk)}), + data=json.dumps({"approver_id": str(self.approver1.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -496,7 +497,7 @@ class ApproverGroupAPITest(TestCase): "api-1:remove_approver_from_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(self.approver2.pk)}), + data=json.dumps({"approver_id": str(self.approver2.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -515,7 +516,7 @@ class ApproverGroupAPITest(TestCase): "api-1:remove_approver_from_group", kwargs={"group_id": str(group.pk)}, ), - data=json.dumps({"approverId": str(self.approver1.pk)}), + data=json.dumps({"approver_id": str(self.approver1.pk)}), content_type="application/json", HTTP_AUTHORIZATION=self.viewer_auth, ) @@ -523,14 +524,15 @@ class ApproverGroupAPITest(TestCase): class ApproverGroupByExperimenterAPITest(TestCase): + @override def setUp(self) -> None: self.client = Client() - self.admin: User = _make_admin("_byexp") - self.experimenter: User = _make_experimenter("_byexp") - self.approver: User = _make_approver("_byexp") - self.admin_auth: str = _auth_header(self.admin) + self.admin: User = make_admin("_byexp") + self.experimenter: User = make_experimenter("_byexp") + self.approver: User = make_approver("_byexp") + self.admin_auth: str = auth_header(self.admin) - def test_get_by_experimenter_found(self) -> None: + def testget_by_experimenter_found(self) -> None: group: ApproverGroup = approver_group_create( experimenter=self.experimenter, approver_ids=[str(self.approver.pk)], @@ -545,8 +547,8 @@ class ApproverGroupByExperimenterAPITest(TestCase): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json()["id"], str(group.pk)) - def test_get_by_experimenter_not_found(self) -> None: - exp2: User = _make_experimenter("_byexp2") + def testget_by_experimenter_not_found(self) -> None: + exp2: User = make_experimenter("_byexp2") resp = self.client.get( reverse( "api-1:get_approver_group_by_experimenter", @@ -556,7 +558,7 @@ class ApproverGroupByExperimenterAPITest(TestCase): ) self.assertEqual(resp.status_code, 404) - def test_get_by_experimenter_invalid_id(self) -> None: + def testget_by_experimenter_invalid_id(self) -> None: resp = self.client.get( reverse( "api-1:get_approver_group_by_experimenter", @@ -566,7 +568,7 @@ class ApproverGroupByExperimenterAPITest(TestCase): ) self.assertEqual(resp.status_code, 404) - def test_get_by_experimenter_unauthenticated(self) -> None: + def testget_by_experimenter_unauthenticated(self) -> None: resp = self.client.get( reverse( "api-1:get_approver_group_by_experimenter", @@ -577,30 +579,29 @@ class ApproverGroupByExperimenterAPITest(TestCase): class ReviewSettingsAPITest(TestCase): + @override def setUp(self) -> None: self.client = Client() - self.admin: User = _make_admin("_rs") - self.viewer: User = _make_viewer("_rs") - self.experimenter: User = _make_experimenter("_rs") - self.approver: User = _make_approver("_rs") - self.admin_auth: str = _auth_header(self.admin) - self.viewer_auth: str = _auth_header(self.viewer) - self.exp_auth: str = _auth_header(self.experimenter) - self.appr_auth: str = _auth_header(self.approver) + self.admin: User = make_admin("_rs") + self.viewer: User = make_viewer("_rs") + self.experimenter: User = make_experimenter("_rs") + self.approver: User = make_approver("_rs") + self.admin_auth: str = auth_header(self.admin) + self.viewer_auth: str = auth_header(self.viewer) + self.exp_auth: str = auth_header(self.experimenter) + self.appr_auth: str = auth_header(self.approver) - def test_get_settings_admin(self) -> None: + def testget_settings_admin(self) -> None: resp = self.client.get( reverse("api-1:get_review_settings"), HTTP_AUTHORIZATION=self.admin_auth, ) self.assertEqual(resp.status_code, 200) data = resp.json() - min_val = _get(data, "defaultMinApprovals", "default_min_approvals") - any_val = _get(data, "allowAnyApprover", "allow_any_approver") - self.assertEqual(min_val, 1) - self.assertFalse(any_val) + self.assertEqual(data["default_min_approvals"], 1) + self.assertFalse(data["allow_any_approver"]) - def test_get_settings_any_authenticated_role(self) -> None: + def testget_settings_any_authenticated_role(self) -> None: for auth in [self.viewer_auth, self.exp_auth, self.appr_auth]: resp = self.client.get( reverse("api-1:get_review_settings"), @@ -608,7 +609,7 @@ class ReviewSettingsAPITest(TestCase): ) self.assertEqual(resp.status_code, 200) - def test_get_settings_unauthenticated(self) -> None: + def testget_settings_unauthenticated(self) -> None: resp = self.client.get(reverse("api-1:get_review_settings")) self.assertEqual(resp.status_code, 401) @@ -616,34 +617,31 @@ class ReviewSettingsAPITest(TestCase): resp = self.client.put( reverse("api-1:update_review_settings"), data=json.dumps( - {"defaultMinApprovals": 3, "allowAnyApprover": False} + {"default_min_approvals": 3, "allow_any_approver": False} ), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) self.assertEqual(resp.status_code, 200) data = resp.json() - min_val = _get(data, "defaultMinApprovals", "default_min_approvals") - any_val = _get(data, "allowAnyApprover", "allow_any_approver") - self.assertEqual(min_val, 3) - self.assertFalse(any_val) + self.assertEqual(data["default_min_approvals"], 3) + self.assertFalse(data["allow_any_approver"]) def test_update_settings_partial(self) -> None: resp = self.client.put( reverse("api-1:update_review_settings"), - data=json.dumps({"defaultMinApprovals": 5}), + data=json.dumps({"default_min_approvals": 5}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) self.assertEqual(resp.status_code, 200) data = resp.json() - min_val = _get(data, "defaultMinApprovals", "default_min_approvals") - self.assertEqual(min_val, 5) + self.assertEqual(data["default_min_approvals"], 5) def test_update_settings_viewer_denied(self) -> None: resp = self.client.put( reverse("api-1:update_review_settings"), - data=json.dumps({"defaultMinApprovals": 2}), + data=json.dumps({"default_min_approvals": 2}), content_type="application/json", HTTP_AUTHORIZATION=self.viewer_auth, ) @@ -652,7 +650,7 @@ class ReviewSettingsAPITest(TestCase): def test_update_settings_experimenter_denied(self) -> None: resp = self.client.put( reverse("api-1:update_review_settings"), - data=json.dumps({"defaultMinApprovals": 2}), + data=json.dumps({"default_min_approvals": 2}), content_type="application/json", HTTP_AUTHORIZATION=self.exp_auth, ) @@ -661,7 +659,7 @@ class ReviewSettingsAPITest(TestCase): def test_update_settings_approver_denied(self) -> None: resp = self.client.put( reverse("api-1:update_review_settings"), - data=json.dumps({"defaultMinApprovals": 2}), + data=json.dumps({"default_min_approvals": 2}), content_type="application/json", HTTP_AUTHORIZATION=self.appr_auth, ) @@ -670,7 +668,7 @@ class ReviewSettingsAPITest(TestCase): def test_update_settings_invalid_min_approvals(self) -> None: resp = self.client.put( reverse("api-1:update_review_settings"), - data=json.dumps({"defaultMinApprovals": 0}), + data=json.dumps({"default_min_approvals": 0}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -678,15 +676,16 @@ class ReviewSettingsAPITest(TestCase): class EffectivePolicyAPITest(TestCase): + @override def setUp(self) -> None: self.client = Client() - self.admin: User = _make_admin("_pol") - self.experimenter: User = _make_experimenter("_pol") - self.exp_no_group: User = _make_experimenter("_pol2") - self.approver: User = _make_approver("_pol") - self.admin_auth: str = _auth_header(self.admin) - self.viewer: User = _make_viewer("_pol") - self.viewer_auth: str = _auth_header(self.viewer) + self.admin: User = make_admin("_pol") + self.experimenter: User = make_experimenter("_pol") + self.exp_no_group: User = make_experimenter("_pol2") + self.approver: User = make_approver("_pol") + self.admin_auth: str = auth_header(self.admin) + self.viewer: User = make_viewer("_pol") + self.viewer_auth: str = auth_header(self.viewer) self.group: ApproverGroup = approver_group_create( experimenter=self.experimenter, @@ -704,13 +703,10 @@ class EffectivePolicyAPITest(TestCase): ) self.assertEqual(resp.status_code, 200) data = resp.json() - exp_id = _get(data, "experimenterId", "experimenter_id") - min_val = _get(data, "minApprovals", "min_approvals") - has_group = _get(data, "hasExplicitGroup", "has_explicit_group") - self.assertEqual(exp_id, str(self.experimenter.pk)) - self.assertEqual(min_val, 1) + self.assertEqual(data["experimenter_id"], str(self.experimenter.pk)) + self.assertEqual(data["min_approvals"], 1) self.assertEqual(data["source"], "approver_group") - self.assertTrue(has_group) + self.assertTrue(data["has_explicit_group"]) self.assertEqual(len(data["approvers"]), 1) self.assertEqual(data["approvers"][0]["id"], str(self.approver.pk)) @@ -727,11 +723,9 @@ class EffectivePolicyAPITest(TestCase): ) self.assertEqual(resp.status_code, 200) data = resp.json() - min_val = _get(data, "minApprovals", "min_approvals") - has_group = _get(data, "hasExplicitGroup", "has_explicit_group") self.assertEqual(data["source"], "global_fallback") - self.assertFalse(has_group) - self.assertEqual(min_val, 2) + self.assertFalse(data["has_explicit_group"]) + self.assertEqual(data["min_approvals"], 2) def test_effective_policy_fallback_deny(self) -> None: review_settings_update(allow_any_approver=False) diff --git a/src/backend/api/v1/reviews/tests/test_reviews_rbac_edge.py b/src/backend/api/v1/reviews/tests/test_reviews_rbac_edge.py index a4387b7..2cff8d0 100644 --- a/src/backend/api/v1/reviews/tests/test_reviews_rbac_edge.py +++ b/src/backend/api/v1/reviews/tests/test_reviews_rbac_edge.py @@ -1,4 +1,5 @@ import json +from typing import override from django.core.exceptions import ValidationError from django.test import Client, TestCase @@ -15,27 +16,28 @@ from apps.reviews.services import ( approver_group_update, review_settings_update, ) -from apps.reviews.tests._helpers import ( - _make_admin, - _make_approver, - _make_experimenter, - _make_viewer, +from apps.reviews.tests.helpers import ( + make_admin, + make_approver, + make_experimenter, + make_viewer, ) from apps.users.models import User -from apps.users.tests._helpers import _auth_header +from apps.users.tests.helpers import auth_header class ReviewRBACTest(TestCase): + @override def setUp(self) -> None: self.client = Client() - self.admin: User = _make_admin("_rbac") - self.experimenter: User = _make_experimenter("_rbac") - self.approver: User = _make_approver("_rbac") - self.viewer: User = _make_viewer("_rbac") + self.admin: User = make_admin("_rbac") + self.experimenter: User = make_experimenter("_rbac") + self.approver: User = make_approver("_rbac") + self.viewer: User = make_viewer("_rbac") self.non_admin_users: dict[str, str] = { - "experimenter": _auth_header(self.experimenter), - "approver": _auth_header(self.approver), - "viewer": _auth_header(self.viewer), + "experimenter": auth_header(self.experimenter), + "approver": auth_header(self.approver), + "viewer": auth_header(self.viewer), } self.group: ApproverGroup = approver_group_create( experimenter=self.experimenter, @@ -52,13 +54,13 @@ class ReviewRBACTest(TestCase): def test_create_group_denied_for_non_admins(self) -> None: for role_name, auth in self.non_admin_users.items(): - exp2: User = _make_experimenter(f"_rbac_cr_{role_name}") + exp2: User = make_experimenter(f"_rbac_cr_{role_name}") resp = self.client.post( reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(exp2.pk), - "minApprovals": 1, + "experimenter_id": str(exp2.pk), + "min_approvals": 1, } ), content_type="application/json", @@ -84,7 +86,7 @@ class ReviewRBACTest(TestCase): "api-1:update_approver_group", kwargs={"group_id": str(self.group.pk)}, ), - data=json.dumps({"minApprovals": 1}), + data=json.dumps({"min_approvals": 1}), content_type="application/json", HTTP_AUTHORIZATION=auth, ) @@ -102,14 +104,14 @@ class ReviewRBACTest(TestCase): self.assertEqual(resp.status_code, 403, role_name) def test_add_approver_denied_for_non_admins(self) -> None: - appr2: User = _make_approver("_rbac_add") + appr2: User = make_approver("_rbac_add") for role_name, auth in self.non_admin_users.items(): resp = self.client.post( reverse( "api-1:add_approver_to_group", kwargs={"group_id": str(self.group.pk)}, ), - data=json.dumps({"approverId": str(appr2.pk)}), + data=json.dumps({"approver_id": str(appr2.pk)}), content_type="application/json", HTTP_AUTHORIZATION=auth, ) @@ -122,7 +124,7 @@ class ReviewRBACTest(TestCase): "api-1:remove_approver_from_group", kwargs={"group_id": str(self.group.pk)}, ), - data=json.dumps({"approverId": str(self.approver.pk)}), + data=json.dumps({"approver_id": str(self.approver.pk)}), content_type="application/json", HTTP_AUTHORIZATION=auth, ) @@ -132,7 +134,7 @@ class ReviewRBACTest(TestCase): for role_name, auth in self.non_admin_users.items(): resp = self.client.put( reverse("api-1:update_review_settings"), - data=json.dumps({"defaultMinApprovals": 99}), + data=json.dumps({"default_min_approvals": 99}), content_type="application/json", HTTP_AUTHORIZATION=auth, ) @@ -140,14 +142,15 @@ class ReviewRBACTest(TestCase): class ReviewEdgeCasesTest(TestCase): + @override def setUp(self) -> None: self.client = Client() - self.admin: User = _make_admin("_edge") - self.admin_auth: str = _auth_header(self.admin) + self.admin: User = make_admin("_edge") + self.admin_auth: str = auth_header(self.admin) def test_delete_group_then_fallback_applies(self) -> None: - exp: User = _make_experimenter("_edge1") - appr: User = _make_approver("_edge1") + exp: User = make_experimenter("_edge1") + appr: User = make_approver("_edge1") group: ApproverGroup = approver_group_create( experimenter=exp, approver_ids=[str(appr.pk)], @@ -162,8 +165,8 @@ class ReviewEdgeCasesTest(TestCase): self.assertEqual(min_app, 3) def test_inactive_approver_excluded_from_effective_policy(self) -> None: - exp: User = _make_experimenter("_edge2") - appr: User = _make_approver("_edge2") + exp: User = make_experimenter("_edge2") + appr: User = make_approver("_edge2") approver_group_create( experimenter=exp, approver_ids=[str(appr.pk)], @@ -174,8 +177,8 @@ class ReviewEdgeCasesTest(TestCase): self.assertEqual(approvers.count(), 0) def test_create_group_with_all_three_approvers(self) -> None: - exp: User = _make_experimenter("_edge3") - apprs: list[User] = [_make_approver(f"_edge3_{i}") for i in range(3)] + exp: User = make_experimenter("_edge3") + apprs: list[User] = [make_approver(f"_edge3_{i}") for i in range(3)] group: ApproverGroup = approver_group_create( experimenter=exp, approver_ids=[str(a.pk) for a in apprs], @@ -185,8 +188,8 @@ class ReviewEdgeCasesTest(TestCase): self.assertEqual(group.min_approvals, 2) def test_update_group_to_empty_approvers(self) -> None: - exp: User = _make_experimenter("_edge4") - appr: User = _make_approver("_edge4") + exp: User = make_experimenter("_edge4") + appr: User = make_approver("_edge4") group: ApproverGroup = approver_group_create( experimenter=exp, approver_ids=[str(appr.pk)], @@ -198,15 +201,15 @@ class ReviewEdgeCasesTest(TestCase): ) def test_api_output_format_approver_group(self) -> None: - exp: User = _make_experimenter("_edge5") - appr: User = _make_approver("_edge5") + exp: User = make_experimenter("_edge5") + appr: User = make_approver("_edge5") resp = self.client.post( reverse("api-1:create_approver_group"), data=json.dumps( { - "experimenterId": str(exp.pk), - "approverIds": [str(appr.pk)], - "minApprovals": 1, + "experimenter_id": str(exp.pk), + "approver_ids": [str(appr.pk)], + "min_approvals": 1, } ), content_type="application/json", @@ -217,9 +220,9 @@ class ReviewEdgeCasesTest(TestCase): self.assertIn("id", data) self.assertIn("experimenter", data) self.assertIn("approvers", data) - self.assertTrue("minApprovals" in data or "min_approvals" in data) - self.assertTrue("createdAt" in data or "created_at" in data) - self.assertTrue("updatedAt" in data or "updated_at" in data) + self.assertIn("min_approvals", data) + self.assertIn("created_at", data) + self.assertIn("updated_at", data) self.assertIn("id", data["experimenter"]) self.assertIn("username", data["experimenter"]) self.assertIn("email", data["experimenter"]) @@ -237,17 +240,13 @@ class ReviewEdgeCasesTest(TestCase): self.assertEqual(resp.status_code, 200) data = resp.json() self.assertIn("id", data) - self.assertTrue( - "defaultMinApprovals" in data or "default_min_approvals" in data - ) - self.assertTrue( - "allowAnyApprover" in data or "allow_any_approver" in data - ) - self.assertTrue("updatedAt" in data or "updated_at" in data) + self.assertIn("default_min_approvals", data) + self.assertIn("allow_any_approver", data) + self.assertIn("updated_at", data) def test_api_output_format_effective_policy(self) -> None: - exp: User = _make_experimenter("_edge6") - appr: User = _make_approver("_edge6") + exp: User = make_experimenter("_edge6") + appr: User = make_approver("_edge6") approver_group_create( experimenter=exp, approver_ids=[str(appr.pk)], @@ -261,19 +260,17 @@ class ReviewEdgeCasesTest(TestCase): ) self.assertEqual(resp.status_code, 200) data = resp.json() - self.assertTrue("experimenterId" in data or "experimenter_id" in data) - self.assertTrue("minApprovals" in data or "min_approvals" in data) + self.assertIn("experimenter_id", data) + self.assertIn("min_approvals", data) self.assertIn("approvers", data) self.assertIn("source", data) - self.assertTrue( - "hasExplicitGroup" in data or "has_explicit_group" in data - ) + self.assertIn("has_explicit_group", data) def test_multiple_experimenters_independent_groups(self) -> None: - exp1: User = _make_experimenter("_edge_m1") - exp2: User = _make_experimenter("_edge_m2") - appr1: User = _make_approver("_edge_m1") - appr2: User = _make_approver("_edge_m2") + exp1: User = make_experimenter("_edge_m1") + exp2: User = make_experimenter("_edge_m2") + appr1: User = make_approver("_edge_m1") + appr2: User = make_approver("_edge_m2") g1: ApproverGroup = approver_group_create( experimenter=exp1, @@ -293,9 +290,9 @@ class ReviewEdgeCasesTest(TestCase): self.assertFalse(g2.can_approve(appr1)) def test_concurrent_fallback_and_explicit_group(self) -> None: - exp_with: User = _make_experimenter("_edge_c1") - exp_without: User = _make_experimenter("_edge_c2") - appr: User = _make_approver("_edge_c") + exp_with: User = make_experimenter("_edge_c1") + exp_without: User = make_experimenter("_edge_c2") + appr: User = make_approver("_edge_c") approver_group_create( experimenter=exp_with, diff --git a/src/backend/api/v1/router.py b/src/backend/api/v1/router.py index b17edac..452fb95 100644 --- a/src/backend/api/v1/router.py +++ b/src/backend/api/v1/router.py @@ -8,6 +8,7 @@ from ninja.renderers import BaseRenderer from api.v1 import handlers from api.v1.auth.endpoints import router as auth_router +from api.v1.flags.endpoints import router as flags_router from api.v1.reviews.endpoints import router as reviews_router from api.v1.users.endpoints import router as users_router @@ -23,7 +24,7 @@ class ORJSONRenderer(BaseRenderer): def default(self, obj: Any) -> Any: if isinstance(obj, Schema): - return obj.model_dump(by_alias=True) + return obj.model_dump() raise TypeError @@ -41,6 +42,11 @@ router.add_router( auth_router, ) +router.add_router( + "flags", + flags_router, +) + router.add_router( "users", users_router, diff --git a/src/backend/api/v1/schemas.py b/src/backend/api/v1/schemas.py index ba4812d..0f3b0a8 100644 --- a/src/backend/api/v1/schemas.py +++ b/src/backend/api/v1/schemas.py @@ -12,18 +12,18 @@ class FieldError(Schema): ) issue: str = Field(..., description="Problem description") rejected_value: Any = Field( - None, alias="rejectedValue", description="Value that failed validation" + None, description="Value that failed validation" ) class ApiError(Schema): code: str message: str - trace_id: str = Field(..., alias="traceId") + trace_id: str timestamp: datetime path: str details: dict[str, Any] | None = None class ValidationError(ApiError): - field_errors: list[FieldError] = Field(..., alias="fieldErrors") + field_errors: list[FieldError] diff --git a/src/backend/api/v1/users/endpoints.py b/src/backend/api/v1/users/endpoints.py index 164c2af..56ff5a4 100644 --- a/src/backend/api/v1/users/endpoints.py +++ b/src/backend/api/v1/users/endpoints.py @@ -38,7 +38,7 @@ router = Router(tags=["users"]) def list_users( request: HttpRequest, role: str | None = None, - is_active: bool | None = None, + is_active: bool | None = None, # noqa: FBT001 search: str | None = None, limit: int = 50, offset: int = 0, diff --git a/src/backend/api/v1/users/schemas.py b/src/backend/api/v1/users/schemas.py index e69648d..0822054 100644 --- a/src/backend/api/v1/users/schemas.py +++ b/src/backend/api/v1/users/schemas.py @@ -7,8 +7,7 @@ from apps.users.models import User class UserOut(ModelSchema): - first_name: str = Field("", alias="firstName") - last_name: str = Field("", alias="lastName") + is_active: bool class Meta: @@ -52,13 +51,11 @@ class UserCreateIn(Schema): ) first_name: str = Field( "", - alias="firstName", max_length=150, description="First name.", ) last_name: str = Field( "", - alias="lastName", max_length=150, description="Last name.", ) @@ -91,19 +88,16 @@ class UserUpdateIn(Schema): ) first_name: str | None = Field( None, - alias="firstName", max_length=150, description="New first name.", ) last_name: str | None = Field( None, - alias="lastName", max_length=150, description="New last name.", ) is_active: bool | None = Field( None, - alias="isActive", description="Set active/inactive status.", ) diff --git a/src/backend/api/v1/users/tests/_crud_base.py b/src/backend/api/v1/users/tests/_crud_base.py index 9a5a959..2972090 100644 --- a/src/backend/api/v1/users/tests/_crud_base.py +++ b/src/backend/api/v1/users/tests/_crud_base.py @@ -1,23 +1,26 @@ +from typing import override + from django.test import Client, TestCase from apps.users.models import User, UserRole -from apps.users.tests._helpers import _auth_header, _make_user +from apps.users.tests.helpers import auth_header, make_user class BaseUsersAPITest(TestCase): + @override def setUp(self) -> None: self.client = Client() - self.admin: User = _make_user( + self.admin: User = make_user( username="mgmt_admin", email="mgmt_admin@x.com", password="adminpass1", role=UserRole.ADMIN, ) - self.viewer: User = _make_user( + self.viewer: User = make_user( username="mgmt_viewer", email="mgmt_viewer@x.com", password="viewerpass", role=UserRole.VIEWER, ) - self.admin_auth: str = _auth_header(self.admin) - self.viewer_auth: str = _auth_header(self.viewer) + self.admin_auth: str = auth_header(self.admin) + self.viewer_auth: str = auth_header(self.viewer) diff --git a/src/backend/api/v1/users/tests/test_crud_delete_assign_role.py b/src/backend/api/v1/users/tests/test_crud_delete_assign_role.py index 0865410..a7189f1 100644 --- a/src/backend/api/v1/users/tests/test_crud_delete_assign_role.py +++ b/src/backend/api/v1/users/tests/test_crud_delete_assign_role.py @@ -4,14 +4,14 @@ import uuid from django.urls import reverse from apps.users.models import User, UserRole -from apps.users.tests._helpers import _make_user +from apps.users.tests.helpers import make_user from ._crud_base import BaseUsersAPITest class UsersAPIDeleteAssignRoleTest(BaseUsersAPITest): def test_delete_user_admin(self) -> None: - target: User = _make_user( + target: User = make_user( username="to_delete", email="del@lotty.local", role=UserRole.VIEWER, diff --git a/src/backend/api/v1/users/tests/test_crud_list_create.py b/src/backend/api/v1/users/tests/test_crud_list_create.py index 5206f26..05aea0a 100644 --- a/src/backend/api/v1/users/tests/test_crud_list_create.py +++ b/src/backend/api/v1/users/tests/test_crud_list_create.py @@ -66,8 +66,8 @@ class UsersAPIListCreateTest(BaseUsersAPITest): "email": "new@lotty.local", "password": "newpass123", "role": "experimenter", - "firstName": "New", - "lastName": "User", + "first_name": "New", + "last_name": "User", } ), content_type="application/json", diff --git a/src/backend/api/v1/users/tests/test_crud_read_update.py b/src/backend/api/v1/users/tests/test_crud_read_update.py index a8f8092..0cfcd76 100644 --- a/src/backend/api/v1/users/tests/test_crud_read_update.py +++ b/src/backend/api/v1/users/tests/test_crud_read_update.py @@ -51,7 +51,7 @@ class UsersAPIReadUpdateTest(BaseUsersAPITest): reverse( "api-1:update_user", kwargs={"user_id": str(self.viewer.pk)} ), - data=json.dumps({"firstName": "Updated"}), + data=json.dumps({"first_name": "Updated"}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) @@ -75,7 +75,7 @@ class UsersAPIReadUpdateTest(BaseUsersAPITest): reverse( "api-1:update_user", kwargs={"user_id": str(self.admin.pk)} ), - data=json.dumps({"firstName": "Hacked"}), + data=json.dumps({"first_name": "Hacked"}), content_type="application/json", HTTP_AUTHORIZATION=self.viewer_auth, ) @@ -86,7 +86,7 @@ class UsersAPIReadUpdateTest(BaseUsersAPITest): reverse( "api-1:update_user", kwargs={"user_id": str(uuid.uuid4())} ), - data=json.dumps({"firstName": "Ghost"}), + data=json.dumps({"first_name": "Ghost"}), content_type="application/json", HTTP_AUTHORIZATION=self.admin_auth, ) diff --git a/src/backend/api/v1/users/tests/test_roles.py b/src/backend/api/v1/users/tests/test_roles.py index 0f36cc0..a09cc9a 100644 --- a/src/backend/api/v1/users/tests/test_roles.py +++ b/src/backend/api/v1/users/tests/test_roles.py @@ -1,13 +1,15 @@ import json +from typing import override from django.test import Client, TestCase from django.urls import reverse from apps.users.models import User, UserRole -from apps.users.tests._helpers import _auth_header, _make_user +from apps.users.tests.helpers import auth_header, make_user class RoleBasedAccessControlTest(TestCase): + @override def setUp(self) -> None: self.client = Client() self.roles = {} @@ -17,7 +19,7 @@ class RoleBasedAccessControlTest(TestCase): UserRole.APPROVER, UserRole.VIEWER, ]: - user: User = _make_user( + user: User = make_user( username=f"rbac_{role_val}", email=f"rbac_{role_val}@x.com", password="password1", @@ -25,7 +27,7 @@ class RoleBasedAccessControlTest(TestCase): ) self.roles[role_val] = { "user": user, - "auth": _auth_header(user), + "auth": auth_header(user), } def test_admin_can_list(self) -> None: @@ -93,7 +95,7 @@ class RoleBasedAccessControlTest(TestCase): target = self.roles[UserRole.VIEWER]["user"] resp = self.client.patch( reverse("api-1:update_user", kwargs={"user_id": str(target.pk)}), - data=json.dumps({"firstName": "Nope"}), + data=json.dumps({"first_name": "Nope"}), content_type="application/json", HTTP_AUTHORIZATION=self.roles[UserRole.EXPERIMENTER]["auth"], ) diff --git a/src/backend/apps/reviews/admin.py b/src/backend/apps/reviews/admin.py index ec20f66..c8313ff 100644 --- a/src/backend/apps/reviews/admin.py +++ b/src/backend/apps/reviews/admin.py @@ -1,3 +1,5 @@ +from typing import override + from django.contrib import admin from django.utils.translation import gettext_lazy as _ @@ -34,11 +36,13 @@ class ReviewSettingsAdmin(admin.ModelAdmin): ), ) + @override def has_add_permission(self, request) -> bool: if ReviewSettings.objects.exists(): return False return super().has_add_permission(request) + @override def has_delete_permission(self, request, obj=None) -> bool: return False diff --git a/src/backend/apps/reviews/migrations/0002_alter_reviewsettings_allow_any_approver.py b/src/backend/apps/reviews/migrations/0002_alter_reviewsettings_allow_any_approver.py new file mode 100644 index 0000000..b25406d --- /dev/null +++ b/src/backend/apps/reviews/migrations/0002_alter_reviewsettings_allow_any_approver.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.11 on 2026-02-12 18:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reviews', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='reviewsettings', + name='allow_any_approver', + field=models.BooleanField(default=False, help_text='When True, any user with the Approver role can approve experiments that have no explicit approver group. When False, experiments without an approver group cannot proceed to review.', verbose_name='allow any approver'), + ), + ] diff --git a/src/backend/apps/reviews/models.py b/src/backend/apps/reviews/models.py index 71c1729..68f74e9 100644 --- a/src/backend/apps/reviews/models.py +++ b/src/backend/apps/reviews/models.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, override from django.conf import settings from django.core.validators import MinValueValidator @@ -40,12 +40,14 @@ class ReviewSettings(BaseModel): verbose_name = _("review settings") verbose_name_plural = _("review settings") + @override def __str__(self) -> str: return ( f"ReviewSettings(min_approvals={self.default_min_approvals}, " f"allow_any_approver={self.allow_any_approver})" ) + @override def save(self, *args, **kwargs) -> None: existing: ReviewSettings | None = ReviewSettings.objects.first() if existing and existing.pk != self.pk: @@ -77,7 +79,8 @@ class ApproverGroup(BaseModel): limit_choices_to={"role": "approver"}, verbose_name=_("approvers"), help_text=_( - "Approver-role users who may approve this experimenter's experiments." + "Approver-role users who may approve this " + "experimenter's experiments." ), ) min_approvals: models.PositiveIntegerField[Any, Any] = ( @@ -104,6 +107,7 @@ class ApproverGroup(BaseModel): verbose_name = _("approver group") verbose_name_plural = _("approver groups") + @override def __str__(self) -> str: return ( f"ApproverGroup(experimenter={self.experimenter.pk}, " diff --git a/src/backend/apps/reviews/tests/_helpers.py b/src/backend/apps/reviews/tests/helpers.py similarity index 55% rename from src/backend/apps/reviews/tests/_helpers.py rename to src/backend/apps/reviews/tests/helpers.py index c7b5f09..2dede6d 100644 --- a/src/backend/apps/reviews/tests/_helpers.py +++ b/src/backend/apps/reviews/tests/helpers.py @@ -1,40 +1,34 @@ from apps.users.models import User, UserRole -from apps.users.tests._helpers import _make_user +from apps.users.tests.helpers import make_user -def _make_experimenter(suffix="") -> User: - return _make_user( +def make_experimenter(suffix="") -> User: + return make_user( username=f"exp{suffix}", email=f"exp{suffix}@lotty.local", role=UserRole.EXPERIMENTER, ) -def _make_approver(suffix="") -> User: - return _make_user( +def make_approver(suffix="") -> User: + return make_user( username=f"appr{suffix}", email=f"appr{suffix}@lotty.local", role=UserRole.APPROVER, ) -def _make_admin(suffix="") -> User: - return _make_user( +def make_admin(suffix="") -> User: + return make_user( username=f"admin{suffix}", email=f"admin{suffix}@lotty.local", role=UserRole.ADMIN, ) -def _make_viewer(suffix="") -> User: - return _make_user( +def make_viewer(suffix="") -> User: + return make_user( username=f"viewer{suffix}", email=f"viewer{suffix}@lotty.local", role=UserRole.VIEWER, ) - - -def _get(data, camel_key, snake_key): - if camel_key in data: - return data[camel_key] - return data[snake_key] diff --git a/src/backend/apps/reviews/tests/test_reviews_approver_groups.py b/src/backend/apps/reviews/tests/test_reviews_approver_groups.py index 933c016..36e1e9f 100644 --- a/src/backend/apps/reviews/tests/test_reviews_approver_groups.py +++ b/src/backend/apps/reviews/tests/test_reviews_approver_groups.py @@ -1,5 +1,5 @@ import uuid -from typing import Any +from typing import Any, override from django.core.exceptions import ValidationError from django.db.models import QuerySet @@ -22,18 +22,19 @@ from apps.reviews.services import ( ) from apps.users.models import User -from ._helpers import ( - _make_admin, - _make_approver, - _make_experimenter, - _make_viewer, +from .helpers import ( + make_admin, + make_approver, + make_experimenter, + make_viewer, ) class ApproverGroupModelTest(TestCase): + @override def setUp(self) -> None: - self.experimenter: User = _make_experimenter("_model") - self.approver: User = _make_approver("_model") + self.experimenter: User = make_experimenter("_model") + self.approver: User = make_approver("_model") def test_create_group(self) -> None: group: ApproverGroup = approver_group_create( @@ -63,7 +64,7 @@ class ApproverGroupModelTest(TestCase): self.assertTrue(group.can_approve(self.approver)) def test_can_approve_false_not_in_group(self) -> None: - other_approver: User = _make_approver("_other") + other_approver: User = make_approver("_other") group: ApproverGroup = approver_group_create( experimenter=self.experimenter, approver_ids=[str(self.approver.pk)], @@ -80,10 +81,11 @@ class ApproverGroupModelTest(TestCase): class ApproverGroupCreateServiceTest(TestCase): + @override def setUp(self) -> None: - self.experimenter: User = _make_experimenter("_create") - self.approver1: User = _make_approver("_create1") - self.approver2: User = _make_approver("_create2") + self.experimenter: User = make_experimenter("_create") + self.approver1: User = make_approver("_create1") + self.approver2: User = make_approver("_create2") def test_create_with_approvers(self) -> None: group: ApproverGroup = approver_group_create( @@ -109,17 +111,17 @@ class ApproverGroupCreateServiceTest(TestCase): self.assertEqual(group.min_approvals, 1) def test_create_rejects_non_experimenter_user(self) -> None: - admin: User = _make_admin("_cre_admin") + admin: User = make_admin("_cre_admin") with self.assertRaises(ValidationError): approver_group_create(experimenter=admin) def test_create_rejects_viewer_as_experimenter(self) -> None: - viewer: User = _make_viewer("_cre_viewer") + viewer: User = make_viewer("_cre_viewer") with self.assertRaises(ValidationError): approver_group_create(experimenter=viewer) def test_create_rejects_approver_as_experimenter(self) -> None: - approver: User = _make_approver("_cre_as_exp") + approver: User = make_approver("_cre_as_exp") with self.assertRaises(ValidationError): approver_group_create(experimenter=approver) @@ -130,7 +132,7 @@ class ApproverGroupCreateServiceTest(TestCase): approver_group_create(experimenter=self.experimenter) def test_create_rejects_non_approver_in_approver_list(self) -> None: - viewer: User = _make_viewer("_cre_bad_appr") + viewer: User = make_viewer("_cre_bad_appr") with self.assertRaises(ValidationError): approver_group_create( experimenter=self.experimenter, @@ -166,11 +168,12 @@ class ApproverGroupCreateServiceTest(TestCase): class ApproverGroupUpdateServiceTest(TestCase): + @override def setUp(self) -> None: - self.experimenter: User = _make_experimenter("_upd") - self.approver1: User = _make_approver("_upd1") - self.approver2: User = _make_approver("_upd2") - self.approver3: User = _make_approver("_upd3") + self.experimenter: User = make_experimenter("_upd") + self.approver1: User = make_approver("_upd1") + self.approver2: User = make_approver("_upd2") + self.approver3: User = make_approver("_upd3") self.group: ApproverGroup = approver_group_create( experimenter=self.experimenter, approver_ids=[str(self.approver1.pk), str(self.approver2.pk)], @@ -225,7 +228,7 @@ class ApproverGroupUpdateServiceTest(TestCase): approver_group_update(group=self.group, min_approvals=10) def test_update_rejects_non_approver_role(self) -> None: - viewer: User = _make_viewer("_upd_bad") + viewer: User = make_viewer("_upd_bad") with self.assertRaises(ValidationError): approver_group_update( group=self.group, @@ -242,14 +245,14 @@ class ApproverGroupUpdateServiceTest(TestCase): class ApproverGroupDeleteServiceTest(TestCase): def test_delete_removes_group(self) -> None: - exp: User = _make_experimenter("_del") + exp: User = make_experimenter("_del") group: ApproverGroup = approver_group_create(experimenter=exp) pk = group.pk approver_group_delete(group=group) self.assertFalse(ApproverGroup.objects.filter(pk=pk).exists()) def test_delete_allows_recreating_group(self) -> None: - exp: User = _make_experimenter("_del2") + exp: User = make_experimenter("_del2") group: ApproverGroup = approver_group_create(experimenter=exp) approver_group_delete(group=group) new_group: ApproverGroup = approver_group_create(experimenter=exp) @@ -257,10 +260,11 @@ class ApproverGroupDeleteServiceTest(TestCase): class ApproverGroupAddRemoveServiceTest(TestCase): + @override def setUp(self) -> None: - self.experimenter: User = _make_experimenter("_ar") - self.approver1: User = _make_approver("_ar1") - self.approver2: User = _make_approver("_ar2") + self.experimenter: User = make_experimenter("_ar") + self.approver1: User = make_approver("_ar1") + self.approver2: User = make_approver("_ar2") self.group: ApproverGroup = approver_group_create( experimenter=self.experimenter, approver_ids=[str(self.approver1.pk)], @@ -275,7 +279,7 @@ class ApproverGroupAddRemoveServiceTest(TestCase): ) def test_add_approver_rejects_non_approver_role(self) -> None: - viewer: User = _make_viewer("_ar_bad") + viewer: User = make_viewer("_ar_bad") with self.assertRaises(ValidationError): approver_group_add_approver(group=self.group, approver=viewer) @@ -309,11 +313,12 @@ class ApproverGroupAddRemoveServiceTest(TestCase): class ApproverGroupSelectorsTest(TestCase): + @override def setUp(self) -> None: - self.exp1: User = _make_experimenter("_sel1") - self.exp2: User = _make_experimenter("_sel2") - self.appr1: User = _make_approver("_sel1") - self.appr2: User = _make_approver("_sel2") + self.exp1: User = make_experimenter("_sel1") + self.exp2: User = make_experimenter("_sel2") + self.appr1: User = make_approver("_sel1") + self.appr2: User = make_approver("_sel2") self.group1: ApproverGroup = approver_group_create( experimenter=self.exp1, approver_ids=[str(self.appr1.pk)], @@ -344,7 +349,7 @@ class ApproverGroupSelectorsTest(TestCase): self.assertEqual(found, self.group1) def test_get_by_experimenter_no_group(self) -> None: - exp3: User = _make_experimenter("_sel3") + exp3: User = make_experimenter("_sel3") self.assertIsNone(approver_group_get_by_experimenter(exp3)) def test_get_by_experimenter_id(self) -> None: diff --git a/src/backend/apps/reviews/tests/test_reviews_policy.py b/src/backend/apps/reviews/tests/test_reviews_policy.py index 30e11a5..e6cff47 100644 --- a/src/backend/apps/reviews/tests/test_reviews_policy.py +++ b/src/backend/apps/reviews/tests/test_reviews_policy.py @@ -1,3 +1,5 @@ +from typing import override + from django.test import TestCase from apps.reviews.models import ApproverGroup @@ -9,15 +11,16 @@ from apps.reviews.selectors import ( from apps.reviews.services import approver_group_create, review_settings_update from apps.users.models import User -from ._helpers import _make_admin, _make_approver, _make_experimenter +from .helpers import make_admin, make_approver, make_experimenter class EffectiveReviewPolicyTest(TestCase): + @override def setUp(self) -> None: - self.exp_with_group: User = _make_experimenter("_eff1") - self.exp_without_group: User = _make_experimenter("_eff2") - self.appr1: User = _make_approver("_eff1") - self.appr2: User = _make_approver("_eff2") + self.exp_with_group: User = make_experimenter("_eff1") + self.exp_without_group: User = make_experimenter("_eff2") + self.appr1: User = make_approver("_eff1") + self.appr2: User = make_approver("_eff2") self.group: ApproverGroup = approver_group_create( experimenter=self.exp_with_group, approver_ids=[str(self.appr1.pk)], @@ -63,10 +66,11 @@ class EffectiveReviewPolicyTest(TestCase): class CanUserApproveTest(TestCase): + @override def setUp(self) -> None: - self.exp: User = _make_experimenter("_can") - self.appr_in: User = _make_approver("_can_in") - self.appr_out: User = _make_approver("_can_out") + self.exp: User = make_experimenter("_can") + self.appr_in: User = make_approver("_can_in") + self.appr_out: User = make_approver("_can_out") self.group: ApproverGroup = approver_group_create( experimenter=self.exp, approver_ids=[str(self.appr_in.pk)], @@ -81,7 +85,7 @@ class CanUserApproveTest(TestCase): ) def test_non_approver_role_cannot_approve(self) -> None: - admin: User = _make_admin("_can_a") + admin: User = make_admin("_can_a") self.assertFalse(can_user_approve_experimenter(admin, self.exp)) def test_inactive_approver_cannot_approve(self) -> None: @@ -90,11 +94,11 @@ class CanUserApproveTest(TestCase): self.assertFalse(can_user_approve_experimenter(self.appr_in, self.exp)) def test_fallback_any_approver_can_approve(self) -> None: - exp2: User = _make_experimenter("_can2") + exp2: User = make_experimenter("_can2") review_settings_update(allow_any_approver=True) self.assertTrue(can_user_approve_experimenter(self.appr_out, exp2)) def test_fallback_deny_blocks_approval(self) -> None: - exp2: User = _make_experimenter("_can3") + exp2: User = make_experimenter("_can3") review_settings_update(allow_any_approver=False) self.assertFalse(can_user_approve_experimenter(self.appr_out, exp2)) diff --git a/src/backend/apps/users/auth/bearer.py b/src/backend/apps/users/auth/bearer.py index e2450eb..852f48b 100644 --- a/src/backend/apps/users/auth/bearer.py +++ b/src/backend/apps/users/auth/bearer.py @@ -1,7 +1,7 @@ import logging from collections.abc import Callable from functools import wraps -from typing import Any +from typing import Any, override from django.http import HttpRequest from ninja.security import HttpBearer @@ -14,6 +14,7 @@ logger: logging.Logger = logging.getLogger("lotty") class JWTBearer(HttpBearer): + @override def authenticate( self, request: HttpRequest, diff --git a/src/backend/apps/users/management/commands/seed_users.py b/src/backend/apps/users/management/commands/seed_users.py index c8c4904..abd2146 100644 --- a/src/backend/apps/users/management/commands/seed_users.py +++ b/src/backend/apps/users/management/commands/seed_users.py @@ -1,3 +1,5 @@ +from typing import override + from django.core.management.base import BaseCommand, CommandParser from apps.users.models import User, UserRole @@ -59,6 +61,7 @@ class Command(BaseCommand): "(admin, experimenter, approver, viewer)." ) + @override def add_arguments(self, parser: CommandParser) -> None: parser.add_argument( "--password", @@ -79,6 +82,7 @@ class Command(BaseCommand): ), ) + @override def handle(self, *args, **options) -> None: password: str = options["password"] force: bool = options["force"] diff --git a/src/backend/apps/users/migrations/0002_alter_user_role.py b/src/backend/apps/users/migrations/0002_alter_user_role.py new file mode 100644 index 0000000..fd812a4 --- /dev/null +++ b/src/backend/apps/users/migrations/0002_alter_user_role.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.11 on 2026-02-12 18:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='role', + field=models.CharField(choices=[('admin', 'Admin'), ('experimenter', 'Experimenter'), ('approver', 'Approver'), ('viewer', 'Viewer')], db_index=True, default='viewer', help_text='Platform role that defines user permissions', max_length=20, verbose_name='role'), + ), + ] diff --git a/src/backend/apps/users/tests/_helpers.py b/src/backend/apps/users/tests/helpers.py similarity index 91% rename from src/backend/apps/users/tests/_helpers.py rename to src/backend/apps/users/tests/helpers.py index d3309b4..faa2ae4 100644 --- a/src/backend/apps/users/tests/_helpers.py +++ b/src/backend/apps/users/tests/helpers.py @@ -3,7 +3,7 @@ from apps.users.models import User, UserRole from apps.users.services import user_create -def _make_user( +def make_user( username="testuser", email="test@lotty.local", password="testpass123", # noqa: S107 @@ -19,6 +19,6 @@ def _make_user( ) -def _auth_header(user) -> str: +def auth_header(user) -> str: token: str = create_access_token(user.pk, user.role) return f"Bearer {token}" diff --git a/src/backend/apps/users/tests/test_jwt.py b/src/backend/apps/users/tests/test_jwt.py index c95a7ca..8017abb 100644 --- a/src/backend/apps/users/tests/test_jwt.py +++ b/src/backend/apps/users/tests/test_jwt.py @@ -1,6 +1,6 @@ import uuid from datetime import timedelta -from typing import Any +from typing import Any, override from django.core.handlers.wsgi import WSGIRequest from django.test import RequestFactory, TestCase @@ -17,7 +17,7 @@ from apps.users.auth.jwt import ( ) from apps.users.models import User, UserRole -from ._helpers import _make_user +from .helpers import make_user class JWTCreateTest(TestCase): @@ -37,6 +37,7 @@ class JWTCreateTest(TestCase): class JWTDecodeTest(TestCase): + @override def setUp(self) -> None: self.uid: uuid.UUID = uuid.uuid4() @@ -78,9 +79,10 @@ class JWTDecodeTest(TestCase): class JWTBearerTest(TestCase): + @override def setUp(self) -> None: self.bearer = JWTBearer() - self.user: User = _make_user( + self.user: User = make_user( username="bearer_user", email="bearer@x.com", role=UserRole.ADMIN, diff --git a/src/backend/apps/users/tests/test_models.py b/src/backend/apps/users/tests/test_models.py index fa00aee..bff70d7 100644 --- a/src/backend/apps/users/tests/test_models.py +++ b/src/backend/apps/users/tests/test_models.py @@ -4,7 +4,7 @@ from django.test import TestCase from apps.users.models import User, UserRole -from ._helpers import _make_user +from .helpers import make_user class UserRoleChoicesTest(TestCase): @@ -20,11 +20,11 @@ class UserRoleChoicesTest(TestCase): class UserModelTest(TestCase): def test_default_role_is_viewer(self) -> None: - user: User = _make_user() + user: User = make_user() self.assertEqual(user.role, UserRole.VIEWER) def test_role_properties(self) -> None: - admin: User = _make_user( + admin: User = make_user( username="a", email="a@x.com", role=UserRole.ADMIN ) self.assertTrue(admin.is_admin_role) @@ -32,25 +32,25 @@ class UserModelTest(TestCase): self.assertFalse(admin.is_approver) self.assertFalse(admin.is_viewer) - exp: User = _make_user( + exp: User = make_user( username="e", email="e@x.com", role=UserRole.EXPERIMENTER ) self.assertTrue(exp.is_experimenter) - appr: User = _make_user( + appr: User = make_user( username="ap", email="ap@x.com", role=UserRole.APPROVER ) self.assertTrue(appr.is_approver) - viewer: User = _make_user( + viewer: User = make_user( username="v", email="v@x.com", role=UserRole.VIEWER ) self.assertTrue(viewer.is_viewer) def test_uuid_primary_key(self) -> None: - user: User = _make_user() + user: User = make_user() self.assertIsInstance(user.pk, uuid.UUID) def test_str_representation(self) -> None: - user: User = _make_user(username="hello") + user: User = make_user(username="hello") self.assertEqual(str(user), "hello") diff --git a/src/backend/apps/users/tests/test_require_roles.py b/src/backend/apps/users/tests/test_require_roles.py index 0f17964..65a9e45 100644 --- a/src/backend/apps/users/tests/test_require_roles.py +++ b/src/backend/apps/users/tests/test_require_roles.py @@ -1,3 +1,5 @@ +from typing import override + from django.core.handlers.wsgi import WSGIRequest from django.test import RequestFactory, TestCase @@ -5,15 +7,16 @@ from apps.users.auth.bearer import require_admin, require_roles from apps.users.models import User, UserRole from config.errors import ForbiddenError -from ._helpers import _make_user +from .helpers import make_user class RequireRolesTest(TestCase): + @override def setUp(self) -> None: - self.admin: User = _make_user( + self.admin: User = make_user( username="rr_admin", email="rr_admin@x.com", role=UserRole.ADMIN ) - self.viewer: User = _make_user( + self.viewer: User = make_user( username="rr_viewer", email="rr_viewer@x.com", role=UserRole.VIEWER ) diff --git a/src/backend/apps/users/tests/test_selectors.py b/src/backend/apps/users/tests/test_selectors.py index ba744b1..8a6fb0a 100644 --- a/src/backend/apps/users/tests/test_selectors.py +++ b/src/backend/apps/users/tests/test_selectors.py @@ -1,4 +1,5 @@ import uuid +from typing import override from django.db.models import QuerySet from django.test import TestCase @@ -18,27 +19,28 @@ from apps.users.selectors import ( user_list_viewers, ) -from ._helpers import _make_user +from .helpers import make_user class UserSelectorsTest(TestCase): + @override def setUp(self) -> None: - self.admin: User = _make_user( + self.admin: User = make_user( username="sel_admin", email="sel_admin@x.com", role=UserRole.ADMIN, ) - self.exp: User = _make_user( + self.exp: User = make_user( username="sel_exp", email="sel_exp@x.com", role=UserRole.EXPERIMENTER, ) - self.appr: User = _make_user( + self.appr: User = make_user( username="sel_appr", email="sel_appr@x.com", role=UserRole.APPROVER, ) - self.viewer: User = _make_user( + self.viewer: User = make_user( username="sel_viewer", email="sel_viewer@x.com", role=UserRole.VIEWER, diff --git a/src/backend/apps/users/tests/test_services.py b/src/backend/apps/users/tests/test_services.py index b363266..63cba4e 100644 --- a/src/backend/apps/users/tests/test_services.py +++ b/src/backend/apps/users/tests/test_services.py @@ -1,3 +1,5 @@ +from typing import override + from django.core.exceptions import ValidationError from django.test import TestCase @@ -11,7 +13,7 @@ from apps.users.services import ( user_update, ) -from ._helpers import _make_user +from .helpers import make_user class UserCreateServiceTest(TestCase): @@ -54,8 +56,9 @@ class UserCreateServiceTest(TestCase): class UserUpdateServiceTest(TestCase): + @override def setUp(self) -> None: - self.user: User = _make_user() + self.user: User = make_user() def test_update_username(self) -> None: updated: User = user_update(user=self.user, username="newname") @@ -91,8 +94,9 @@ class UserUpdateServiceTest(TestCase): class UserAssignRoleServiceTest(TestCase): + @override def setUp(self) -> None: - self.user: User = _make_user() + self.user: User = make_user() def test_assign_valid_role(self) -> None: updated: User = user_assign_role( @@ -107,15 +111,16 @@ class UserAssignRoleServiceTest(TestCase): class UserDeleteServiceTest(TestCase): def test_hard_delete(self) -> None: - user: User = _make_user() + user: User = make_user() pk = user.pk user_delete(user=user) self.assertFalse(User.objects.filter(pk=pk).exists()) class UserActivateDeactivateServiceTest(TestCase): + @override def setUp(self) -> None: - self.user: User = _make_user() + self.user: User = make_user() def test_deactivate(self) -> None: updated: User = user_deactivate(user=self.user) diff --git a/src/backend/config/utils.py b/src/backend/config/utils.py index a2afa57..73ccbfc 100644 --- a/src/backend/config/utils.py +++ b/src/backend/config/utils.py @@ -18,11 +18,11 @@ def build_error_payload( payload = { "code": code, "message": message, - "traceId": str(trace_id), + "trace_id": str(trace_id), "timestamp": now(), "path": request.path, "details": details, } if field_errors is not None: - payload["fieldErrors"] = field_errors + payload["field_errors"] = field_errors return payload diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 34b01c5..57dd1d8 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -4,29 +4,21 @@ dependencies = [ "django-extensions>=4.1.0,<5.0.0", "django-stubs-ext>=5.1.3,<6.0.0", "django-cors-headers>=4.7.0,<5.0.0", - "django-ninja>=1.3.0,<2.0.0", "orjson>=3.10.15,<4.0.0", "pydantic>=2.10.5,<3.0.0", - "pyjwt>=2.10.1,<3.0.0", - "django-redis>=6.0.0,<7.0.0", "psycopg2-binary>=2.9.10,<3.0.0", "redis>=6.2.0,<7.0.0", - "celery>=5.5.0,<6.0.0", - "django-storages[s3]>=1.14,<2.0", - "django-guid>=3.5.1,<4.0.0", "django-health-check>=3.18.3,<4.0.0", "django-prometheus>=2.4.1,<3.0.0", "django-silk[formatting]>=5.4.0,<6.0.0", - "colorlog>=6.9.0,<7.0.0", "python-json-logger>=3.2.1,<4.0.0", - "opentelemetry-api>=1.35.0", "opentelemetry-distro>=0.56b0", "opentelemetry-exporter-otlp>=1.35.0", @@ -41,7 +33,6 @@ dependencies = [ "opentelemetry-instrumentation-urllib>=0.56b0", "opentelemetry-instrumentation-urllib3>=0.56b0", "opentelemetry-instrumentation-wsgi>=0.56b0", - "gunicorn>=23.0.0,<24.0.0", "uvicorn[standard]>=0.34.0,<1.0.0", "uvicorn-worker>=0.2.0,<1.0.0", @@ -191,7 +182,7 @@ quote-style = "double" skip-magic-trailing-comma = false [tool.mypy] -strict = true +strict = false strict_bytes = true local_partial_types = true warn_unreachable = true diff --git a/src/backend/uv.lock b/src/backend/uv.lock index b1e2f32..f474682 100644 --- a/src/backend/uv.lock +++ b/src/backend/uv.lock @@ -378,15 +378,15 @@ wheels = [ [[package]] name = "django-health-check" -version = "3.23.5" +version = "3.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/90/1b7ee57c755ab551ffc1e50c0ddf5e4f3e57c8f127e444d0659c6a51bf81/django_health_check-3.23.5.tar.gz", hash = "sha256:b3be81841ae91aac11005e9ef594857339d4d7f317fd05243f6825755650c911", size = 20774, upload-time = "2026-02-06T14:19:51.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a6/f474f443b0a7f9e7e52a25a3773a31c5c061bdb28cf311b7801fc250db9b/django_health_check-3.24.0.tar.gz", hash = "sha256:b5e01d2013a254cc5a2c7b19c62f11ea6fd2624c87a9d990e11a1b3874df78b5", size = 20807, upload-time = "2026-02-12T10:02:18.482Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/b7/ab27628c23010e9f74c42a16ea47cdc43754fd9fbd9300ab49037f3e28e5/django_health_check-3.23.5-py3-none-any.whl", hash = "sha256:32bef7a2a4807137720cdf8d03713591251a43d3afc691d2b3461021b1035c76", size = 37856, upload-time = "2026-02-06T14:19:49.974Z" }, + { url = "https://files.pythonhosted.org/packages/38/17/51aabb4908e007accf3c596b92a62ed7e456c581a1e45f06e0f2219dc6c0/django_health_check-3.24.0-py3-none-any.whl", hash = "sha256:bd568d7d3813980668dfbe730214aef5da7c8da73bc2aa60ec2eeca8a3788da6", size = 37964, upload-time = "2026-02-12T10:02:17.08Z" }, ] [[package]] @@ -639,43 +639,49 @@ wheels = [ [[package]] name = "librt" -version = "0.7.8" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, - { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, - { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, - { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, - { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, - { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, - { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, - { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, - { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, - { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, - { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, - { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, - { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, - { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, - { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, - { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, - { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, - { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, + { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, + { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, + { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, + { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, + { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, + { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, + { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, + { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, ] [[package]]