• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwissDataScienceCenter / renku-data-services / 9446003648

10 Jun 2024 09:35AM UTC coverage: 90.298% (+0.06%) from 90.239%
9446003648

Pull #248

github

web-flow
Merge 9ff3e8a6c into 1e340ea36
Pull Request #248: feat: add support for bitbucket

40 of 46 new or added lines in 3 files covered. (86.96%)

4 existing lines in 4 files now uncovered.

8488 of 9400 relevant lines covered (90.3%)

1.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.45
/components/renku_data_services/crc/blueprints.py
1
"""Compute resource control (CRC) app."""
2✔
2

3
import asyncio
2✔
4
from dataclasses import asdict, dataclass
2✔
5

6
from sanic import HTTPResponse, Request, empty, json
2✔
7
from sanic_ext import validate
2✔
8

9
import renku_data_services.base_models as base_models
2✔
10
from renku_data_services import errors
2✔
11
from renku_data_services.base_api.auth import authenticate, only_admins
2✔
12
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
2✔
13
from renku_data_services.base_api.misc import validate_db_ids
2✔
14
from renku_data_services.crc import apispec, models
2✔
15
from renku_data_services.crc.apispec_base import ResourceClassesFilter
2✔
16
from renku_data_services.crc.db import ResourcePoolRepository, UserRepository
2✔
17
from renku_data_services.k8s.quota import QuotaRepository
2✔
18
from renku_data_services.users.db import UserRepo as KcUserRepo
2✔
19
from renku_data_services.users.models import UserInfo
2✔
20

21

22
@dataclass(kw_only=True)
2✔
23
class ResourcePoolsBP(CustomBlueprint):
2✔
24
    """Handlers for manipulating resource pools."""
2✔
25

26
    rp_repo: ResourcePoolRepository
2✔
27
    user_repo: UserRepository
2✔
28
    authenticator: base_models.Authenticator
2✔
29

30
    def get_all(self) -> BlueprintFactoryResponse:
2✔
31
        """List all resource pools."""
32

33
        @authenticate(self.authenticator)
2✔
34
        async def _get_all(request: Request, user: base_models.APIUser) -> HTTPResponse:
2✔
35
            res_filter = ResourceClassesFilter.model_validate(dict(request.query_args))
2✔
36
            rps = await self.rp_repo.filter_resource_pools(api_user=user, **res_filter.dict())
2✔
37
            return json(
2✔
38
                [apispec.ResourcePoolWithIdFiltered.model_validate(r).model_dump(exclude_none=True) for r in rps]
39
            )
40

41
        return "/resource_pools", ["GET"], _get_all
2✔
42

43
    def post(self) -> BlueprintFactoryResponse:
2✔
44
        """Add a new resource pool."""
45

46
        @authenticate(self.authenticator)
2✔
47
        @only_admins
2✔
48
        @validate(json=apispec.ResourcePool)
2✔
49
        async def _post(_: Request, user: base_models.APIUser, body: apispec.ResourcePool) -> HTTPResponse:
2✔
50
            rp = models.ResourcePool.from_dict(body.model_dump(exclude_none=True))
2✔
51
            res = await self.rp_repo.insert_resource_pool(api_user=user, resource_pool=rp)
2✔
52
            return json(apispec.ResourcePoolWithId.model_validate(res).model_dump(exclude_none=True), 201)
2✔
53

54
        return "/resource_pools", ["POST"], _post
2✔
55

56
    def get_one(self) -> BlueprintFactoryResponse:
2✔
57
        """Get a specific resource pool."""
58

59
        @authenticate(self.authenticator)
2✔
60
        @validate_db_ids
2✔
61
        async def _get_one(request: Request, user: base_models.APIUser, resource_pool_id: int) -> HTTPResponse:
2✔
62
            rps: list[models.ResourcePool]
63
            rps = await self.rp_repo.get_resource_pools(
1✔
64
                api_user=user, id=resource_pool_id, name=request.args.get("name")
65
            )
66
            if len(rps) < 1:
1✔
67
                raise errors.MissingResourceError(
1✔
68
                    message=f"The resource pool with id {resource_pool_id} cannot be found."
69
                )
