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

SwissDataScienceCenter / renku-data-services / 14377770991

10 Apr 2025 10:04AM UTC coverage: 86.244% (-0.1%) from 86.351%
14377770991

Pull #785

github

web-flow
Merge 5b3aea87d into 74eb7d965
Pull Request #785: feat: add endpoint to get v1 project properties

32 of 73 new or added lines in 6 files covered. (43.84%)

1 existing line in 1 file now uncovered.

20013 of 23205 relevant lines covered (86.24%)

1.53 hits per line

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

95.77
/components/renku_data_services/project/blueprints.py
1
"""Project blueprint."""
2

3
from dataclasses import dataclass
2✔
4
from typing import Any
2✔
5
from urllib.parse import unquote
2✔
6

7
from sanic import HTTPResponse, Request
2✔
8
from sanic.response import JSONResponse
2✔
9
from sanic_ext import validate
2✔
10
from ulid import ULID
2✔
11

12
import renku_data_services.base_models as base_models
2✔
13
from renku_data_services.authz.models import Member, Role, Visibility
2✔
14
from renku_data_services.base_api.auth import (
2✔
15
    authenticate,
16
    authenticate_2,
17
    only_authenticated,
18
    validate_path_user_id,
19
)
20
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
2✔
21
from renku_data_services.base_api.etag import extract_if_none_match, if_match_required
2✔
22
from renku_data_services.base_api.misc import validate_body_root_model, validate_query
2✔
23
from renku_data_services.base_api.pagination import PaginationRequest, paginate
2✔
24
from renku_data_services.base_models.core import Slug
2✔
25
from renku_data_services.base_models.validation import validate_and_dump, validated_json
2✔
26
from renku_data_services.data_connectors.db import DataConnectorRepository
2✔
27
from renku_data_services.errors import errors
2✔
28
from renku_data_services.project import apispec
2✔
29
from renku_data_services.project import models as project_models
2✔
30
from renku_data_services.project.core import (
2✔
31
    copy_project,
32
    get_v1_project_info,
33
    validate_project_patch,
34
    validate_session_secret_slot_patch,
35
    validate_session_secrets_patch,
36
    validate_unsaved_project,
37
    validate_unsaved_session_secret_slot,
38
)
39
from renku_data_services.project.db import (
2✔
40
    ProjectMemberRepository,
41
    ProjectMigrationRepository,
42
    ProjectRepository,
43
    ProjectSessionSecretRepository,
44
)
45
from renku_data_services.session.db import SessionRepository
2✔
46
from renku_data_services.users.db import UserRepo
2✔
47

48

49
@dataclass(kw_only=True)
2✔
50
class ProjectsBP(CustomBlueprint):
2✔
51
    """Handlers for manipulating projects."""
52

53
    project_repo: ProjectRepository
2✔
54
    project_member_repo: ProjectMemberRepository
2✔
55
    user_repo: UserRepo
2✔
56
    authenticator: base_models.Authenticator
2✔
57
    session_repo: SessionRepository
2✔
58
    data_connector_repo: DataConnectorRepository
2✔
59
    project_migration_repo: ProjectMigrationRepository
2✔
60
    internal_gitlab_authenticator: base_models.Authenticator
2✔
61
    gitlab_client: base_models.GitlabAPIProtocol
2✔
62
    core_scv_url: str | None = None
2✔
63

64
    def get_all(self) -> BlueprintFactoryResponse:
2✔
65
        """List all projects."""
66

67
        @authenticate(self.authenticator)
2✔
68
        @validate_query(query=apispec.ProjectGetQuery)
2✔
69
        @paginate
2✔
70
        async def _get_all(
2✔
71
            _: Request, user: base_models.APIUser, pagination: PaginationRequest, query: apispec.ProjectGetQuery
72
        ) -> tuple[list[dict[str, Any]], int]:
73
            projects, total_num = await self.project_repo.get_projects(
2✔
74
                user=user, pagination=pagination, namespace=query.namespace, direct_member=query.direct_member
75
            )
76
            return [validate_and_dump(apispec.Project, self._dump_project(p)) for p in projects], total_num
2✔
77

78
        return "/projects", ["GET"], _get_all
2✔
79

80
    def post(self) -> BlueprintFactoryResponse:
2✔
81
        """Create a new project."""
82

83
        @authenticate(self.authenticator)
2✔
84
        @only_authenticated
2✔
85
        @validate(json=apispec.ProjectPost)
