Skip to content

Commit be7fe5f

Browse files
feat: searching data connector by their DOI (#1182)
* feat: allow using doi to create/get data connector links to projects * address review comments * address review comments
1 parent bbc8d95 commit be7fe5f

12 files changed

Lines changed: 241 additions & 145 deletions

File tree

components/renku_data_services/data_connectors/api.spec.yaml

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,33 @@ paths:
9696
$ref: "#/components/responses/Error"
9797
tags:
9898
- data_connectors
99+
/data_connectors/search:
100+
get:
101+
summary: Get data connector details by DOI
102+
parameters:
103+
- in: query
104+
name: doi
105+
required: true
106+
schema:
107+
$ref: "#/components/schemas/DOI"
108+
description: The DOI of the data connector
109+
responses:
110+
"200":
111+
description: The data connector
112+
content:
113+
application/json:
114+
schema:
115+
$ref: "#/components/schemas/DataConnector"
116+
"404":
117+
description: The data connector with the given DOI does not exist or user does not have access to it
118+
content:
119+
application/json:
120+
schema:
121+
$ref: "#/components/schemas/ErrorResponse"
122+
default:
123+
$ref: "#/components/responses/Error"
124+
tags:
125+
- data_connectors
99126
/data_connectors/{data_connector_id}:
100127
parameters:
101128
- in: path
@@ -286,7 +313,7 @@ paths:
286313
required: true
287314
schema:
288315
$ref: "#/components/schemas/Ulid"
289-
description: the ID of the data connector
316+
description: the ID of the data connector that can be ULID or DOI
290317
get:
291318
summary: Get all links from a given data connector to projects
292319
parameters:
@@ -668,6 +695,10 @@ components:
668695
type: array
669696
items:
670697
$ref: "#/components/schemas/DataConnectorToProjectLink"
698+
ProjectPath:
699+
description: The path to the project page
700+
type: string
701+
example: "namespace/project-slug"
671702
DataConnectorToProjectLink:
672703
description: A link from a data connector to a project in Renku 2.0
673704
type: object
@@ -679,6 +710,8 @@ components:
679710
$ref: "#/components/schemas/Ulid"
680711
project_id:
681712
$ref: "#/components/schemas/Ulid"
713+
project_path:
714+
$ref: "#/components/schemas/ProjectPath"
682715
creation_date:
683716
$ref: "#/components/schemas/CreationDate"
684717
created_by:
@@ -687,6 +720,7 @@ components:
687720
- id
688721
- data_connector_id
689722
- project_id
723+
- project_path
690724
- creation_date
691725
- created_by
692726
DataConnectorToProjectLinkPost:
@@ -990,7 +1024,6 @@ components:
9901024
type: integer
9911025
minimum: 0
9921026
description: The number of data links the user does not have access to
993-
9941027
responses:
9951028
Error:
9961029
description: The schema for all 4xx and 5xx responses

components/renku_data_services/data_connectors/apispec.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: api.spec.yaml
3-
# timestamp: 2025-12-03T09:49:11+00:00
3+
# timestamp: 2026-01-27T08:42:00+00:00
44

55
from __future__ import annotations
66

@@ -129,6 +129,14 @@ class InaccessibleDataConnectorLinks(BaseAPISpec):
129129
)
130130

131131

