Skip to content

Links

The Links endpoint provides access to relation links between research products via the OpenAIRE Graph API V3 (/research-products/links).

!!! note "Pagination" This endpoint uses 0-indexed pagination (first page is page=0). The server silently caps page size at 99 (requesting 100 returns only 10 results). AIREloom handles this transparently by clamping to 99.

Unlike other endpoints, there is no standalone LinksClient. Link operations are accessed through the ResearchProductsClient:

  • search_links() — search for relation links between research products
  • iterate_links() — iterate through all matching relation links
  • get_relations_info() — retrieve available relation types

LinksFilters

Bases: BaseModel

Filter model for Graph API /research-products/links endpoint.

These filters are for the Graph API's built-in link retrieval endpoint, which is separate from the Scholix API. Parameters accept singular string values (unlike other Graph API filters which accept arrays).

Reference: https://api.openaire.eu/graph/v3/research-products/links

Source code in src/aireloom/endpoints.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
class LinksFilters(BaseModel):
    """Filter model for Graph API /research-products/links endpoint.

    These filters are for the Graph API's built-in link retrieval endpoint,
    which is separate from the Scholix API. Parameters accept singular string values
    (unlike other Graph API filters which accept arrays).

    Reference: https://api.openaire.eu/graph/v3/research-products/links
    """

    sourcePid: str | None = Field(
        default=None, description="Filter by source persistent identifier (e.g. DOI)"
    )
    targetPid: str | None = Field(
        default=None, description="Filter by target persistent identifier"
    )
    sourcePublisher: str | None = Field(
        default=None, description="Filter by source publisher name"
    )
    targetPublisher: str | None = Field(
        default=None, description="Filter by target publisher name"
    )
    sourceType: str | None = Field(
        default=None,
        description="Filter by source type: publication, dataset, software, other",
    )
    targetType: str | None = Field(
        default=None,
        description="Filter by target type: publication, dataset, software, other",
    )
    relation: str | None = Field(
        default=None, description="Filter by specific relationship type"
    )
    fromDate: str | None = Field(
        default=None, description="From date (YYYY or YYYY-MM-DD)"
    )
    toDate: str | None = Field(default=None, description="To date (YYYY or YYYY-MM-DD)")

    model_config = ConfigDict(extra="forbid")

fromDate = Field(default=None, description='From date (YYYY or YYYY-MM-DD)') class-attribute instance-attribute

model_config = ConfigDict(extra='forbid') class-attribute instance-attribute

relation = Field(default=None, description='Filter by specific relationship type') class-attribute instance-attribute

sourcePid = Field(default=None, description='Filter by source persistent identifier (e.g. DOI)') class-attribute instance-attribute

sourcePublisher = Field(default=None, description='Filter by source publisher name') class-attribute instance-attribute

sourceType = Field(default=None, description='Filter by source type: publication, dataset, software, other') class-attribute instance-attribute

targetPid = Field(default=None, description='Filter by target persistent identifier') class-attribute instance-attribute

targetPublisher = Field(default=None, description='Filter by target publisher name') class-attribute instance-attribute

targetType = Field(default=None, description='Filter by target type: publication, dataset, software, other') class-attribute instance-attribute

toDate = Field(default=None, description='To date (YYYY or YYYY-MM-DD)') class-attribute instance-attribute

Access via ResearchProductsClient

Bases: GraphV3FilterSerializationMixin, BatchMixin, GettableMixin, SearchableMixin, CursorIterableMixin, BaseResourceClient

Client for the OpenAIRE Research Products API endpoint.

This client provides standardized methods (get, search, iterate) for accessing research product data, by inheriting from bibliofabric mixins. It also provides search_links, iterate_links, and get_relations_info for the V3 /research-products/links sub-endpoint.

Attributes:

Name Type Description
_entity_path str

The API path for research products.

_entity_model type[ResearchProduct]

Pydantic model for a single research product.

_search_response_model type[ResearchProductResponse]

Pydantic model for the search response envelope.

