33from dataclasses import dataclass
44from typing import Any
55
6- from sanic import HTTPResponse , Request , json
6+ from sanic import HTTPResponse , Request
77from sanic .response import JSONResponse
88from sanic_ext import validate
99from ulid import ULID
1313from renku_data_services .base_api .auth import (
1414 authenticate ,
1515 only_authenticated ,
16- validate_path_project_id ,
1716 validate_path_user_id ,
1817)
1918from renku_data_services .base_api .blueprint import BlueprintFactoryResponse , CustomBlueprint
2019from renku_data_services .base_api .etag import if_match_required
2120from renku_data_services .base_api .misc import validate_body_root_model , validate_query
2221from renku_data_services .base_api .pagination import PaginationRequest , paginate
22+ from renku_data_services .base_models .validation import validate_and_dump , validated_json
2323from renku_data_services .errors import errors
2424from renku_data_services .project import apispec
2525from renku_data_services .project import models as project_models
@@ -48,22 +48,25 @@ async def _get_all(
4848 projects , total_num = await self .project_repo .get_projects (
4949 user = user , pagination = pagination , namespace = query .namespace
5050 )
51- return [
52- dict (
53- id = str (p .id ),
54- name = p .name ,
55- namespace = p .namespace .slug ,
56- slug = p .slug ,
57- creation_date = p .creation_date .isoformat (),
58- created_by = p .created_by ,
59- repositories = p .repositories ,
60- visibility = p .visibility .value ,
61- description = p .description ,
62- etag = p .etag ,
63- keywords = p .keywords or [],
64- )
65- for p in projects
66- ], total_num
51+ return validate_and_dump (
52+ apispec .ProjectsList ,
53+ [
54+ dict (
55+ id = p .id ,
56+ name = p .name ,
57+ namespace = p .namespace .slug ,
58+ slug = p .slug ,
59+ creation_date = p .creation_date .isoformat (),
60+ created_by = p .created_by ,
61+ repositories = p .repositories ,
62+ visibility = p .visibility .value ,
63+ description = p .description ,
64+ etag = p .etag ,
65+ keywords = p .keywords or [],
66+ )
67+ for p in projects
68+ ],
69+ ), total_num
6770
6871 return "/projects" , ["GET" ], _get_all
6972
@@ -86,9 +89,10 @@ async def _post(_: Request, user: base_models.APIUser, body: apispec.ProjectPost
8689 keywords = keywords ,
8790 )
8891 result = await self .project_repo .insert_project (user , project )
89- return json (
92+ return validated_json (
93+ apispec .Project ,
9094 dict (
91- id = str ( result .id ) ,
95+ id = result .id ,
9296 name = result .name ,
9397 namespace = result .namespace .slug ,
9498 slug = result .slug ,
@@ -109,18 +113,20 @@ def get_one(self) -> BlueprintFactoryResponse:
109113 """Get a specific project."""
110114
111115 @authenticate (self .authenticator )
112- @validate_path_project_id
113- async def _get_one (request : Request , user : base_models .APIUser , project_id : str ) -> JSONResponse | HTTPResponse :
114- project = await self .project_repo .get_project (user = user , project_id = ULID .from_str (project_id ))
116+ async def _get_one (
117+ request : Request , user : base_models .APIUser , project_id : ULID
118+ ) -> JSONResponse | HTTPResponse :
119+ project = await self .project_repo .get_project (user = user , project_id = project_id )
115120
116121 etag = request .headers .get ("If-None-Match" )
117122 if project .etag is not None and project .etag == etag :
118123 return HTTPResponse (status = 304 )
119124
120125 headers = {"ETag" : project .etag } if project .etag is not None else None
121- return json (
126+ return validated_json (
127+ apispec .Project ,
122128 dict (
123- id = str ( project .id ) ,
129+ id = project .id ,
124130 name = project .name ,
125131 namespace = project .namespace .slug ,
126132 slug = project .slug ,
@@ -135,7 +141,7 @@ async def _get_one(request: Request, user: base_models.APIUser, project_id: str)
135141 headers = headers ,
136142 )
137143
138- return "/projects/<project_id>" , ["GET" ], _get_one
144+ return "/projects/<project_id:ulid >" , ["GET" ], _get_one
139145
140146 def get_one_by_namespace_slug (self ) -> BlueprintFactoryResponse :
141147 """Get a specific project by namespace/slug."""
@@ -151,9 +157,10 @@ async def _get_one_by_namespace_slug(
151157 return HTTPResponse (status = 304 )
152158
153159 headers = {"ETag" : project .etag } if project .etag is not None else None
154- return json (
160+ return validated_json (
161+ apispec .Project ,
155162 dict (
156- id = str ( project .id ) ,
163+ id = project .id ,
157164 name = project .name ,
158165 namespace = project .namespace .slug ,
159166 slug = project .slug ,
@@ -168,35 +175,33 @@ async def _get_one_by_namespace_slug(
168175 headers = headers ,
169176 )
170177
171- return "/projects /<namespace>/<slug:renku_slug>" , ["GET" ], _get_one_by_namespace_slug
178+ return "/namespaces /<namespace>/projects /<slug:renku_slug>" , ["GET" ], _get_one_by_namespace_slug
172179
173180 def delete (self ) -> BlueprintFactoryResponse :
174181 """Delete a specific project."""
175182
176183 @authenticate (self .authenticator )
177184 @only_authenticated
178- @validate_path_project_id
179- async def _delete (_ : Request , user : base_models .APIUser , project_id : str ) -> HTTPResponse :
180- await self .project_repo .delete_project (user = user , project_id = ULID .from_str (project_id ))
185+ async def _delete (_ : Request , user : base_models .APIUser , project_id : ULID ) -> HTTPResponse :
186+ await self .project_repo .delete_project (user = user , project_id = project_id )
181187 return HTTPResponse (status = 204 )
182188
183- return "/projects/<project_id>" , ["DELETE" ], _delete
189+ return "/projects/<project_id:ulid >" , ["DELETE" ], _delete
184190
185191 def patch (self ) -> BlueprintFactoryResponse :
186192 """Partially update a specific project."""
187193
188194 @authenticate (self .authenticator )
189195 @only_authenticated
190- @validate_path_project_id
191196 @if_match_required
192197 @validate (json = apispec .ProjectPatch )
193198 async def _patch (
194- _ : Request , user : base_models .APIUser , project_id : str , body : apispec .ProjectPatch , etag : str
199+ _ : Request , user : base_models .APIUser , project_id : ULID , body : apispec .ProjectPatch , etag : str
195200 ) -> JSONResponse :
196201 body_dict = body .model_dump (exclude_none = True )
197202
198203 project_update = await self .project_repo .update_project (
199- user = user , project_id = ULID . from_str ( project_id ) , etag = etag , payload = body_dict
204+ user = user , project_id = project_id , etag = etag , payload = body_dict
200205 )
201206 if not isinstance (project_update , project_models .ProjectUpdate ):
202207 raise errors .ProgrammingError (
@@ -205,9 +210,10 @@ async def _patch(
205210 )
206211
207212 updated_project = project_update .new
208- return json (
213+ return validated_json (
214+ apispec .Project ,
209215 dict (
210- id = str ( updated_project .id ) ,
216+ id = updated_project .id ,
211217 name = updated_project .name ,
212218 namespace = updated_project .namespace .slug ,
213219 slug = updated_project .slug ,
@@ -222,15 +228,14 @@ async def _patch(
222228 200 ,
223229 )
224230
225- return "/projects/<project_id>" , ["PATCH" ], _patch
231+ return "/projects/<project_id:ulid >" , ["PATCH" ], _patch
226232
227233 def get_all_members (self ) -> BlueprintFactoryResponse :
228234 """List all project members."""
229235
230236 @authenticate (self .authenticator )
231- @validate_path_project_id
232- async def _get_all_members (_ : Request , user : base_models .APIUser , project_id : str ) -> JSONResponse :
233- members = await self .project_member_repo .get_members (user , ULID .from_str (project_id ))
237+ async def _get_all_members (_ : Request , user : base_models .APIUser , project_id : ULID ) -> JSONResponse :
238+ members = await self .project_member_repo .get_members (user , project_id )
234239
235240 users = []
236241
@@ -250,35 +255,33 @@ async def _get_all_members(_: Request, user: base_models.APIUser, project_id: st
250255 ).model_dump (exclude_none = True , mode = "json" )
251256 users .append (user_with_id )
252257
253- return json ( users )
258+ return validated_json ( apispec . ProjectMemberListResponse , users )
254259
255- return "/projects/<project_id>/members" , ["GET" ], _get_all_members
260+ return "/projects/<project_id:ulid >/members" , ["GET" ], _get_all_members
256261
257262 def update_members (self ) -> BlueprintFactoryResponse :
258263 """Update or add project members."""
259264
260265 @authenticate (self .authenticator )
261- @validate_path_project_id
262266 @validate_body_root_model (json = apispec .ProjectMemberListPatchRequest )
263267 async def _update_members (
264- _ : Request , user : base_models .APIUser , project_id : str , body : apispec .ProjectMemberListPatchRequest
268+ _ : Request , user : base_models .APIUser , project_id : ULID , body : apispec .ProjectMemberListPatchRequest
265269 ) -> HTTPResponse :
266270 members = [Member (Role (i .role .value ), i .id , project_id ) for i in body .root ]
267- await self .project_member_repo .update_members (user , ULID . from_str ( project_id ) , members )
271+ await self .project_member_repo .update_members (user , project_id , members )
268272 return HTTPResponse (status = 200 )
269273
270- return "/projects/<project_id>/members" , ["PATCH" ], _update_members
274+ return "/projects/<project_id:ulid >/members" , ["PATCH" ], _update_members
271275
272276 def delete_member (self ) -> BlueprintFactoryResponse :
273277 """Delete a specific project."""
274278
275279 @authenticate (self .authenticator )
276- @validate_path_project_id
277280 @validate_path_user_id
278281 async def _delete_member (
279- _ : Request , user : base_models .APIUser , project_id : str , member_id : str
282+ _ : Request , user : base_models .APIUser , project_id : ULID , member_id : str
280283 ) -> HTTPResponse :
281- await self .project_member_repo .delete_members (user , ULID . from_str ( project_id ) , [member_id ])
284+ await self .project_member_repo .delete_members (user , project_id , [member_id ])
282285 return HTTPResponse (status = 204 )
283286
284- return "/projects/<project_id>/members/<member_id>" , ["DELETE" ], _delete_member
287+ return "/projects/<project_id:ulid >/members/<member_id>" , ["DELETE" ], _delete_member
0 commit comments