3
3
from dataclasses import dataclass
4
4
from typing import Any
5
5
6
- from sanic import HTTPResponse , Request , json
6
+ from sanic import HTTPResponse , Request
7
7
from sanic .response import JSONResponse
8
8
from sanic_ext import validate
9
9
from ulid import ULID
13
13
from renku_data_services .base_api .auth import (
14
14
authenticate ,
15
15
only_authenticated ,
16
- validate_path_project_id ,
17
16
validate_path_user_id ,
18
17
)
19
18
from renku_data_services .base_api .blueprint import BlueprintFactoryResponse , CustomBlueprint
20
19
from renku_data_services .base_api .etag import if_match_required
21
20
from renku_data_services .base_api .misc import validate_query
22
21
from renku_data_services .base_api .pagination import PaginationRequest , paginate
22
+ from renku_data_services .base_models .validation import validate_and_dump , validated_json
23
23
from renku_data_services .errors import errors
24
24
from renku_data_services .project import apispec
25
25
from renku_data_services .project import models as project_models
@@ -48,22 +48,25 @@ async def _get_all(
48
48
projects , total_num = await self .project_repo .get_projects (
49
49
user = user , pagination = pagination , namespace = query .namespace
50
50
)
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
67
70
68
71
return "/projects" , ["GET" ], _get_all
69
72
@@ -86,9 +89,10 @@ async def _post(_: Request, user: base_models.APIUser, body: apispec.ProjectPost
86
89
keywords = keywords ,
87
90
)
88
91
result = await self .project_repo .insert_project (user , project )
89
- return json (
92
+ return validated_json (
93
+ apispec .Project ,
90
94
dict (
91
- id = str ( result .id ) ,
95
+ id = result .id ,
92
96
name = result .name ,
93
97
namespace = result .namespace .slug ,
94
98
slug = result .slug ,
@@ -109,18 +113,20 @@ def get_one(self) -> BlueprintFactoryResponse:
109
113
"""Get a specific project."""
110
114
111
115
@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 )
115
120
116
121
etag = request .headers .get ("If-None-Match" )
117
122
if project .etag is not None and project .etag == etag :
118
123
return HTTPResponse (status = 304 )
119
124
120
125
headers = {"ETag" : project .etag } if project .etag is not None else None
121
- return json (
126
+ return validated_json (
127
+ apispec .Project ,
122
128
dict (
123
- id = str ( project .id ) ,
129
+ id = project .id ,
124
130
name = project .name ,
125
131
namespace = project .namespace .slug ,
126
132
slug = project .slug ,
@@ -135,7 +141,7 @@ async def _get_one(request: Request, user: base_models.APIUser, project_id: str)
135
141
headers = headers ,
136
142
)
137
143
138
- return "/projects/<project_id>" , ["GET" ], _get_one
144
+ return "/projects/<project_id:ulid >" , ["GET" ], _get_one
139
145
140
146
def get_one_by_namespace_slug (self ) -> BlueprintFactoryResponse :
141
147
"""Get a specific project by namespace/slug."""
@@ -151,9 +157,10 @@ async def _get_one_by_namespace_slug(
151
157
return HTTPResponse (status = 304 )
152
158
153
159
headers = {"ETag" : project .etag } if project .etag is not None else None
154
- return json (
160
+ return validated_json (
161
+ apispec .Project ,
155
162
dict (
156
- id = str ( project .id ) ,
163
+ id = project .id ,
157
164
name = project .name ,
158
165
namespace = project .namespace .slug ,
159
166
slug = project .slug ,
@@ -168,35 +175,33 @@ async def _get_one_by_namespace_slug(
168
175
headers = headers ,
169
176
)
170
177
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
172
179
173
180
def delete (self ) -> BlueprintFactoryResponse :
174
181
"""Delete a specific project."""
175
182
176
183
@authenticate (self .authenticator )
177
184
@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 )
181
187
return HTTPResponse (status = 204 )
182
188
183
- return "/projects/<project_id>" , ["DELETE" ], _delete
189
+ return "/projects/<project_id:ulid >" , ["DELETE" ], _delete
184
190
185
191
def patch (self ) -> BlueprintFactoryResponse :
186
192
"""Partially update a specific project."""
187
193
188
194
@authenticate (self .authenticator )
189
195
@only_authenticated
190
- @validate_path_project_id
191
196
@if_match_required
192
197
@validate (json = apispec .ProjectPatch )
193
198
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
195
200
) -> JSONResponse :
196
201
body_dict = body .model_dump (exclude_none = True )
197
202
198
203
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
200
205
)
201
206
if not isinstance (project_update , project_models .ProjectUpdate ):
202
207
raise errors .ProgrammingError (
@@ -205,9 +210,10 @@ async def _patch(
205
210
)
206
211
207
212
updated_project = project_update .new
208
- return json (
213
+ return validated_json (
214
+ apispec .Project ,
209
215
dict (
210
- id = str ( updated_project .id ) ,
216
+ id = updated_project .id ,
211
217
name = updated_project .name ,
212
218
namespace = updated_project .namespace .slug ,
213
219
slug = updated_project .slug ,
@@ -222,15 +228,14 @@ async def _patch(
222
228
200 ,
223
229
)
224
230
225
- return "/projects/<project_id>" , ["PATCH" ], _patch
231
+ return "/projects/<project_id:ulid >" , ["PATCH" ], _patch
226
232
227
233
def get_all_members (self ) -> BlueprintFactoryResponse :
228
234
"""List all project members."""
229
235
230
236
@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 )
234
239
235
240
users = []
236
241
@@ -250,33 +255,31 @@ async def _get_all_members(_: Request, user: base_models.APIUser, project_id: st
250
255
).model_dump (exclude_none = True , mode = "json" )
251
256
users .append (user_with_id )
252
257
253
- return json ( users )
258
+ return validated_json ( apispec . ProjectMemberListResponse , users )
254
259
255
- return "/projects/<project_id>/members" , ["GET" ], _get_all_members
260
+ return "/projects/<project_id:ulid >/members" , ["GET" ], _get_all_members
256
261
257
262
def update_members (self ) -> BlueprintFactoryResponse :
258
263
"""Update or add project members."""
259
264
260
265
@authenticate (self .authenticator )
261
- @validate_path_project_id
262
- async def _update_members (request : Request , user : base_models .APIUser , project_id : str ) -> HTTPResponse :
266
+ async def _update_members (request : Request , user : base_models .APIUser , project_id : ULID ) -> HTTPResponse :
263
267
body_dump = apispec .ProjectMemberListPatchRequest .model_validate (request .json )
264
- members = [Member (Role (i .role .value ), i .id , project_id ) for i in body_dump .root ]
265
- await self .project_member_repo .update_members (user , ULID . from_str ( project_id ) , members )
268
+ members = [Member (Role (i .role .value ), i .id , str ( project_id ) ) for i in body_dump .root ]
269
+ await self .project_member_repo .update_members (user , project_id , members )
266
270
return HTTPResponse (status = 200 )
267
271
268
- return "/projects/<project_id>/members" , ["PATCH" ], _update_members
272
+ return "/projects/<project_id:ulid >/members" , ["PATCH" ], _update_members
269
273
270
274
def delete_member (self ) -> BlueprintFactoryResponse :
271
275
"""Delete a specific project."""
272
276
273
277
@authenticate (self .authenticator )
274
- @validate_path_project_id
275
278
@validate_path_user_id
276
279
async def _delete_member (
277
- _ : Request , user : base_models .APIUser , project_id : str , member_id : str
280
+ _ : Request , user : base_models .APIUser , project_id : ULID , member_id : str
278
281
) -> HTTPResponse :
279
- await self .project_member_repo .delete_members (user , ULID . from_str ( project_id ) , [member_id ])
282
+ await self .project_member_repo .delete_members (user , project_id , [member_id ])
280
283
return HTTPResponse (status = 204 )
281
284
282
- return "/projects/<project_id>/members/<member_id>" , ["DELETE" ], _delete_member
285
+ return "/projects/<project_id:ulid >/members/<member_id>" , ["DELETE" ], _delete_member
0 commit comments