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,23 +48,25 @@ async def _get_all(
4848 projects , total_num = await self .project_repo .get_projects (
4949 user = user , pagination = pagination , namespace = query .namespace , direct_member = query .direct_member
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- updated_at = p .updated_at .isoformat () if p .updated_at else None ,
60- repositories = p .repositories ,
61- visibility = p .visibility .value ,
62- description = p .description ,
63- etag = p .etag ,
64- keywords = p .keywords or [],
65- )
66- for p in projects
67- ], 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
6870
6971 return "/projects" , ["GET" ], _get_all
7072
@@ -87,9 +89,10 @@ async def _post(_: Request, user: base_models.APIUser, body: apispec.ProjectPost
8789 keywords = keywords ,
8890 )
8991 result = await self .project_repo .insert_project (user , project )
90- return json (
92+ return validated_json (
93+ apispec .Project ,
9194 dict (
92- id = str ( result .id ) ,
95+ id = result .id ,
9396 name = result .name ,
9497 namespace = result .namespace .slug ,
9598 slug = result .slug ,
@@ -110,18 +113,20 @@ def get_one(self) -> BlueprintFactoryResponse:
110113 """Get a specific project."""
111114
112115 @authenticate (self .authenticator )
113- @validate_path_project_id
114- async def _get_one (request : Request , user : base_models .APIUser , project_id : str ) -> JSONResponse | HTTPResponse :
115- 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 )
116120
117121 etag = request .headers .get ("If-None-Match" )
118122 if project .etag is not None and project .etag == etag :
119123 return HTTPResponse (status = 304 )
120124
121125 headers = {"ETag" : project .etag } if project .etag is not None else None
122- return json (
126+ return validated_json (
127+ apispec .Project ,
123128 dict (
124- id = str ( project .id ) ,
129+ id = project .id ,
125130 name = project .name ,
126131 namespace = project .namespace .slug ,
127132 slug = project .slug ,
@@ -136,7 +141,7 @@ async def _get_one(request: Request, user: base_models.APIUser, project_id: str)
136141 headers = headers ,
137142 )
138143
139- return "/projects/<project_id>" , ["GET" ], _get_one
144+ return "/projects/<project_id:ulid >" , ["GET" ], _get_one
140145
141146 def get_one_by_namespace_slug (self ) -> BlueprintFactoryResponse :
142147 """Get a specific project by namespace/slug."""
@@ -152,9 +157,10 @@ async def _get_one_by_namespace_slug(
152157 return HTTPResponse (status = 304 )
153158
154159 headers = {"ETag" : project .etag } if project .etag is not None else None
155- return json (
160+ return validated_json (
161+ apispec .Project ,
156162 dict (
157- id = str ( project .id ) ,
163+ id = project .id ,
158164 name = project .name ,
159165 namespace = project .namespace .slug ,
160166 slug = project .slug ,
@@ -169,35 +175,33 @@ async def _get_one_by_namespace_slug(
169175 headers = headers ,
170176 )
171177
172- 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
173179
174180 def delete (self ) -> BlueprintFactoryResponse :
175181 """Delete a specific project."""
176182
177183 @authenticate (self .authenticator )
178184 @only_authenticated
179- @validate_path_project_id
180- async def _delete (_ : Request , user : base_models .APIUser , project_id : str ) -> HTTPResponse :
181- 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 )
182187 return HTTPResponse (status = 204 )
183188
184- return "/projects/<project_id>" , ["DELETE" ], _delete
189+ return "/projects/<project_id:ulid >" , ["DELETE" ], _delete
185190
186191 def patch (self ) -> BlueprintFactoryResponse :
187192 """Partially update a specific project."""
188193
189194 @authenticate (self .authenticator )
190195 @only_authenticated
191- @validate_path_project_id
192196 @if_match_required
193197 @validate (json = apispec .ProjectPatch )
194198 async def _patch (
195- _ : 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
196200 ) -> JSONResponse :
197201 body_dict = body .model_dump (exclude_none = True )
198202
199203 project_update = await self .project_repo .update_project (
200- 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
201205 )
202206 if not isinstance (project_update , project_models .ProjectUpdate ):
203207 raise errors .ProgrammingError (
@@ -206,9 +210,10 @@ async def _patch(
206210 )
207211
208212 updated_project = project_update .new
209- return json (
213+ return validated_json (
214+ apispec .Project ,
210215 dict (
211- id = str ( updated_project .id ) ,
216+ id = updated_project .id ,
212217 name = updated_project .name ,
213218 namespace = updated_project .namespace .slug ,
214219 slug = updated_project .slug ,
@@ -223,15 +228,14 @@ async def _patch(
223228 200 ,
224229 )
225230
226- return "/projects/<project_id>" , ["PATCH" ], _patch
231+ return "/projects/<project_id:ulid >" , ["PATCH" ], _patch
227232
228233 def get_all_members (self ) -> BlueprintFactoryResponse :
229234 """List all project members."""
230235
231236 @authenticate (self .authenticator )
232- @validate_path_project_id
233- async def _get_all_members (_ : Request , user : base_models .APIUser , project_id : str ) -> JSONResponse :
234- 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 )
235239
236240 users = []
237241
@@ -251,35 +255,33 @@ async def _get_all_members(_: Request, user: base_models.APIUser, project_id: st
251255 ).model_dump (exclude_none = True , mode = "json" )
252256 users .append (user_with_id )
253257
254- return json ( users )
258+ return validated_json ( apispec . ProjectMemberListResponse , users )
255259
256- return "/projects/<project_id>/members" , ["GET" ], _get_all_members
260+ return "/projects/<project_id:ulid >/members" , ["GET" ], _get_all_members
257261
258262 def update_members (self ) -> BlueprintFactoryResponse :
259263 """Update or add project members."""
260264
261265 @authenticate (self .authenticator )
262- @validate_path_project_id
263266 @validate_body_root_model (json = apispec .ProjectMemberListPatchRequest )
264267 async def _update_members (
265- _ : Request , user : base_models .APIUser , project_id : str , body : apispec .ProjectMemberListPatchRequest
268+ _ : Request , user : base_models .APIUser , project_id : ULID , body : apispec .ProjectMemberListPatchRequest
266269 ) -> HTTPResponse :
267270 members = [Member (Role (i .role .value ), i .id , project_id ) for i in body .root ]
268- 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 )
269272 return HTTPResponse (status = 200 )
270273
271- return "/projects/<project_id>/members" , ["PATCH" ], _update_members
274+ return "/projects/<project_id:ulid >/members" , ["PATCH" ], _update_members
272275
273276 def delete_member (self ) -> BlueprintFactoryResponse :
274277 """Delete a specific project."""
275278
276279 @authenticate (self .authenticator )
277- @validate_path_project_id
278280 @validate_path_user_id
279281 async def _delete_member (
280- _ : Request , user : base_models .APIUser , project_id : str , member_id : str
282+ _ : Request , user : base_models .APIUser , project_id : ULID , member_id : str
281283 ) -> HTTPResponse :
282- 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 ])
283285 return HTTPResponse (status = 204 )
284286
285- 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