70
            rp = rps[0]
1✔
71
            return json(apispec.ResourcePoolWithId.model_validate(rp).model_dump(exclude_none=True))
1✔
72

73
        return "/resource_pools/<resource_pool_id>", ["GET"], _get_one
2✔
74

75
    def delete(self) -> BlueprintFactoryResponse:
2✔
76
        """Delete a specific resource pool."""
77

78
        @authenticate(self.authenticator)
2✔
79
        @only_admins
2✔
80
        @validate_db_ids
2✔
81
        async def _delete(_: Request, user: base_models.APIUser, resource_pool_id: int) -> HTTPResponse:
2✔
82
            await self.rp_repo.delete_resource_pool(api_user=user, id=resource_pool_id)
1✔
83
            return HTTPResponse(status=204)
1✔
84

85
        return "/resource_pools/<resource_pool_id>", ["DELETE"], _delete
2✔
86

87
    def put(self) -> BlueprintFactoryResponse:
2✔
88
        """Update all fields of a specific resource pool."""
89

90
        @authenticate(self.authenticator)
2✔
91
        @only_admins
2✔
92
        @validate_db_ids
2✔
93
        @validate(json=apispec.ResourcePoolPut)
2✔
94
        async def _put(
2✔
95
            _: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.ResourcePoolPut
96
        ) -> HTTPResponse:
UNCOV
97
            res = await self.rp_repo.update_resource_pool(
×
98
                api_user=user,
99
                id=resource_pool_id,
100
                **body.model_dump(exclude_none=True),
101
            )
102
            if res is None:
×
103
                raise errors.MissingResourceError(
×
104
                    message=f"The resource pool with ID {resource_pool_id} cannot be found."
105
                )
106
            return json(apispec.ResourcePoolWithId.model_validate(res).model_dump(exclude_none=True))
×
107

108
        return "/resource_pools/<resource_pool_id>", ["PUT"], _put
2✔
109

110
    def patch(self) -> BlueprintFactoryResponse:
2✔
111
        """Partially update a specific resource pool."""
112

113
        @authenticate(self.authenticator)
2✔
114
        @only_admins
2✔
115
        @validate_db_ids
2✔
116
        @validate(json=apispec.ResourcePoolPatch)
2✔
117
        async def _patch(
2✔
118
            _: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.ResourcePoolPatch
119
        ) -> HTTPResponse:
120
            res = await self.rp_repo.update_resource_pool(
×
121
                api_user=user,
122
                id=resource_pool_id,
123
                **body.model_dump(exclude_none=True),
124
            )
125
            if res is None:
×
126
                raise errors.MissingResourceError(
×
127
                    message=f"The resource pool with ID {resource_pool_id} cannot be found."
128
                )
129
            return json(apispec.ResourcePoolWithId.model_validate(res).model_dump(exclude_none=True))
×
130

131
        return "/resource_pools/<resource_pool_id>", ["PATCH"], _patch
2✔
132

133

134
@dataclass(kw_only=True)
2✔
135
class ResourcePoolUsersBP(CustomBlueprint):
2✔
136
    """Handlers for dealing with the users of individual resource pools."""
2✔
137

138
    repo: UserRepository
2✔
139
    kc_user_repo: KcUserRepo
2✔
140
    authenticator: base_models.Authenticator
2✔
141

142
    def get_all(self) -> BlueprintFactoryResponse:
2✔
143
        """Get all users of a specific resource pool."""
144

145
        @authenticate(self.authenticator)
2✔
146
        @only_admins
2✔
147
        @validate_db_ids
2✔
148
        async def _get_all(_: Request, user: base_models.APIUser, resource_pool_id: int) -> HTTPResponse:
2✔
149
            res = await self.repo.get_resource_pool_users(api_user=user, resource_pool_id=resource_pool_id)
1✔
150
            return json(
1✔
151
                [
152
                    apispec.PoolUserWithId(id=r.keycloak_id, no_default_access=r.no_default_access).model_dump(
153
                        exclude_none=True
154
                    )
155
                    for r in res.allowed
156
                ]
157
            )
