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

SwissDataScienceCenter / renku-data-services / 12318338392

13 Dec 2024 03:21PM UTC coverage: 86.388% (-0.001%) from 86.389%
12318338392

Pull #572

github

web-flow
Merge 0eb4f5dce into 9cb5d8146
Pull Request #572: feat: manage v1 sessions

17 of 32 new or added lines in 5 files covered. (53.13%)

4 existing lines in 4 files now uncovered.

14641 of 16948 relevant lines covered (86.39%)

1.52 hits per line

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

93.06
/components/renku_data_services/crc/blueprints.py
1
"""Compute resource control (CRC) app."""
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_body_root_model, validate_db_ids, validate_query
2✔
14
from renku_data_services.base_models.validation import validated_json
2✔
15
from renku_data_services.crc import apispec, models
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."""
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
        @validate_query(query=apispec.ResourcePoolsParams)
2✔
35
        async def _get_all(
2✔
36
            request: Request, user: base_models.APIUser, query: apispec.ResourcePoolsParams
37
        ) -> HTTPResponse:
38
            rps = await self.rp_repo.filter_resource_pools(api_user=user, **query.model_dump())
2✔
39
            return validated_json(apispec.ResourcePoolsWithIdFiltered, rps)
2✔
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 validated_json(apispec.ResourcePoolWithId, res, status=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_query(query=apispec.UserResourceParams)
2✔
61
        @validate_db_ids
2✔
62
        async def _get_one(
2✔
63
            request: Request, user: base_models.APIUser, resource_pool_id: int, query: apispec.UserResourceParams
64
        ) -> HTTPResponse:
65
            rps: list[models.ResourcePool]
66
            rps = await self.rp_repo.get_resource_pools(api_user=user, id=resource_pool_id, name=query.name)
2✔
67
            if len(rps) < 1:
2✔
68
                raise errors.MissingResourceError(
2✔
69
                    message=f"The resource pool with id {resource_pool_id} cannot be found."
70
                )
71
            rp = rps[0]
1✔
72
            return validated_json(apispec.ResourcePoolWithId, rp)
1✔
73

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

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

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

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

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

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

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

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

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

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

134

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

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

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

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

156
        return "/resource_pools/<resource_pool_id>/users", ["GET"], _get_all
2✔
157

158
    def post(self) -> BlueprintFactoryResponse:
2✔
159
        """Add users to a specific resource pool."""
160

161
        @authenticate(self.authenticator)
2✔
162
        @only_admins
2✔
163
        @validate_db_ids
2✔
164
        @validate_body_root_model(json=apispec.PoolUsersWithId)
2✔
165
        async def _post(
2✔
166
            _: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.PoolUsersWithId
167
        ) -> HTTPResponse:
168
            return await self._put_post(api_user=user, resource_pool_id=resource_pool_id, body=body, post=True)
2✔
169

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

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

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

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

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

209
    def get(self) -> BlueprintFactoryResponse:
2✔
210
        """Check if a specific user has access to a resource pool."""
211

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

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

229
    def delete(self) -> BlueprintFactoryResponse:
2✔
230
        """Remove access for a specific user to a specific resource pool."""
231

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

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

257

258
@dataclass(kw_only=True)
2✔
259
class ClassesBP(CustomBlueprint):
2✔
260
    """Handlers for dealing with resource classes of an individual resource pool."""
261

262
    repo: ResourcePoolRepository
2✔
263
    authenticator: base_models.Authenticator
2✔
264

265
    def get_all(self) -> BlueprintFactoryResponse:
2✔
266
        """Get the classes of a specific resource pool."""
267

268
        @authenticate(self.authenticator)
2✔
269
        @validate_query(query=apispec.ResourceClassParams)
2✔
270
        @validate_db_ids
2✔
271
        async def _get_all(
2✔
272
            request: Request, user: base_models.APIUser, resource_pool_id: int, query: apispec.ResourceClassParams
273
        ) -> HTTPResponse:
274
            res = await self.repo.get_classes(api_user=user, resource_pool_id=resource_pool_id, name=query.name)
1✔
275
            return validated_json(apispec.ResourceClassesWithIdResponse, res)
1✔
276

277
        return "/resource_pools/<resource_pool_id>/classes", ["GET"], _get_all
2✔
278

279
    def post(self) -> BlueprintFactoryResponse:
2✔
280
        """Add a class to a specific resource pool."""
281

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

295
        return "/resource_pools/<resource_pool_id>/classes", ["POST"], _post
2✔
296

297
    def get(self) -> BlueprintFactoryResponse:
2✔
298
        """Get a specific class of a specific resource pool."""
299

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

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

312
    def get_no_pool(self) -> BlueprintFactoryResponse:
2✔
313
        """Get a specific class."""
314

315
        @validate_db_ids
2✔
316
        async def _get_no_pool(_: Request, class_id: int) -> HTTPResponse:
2✔
317
            res = await self.repo.get_classes(api_user=None, id=class_id)
×
318
            if len(res) < 1:
×
319
                raise errors.MissingResourceError(message=f"The class with id {class_id} cannot be found.")
×
320
            return validated_json(apispec.ResourceClassWithId, res[0])
×
321

322
        return "/classes/<class_id>", ["GET"], _get_no_pool
2✔
323

324
    def delete(self) -> BlueprintFactoryResponse:
2✔
325
        """Delete a specific class from a specific resource pool."""
326

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

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

338
    def put(self) -> BlueprintFactoryResponse:
2✔
339
        """Update all fields of a specific resource class for a specific resource pool."""
340

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

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

355
    def patch(self) -> BlueprintFactoryResponse:
2✔
356
        """Partially update a specific resource class for a specific resource pool."""
357

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

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

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

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

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

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

393
        @authenticate(self.authenticator)
2✔
394
        @only_admins
2✔
395
        @validate_db_ids
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)
2✔
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
        @validate_db_ids
2✔
410
        async def _get_affinities(
2✔
411
            _: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
412
        ) -> HTTPResponse:
413
            res = await self.repo.get_affinities(user, resource_pool_id, class_id)
1✔
414
            return validated_json(apispec.NodeAffinityListResponse, res)
1✔
415

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

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

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

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

432

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

437
    rp_repo: ResourcePoolRepository
2✔
438
    quota_repo: QuotaRepository
2✔
439
    authenticator: base_models.Authenticator
2✔
440

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

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

459
        return "/resource_pools/<resource_pool_id>/quota", ["GET"], _get
2✔
460

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

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

473
        return "/resource_pools/<resource_pool_id>/quota", ["PUT"], _put
2✔
474

475
    def patch(self) -> BlueprintFactoryResponse:
2✔
476
        """Partially update the quota of a specific resource pool."""
477

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

487
        return "/resource_pools/<resource_pool_id>/quota", ["PATCH"], _patch
2✔
488

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

510

511
@dataclass(kw_only=True)
2✔
512
class UserResourcePoolsBP(CustomBlueprint):
2✔
513
    """Handlers for dealing with the resource pools of a specific user."""
514

515
    repo: UserRepository
2✔
516
    kc_user_repo: KcUserRepo
2✔
517
    authenticator: base_models.Authenticator
2✔
518

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

522
        @authenticate(self.authenticator)
2✔
523
        @only_admins
2✔
524
        async def _get(_: Request, user: base_models.APIUser, user_id: str) -> HTTPResponse:
2✔
525
            rps = await self.repo.get_user_resource_pools(keycloak_id=user_id, api_user=user)
2✔
526
            return validated_json(apispec.ResourcePoolsWithId, rps)
2✔
527

528
        return "/users/<user_id>/resource_pools", ["GET"], _get
2✔
529

530
    def post(self) -> BlueprintFactoryResponse:
2✔
531
        """Give a specific user access to a specific resource pool."""
532

533
        @authenticate(self.authenticator)
2✔
534
        @only_admins
2✔
535
        @validate_body_root_model(json=apispec.IntegerIds)
2✔
536
        async def _post(_: Request, user: base_models.APIUser, user_id: str, body: apispec.IntegerIds) -> HTTPResponse:
2✔
537
            return await self._post_put(user_id=user_id, post=True, resource_pool_ids=body, api_user=user)
2✔
538

539
        return "/users/<user_id>/resource_pools", ["POST"], _post
2✔
540

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

544
        @authenticate(self.authenticator)
2✔
545
        @only_admins
2✔
546
        @validate_body_root_model(json=apispec.IntegerIds)
2✔
547
        async def _put(_: Request, user: base_models.APIUser, user_id: str, body: apispec.IntegerIds) -> HTTPResponse:
2✔
548
            return await self._post_put(user_id=user_id, post=False, resource_pool_ids=body, api_user=user)
2✔
549

550
        return "/users/<user_id>/resource_pools", ["PUT"], _put
2✔
551

552
    async def _post_put(
2✔
553
        self, user_id: str, resource_pool_ids: apispec.IntegerIds, api_user: base_models.APIUser, post: bool = True
554
    ) -> HTTPResponse:
555
        user_check = await self.kc_user_repo.get_user(id=user_id)
2✔
556
        if not user_check:
2✔
557
            raise errors.MissingResourceError(message=f"User with user ID {user_id} cannot be found")
1✔
558
        rps = await self.repo.update_user_resource_pools(
1✔
559
            keycloak_id=user_id, resource_pool_ids=resource_pool_ids.root, append=post, api_user=api_user
560
        )
561
        return validated_json(
1✔
562
            apispec.ResourcePoolsWithId,
563
            rps,
564
            status=201 if post else 200,
565
        )
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