2✔
86
        async def _post(_: Request, user: base_models.APIUser, body: apispec.ProjectPost) -> JSONResponse:
2✔
87
            new_project = validate_unsaved_project(body, created_by=user.id or "")
2✔
88
            result = await self.project_repo.insert_project(user, new_project)
2✔
89
            return validated_json(apispec.Project, self._dump_project(result), status=201)
1✔
90

91
        return "/projects", ["POST"], _post
2✔
92

93
    def get_migration(self) -> BlueprintFactoryResponse:
2✔
94
        """Get project migration by project v1 id."""
95

96
        @authenticate(self.authenticator)
2✔
97
        @only_authenticated
2✔
98
        async def _get_migration(_: Request, user: base_models.APIUser, v1_id: int) -> JSONResponse:
2✔
99
            project = await self.project_migration_repo.get_migration_by_v1_id(user, v1_id)
2✔
100
            project_dump = self._dump_project(project)
1✔
101
            return validated_json(apispec.Project, project_dump)
1✔
102

103
        return "/renku_v1_projects/<v1_id:int>/migrations", ["GET"], _get_migration
2✔
104

105
    def post_migration(self) -> BlueprintFactoryResponse:
2✔
106
        """Migrate v1 project."""
107

108
        @authenticate(self.authenticator)
2✔
109
        @only_authenticated
2✔
110
        @validate(json=apispec.ProjectMigrationPost)
2✔
111
        async def _post_migration(
2✔
112
            _: Request, user: base_models.APIUser, v1_id: int, body: apispec.ProjectMigrationPost
113
        ) -> JSONResponse:
114
            new_project = validate_unsaved_project(body.project, created_by=user.id or "")
2✔
115

116
            result = await self.project_migration_repo.migrate_v1_project(
2✔
117
                user, project=new_project, project_v1_id=v1_id, session_launcher=body.session_launcher
118
            )
119
            return validated_json(apispec.Project, self._dump_project(result), status=201)
1✔
120

121
        return "/renku_v1_projects/<v1_id:int>/migrations", ["POST"], _post_migration
2✔
122

123
    def get_v1_project_by_path(self) -> BlueprintFactoryResponse:
2✔
124
        """Get information about a v1 project from the path."""
125

126
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
127
        async def _get_v1_project_by_path(
2✔
128
            _: Request, user: base_models.APIUser, internal_gitlab_user: base_models.APIUser, path: str
129
        ) -> JSONResponse:
NEW
130
            if self.core_scv_url is None:
×
NEW
131
                raise errors.MissingResourceError(
×
132
                    message="The core service url is not defined so we cannot get project information."
133
                )
NEW
134
            decoded_path = unquote(path)
×
NEW
135
            output = await get_v1_project_info(
×
136
                user, internal_gitlab_user, decoded_path, self.gitlab_client, self.core_scv_url
137
            )
NEW
138
            return validated_json(apispec.V1Project, output)
×
139

140
        return "/renku_v1_projects/path/{path:str}", ["GET"], _get_v1_project_by_path
2✔
141

142
    def get_project_migration_info(self) -> BlueprintFactoryResponse:
2✔
143
        """Get project migration by project v2 id."""
144

145
        @authenticate(self.authenticator)
2✔
146
        @only_authenticated
2✔
147
        async def _get_project_migration_info(
2✔
148
            _: Request, user: base_models.APIUser, project_id: ULID
149
        ) -> JSONResponse | HTTPResponse:
150
            migration_info = await self.project_migration_repo.get_migration_by_project_id(user, project_id)
2✔
151

152
            if migration_info and isinstance(migration_info, project_models.ProjectMigrationInfo):
1✔
153
                dump_migration_info = dict(
1✔
154
                    project_id=migration_info.project_id,
155
                    v1_id=migration_info.v1_id,
156
                    launcher_id=migration_info.launcher_id,
157
                )
158
                return validated_json(apispec.ProjectMigrationInfo, dump_migration_info)
1✔
159

160
            return HTTPResponse(status=404)
×
161

162
        return "/projects/<project_id:ulid>/migration_info", ["GET"], _get_project_migration_info
2✔
163

164
    def copy(self) -> BlueprintFactoryResponse:
2✔
165
        """Create a new project by copying it from a template project."""
166

167
        @authenticate(self.authenticator)
2✔
168
        @only_authenticated
2✔
169
        @validate(json=apispec.ProjectPost)