158

159
        return "/resource_pools/<resource_pool_id>/users", ["GET"], _get_all
2✔
160

161
    def post(self) -> BlueprintFactoryResponse:
2✔
162
        """Add users to a specific resource pool."""
163

164
        @authenticate(self.authenticator)
2✔
165
        @only_admins
2✔
166
        @validate_db_ids
2✔
167
        async def _post(request: Request, user: base_models.APIUser, resource_pool_id: int) -> HTTPResponse:
2✔
168
            users = apispec.PoolUsersWithId.model_validate(request.json)  # validation
2✔
169
            return await self._put_post(api_user=user, resource_pool_id=resource_pool_id, body=users, post=True)
2✔
170

171
        return "/resource_pools/<resource_pool_id>/users", ["POST"], _post
2✔
172

173
    def put(self) -> BlueprintFactoryResponse:
2✔
174
        """Set the users for a specific resource pool."""
175

176
        @authenticate(self.authenticator)
2✔
177
        @only_admins
2✔
178
        @validate_db_ids
2✔
179
        async def _put(request: Request, user: base_models.APIUser, resource_pool_id: int) -> HTTPResponse:
2✔
180
            users = apispec.PoolUsersWithId.model_validate(request.json)  # validation
2✔
181
            return await self._put_post(api_user=user, resource_pool_id=resource_pool_id, body=users, post=False)
2✔
182

183
        return "/resource_pools/<resource_pool_id>/users", ["PUT"], _put
2✔
184

185
    async def _put_post(
2✔
186
        self, api_user: base_models.APIUser, resource_pool_id: int, body: apispec.PoolUsersWithId, post: bool = True
187
    ) -> HTTPResponse:
188
        user_ids_to_add = set([user.id for user in body.root])
2✔
189
        users_checks: list[UserInfo | None] = await asyncio.gather(
2✔
190
            *[self.kc_user_repo.get_user(requested_by=api_user, id=id) for id in user_ids_to_add]
191
        )
192
        existing_user_ids = set([user.id for user in users_checks if user is not None])
2✔
193
        if existing_user_ids != user_ids_to_add:
2✔
194
            missing_ids = user_ids_to_add.difference(existing_user_ids)
1✔
195
            raise errors.MissingResourceError(message=f"The users with IDs {missing_ids} cannot be found")
1✔
196
        updated_users = await self.repo.update_resource_pool_users(
1✔
197
            api_user=api_user,
198
            resource_pool_id=resource_pool_id,
199
            user_ids=user_ids_to_add,
200
            append=post,
201
        )
202
        return json(
1✔
203
            [
204
                apispec.PoolUserWithId(id=r.keycloak_id, no_default_access=r.no_default_access).model_dump(
205
                    exclude_none=True
206
                )
207
                for r in updated_users
208
            ],
209
            status=201 if post else 200,
210
        )
211

212
    def get(self) -> BlueprintFactoryResponse:
2✔
213
        """Check if a specific user has access to a resource pool."""
214

215
        @authenticate(self.authenticator)
2✔
216
        @validate_db_ids
2✔
217
        async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int, user_id: str) -> HTTPResponse:
2✔
218
            res = await self.repo.get_resource_pool_users(
×
219
                keycloak_id=user_id, resource_pool_id=resource_pool_id, api_user=user
220
            )
221
            if len(res.allowed) > 0:
×
222
                return json(
×
223
                    apispec.PoolUserWithId(
224
                        id=res.allowed[0].keycloak_id, no_default_access=res.allowed[0].no_default_access
225
                    ).model_dump(exclude_none=True)
226
                )
227
            raise errors.MissingResourceError(
×
228
                message=f"The user with id {user_id} or resource pool with id {resource_pool_id} cannot be found."
229
            )
230

231
        return "/resource_pools/<resource_pool_id>/users/<user_id>", ["GET"], _get
2✔
232

233
    def delete(self) -> BlueprintFactoryResponse:
2✔
234
        """Remove access for a specific user to a specific resource pool."""
235

236
        @authenticate(self.authenticator)
2✔
237
        @only_admins
