Skip to content

Commit 274052e

Browse files
committed
refactor: add validation to namespaces blueprint
1 parent 4a50714 commit 274052e

File tree

2 files changed

+55
-40
lines changed

2 files changed

+55
-40
lines changed

components/renku_data_services/base_models/validation.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@
1111
from renku_data_services import errors
1212

1313

14+
def validate_and_dump(
15+
model: type[BaseModel],
16+
data: Any,
17+
exclude_none: bool = True,
18+
) -> Any:
19+
"""Validate and dump with a pydantic model, ensuring proper validation errors."""
20+
try:
21+
body = model.model_validate(data).model_dump(exclude_none=exclude_none, mode="json")
22+
except PydanticValidationError as err:
23+
parts = [".".join(str(i) for i in field["loc"]) + ": " + field["msg"] for field in err.errors()]
24+
message = (
25+
f"The server could not construct a valid response. Errors found in the following fields: {', '.join(parts)}"
26+
)
27+
raise errors.ProgrammingError(message=message) from err
28+
return body
29+
30+
1431
def validated_json(
1532
model: type[BaseModel],
1633
data: Any,
@@ -25,12 +42,5 @@ def validated_json(
2542
2643
If the input data fails validation, an HTTP status code 500 will be raised.
2744
"""
28-
try:
29-
body = model.model_validate(data).model_dump(exclude_none=exclude_none, mode="json")
30-
except PydanticValidationError as err:
31-
parts = [".".join(str(i) for i in field["loc"]) + ": " + field["msg"] for field in err.errors()]
32-
message = (
33-
f"The server could not construct a valid response. Errors found in the following fields: {', '.join(parts)}"
34-
)
35-
raise errors.ProgrammingError(message=message) from err
45+
body = validate_and_dump(model, data, exclude_none)
3646
return json(body, status=status, headers=headers, content_type=content_type, dumps=dumps, **kwargs)

components/renku_data_services/namespace/blueprints.py

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from dataclasses import dataclass
44

5-
from sanic import HTTPResponse, Request, json
5+
from sanic import HTTPResponse, Request
66
from sanic.response import JSONResponse
77
from sanic_ext import validate
88

@@ -11,6 +11,7 @@
1111
from renku_data_services.base_api.auth import authenticate, only_authenticated
1212
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
1313
from renku_data_services.base_api.pagination import PaginationRequest, paginate
14+
from renku_data_services.base_models.validation import validate_and_dump, validated_json
1415
from renku_data_services.errors import errors
1516
from renku_data_services.namespace import apispec
1617
from renku_data_services.namespace.db import GroupRepository
@@ -34,7 +35,7 @@ async def _get_all(
3435
) -> tuple[list[dict], int]:
3536
groups, rec_count = await self.group_repo.get_groups(user=user, pagination=pagination)
3637
return (
37-
[apispec.GroupResponse.model_validate(g).model_dump(exclude_none=True, mode="json") for g in groups],
38+
validate_and_dump(apispec.GroupResponseList, groups),
3839
rec_count,
3940
)
4041

@@ -48,7 +49,7 @@ def post(self) -> BlueprintFactoryResponse:
4849
@validate(json=apispec.GroupPostRequest)
4950
async def _post(_: Request, user: base_models.APIUser, body: apispec.GroupPostRequest) -> JSONResponse:
5051
result = await self.group_repo.insert_group(user=user, payload=body)
51-
return json(apispec.GroupResponse.model_validate(result).model_dump(exclude_none=True, mode="json"), 201)
52+
return validated_json(apispec.GroupResponse, result, 201)
5253

5354
return "/groups", ["POST"], _post
5455

@@ -58,7 +59,7 @@ def get_one(self) -> BlueprintFactoryResponse:
5859
@authenticate(self.authenticator)
5960
async def _get_one(_: Request, user: base_models.APIUser, slug: str) -> JSONResponse:
6061
result = await self.group_repo.get_group(user=user, slug=slug)
61-
return json(apispec.GroupResponse.model_validate(result).model_dump(exclude_none=True, mode="json"))
62+
return validated_json(apispec.GroupResponse, result)
6263

6364
return "/groups/<slug:renku_slug>", ["GET"], _get_one
6465

@@ -84,7 +85,7 @@ async def _patch(
8485
) -> JSONResponse:
8586
body_dict = body.model_dump(exclude_none=True)
8687
res = await self.group_repo.update_group(user=user, slug=slug, payload=body_dict)
87-
return json(apispec.GroupResponse.model_validate(res).model_dump(exclude_none=True, mode="json"))
88+
return validated_json(apispec.GroupResponse, res)
8889

8990
return "/groups/<slug:renku_slug>", ["PATCH"], _patch
9091

@@ -94,17 +95,18 @@ def get_all_members(self) -> BlueprintFactoryResponse:
9495
@authenticate(self.authenticator)
9596
async def _get_all_members(_: Request, user: base_models.APIUser, slug: str) -> JSONResponse:
9697
members = await self.group_repo.get_group_members(user, slug)
97-
return json(
98+
return validated_json(
99+
apispec.GroupMemberResponseList,
98100
[
99-
apispec.GroupMemberResponse(
101+
dict(
100102
id=m.id,
101103
email=m.email,
102104
first_name=m.first_name,
103105
last_name=m.last_name,
104106
role=apispec.GroupRole(m.role.value),
105-
).model_dump(exclude_none=True, mode="json")
107+
)
106108
for m in members
107-
]
109+
],
108110
)
109111

110112
return "/groups/<slug:renku_slug>/members", ["GET"], _get_all_members
@@ -114,25 +116,24 @@ def update_members(self) -> BlueprintFactoryResponse:
114116

115117
@authenticate(self.authenticator)
116118
@only_authenticated
119+
@validate(json=apispec.GroupMemberPatchRequestList)
117120
async def _update_members(
118-
request: Request,
119-
user: base_models.APIUser,
120-
slug: str,
121+
request: Request, user: base_models.APIUser, slug: str, body: apispec.GroupMemberPatchRequestList
121122
) -> JSONResponse:
122-
body_validated = apispec.GroupMemberPatchRequestList.model_validate(request.json)
123123
res = await self.group_repo.update_group_members(
124124
user=user,
125125
slug=slug,
126-
payload=body_validated,
126+
payload=body,
127127
)
128-
return json(
128+
return validated_json(
129+
apispec.GroupMemberPatchRequest,
129130
[
130-
apispec.GroupMemberPatchRequest(
131+
dict(
131132
id=m.member.user_id,
132133
role=apispec.GroupRole(m.member.role.value),
133-
).model_dump(exclude_none=True, mode="json")
134+
)
134135
for m in res
135-
]
136+
],
136137
)
137138

138139
return "/groups/<slug:renku_slug>/members", ["PATCH"], _update_members
@@ -163,17 +164,20 @@ async def _get_namespaces(
163164
nss, total_count = await self.group_repo.get_namespaces(
164165
user=user, pagination=pagination, minimum_role=minimum_role
165166
)
166-
return [
167-
apispec.NamespaceResponse(
168-
id=ns.id,
169-
name=ns.name,
170-
slug=ns.latest_slug if ns.latest_slug else ns.slug,
171-
created_by=ns.created_by,
172-
creation_date=None, # NOTE: we do not save creation date in the DB
173-
namespace_kind=apispec.NamespaceKind(ns.kind.value),
174-
).model_dump(exclude_none=True, mode="json")
175-
for ns in nss
176-
], total_count
167+
return validate_and_dump(
168+
apispec.NamespaceResponseList,
169+
[
170+
dict(
171+
id=ns.id,
172+
name=ns.name,
173+
slug=ns.latest_slug if ns.latest_slug else ns.slug,
174+
created_by=ns.created_by,
175+
creation_date=None, # NOTE: we do not save creation date in the DB
176+
namespace_kind=apispec.NamespaceKind(ns.kind.value),
177+
)
178+
for ns in nss
179+
],
180+
), total_count
177181

178182
return "/namespaces", ["GET"], _get_namespaces
179183

@@ -185,15 +189,16 @@ async def _get_namespace(_: Request, user: base_models.APIUser, slug: str) -> JS
185189
ns = await self.group_repo.get_namespace_by_slug(user=user, slug=slug)
186190
if not ns:
187191
raise errors.MissingResourceError(message=f"The namespace with slug {slug} does not exist")
188-
return json(
189-
apispec.NamespaceResponse(
192+
return validated_json(
193+
apispec.NamespaceResponse,
194+
dict(
190195
id=ns.id,
191196
name=ns.name,
192197
slug=ns.latest_slug if ns.latest_slug else ns.slug,
193198
created_by=ns.created_by,
194199
creation_date=None, # NOTE: we do not save creation date in the DB
195200
namespace_kind=apispec.NamespaceKind(ns.kind.value),
196-
).model_dump(exclude_none=True, mode="json")
201+
),
197202
)
198203

199204
return "/namespaces/<slug:renku_slug>", ["GET"], _get_namespace

0 commit comments

Comments
 (0)