2✔
170
        async def _copy(
2✔
171
            _: Request, user: base_models.APIUser, project_id: ULID, body: apispec.ProjectPost
172
        ) -> JSONResponse:
173
            project = await copy_project(
2✔
174
                project_id=project_id,
175
                user=user,
176
                name=body.name,
177
                namespace=body.namespace,
178
                slug=body.slug,
179
                description=body.description,
180
                repositories=body.repositories,
181
                visibility=Visibility(body.visibility.value) if body.visibility is not None else None,
182
                keywords=[kw.root for kw in body.keywords] if body.keywords is not None else [],
183
                secrets_mount_directory=body.secrets_mount_directory,
184
                project_repo=self.project_repo,
185
                session_repo=self.session_repo,
186
                data_connector_repo=self.data_connector_repo,
187
            )
188
            return validated_json(apispec.Project, self._dump_project(project), status=201)
1✔
189

190
        return "/projects/<project_id:ulid>/copies", ["POST"], _copy
2✔
191

192
    def get_all_copies(self) -> BlueprintFactoryResponse:
2✔
193
        """Get all copies of a specific project that the user has access to."""
194

195
        @authenticate(self.authenticator)
2✔
196
        @only_authenticated
2✔
197
        @validate(query=apispec.ProjectsProjectIdCopiesGetParametersQuery)
2✔
198
        async def _get_all_copies(
2✔
199
            _: Request,
200
            user: base_models.APIUser,
201
            project_id: ULID,
202
            query: apispec.ProjectsProjectIdCopiesGetParametersQuery,
203
        ) -> JSONResponse:
204
            projects = await self.project_repo.get_all_copied_projects(
2✔
205
                user=user, project_id=project_id, only_writable=query.writable
206
            )
207
            projects_dump = [self._dump_project(p) for p in projects]
1✔
208
            return validated_json(apispec.ProjectsList, projects_dump)
1✔
209

210
        return "/projects/<project_id:ulid>/copies", ["GET"], _get_all_copies
2✔
211

212
    def get_one(self) -> BlueprintFactoryResponse:
2✔
213
        """Get a specific project."""
214

215
        @authenticate(self.authenticator)
2✔
216
        @extract_if_none_match
2✔
217
        @validate_query(query=apispec.ProjectsProjectIdGetParametersQuery)
2✔
218
        async def _get_one(
2✔
219
            _: Request,
220
            user: base_models.APIUser,
221
            project_id: ULID,
222
            etag: str | None,
223
            query: apispec.ProjectsProjectIdGetParametersQuery,
224
        ) -> JSONResponse | HTTPResponse:
225
            with_documentation = query.with_documentation is True
2✔
226
            project = await self.project_repo.get_project(
2✔
227
                user=user, project_id=project_id, with_documentation=with_documentation
228
            )
229

230
            if project.etag is not None and project.etag == etag:
1✔
231
                return HTTPResponse(status=304)
×
232

233
            headers = {"ETag": project.etag} if project.etag is not None else None
1✔
234
            return validated_json(
1✔
235
                apispec.Project, self._dump_project(project, with_documentation=with_documentation), headers=headers
236
            )
237

238
        return "/projects/<project_id:ulid>", ["GET"], _get_one
2✔
239

240
    def get_one_by_namespace_slug(self) -> BlueprintFactoryResponse:
2✔
241
        """Get a specific project by namespace/slug."""
242

243
        @authenticate(self.authenticator)
2✔
244
        @extract_if_none_match
2✔
245
        @validate_query(query=apispec.NamespacesNamespaceProjectsSlugGetParametersQuery)
2✔
246
        async def _get_one_by_namespace_slug(
2✔
247
            _: Request,
248
            user: base_models.APIUser,
249
            namespace: str,
250
            slug: Slug,
251
            etag: str | None,
252
            query: apispec.NamespacesNamespaceProjectsSlugGetParametersQuery,
253
        ) -> JSONResponse | HTTPResponse:
254
            with_documentation = query.with_documentation is True
2✔
255
            project = await self.project_repo.get_project_by_namespace_slug(
2✔
256
                user=user, namespace=namespace, slug=slug, with_documentation=with_documentation
257
            )
258

259
            if project.etag is not None and project.etag == etag:
1✔
260
                return HTTPResponse(status=304)
×
261

262
            headers = {"ETag": project.etag} if project.etag is not None else None