2✔
238
        @validate_db_ids
2✔
239
        async def _delete(
2✔
240
            _: Request, user: base_models.APIUser, resource_pool_id: int, user_id: str
241
        ) -> HTTPResponse | HTTPResponse:
242
            user_exists = await self.kc_user_repo.get_user(requested_by=user, id=user_id)
2✔
243
            if not user_exists:
2✔
244
                raise errors.MissingResourceError(message=f"The user with id {user_id} cannot be found.")
1✔
245
            resource_pools = await self.repo.get_user_resource_pools(
1✔
246
                api_user=user, keycloak_id=user_id, resource_pool_id=resource_pool_id
247
            )
248
            if len(resource_pools) == 0:
1✔
249
                return HTTPResponse(status=204)
×
250
            resource_pool = resource_pools[0]
1✔
251
            if resource_pool.default:
1✔
252
                await self.repo.update_user(api_user=user, keycloak_id=user_id, no_default_access=True)
1✔
253
            else:
254
                await self.repo.delete_resource_pool_user(
1✔
255
                    resource_pool_id=resource_pool_id, keycloak_id=user_id, api_user=user
256
                )
257
            return HTTPResponse(status=204)
1✔
258

259
        return "/resource_pools/<resource_pool_id>/users/<user_id>", ["DELETE"], _delete
2✔
260

261

262
@dataclass(kw_only=True)
2✔
263
class ClassesBP(CustomBlueprint):
2✔
264
    """Handlers for dealing with resource classes of an individual resource pool."""
2✔
265

266
    repo: ResourcePoolRepository
2✔
267
    authenticator: base_models.Authenticator
2✔
268

269
    def get_all(self) -> BlueprintFactoryResponse:
2✔
270
        """Get the classes of a specific resource pool."""
271

272
        @authenticate(self.authenticator)
2✔
273
        @validate_db_ids
2✔
274
        async def _get_all(request: Request, user: base_models.APIUser, resource_pool_id: int) -> HTTPResponse:
2✔
275
            res = await self.repo.get_classes(
×
276
                api_user=user, resource_pool_id=resource_pool_id, name=request.args.get("name")
277
            )
278
            return json([apispec.ResourceClassWithId.model_validate(r).model_dump(exclude_none=True) for r in res])
×
279

280
        return "/resource_pools/<resource_pool_id>/classes", ["GET"], _get_all
2✔
281

282
    def post(self) -> BlueprintFactoryResponse:
2✔
283
        """Add a class to a specific resource pool."""
284

285
        @authenticate(self.authenticator)
2✔
286
        @only_admins
2✔
287
        @validate_db_ids
2✔
288
        @validate(json=apispec.ResourceClass)
2✔
289
        async def _post(
2✔
290
            _: Request, user: base_models.APIUser, body: apispec.ResourceClass, resource_pool_id: int
291
        ) -> HTTPResponse:
292
            cls = models.ResourceClass.from_dict(body.model_dump())
×
293
            res = await self.repo.insert_resource_class(
×
294
                api_user=user, resource_class=cls, resource_pool_id=resource_pool_id
295
            )
296
            return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True), 201)
×
297

298
        return "/resource_pools/<resource_pool_id>/classes", ["POST"], _post
2✔
299

300
    def get(self) -> BlueprintFactoryResponse:
2✔
301
        """Get a specific class of a specific resource pool."""
302

303
        @authenticate(self.authenticator)
2✔
304
        @validate_db_ids
2✔
305
        async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int) -> HTTPResponse:
2✔
306
            res = await self.repo.get_classes(api_user=user, resource_pool_id=resource_pool_id, id=class_id)
1✔
307
            if len(res) < 1:
1✔
308
                raise errors.MissingResourceError(
×
309
                    message=f"The class with id {class_id} or resource pool with id {resource_pool_id} cannot be found."
310
                )
311
            return json(apispec.ResourceClassWithId.model_validate(res[0]).model_dump(exclude_none=True))
1✔
312

313
        return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["GET"], _get
2✔
314

315
    def get_no_pool(self) -> BlueprintFactoryResponse:
2✔
316
        """Get a specific class."""
