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

SwissDataScienceCenter / renku-data-services / 10281053350

07 Aug 2024 08:41AM UTC coverage: 90.451% (-0.02%) from 90.467%
10281053350

Pull #340

github

web-flow
Merge 381662897 into d92932700
Pull Request #340: test: fix schemathesis query parameters test

124 of 124 new or added lines in 18 files covered. (100.0%)

9 existing lines in 4 files now uncovered.

9084 of 10043 relevant lines covered (90.45%)

1.61 hits per line

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

92.68
/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, validate_query
2✔
14
from renku_data_services.crc import apispec, models
2✔
15
from renku_data_services.crc.db import ResourcePoolRepository, UserRepository
2✔
16
from renku_data_services.k8s.quota import QuotaRepository
2✔
17
from renku_data_services.users.db import UserRepo as KcUserRepo
2✔
18
from renku_data_services.users.models import UserWithNamespace
2✔
19

20

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

135

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

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

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

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

161
        return "/resource_pools/<resource_pool_id>/users", ["GET"], _get_all
2✔
162

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

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

173
        return "/resource_pools/<resource_pool_id>/users", ["POST"], _post
2✔
174

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

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

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

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

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

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

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

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

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

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

263

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

268
    repo: ResourcePoolRepository
2✔
269
    authenticator: base_models.Authenticator
2✔
270

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

274
        @authenticate(self.authenticator)
2✔
275
        @validate_query(query=apispec.ResourceClassParams)
2✔
276
        @validate_db_ids
2✔
277
        async def _get_all(
2✔
278
            request: Request, user: base_models.APIUser, resource_pool_id: int, query: apispec.ResourceClassParams
279
        ) -> HTTPResponse:
280
            res = await self.repo.get_classes(api_user=user, resource_pool_id=resource_pool_id, name=query.name)
1✔
281
            return json([apispec.ResourceClassWithId.model_validate(r).model_dump(exclude_none=True) for r in res])
1✔
282

283
        return "/resource_pools/<resource_pool_id>/classes", ["GET"], _get_all
2✔
284

285
    def post(self) -> BlueprintFactoryResponse:
2✔
286
        """Add a class to a specific resource pool."""
287

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

301
        return "/resource_pools/<resource_pool_id>/classes", ["POST"], _post
2✔
302

303
    def get(self) -> BlueprintFactoryResponse:
2✔
304
        """Get a specific class of a specific resource pool."""
305

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

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

318
    def get_no_pool(self) -> BlueprintFactoryResponse:
2✔
319
        """Get a specific class."""
320

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

328
        return "/classes/<class_id>", ["GET"], _get_no_pool
2✔
329

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

422
        @authenticate(self.authenticator)
2✔
423
        @only_admins
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."""
2✔
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 json(apispec.QuotaWithId.model_validate(rp.quota).model_dump(exclude_none=True))
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(json=apispec.QuotaWithId)
2✔
467
        async def _put(
2✔
468
            _: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.QuotaWithId
469
        ) -> HTTPResponse:
470
            return await self._put_patch(resource_pool_id, body, api_user=user)
2✔
471

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

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

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

485
        return "/resource_pools/<resource_pool_id>/quota", ["PATCH"], _patch
2✔
486

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

508

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

513
    repo: UserRepository
2✔
514
    kc_user_repo: KcUserRepo
2✔
515
    authenticator: base_models.Authenticator
2✔
516

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

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

526
        return "/users/<user_id>/resource_pools", ["GET"], _get
2✔
527

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

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

537
        return "/users/<user_id>/resource_pools", ["POST"], _post
2✔
538

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

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

548
        return "/users/<user_id>/resource_pools", ["PUT"], _put
2✔
549

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