1✔
263
            return validated_json(
1✔
264
                apispec.Project,
265
                self._dump_project(project, with_documentation=with_documentation),
266
                headers=headers,
267
            )
268

269
        return "/namespaces/<namespace>/projects/<slug:renku_slug>", ["GET"], _get_one_by_namespace_slug
2✔
270

271
    def delete(self) -> BlueprintFactoryResponse:
2✔
272
        """Delete a specific project."""
273

274
        @authenticate(self.authenticator)
2✔
275
        @only_authenticated
2✔
276
        async def _delete(_: Request, user: base_models.APIUser, project_id: ULID) -> HTTPResponse:
2✔
277
            await self.project_repo.delete_project(user=user, project_id=project_id)
2✔
278
            return HTTPResponse(status=204)
1✔
279

280
        return "/projects/<project_id:ulid>", ["DELETE"], _delete
2✔
281

282
    def patch(self) -> BlueprintFactoryResponse:
2✔
283
        """Partially update a specific project."""
284

285
        @authenticate(self.authenticator)
2✔
286
        @only_authenticated
2✔
287
        @if_match_required
2✔
288
        @validate(json=apispec.ProjectPatch)
2✔
289
        async def _patch(
2✔
290
            _: Request, user: base_models.APIUser, project_id: ULID, body: apispec.ProjectPatch, etag: str
291
        ) -> JSONResponse:
292
            project_patch = validate_project_patch(body)
2✔
293
            project_update = await self.project_repo.update_project(
2✔
294
                user=user, project_id=project_id, etag=etag, patch=project_patch
295
            )
296

297
            if not isinstance(project_update, project_models.ProjectUpdate):
1✔
298
                raise errors.ProgrammingError(
×
299
                    message="Expected the result of a project update to be ProjectUpdate but instead "
300
                    f"got {type(project_update)}"
301
                )
302

303
            updated_project = project_update.new
1✔
304
            return validated_json(apispec.Project, self._dump_project(updated_project))
1✔
305

306
        return "/projects/<project_id:ulid>", ["PATCH"], _patch
2✔
307

308
    def get_all_members(self) -> BlueprintFactoryResponse:
2✔
309
        """List all project members."""
310

311
        @authenticate(self.authenticator)
2✔
312
        async def _get_all_members(_: Request, user: base_models.APIUser, project_id: ULID) -> JSONResponse:
2✔
313
            members = await self.project_member_repo.get_members(user, project_id)
2✔
314

315
            users = []
1✔
316

317
            for member in members:
1✔
318
                user_id = member.user_id
1✔
319
                user_info = await self.user_repo.get_user(id=user_id)
1✔
320
                if not user_info:
1✔
321
                    raise errors.MissingResourceError(message=f"The user with ID {user_id} cannot be found.")
×
322
                namespace_info = user_info.namespace
1✔
323

324
                user_with_id = apispec.ProjectMemberResponse(
1✔
325
                    id=user_id,
326
                    namespace=namespace_info.path.first.value,
327
                    first_name=user_info.first_name,
328
                    last_name=user_info.last_name,
329
                    role=apispec.Role(member.role.value),
330
                ).model_dump(exclude_none=True, mode="json")
331
                users.append(user_with_id)
1✔
332

333
            return validated_json(apispec.ProjectMemberListResponse, users)
1✔
334

335
        return "/projects/<project_id:ulid>/members", ["GET"], _get_all_members
2✔
336

337
    def update_members(self) -> BlueprintFactoryResponse:
2✔
338
        """Update or add project members."""
339

340
        @authenticate(self.authenticator)
2✔
341
        @validate_body_root_model(json=apispec.ProjectMemberListPatchRequest)
2✔
342
        async def _update_members(
2✔
343
            _: Request, user: base_models.APIUser, project_id: ULID, body: apispec.ProjectMemberListPatchRequest
344
        ) -> HTTPResponse:
345
            members = [Member(Role(i.role.value), i.id, project_id) for i in body.root]
2✔
346
            await self.project_member_repo.update_members(user, project_id, members)
2✔
347
            return HTTPResponse(status=200)
1✔
348

349
        return "/projects/<project_id:ulid>/members", ["PATCH"], _update_members
2✔
350

351
    def delete_member(self) -> BlueprintFactoryResponse:
2✔
352
        """Delete a specific project."""
353

354
        @authenticate(self.authenticator)
2✔
355
        @validate_path_user_id