132+
class DataConnectorsSearchGetParametersQuery(BaseAPISpec):
133+
doi: str = Field(..., description="A DOI.", examples=["10.16904/envidat.33"])
134+
135+
136+
class DataConnectorsDataConnectorIdProjectLinksGetParametersQuery(BaseAPISpec):
137+
params: Optional[PaginationRequest] = None
138+
139+
132140
class CloudStorageCore(BaseAPISpec):
133141
model_config = ConfigDict(
134142
extra="forbid",
@@ -241,6 +249,11 @@ class DataConnectorToProjectLink(BaseAPISpec):
241249
min_length=26,
242250
pattern="^[0-7][0-9A-HJKMNP-TV-Z]{25}$",
243251
)
252+
project_path: str = Field(
253+
...,
254+
description="The path to the project page",
255+
examples=["namespace/project-slug"],
256+
)
244257
creation_date: datetime = Field(
245258
...,
246259
description="The date and time the resource was created (in UTC and ISO-8601 format)",

components/renku_data_services/data_connectors/blueprints.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,25 @@ async def _get_permissions(_: Request, user: base_models.APIUser, data_connector
308308

309309
return "/data_connectors/<data_connector_id:ulid>/permissions", ["GET"], _get_permissions
310310

311+
def get_one_by_doi(self) -> BlueprintFactoryResponse:
312+
"""Get data connector by DOI."""
313+
314+
@authenticate(self.authenticator)
315+
@validate(query=apispec.DataConnectorsSearchGetParametersQuery)
316+
async def _get_one_by_doi(
317+
_: Request,
318+
user: base_models.APIUser,
319+
query: apispec.DataConnectorsSearchGetParametersQuery,
320+
validator: RCloneValidator,
321+
) -> JSONResponse:
322+
data_connector = await self.data_connector_repo.get_data_connector_by_doi(user=user, doi=query.doi)
323+
return validated_json(
324+
apispec.DataConnector,
325+
self._dump_data_connector(data_connector, validator=validator),
326+
)
327+
328+
return "/data_connectors/search", ["GET"], _get_one_by_doi
329+
311330
def get_all_project_links(self) -> BlueprintFactoryResponse:
312331
"""List all links from a given data connector to projects."""
313332

@@ -533,6 +552,7 @@ def _dump_data_connector_to_project_link(link: models.DataConnectorToProjectLink
533552
id=str(link.id),
534553
data_connector_id=str(link.data_connector_id),
535554
project_id=str(link.project_id),
555+
project_path=link.project_path,
536556
creation_date=link.creation_date,
537557
created_by=link.created_by,
538558
)

components/renku_data_services/data_connectors/db.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,33 @@ async def get_data_connector_by_slug(
195195

196196
return data_connector.dump()
197197

198+
async def get_data_connector_by_doi(
199+
self,
200+
user: base_models.APIUser,
201+
doi: str,
202+
) -> models.DataConnector | models.GlobalDataConnector:
203+
"""Get a data connector from the database by DOI."""
204+
not_found_msg = f"Data connector with DOI '{doi}' does not exist or you do not have access to it."
205+
206+
async with self.session_maker() as session:
207+
stmt = select(schemas.DataConnectorORM).where(schemas.DataConnectorORM.doi == doi)
208+
result = await session.scalars(stmt)
209+
data_connector = result.one_or_none()
210+
211+
if data_connector is None:
212+
raise errors.MissingResourceError(message=not_found_msg)
213+
214+
authorized = await self.authz.has_permission(
215+
user=user,
216+
resource_type=ResourceType.data_connector,
217+
resource_id=data_connector.id,
218+
scope=Scope.READ,
219+
)
220+
if not authorized:
221+
raise errors.MissingResourceError(message=not_found_msg)
222+
223+
return data_connector.dump()
224+
198225
async def get_global_data_connector_by_slug(
199226
self,
200227
user: base_models.APIUser,

components/renku_data_services/data_connectors/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class DataConnectorToProjectLink(UnsavedDataConnectorToProjectLink):
174174
"""A link from a data connector to a project."""
175175

176176
id: ULID
177+
project_path: str
177178
created_by: str
178179
creation_date: datetime
179180
updated_at: datetime

components/renku_data_services/data_connectors/orm.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,18 @@ class DataConnectorToProjectLinkORM(BaseORM):
193193
nullable=False,
194194
)
195195

196+
project: Mapped["ProjectORM"] = relationship(init=False, repr=False, viewonly=True, lazy="joined")
197+
"""The project this link points to."""
198+
196199
def dump(self) -> models.DataConnectorToProjectLink:
197200
"""Create a link model from the DataConnectorProjectLinkORM."""
201+
project_path = f"{self.project.slug.namespace.slug}/{self.project.slug.slug}"
202+
198203
return models.DataConnectorToProjectLink(
199204
id=self.id,
200205
data_connector_id=self.data_connector_id,
201206
project_id=self.project_id,
207+
project_path=project_path,
202208
created_by=self.created_by_id,
203209
creation_date=self.creation_date,
204210
updated_at=self.updated_at,

components/renku_data_services/project/api.spec.yaml

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -992,32 +992,6 @@ components:
992992
type: string
993993
description: Entity Tag
994994
example: "9EE498F9D565D0C41E511377425F32F3"
995-
DataConnectorToProjectLinksList:
996-
description: A list of links from a data connector to a project
997-
type: array
998-
items:
999-
$ref: "#/components/schemas/DataConnectorToProjectLink"
1000-
DataConnectorToProjectLink:
1001-
description: A link from a data connector to a project in Renku 2.0
1002-
type: object
1003-
additionalProperties: false
1004-
properties:
1005-
id:
1006-
$ref: "#/components/schemas/Ulid"
1007-
data_connector_id:
1008-
$ref: "#/components/schemas/Ulid"
1009-
project_id:
1010-
$ref: "#/components/schemas/Ulid"
1011-
creation_date:
1012-
$ref: "#/components/schemas/CreationDate"
1013-
created_by:
1014-
$ref: "#/components/schemas/UserId"
1015-
required:
1016-
- id
1017-
- data_connector_id
1018-
- project_id
1019-
- creation_date
1020-
- created_by
1021995
ProjectGetQuery:
1022996
description: Query params for project get request
1023997
allOf:

components/renku_data_services/project/apispec.py

Lines changed: 7 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: api.spec.yaml
3-
# timestamp: 2025-05-06T08:33:41+00:00
3+
# timestamp: 2026-01-23T15:08:41+00:00
44

55
from __future__ import annotations
66

@@ -27,44 +27,6 @@ class Role(Enum):
2727
owner = "owner"
2828

2929

30-
class DataConnectorToProjectLink(BaseAPISpec):
31-
model_config = ConfigDict(
32-
extra="forbid",
33-
)
34-
id: str = Field(
35-
...,
36-
description="ULID identifier",
37-
max_length=26,
38-
min_length=26,
39-
pattern="^[0-7][0-9A-HJKMNP-TV-Z]{25}$",
40-
)
41-
data_connector_id: str = Field(
42-
...,
43-
description="ULID identifier",
44-
max_length=26,
45-
min_length=26,
46-
pattern="^[0-7][0-9A-HJKMNP-TV-Z]{25}$",
47-
)
48-
project_id: str = Field(
49-
...,
50-
description="ULID identifier",
51-
max_length=26,
52-
min_length=26,
53-
pattern="^[0-7][0-9A-HJKMNP-TV-Z]{25}$",
54-
)
55-
creation_date: datetime = Field(
56-
...,
57-
description="The date and time the resource was created (in UTC and ISO-8601 format)",
58-
examples=["2023-11-01T17:32:28Z"],
59-
)
60-
created_by: str = Field(
61-
...,
62-
description="Keycloak user ID",
63-
examples=["f74a228b-1790-4276-af5f-25c2424e9b0c"],
64-
pattern="^[A-Za-z0-9]{1}[A-Za-z0-9-]+$",
65-
)
66-
67-
6830
class ProjectMigrationInfo(BaseAPISpec):
6931
project_id: str = Field(
7032
...,
@@ -138,22 +100,22 @@ class ErrorResponse(BaseAPISpec):
138100
error: Error
139101

140102

141-
class NamespacesNamespaceProjectsSlugGetParametersQuery(BaseAPISpec):
103+
class ProjectsProjectIdGetParametersQuery(BaseAPISpec):
142104
with_documentation: Optional[bool] = Field(
143105
None, description="Projects with or without possibly extensive documentation?"
144106
)
145107

146108

147-
class ProjectsProjectIdCopiesGetParametersQuery(BaseAPISpec):
148-
writable: bool = False
149-
150-
151-
class ProjectsProjectIdGetParametersQuery(BaseAPISpec):
109+
class NamespacesNamespaceProjectsSlugGetParametersQuery(BaseAPISpec):
152110
with_documentation: Optional[bool] = Field(
153111
None, description="Projects with or without possibly extensive documentation?"
154112
)
155113

156114

115+
class ProjectsProjectIdCopiesGetParametersQuery(BaseAPISpec):
116+
writable: bool = False
117+
118+
157119
class MigrationSessionLauncherPost(BaseAPISpec):
158120
model_config = ConfigDict(
159121
extra="forbid",
@@ -235,12 +197,6 @@ class ProjectMemberResponse(BaseAPISpec):
235197
role: Role
236198

237199

238-
class DataConnectorToProjectLinksList(RootModel[List[DataConnectorToProjectLink]]):
239-
root: List[DataConnectorToProjectLink] = Field(
240-
..., description="A list of links from a data connector to a project"
241-
)
242-
243-
244200
class ProjectGetQuery(PaginationRequest):
245201
namespace: str = Field("", description="A namespace, used as a filter.")
246202
direct_member: bool = Field(
@@ -386,11 +342,6 @@ class ProjectsGetParametersQuery(BaseAPISpec):
386342
params: Optional[ProjectGetQuery] = None
387343

388344

389-
class RenkuV1ProjectsMigrationsGetParametersQuery(BaseAPISpec):
390-
"""This class no longer includes any parameters."""
391-
pass
392-
393-
394345
class Project(BaseAPISpec):
395346
id: str = Field(
396347
...,

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ module = [
274274
]
275275
ignore_missing_imports = true
276276

277+
[[tool.mypy.overrides]]
278+
module = ["test.*"]
279+
disable_error_code = ["no-untyped-def"]
280+
277281
[tool.coverage.run]
278282
source = ["bases/", "components/"]
279283
omit = ["components/renku_data_services/notebooks"]

0 commit comments

Comments
 (0)