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

SwissDataScienceCenter / renku-data-services / 11288123511

11 Oct 2024 07:23AM UTC coverage: 90.667% (+0.2%) from 90.477%
11288123511

Pull #407

github

web-flow
Merge 20c6c8af6 into 5b095d795
Pull Request #407: feat!: add data connectors

1226 of 1325 new or added lines in 28 files covered. (92.53%)

3 existing lines in 3 files now uncovered.

10589 of 11679 relevant lines covered (90.67%)

1.6 hits per line

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

98.58
/components/renku_data_services/data_connectors/blueprints.py
1
"""Data connectors blueprint."""
2✔
2

3
from dataclasses import dataclass
2✔
4
from typing import Any
2✔
5

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

11
from renku_data_services import base_models
2✔
12
from renku_data_services.base_api.auth import (
2✔
13
    authenticate,
14
    only_authenticated,
15
)
16
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
2✔
17
from renku_data_services.base_api.etag import extract_if_none_match, if_match_required
2✔
18
from renku_data_services.base_api.misc import validate_body_root_model, validate_query
2✔
19
from renku_data_services.base_api.pagination import PaginationRequest, paginate
2✔
20
from renku_data_services.base_models.validation import validate_and_dump, validated_json
2✔
21
from renku_data_services.data_connectors import apispec, models
2✔
22
from renku_data_services.data_connectors.core import (
2✔
23
    dump_storage_with_sensitive_fields,
24
    validate_data_connector_patch,
25
    validate_data_connector_secrets_patch,
26
    validate_unsaved_data_connector,
27
)
28
from renku_data_services.data_connectors.db import (
2✔
29
    DataConnectorProjectLinkRepository,
30
    DataConnectorRepository,
31
    DataConnectorSecretRepository,
32
)
33
from renku_data_services.storage.rclone import RCloneValidator
2✔
34

35

36
@dataclass(kw_only=True)
2✔
37
class DataConnectorsBP(CustomBlueprint):
2✔
38
    """Handlers for manipulating data connectors."""
2✔
39

40
    data_connector_repo: DataConnectorRepository
2✔
41
    data_connector_to_project_link_repo: DataConnectorProjectLinkRepository
2✔
42
    data_connector_secret_repo: DataConnectorSecretRepository
2✔
43
    authenticator: base_models.Authenticator
2✔
44

45
    def get_all(self) -> BlueprintFactoryResponse:
2✔
46
        """List data connectors."""
47

48
        @authenticate(self.authenticator)
2✔
49
        @validate_query(query=apispec.DataConnectorsGetQuery)
2✔
50
        @paginate
2✔
51
        async def _get_all(
2✔
52
            _: Request,
53
            user: base_models.APIUser,
54
            pagination: PaginationRequest,
55
            query: apispec.DataConnectorsGetQuery,
56
            validator: RCloneValidator,
57
        ) -> tuple[list[dict[str, Any]], int]:
58
            data_connectors, total_num = await self.data_connector_repo.get_data_connectors(
2✔
59
                user=user,
60
                pagination=pagination,
61
                namespace=query.namespace,
62
            )
63
            return [
2✔
64
                validate_and_dump(
65
                    apispec.DataConnector,
66
                    self._dump_data_connector(dc, validator=validator),
67
                )
68
                for dc in data_connectors
69
            ], total_num
70

71
        return "/data_connectors", ["GET"], _get_all
2✔
72

73
    def post(self) -> BlueprintFactoryResponse:
2✔
74
        """Create a new data connector."""
75

76
        @authenticate(self.authenticator)
2✔
77
        @only_authenticated
2✔
78
        @validate(json=apispec.DataConnectorPost)
2✔
79
        async def _post(
2✔
80
            _: Request, user: base_models.APIUser, body: apispec.DataConnectorPost, validator: RCloneValidator
81
        ) -> JSONResponse:
82
            data_connector = validate_unsaved_data_connector(body, validator=validator)
2✔
83
            result = await self.data_connector_repo.insert_data_connector(user=user, data_connector=data_connector)
1✔
84
            return validated_json(
1✔
85
                apispec.DataConnector,
86
                self._dump_data_connector(result, validator=validator),
87
                status=201,
88
            )