317

318
        @authenticate(self.authenticator)
2✔
319
        @validate_db_ids
2✔
320
        async def _get_no_pool(_: Request, user: base_models.APIUser, class_id: int) -> HTTPResponse:
2✔
321
            res = await self.repo.get_classes(api_user=user, id=class_id)
×
322
            if len(res) < 1:
×
323
                raise errors.MissingResourceError(message=f"The class with id {class_id} cannot be found.")
×
324
            return json(apispec.ResourceClassWithId.model_validate(res[0]).model_dump(exclude_none=True))
×
325

326
        return "/classes/<class_id>", ["GET"], _get_no_pool
2✔
327

328
    def delete(self) -> BlueprintFactoryResponse:
2✔
329
        """Delete a specific class from a specific resource pool."""
330

331
        @authenticate(self.authenticator)
2✔
332
        @only_admins
2✔
333
        @validate_db_ids
2✔
334
        async def _delete(_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int) -> HTTPResponse:
2✔
335
            await self.repo.delete_resource_class(
×
336
                api_user=user, resource_pool_id=resource_pool_id, resource_class_id=class_id
337
            )
338
            return HTTPResponse(status=204)
×
339

340
        return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["DELETE"], _delete
2✔
341

342
    def put(self) -> BlueprintFactoryResponse:
2✔
343
        """Update all fields of a specific resource class for a specific resource pool."""
344

345
        @authenticate(self.authenticator)
2✔
346
        @only_admins
2✔
347
        @validate(json=apispec.ResourceClass)
2✔
348
        async def _put(
2✔
349
            _: Request, user: base_models.APIUser, body: apispec.ResourceClass, resource_pool_id: int, class_id: int
350
        ) -> HTTPResponse:
351
            res = await self.repo.update_resource_class(
1✔
352
                user, resource_pool_id, class_id, put=True, **body.model_dump(exclude_none=True)
353
            )
354
            return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True))
1✔
355

356
        return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["PUT"], _put
2✔
357

358
    def patch(self) -> BlueprintFactoryResponse:
2✔
359
        """Partially update a specific resource class for a specific resource pool."""
360

361
        @authenticate(self.authenticator)
2✔
362
        @only_admins
2✔
363
        @validate(json=apispec.ResourceClassPatch)
2✔
364
        async def _patch(
2✔
365
            _: Request,
366
            user: base_models.APIUser,
367
            body: apispec.ResourceClassPatch,
368
            resource_pool_id: int,
369
            class_id: int,
370
        ) -> HTTPResponse:
371
            res = await self.repo.update_resource_class(
1✔
372
                user, resource_pool_id, class_id, put=False, **body.model_dump(exclude_none=True)
373
            )
374
            return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True))
1✔
375

376
        return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["PATCH"], _patch
2✔
377

378
    def get_tolerations(self) -> BlueprintFactoryResponse:
2✔
379
        """Get all tolerations of a resource class."""
380

381
        @authenticate(self.authenticator)
2✔
382
        @only_admins
2✔
383
        async def _get_tolerations(
2✔
384
            _: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
385
        ) -> HTTPResponse:
386
            res = await self.repo.get_tolerations(user, resource_pool_id, class_id)
1✔
387
            return json(list(res))
1✔
388

389
        return "/resource_pools/<resource_pool_id>/classes/<class_id>/tolerations", ["GET"], _get_tolerations
2✔
390

391
    def delete_tolerations(self) -> BlueprintFactoryResponse:
2✔
392
        """Delete all tolerations of a resource class."""
393

394
        @authenticate(self.authenticator)
2✔
395
        @only_admins
2✔
396
        async def _delete_tolerations(
2✔
397
            _: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
398
        ) -> HTTPResponse:
399
            await self.repo.delete_tolerations(user, resource_pool_id, class_id)
1✔
400
            return empty()
1✔
401

402
        return "/resource_pools/<resource_pool_id>/classes/<class_id>/tolerations", ["DELETE"], _delete_tolerations
2✔
403

404
    def get_affinities(self) -> BlueprintFactoryResponse:
2✔
405
        """Get all affinities of a resource class."""
