chore(): switched all fields in contract to snake_case and linting improvements
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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}, "
|
||||
|
||||
+9
-15
@@ -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]
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+44
-38
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user