89

90
        return "/data_connectors", ["POST"], _post
2✔
91

92
    def get_one(self) -> BlueprintFactoryResponse:
2✔
93
        """Get a specific data connector."""
94

95
        @authenticate(self.authenticator)
2✔
96
        @extract_if_none_match
2✔
97
        async def _get_one(
2✔
98
            _: Request, user: base_models.APIUser, data_connector_id: ULID, etag: str | None, validator: RCloneValidator
99
        ) -> HTTPResponse:
100
            data_connector = await self.data_connector_repo.get_data_connector(
2✔
101
                user=user, data_connector_id=data_connector_id
102
            )
103

104
            if data_connector.etag == etag:
1✔
NEW
105
                return HTTPResponse(status=304)
×
106

107
            headers = {"ETag": data_connector.etag}
1✔
108
            return validated_json(
1✔
109
                apispec.DataConnector,
110
                self._dump_data_connector(data_connector, validator=validator),
111
                headers=headers,
112
            )
113

114
        return "/data_connectors/<data_connector_id:ulid>", ["GET"], _get_one
2✔
115

116
    def get_one_by_slug(self) -> BlueprintFactoryResponse:
2✔
117
        """Get a specific data connector by namespace/entity slug."""
118

119
        @authenticate(self.authenticator)
2✔
120
        @extract_if_none_match
2✔
121
        async def _get_one_by_slug(
2✔
122
            _: Request,
123
            user: base_models.APIUser,
124
            namespace: str,
125
            slug: str,
126
            etag: str | None,
127
            validator: RCloneValidator,
128
        ) -> HTTPResponse:
129
            data_connector = await self.data_connector_repo.get_data_connector_by_slug(
2✔
130
                user=user, namespace=namespace, slug=slug
131
            )
132

133
            if data_connector.etag == etag:
1✔
NEW
134
                return HTTPResponse(status=304)
×
135

136
            headers = {"ETag": data_connector.etag}
1✔
137
            return validated_json(
1✔
138
                apispec.DataConnector,
139
                self._dump_data_connector(data_connector, validator=validator),
140
                headers=headers,
141
            )
142

143
        return "/namespaces/<namespace>/data_connectors/<slug:renku_slug>", ["GET"], _get_one_by_slug
2✔
144

145
    def patch(self) -> BlueprintFactoryResponse:
2✔
146
        """Partially update a data connector."""
147

148
        @authenticate(self.authenticator)
2✔
149
        @only_authenticated
2✔
150
        @if_match_required
2✔
151
        @validate(json=apispec.DataConnectorPatch)
2✔
152
        async def _patch(
2✔
153
            _: Request,
154
            user: base_models.APIUser,
155
            data_connector_id: ULID,
156
            body: apispec.DataConnectorPatch,
157
            etag: str,
158
            validator: RCloneValidator,
159
        ) -> JSONResponse:
160
            existing_dc = await self.data_connector_repo.get_data_connector(
2✔
161
                user=user, data_connector_id=data_connector_id
162
            )
163
            dc_patch = validate_data_connector_patch(existing_dc, body, validator=validator)
1✔
164
            data_connector_update = await self.data_connector_repo.update_data_connector(
1✔
165
                user=user, data_connector_id=data_connector_id, patch=dc_patch, etag=etag
166
            )
167

168
            return validated_json(
1✔
169
                apispec.DataConnector,
170
                self._dump_data_connector(data_connector_update.new, validator=validator),
171
            )
172

173
        return "/data_connectors/<data_connector_id:ulid>", ["PATCH"], _patch
2✔
174

175
    def delete(self) -> BlueprintFactoryResponse:
2✔
176
        """Delete a data connector."""
177

178
        @authenticate(self.authenticator)
2✔
179
        @only_authenticated
2✔
180
        async def _delete(
2✔
181
            _: Request,
182
            user: base_models.APIUser,
183
            data_connector_id: ULID,
184
        ) -> HTTPResponse:
185
            await self.data_connector_repo.delete_data_connector(user=user, data_connector_id=data_connector_id)