Source code in src/aireloom/resources/research_products_client.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class ResearchProductsClient(
    GraphV3FilterSerializationMixin,
    BatchMixin,
    GettableMixin,
    SearchableMixin,
    CursorIterableMixin,
    BaseResourceClient,
):
    """Client for the OpenAIRE Research Products API endpoint.

    This client provides standardized methods (`get`, `search`, `iterate`) for
    accessing research product data, by inheriting from `bibliofabric` mixins.
    It also provides `search_links`, `iterate_links`, and `get_relations_info`
    for the V3 ``/research-products/links`` sub-endpoint.

    Attributes:
        _entity_path (str): The API path for research products.
        _entity_model (type[ResearchProduct]): Pydantic model for a single research product.
        _search_response_model (type[ResearchProductResponse]): Pydantic model for the
                                                                search response envelope.
    """

    _entity_path: str = RESEARCH_PRODUCTS
    _entity_model: type[ResearchProduct] = ResearchProduct
    _search_response_model: type[ResearchProductResponse] = ResearchProductResponse
    _batch_fields: dict[str, str] = {
        "doi": "pid",
        "openaire_id": "id",
        "original_id": "originalId",
    }

    def __init__(self, api_client: "AireloomClient"):
        """Initializes the ResearchProductsClient.

        Args:
            api_client: An instance of AireloomClient.
        """
        super().__init__(api_client)
        logger.debug(
            f"ResearchProductsClient initialized for path: {self._entity_path}"
        )

    # Mixin-provided methods: get, search, iterate

    # ------------------------------------------------------------------
    # Links sub-endpoint (V3, 0-indexed page-based pagination)
    # ------------------------------------------------------------------

    async def search_links(
        self,
        *,
        filters: LinksFilters | None = None,
        page: int = 0,
        page_size: int = 20,
    ) -> LinksResponse:
        """Search for relation links between research products.

        Uses the V3 ``/research-products/links`` endpoint (NOT the Scholix API).

        Args:
            filters: Optional :class:`LinksFilters` with filter criteria.
            page: 0-indexed page number.
            page_size: Number of results per page (max 99; values >=100 are silently
                truncated to 10 by V3).

        Returns:
            A :class:`LinksResponse` containing the matching relations.
        """
        if page_size > MAX_LINK_PAGE_SIZE:
            logger.warning(
                "page_size=%d exceeds the V3 links maximum of %d "
                "(values >=100 are silently truncated to 10); clamping.",
                page_size,
                MAX_LINK_PAGE_SIZE,
            )
            page_size = MAX_LINK_PAGE_SIZE
        params: dict[str, Any] = {"page": page, "pageSize": page_size}
        if filters is not None:
            params.update(self._serialize_filters(filters))

        response = await self._api_client.request(
            method="GET",
            path=LINKS,
            params=params,
        )
        return LinksResponse.model_validate(response.json())

    async def iterate_links(
        self,
        *,
        filters: LinksFilters | None = None,
        page_size: int = MAX_LINK_PAGE_SIZE,
    ) -> AsyncIterator[Relation]:
        """Iterate through all relation links matching *filters*.

        Automatically handles page-based pagination using ``totalPages``
        from the response header.

        Args:
            filters: Optional :class:`LinksFilters` with filter criteria.
            page_size: Number of results per page (max 99; see :meth:`search_links`).

        Yields:
            :class:`Relation` objects.
        """
        current_page = 0
        total_pages = 1

        while current_page < total_pages:
            response = await self.search_links(
                filters=filters, page=current_page, page_size=page_size
            )

            if not response.results:
                break

            for rel in response.results:
                yield rel

            if current_page == 0 and response.header is not None:
                total_pages = response.header.totalPages or 1
                if total_pages == 0:
                    break

            if current_page >= total_pages:
                break

            current_page += 1

    async def get_relations_info(self) -> list[dict[str, Any]]:
        """Retrieve available relation types from the links endpoint.

        Uses the V3 ``/research-products/links/relations-info`` endpoint.

        Returns:
            A list of dicts describing relation types (name, inverse, description).
        """
        response = await self._api_client.request(
            method="GET",
            path=f"{LINKS}/relations-info",
            params={},
        )
        data = response.json()
        if isinstance(data, list):
            return data
        return [data]