2✔
356
        async def _delete_member(
2✔
357
            _: Request, user: base_models.APIUser, project_id: ULID, member_id: str
358
        ) -> HTTPResponse:
359
            await self.project_member_repo.delete_members(user, project_id, [member_id])
2✔
360
            return HTTPResponse(status=204)
1✔
361

362
        return "/projects/<project_id:ulid>/members/<member_id>", ["DELETE"], _delete_member
2✔
363

364
    def get_permissions(self) -> BlueprintFactoryResponse:
2✔
365
        """Get the permissions of the current user on the project."""
366

367
        @authenticate(self.authenticator)
2✔
368
        async def _get_permissions(_: Request, user: base_models.APIUser, project_id: ULID) -> JSONResponse:
2✔
369
            permissions = await self.project_repo.get_project_permissions(user=user, project_id=project_id)
2✔
370
            return validated_json(apispec.ProjectPermissions, permissions)
1✔
371

372
        return "/projects/<project_id:ulid>/permissions", ["GET"], _get_permissions
2✔
373

374
    @staticmethod
2✔
375
    def _dump_project(project: project_models.Project, with_documentation: bool = False) -> dict[str, Any]:
2✔
376
        """Dumps a project for API responses."""
377
        result = dict(
1✔
378
            id=project.id,
379
            name=project.name,
380
            namespace=project.namespace.path.serialize(),
381
            slug=project.slug,
382
            creation_date=project.creation_date.isoformat(),
383
            created_by=project.created_by,
384
            updated_at=project.updated_at.isoformat() if project.updated_at else None,
385
            repositories=project.repositories,
386
            visibility=project.visibility.value,
387
            description=project.description,
388
            etag=project.etag,
389
            keywords=project.keywords or [],
390
            template_id=project.template_id,
391
            is_template=project.is_template,
392
            secrets_mount_directory=str(project.secrets_mount_directory),
393
        )
394
        if with_documentation:
1✔
395
            result = dict(result, documentation=project.documentation)
1✔
396
        return result
1✔
397

398

399
@dataclass(kw_only=True)
2✔
400
class ProjectSessionSecretBP(CustomBlueprint):
2✔
401
    """Handlers for manipulating session secrets in a project."""
402

403
    session_secret_repo: ProjectSessionSecretRepository
2✔
404
    authenticator: base_models.Authenticator
2✔
405

406
    def get_session_secret_slots(self) -> BlueprintFactoryResponse:
2✔
407
        """Get the session secret slots of a project."""
408

409
        @authenticate(self.authenticator)
2✔
410
        async def _get_session_secret_slots(_: Request, user: base_models.APIUser, project_id: ULID) -> JSONResponse:
2✔
411
            secret_slots = await self.session_secret_repo.get_all_session_secret_slots_from_project(
2✔
412
                user=user, project_id=project_id
413
            )
414
            return validated_json(apispec.SessionSecretSlotList, secret_slots)
1✔
415

416
        return "/projects/<project_id:ulid>/session_secret_slots", ["GET"], _get_session_secret_slots
2✔
417

418
    def post_session_secret_slot(self) -> BlueprintFactoryResponse:
2✔
419
        """Create a new session secret slot on a project."""
420

421
        @authenticate(self.authenticator)
2✔
422
        @only_authenticated
2✔
423
        @validate(json=apispec.SessionSecretSlotPost)
2✔
424
        async def _post_session_secret_slot(
2✔
425
            _: Request, user: base_models.APIUser, body: apispec.SessionSecretSlotPost
426
        ) -> JSONResponse:
427
            unsaved_secret_slot = validate_unsaved_session_secret_slot(body)
2✔
428
            secret_slot = await self.session_secret_repo.insert_session_secret_slot(
2✔
429
                user=user, secret_slot=unsaved_secret_slot
430
            )
431
            return validated_json(apispec.SessionSecretSlot, secret_slot, status=201)
1✔
432

433
        return "/session_secret_slots", ["POST"], _post_session_secret_slot
2✔
434

435
    def get_session_secret_slot(self) -> BlueprintFactoryResponse:
2✔
436
        """Get the details of a session secret slot."""
437

438
        @authenticate(self.authenticator)
2✔
439
        @extract_if_none_match
2✔
440
        async def _get_session_secret_slot(
2✔
441
            _: Request, user: base_models.APIUser, slot_id: ULID, etag: str | None
442
        ) -> HTTPResponse:
443
            secret_slot = await self.session_secret_repo.get_session_secret_slot(user=user, slot_id=slot_id)