2✔
186
            return HTTPResponse(status=204)
1✔
187

188
        return "/data_connectors/<data_connector_id:ulid>", ["DELETE"], _delete
2✔
189

190
    def get_all_project_links(self) -> BlueprintFactoryResponse:
2✔
191
        """List all links from a given data connector to projects."""
192

193
        @authenticate(self.authenticator)
2✔
194
        async def _get_all_project_links(
2✔
195
            _: Request,
196
            user: base_models.APIUser,
197
            data_connector_id: ULID,
198
        ) -> JSONResponse:
199
            links = await self.data_connector_to_project_link_repo.get_links_from(
2✔
200
                user=user, data_connector_id=data_connector_id
201
            )
202
            return validated_json(
1✔
203
                apispec.DataConnectorToProjectLinksList,
204
                [self._dump_data_connector_to_project_link(link) for link in links],
205
            )
206

207
        return "/data_connectors/<data_connector_id:ulid>/project_links", ["GET"], _get_all_project_links
2✔
208

209
    def post_project_link(self) -> BlueprintFactoryResponse:
2✔
210
        """Create a new link from a data connector to a project."""
211

212
        @authenticate(self.authenticator)
2✔
213
        @only_authenticated
2✔
214
        @validate(json=apispec.DataConnectorToProjectLinkPost)
2✔
215
        async def _post_project_link(
2✔
216
            _: Request,
217
            user: base_models.APIUser,
218
            data_connector_id: ULID,
219
            body: apispec.DataConnectorToProjectLinkPost,
220
        ) -> JSONResponse:
221
            unsaved_link = models.UnsavedDataConnectorToProjectLink(
2✔
222
                data_connector_id=data_connector_id,
223
                project_id=ULID.from_str(body.project_id),
224
            )
225
            link = await self.data_connector_to_project_link_repo.insert_link(user=user, link=unsaved_link)
2✔
226
            return validated_json(
1✔
227
                apispec.DataConnectorToProjectLink, self._dump_data_connector_to_project_link(link), status=201
228
            )
229

230
        return "/data_connectors/<data_connector_id:ulid>/project_links", ["POST"], _post_project_link
2✔
231

232
    def delete_project_link(self) -> BlueprintFactoryResponse:
2✔
233
        """Delete a link from a data connector to a project."""
234

235
        @authenticate(self.authenticator)
2✔
236
        @only_authenticated
2✔
237
        async def _delete_project_link(
2✔
238
            _: Request,
239
            user: base_models.APIUser,
240
            data_connector_id: ULID,
241
            link_id: ULID,
242
        ) -> HTTPResponse:
243
            await self.data_connector_to_project_link_repo.delete_link(
2✔
244
                user=user, data_connector_id=data_connector_id, link_id=link_id
245
            )
246
            return HTTPResponse(status=204)
2✔
247

248
        return (
2✔
249
            "/data_connectors/<data_connector_id:ulid>/project_links/<link_id:ulid>",
250
            ["DELETE"],
251
            _delete_project_link,
252
        )
253

254
    def get_all_data_connectors_links_to_project(self) -> BlueprintFactoryResponse:
2✔
255
        """List all links from data connectors to a given project."""
256

257
        @authenticate(self.authenticator)
2✔
258
        async def _get_all_data_connectors_links_to_project(
2✔
259
            _: Request,
260
            user: base_models.APIUser,
261
            project_id: ULID,
262
        ) -> JSONResponse:
263
            links = await self.data_connector_to_project_link_repo.get_links_to(user=user, project_id=project_id)
2✔
264
            return validated_json(
1✔
265
                apispec.DataConnectorToProjectLinksList,
266
                [self._dump_data_connector_to_project_link(link) for link in links],
267
            )
268

269
        return "/projects/<project_id:ulid>/data_connector_links", ["GET"], _get_all_data_connectors_links_to_project
2✔
270

271
    def get_secrets(self) -> BlueprintFactoryResponse:
2✔
272
        """List all saved secrets for a data connector."""
273

274
        @authenticate(self.authenticator)
2✔
275
        @only_authenticated