Search for relation links between research products.

Uses the V3 /research-products/links endpoint (NOT the Scholix API).

Parameters:

Name Type Description Default
filters LinksFilters | None

Optional :class:LinksFilters with filter criteria.

None
page int

0-indexed page number.

0
page_size int

Number of results per page (max 99; values >=100 are silently truncated to 10 by V3).

20

Returns:

Name Type Description
A LinksResponse

class:LinksResponse containing the matching relations.

Source code in src/aireloom/resources/research_products_client.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
async def search_links(
    self,
    *,
    filters: LinksFilters | None = None,
    page: int = 0,
    page_size: int = 20,
) -> LinksResponse:
    """Search for relation links between research products.

    Uses the V3 ``/research-products/links`` endpoint (NOT the Scholix API).

    Args:
        filters: Optional :class:`LinksFilters` with filter criteria.
        page: 0-indexed page number.
        page_size: Number of results per page (max 99; values >=100 are silently
            truncated to 10 by V3).

    Returns:
        A :class:`LinksResponse` containing the matching relations.
    """
    if page_size > MAX_LINK_PAGE_SIZE:
        logger.warning(
            "page_size=%d exceeds the V3 links maximum of %d "
            "(values >=100 are silently truncated to 10); clamping.",
            page_size,
            MAX_LINK_PAGE_SIZE,
        )
        page_size = MAX_LINK_PAGE_SIZE
    params: dict[str, Any] = {"page": page, "pageSize": page_size}
    if filters is not None:
        params.update(self._serialize_filters(filters))

    response = await self._api_client.request(
        method="GET",
        path=LINKS,
        params=params,
    )
    return LinksResponse.model_validate(response.json())

Iterate through all relation links matching filters.

Automatically handles page-based pagination using totalPages from the response header.

Parameters:

Name Type Description Default
filters LinksFilters | None

Optional :class:LinksFilters with filter criteria.

None
page_size int

Number of results per page (max 99; see :meth:search_links).

MAX_LINK_PAGE_SIZE

Yields:

Type Description
AsyncIterator[Relation]

class:Relation objects.

Source code in src/aireloom/resources/research_products_client.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
async def iterate_links(
    self,
    *,
    filters: LinksFilters | None = None,
    page_size: int = MAX_LINK_PAGE_SIZE,
) -> AsyncIterator[Relation]:
    """Iterate through all relation links matching *filters*.

    Automatically handles page-based pagination using ``totalPages``
    from the response header.

    Args:
        filters: Optional :class:`LinksFilters` with filter criteria.
        page_size: Number of results per page (max 99; see :meth:`search_links`).

    Yields:
        :class:`Relation` objects.
    """
    current_page = 0
    total_pages = 1

    while current_page < total_pages:
        response = await self.search_links(
            filters=filters, page=current_page, page_size=page_size
        )

        if not response.results:
            break

        for rel in response.results:
            yield rel

        if current_page == 0 and response.header is not None:
            total_pages = response.header.totalPages or 1
            if total_pages == 0:
                break

        if current_page >= total_pages:
            break

        current_page += 1

get_relations_info() async

Retrieve available relation types from the links endpoint.

Uses the V3 /research-products/links/relations-info endpoint.

Returns:

Type Description
list[dict[str, Any]]

A list of dicts describing relation types (name, inverse, description).

Source code in src/aireloom/resources/research_products_client.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
async def get_relations_info(self) -> list[dict[str, Any]]:
    """Retrieve available relation types from the links endpoint.

    Uses the V3 ``/research-products/links/relations-info`` endpoint.

    Returns:
        A list of dicts describing relation types (name, inverse, description).
    """
    response = await self._api_client.request(
        method="GET",
        path=f"{LINKS}/relations-info",
        params={},
    )
    data = response.json()
    if isinstance(data, list):
        return data
    return [data]