2✔
444

445
            if secret_slot.etag == etag:
1✔
446
                return HTTPResponse(status=304)
×
447

448
            return validated_json(apispec.SessionSecretSlot, secret_slot)
1✔
449

450
        return "/session_secret_slots/<slot_id:ulid>", ["GET"], _get_session_secret_slot
2✔
451

452
    def patch_session_secret_slot(self) -> BlueprintFactoryResponse:
2✔
453
        """Update specific fields of an existing session secret slot."""
454

455
        @authenticate(self.authenticator)
2✔
456
        @only_authenticated
2✔
457
        @if_match_required
2✔
458
        @validate(json=apispec.SessionSecretSlotPatch)
2✔
459
        async def _patch_session_secret_slot(
2✔
460
            _: Request,
461
            user: base_models.APIUser,
462
            slot_id: ULID,
463
            body: apispec.SessionSecretSlotPatch,
464
            etag: str,
465
        ) -> JSONResponse:
466
            secret_slot_patch = validate_session_secret_slot_patch(body)
2✔
467
            secret_slot = await self.session_secret_repo.update_session_secret_slot(
2✔
468
                user=user, slot_id=slot_id, patch=secret_slot_patch, etag=etag
469
            )
470
            return validated_json(apispec.SessionSecretSlot, secret_slot)
1✔
471

472
        return "/session_secret_slots/<slot_id:ulid>", ["PATCH"], _patch_session_secret_slot
2✔
473

474
    def delete_session_secret_slot(self) -> BlueprintFactoryResponse:
2✔
475
        """Remove a session secret slot."""
476

477
        @authenticate(self.authenticator)
2✔
478
        @only_authenticated
2✔
479
        async def _delete_session_secret_slot(_: Request, user: base_models.APIUser, slot_id: ULID) -> HTTPResponse:
2✔
480
            await self.session_secret_repo.delete_session_secret_slot(user=user, slot_id=slot_id)
2✔
481
            return HTTPResponse(status=204)
2✔
482

483
        return "/session_secret_slots/<slot_id:ulid>", ["DELETE"], _delete_session_secret_slot
2✔
484

485
    def get_session_secrets(self) -> BlueprintFactoryResponse:
2✔
486
        """Get the current user's secrets of a project."""
487

488
        @authenticate(self.authenticator)
2✔
489
        @only_authenticated
2✔
490
        async def _get_session_secrets(_: Request, user: base_models.APIUser, project_id: ULID) -> JSONResponse:
2✔
491
            secrets = await self.session_secret_repo.get_all_session_secrets_from_project(
2✔
492
                user=user, project_id=project_id
493
            )
494
            return validated_json(apispec.SessionSecretList, secrets)
1✔
495

496
        return "/projects/<project_id:ulid>/session_secrets", ["GET"], _get_session_secrets
2✔
497

498
    def patch_session_secrets(self) -> BlueprintFactoryResponse:
2✔
499
        """Save user secrets for a project."""
500

501
        @authenticate(self.authenticator)
2✔
502
        @only_authenticated
2✔
503
        @validate_body_root_model(json=apispec.SessionSecretPatchList)
2✔
504
        async def _patch_session_secrets(
2✔
505
            _: Request, user: base_models.APIUser, project_id: ULID, body: apispec.SessionSecretPatchList
506
        ) -> JSONResponse:
507
            secrets_patch = validate_session_secrets_patch(body)
2✔
508
            secrets = await self.session_secret_repo.patch_session_secrets(
2✔
509
                user=user, project_id=project_id, secrets=secrets_patch
510
            )
511
            return validated_json(apispec.SessionSecretList, secrets)
1✔
512

513
        return "/projects/<project_id:ulid>/session_secrets", ["PATCH"], _patch_session_secrets
2✔
514

515
    def delete_session_secrets(self) -> BlueprintFactoryResponse:
2✔
516
        """Remove all user secrets for a project."""
517

518
        @authenticate(self.authenticator)
2✔
519
        @only_authenticated
2✔
520
        async def _delete_session_secrets(_: Request, user: base_models.APIUser, project_id: ULID) -> HTTPResponse:
2✔
521
            await self.session_secret_repo.delete_session_secrets(user=user, project_id=project_id)
2✔
522
            return HTTPResponse(status=204)
2✔
523

524
        return "/projects/<project_id:ulid>/session_secrets", ["DELETE"], _delete_session_secrets
2✔
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