2✔
276
        async def _get_secrets(
2✔
277
            _: Request,
278
            user: base_models.APIUser,
279
            data_connector_id: ULID,
280
        ) -> JSONResponse:
281
            secrets = await self.data_connector_secret_repo.get_data_connector_secrets(
2✔
282
                user=user, data_connector_id=data_connector_id
283
            )
284
            return validated_json(
2✔
285
                apispec.DataConnectorSecretsList, [self._dump_data_connector_secret(secret) for secret in secrets]
286
            )
287

288
        return "/data_connectors/<data_connector_id:ulid>/secrets", ["GET"], _get_secrets
2✔
289

290
    def patch_secrets(self) -> BlueprintFactoryResponse:
2✔
291
        """Create, update or delete saved secrets for a data connector."""
292

293
        @authenticate(self.authenticator)
2✔
294
        @only_authenticated
2✔
295
        @validate_body_root_model(json=apispec.DataConnectorSecretPatchList)
2✔
296
        async def _patch_secrets(
2✔
297
            _: Request,
298
            user: base_models.APIUser,
299
            data_connector_id: ULID,
300
            body: apispec.DataConnectorSecretPatchList,
301
        ) -> JSONResponse:
302
            unsaved_secrets = validate_data_connector_secrets_patch(put=body)
2✔
303
            secrets = await self.data_connector_secret_repo.patch_data_connector_secrets(
2✔
304
                user=user, data_connector_id=data_connector_id, secrets=unsaved_secrets
305
            )
306
            return validated_json(
1✔
307
                apispec.DataConnectorSecretsList, [self._dump_data_connector_secret(secret) for secret in secrets]
308
            )
309

310
        return "/data_connectors/<data_connector_id:ulid>/secrets", ["PATCH"], _patch_secrets
2✔
311

312
    def delete_secrets(self) -> BlueprintFactoryResponse:
2✔
313
        """Delete all saved secrets for a data connector."""
314

315
        @authenticate(self.authenticator)
2✔
316
        @only_authenticated
2✔
317
        async def _delete_secrets(
2✔
318
            _: Request,
319
            user: base_models.APIUser,
320
            data_connector_id: ULID,
321
        ) -> HTTPResponse:
322
            await self.data_connector_secret_repo.delete_data_connector_secrets(
2✔
323
                user=user, data_connector_id=data_connector_id
324
            )
325
            return HTTPResponse(status=204)
2✔
326

327
        return "/data_connectors/<data_connector_id:ulid>/secrets", ["DELETE"], _delete_secrets
2✔
328

329
    @staticmethod
2✔
330
    def _dump_data_connector(data_connector: models.DataConnector, validator: RCloneValidator) -> dict[str, Any]:
2✔
331
        """Dumps a data connector for API responses."""
332
        storage = dump_storage_with_sensitive_fields(data_connector.storage, validator=validator)
1✔
333
        return dict(
1✔
334
            id=str(data_connector.id),
335
            name=data_connector.name,
336
            namespace=data_connector.namespace.slug,
337
            slug=data_connector.slug,
338
            storage=storage,
339
            # secrets=,
340
            creation_date=data_connector.creation_date,
341
            created_by=data_connector.created_by,
342
            visibility=data_connector.visibility.value,
343
            description=data_connector.description,
344
            etag=data_connector.etag,
345
            keywords=data_connector.keywords or [],
346
        )
347

348
    @staticmethod
2✔
349
    def _dump_data_connector_to_project_link(link: models.DataConnectorToProjectLink) -> dict[str, Any]:
2✔
350
        """Dumps a link from a data connector to a project for API responses."""
351
        return dict(
1✔
352
            id=str(link.id),
353
            data_connector_id=str(link.data_connector_id),
354
            project_id=str(link.project_id),
355
            creation_date=link.creation_date,
356
            created_by=link.created_by,
357
        )
358

359
    @staticmethod
2✔
360
    def _dump_data_connector_secret(secret: models.DataConnectorSecret) -> dict[str, Any]:
2✔
361
        """Dumps a data connector secret for API responses."""
362
        return dict(
1✔
363
            name=secret.name,
364
            secret_id=str(secret.secret_id),
365
        )
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