406

407
        @authenticate(self.authenticator)
2✔
408
        @only_admins
2✔
409
        async def _get_affinities(
2✔
410
            _: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
411
        ) -> HTTPResponse:
412
            res = await self.repo.get_affinities(user, resource_pool_id, class_id)
1✔
413
            return json([apispec.NodeAffinity.model_validate(i).model_dump(exclude_none=True) for i in res])
1✔
414

415
        return "/resource_pools/<resource_pool_id>/classes/<class_id>/node_affinities", ["GET"], _get_affinities
2✔
416

417
    def delete_affinities(self) -> BlueprintFactoryResponse:
2✔
418
        """Delete all affinities of a resource class."""
419

420
        @authenticate(self.authenticator)
2✔
421
        @only_admins
2✔
422
        async def _delete_affinities(
2✔
423
            _: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
424
        ) -> HTTPResponse:
425
            await self.repo.delete_affinities(user, resource_pool_id, class_id)
1✔
426
            return empty()
1✔
427

428
        return "/resource_pools/<resource_pool_id>/classes/<class_id>/node_affinities", ["DELETE"], _delete_affinities
2✔
429

430

431
@dataclass(kw_only=True)
2✔
432
class QuotaBP(CustomBlueprint):
2✔
433
    """Handlers for dealing with a quota."""
2✔
434

435
    rp_repo: ResourcePoolRepository
2✔
436
    quota_repo: QuotaRepository
2✔
437
    authenticator: base_models.Authenticator
2✔
438

439
    def get(self) -> BlueprintFactoryResponse:
2✔
440
        """Get the quota for a specific resource pool."""
441

442
        @authenticate(self.authenticator)
2✔
443
        @validate_db_ids
2✔
444
        async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int) -> HTTPResponse:
2✔
445
            rps = await self.rp_repo.get_resource_pools(api_user=user, id=resource_pool_id)
1✔
446
            if len(rps) < 1:
1✔
447
                raise errors.MissingResourceError(
×
448
                    message=f"The resource pool with ID {resource_pool_id} cannot be found."
449
                )
450
            rp = rps[0]
1✔
451
            if rp.quota is None:
1✔
452
                raise errors.MissingResourceError(
×
453
                    message=f"The resource pool with ID {resource_pool_id} does not have a quota."
454
                )
455
            return json(apispec.QuotaWithId.model_validate(rp.quota).model_dump(exclude_none=True))
1✔
456

457
        return "/resource_pools/<resource_pool_id>/quota", ["GET"], _get
2✔
458

459
    def put(self) -> BlueprintFactoryResponse:
2✔
460
        """Update all fields of a quota of a specific resource pool."""
461

462
        @authenticate(self.authenticator)
2✔
463
        @only_admins
2✔
464
        @validate(json=apispec.QuotaWithId)
2✔
465
        async def _put(
2✔
466
            _: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.QuotaWithId
467
        ) -> HTTPResponse:
468
            return await self._put_patch(resource_pool_id, body, api_user=user)
1✔
469

470
        return "/resource_pools/<resource_pool_id>/quota", ["PUT"], _put
2✔
471

472
    def patch(self) -> BlueprintFactoryResponse:
2✔
473
        """Partially update the quota of a specific resource pool."""
474

475
        @authenticate(self.authenticator)
2✔
476
        @only_admins
2✔
477
        @validate(json=apispec.QuotaPatch)
2✔
478
        async def _patch(
2✔
479
            _: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.QuotaPatch
480
        ) -> HTTPResponse:
481
            return await self._put_patch(resource_pool_id, body, api_user=user)
1✔
482

483
        return "/resource_pools/<resource_pool_id>/quota", ["PATCH"], _patch
2✔
484

485
    async def _put_patch(
2✔
486
        self, resource_pool_id: int, body: apispec.QuotaPatch | apispec.QuotaWithId, api_user: base_models.APIUser
487
    ) -> HTTPResponse:
488
        rps = await self.rp_repo.get_resource_pools(api_user=api_user, id=resource_pool_id)
1✔
489
        if len(rps) < 1:
1✔
490
            raise errors.MissingResourceError(message=f"Cannot find the resource pool with ID {resource_pool_id}.")
×
491
        rp = rps[0]
1✔
492
        if rp.quota is None:
1✔
493
            raise errors.MissingResourceError(
×
494
                message=f"The resource pool with ID {resource_pool_id} does not have a quota."
495
            )
496
        old_quota = rp.quota
1✔
497
        new_quota = models.Quota.from_dict({**asdict(old_quota), **body.model_dump(exclude_none=True)})
1✔
498
        for rc in rp.classes:
1✔
499
            if not new_quota.is_resource_class_compatible(rc):
1✔
500
                raise errors.ValidationError(
×
501
                    message=f"The quota {new_quota} is not compatible with the resource class {rc}."
502
                )
503
        new_quota = self.quota_repo.update_quota(new_quota)
1✔
504
        return json(apispec.QuotaWithId.model_validate(new_quota).model_dump(exclude_none=True))
1✔
505

506

507
@dataclass(kw_only=True)
2✔
508
class UserResourcePoolsBP(CustomBlueprint):
2✔
509
    """Handlers for dealing with the resource pools of a specific user."""
2✔
510

511
    repo: UserRepository
2✔
512
    kc_user_repo: KcUserRepo
2✔
513
    authenticator: base_models.Authenticator
2✔
514

515
    def get(self) -> BlueprintFactoryResponse:
2✔
516
        """Get all resource pools that a specific user has access to."""
517

518
        @authenticate(self.authenticator)
2✔
519
        @only_admins
2✔
520
        async def _get(_: Request, user: base_models.APIUser, user_id: str) -> HTTPResponse:
2✔
521
            rps = await self.repo.get_user_resource_pools(keycloak_id=user_id, api_user=user)
2✔
522
            return json([apispec.ResourcePoolWithId.model_validate(rp).model_dump(exclude_none=True) for rp in rps])
2✔
523

524
        return "/users/<user_id>/resource_pools", ["GET"], _get
2✔
525

526
    def post(self) -> BlueprintFactoryResponse:
2✔
527
        """Give a specific user access to a specific resource pool."""
528

529
        @authenticate(self.authenticator)
2✔
530
        @only_admins
2✔
531
        async def _post(request: Request, user: base_models.APIUser, user_id: str) -> HTTPResponse:
2✔
532
            ids = apispec.IntegerIds.model_validate(request.json)  # validation
2✔
533
            return await self._post_put(user_id=user_id, post=True, resource_pool_ids=ids, api_user=user)
2✔
534

535
        return "/users/<user_id>/resource_pools", ["POST"], _post
2✔
536

537
    def put(self) -> BlueprintFactoryResponse:
2✔
538
        """Set the list of resource pools that a specific user has access to."""
539

540
        @authenticate(self.authenticator)
2✔
541
        @only_admins
2✔
542
        async def _put(request: Request, user: base_models.APIUser, user_id: str) -> HTTPResponse:
2✔
543
            ids = apispec.IntegerIds.model_validate(request.json)  # validation
2✔
544
            return await self._post_put(user_id=user_id, post=False, resource_pool_ids=ids, api_user=user)
2✔
545

546
        return "/users/<user_id>/resource_pools", ["PUT"], _put
2✔
547

548
    async def _post_put(
2✔
549
        self, user_id: str, resource_pool_ids: apispec.IntegerIds, api_user: base_models.APIUser, post: bool = True
550
    ) -> HTTPResponse:
551
        user_check = await self.kc_user_repo.get_user(requested_by=api_user, id=user_id)
2✔
552
        if not user_check:
2✔
553
            raise errors.MissingResourceError(message=f"User with user ID {user_id} cannot be found")
1✔
554
        rps = await self.repo.update_user_resource_pools(
1✔
555
            keycloak_id=user_id, resource_pool_ids=resource_pool_ids.root, append=post, api_user=api_user
556
        )
557
        return json(
1✔
558
            [apispec.ResourcePoolWithId.model_validate(rp).model_dump(exclude_none=True) for rp in rps],
559
            status=201 if post else 200,
560
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc