Skip to content

API Reference

This section provides a basic API reference generated from the docstrings in the AIREloom library using mkdocstrings.

AireloomSession

The main session class for interacting with AIREloom.

High-level session manager for interacting with OpenAIRE APIs.

This class acts as the primary entry point for users of the aireloom library. It provides convenient access to various OpenAIRE resource clients (e.g., for research products, projects) through an underlying AireloomClient instance.

The session handles the lifecycle of the AireloomClient, including its creation with appropriate settings (like timeouts and authentication) and its proper closure when the session is no longer needed. It supports asynchronous context management (async with).

Example:

async with AireloomSession(timeout=60) as session:
    product = await session.research_products.get("some_id")
    # ... further API calls

Attributes:

Name Type Description
research_products ResearchProductsClient

Client for research product APIs.

organizations OrganizationsClient

Client for organization APIs.

projects ProjectsClient

Client for project APIs.

data_sources DataSourcesClient

Client for data source APIs.

scholix ScholixClient

Client for Scholix (scholarly link) APIs.

_api_client AireloomClient

The underlying client instance.

Source code in src/aireloom/session.py
 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
class AireloomSession:
    """High-level session manager for interacting with OpenAIRE APIs.

    This class acts as the primary entry point for users of the `aireloom` library.
    It provides convenient access to various OpenAIRE resource clients (e.g., for
    research products, projects) through an underlying `AireloomClient` instance.

    The session handles the lifecycle of the `AireloomClient`, including its
    creation with appropriate settings (like timeouts and authentication) and
    its proper closure when the session is no longer needed. It supports
    asynchronous context management (`async with`).

    Example:
    ```python
    async with AireloomSession(timeout=60) as session:
        product = await session.research_products.get("some_id")
        # ... further API calls
    ```

    Attributes:
        research_products (ResearchProductsClient): Client for research product APIs.
        organizations (OrganizationsClient): Client for organization APIs.
        projects (ProjectsClient): Client for project APIs.
        data_sources (DataSourcesClient): Client for data source APIs.
        scholix (ScholixClient): Client for Scholix (scholarly link) APIs.
        _api_client (AireloomClient): The underlying client instance.
    """

    def __init__(
        self,
        auth_strategy: AuthStrategy | None = None,
        timeout: int | None = None,
        api_base_url: str | None = None,
        scholix_base_url: str | None = None,
    ):
        """Initializes the Aireloom session and its underlying `AireloomClient`.

        The session allows for overriding certain configurations like request timeout
        and API base URLs. Authentication strategy can also be provided directly.
        If not provided, the `AireloomClient` will attempt to determine it based
        on its own settings (loaded from environment or .env files).

        Args:
            auth_strategy: An optional `AuthStrategy` instance to be used for
                all requests made through this session. If `None`, the
                `AireloomClient` will determine authentication based on its settings.
            timeout: An optional integer to override the default request timeout
                (in seconds) for all HTTP requests made during this session.
                If `None`, the timeout from global or client-specific settings is used.
            api_base_url: An optional string to override the default base URL for the
                OpenAIRE Graph API.
            scholix_base_url: An optional string to override the default base URL for
                the OpenAIRE Scholix API.
        """
        _api_base_url = api_base_url or OPENAIRE_GRAPH_API_BASE_URL
        _scholix_base_url = scholix_base_url or OPENAIRE_SCHOLIX_API_BASE_URL

        current_settings = get_settings()
        session_specific_settings: ApiSettings
        if timeout is not None:
            logger.debug(f"Overriding request timeout for this session to: {timeout}s")
            session_specific_settings = current_settings.model_copy(
                update={"request_timeout": timeout}
            )
        else:
            session_specific_settings = current_settings

        # Pass the original auth_strategy (which can be None) to the client.
        # The client will then decide its auth based on this and its settings.
        logger.debug(
            f"AireloomSession: Initializing AireloomClient with auth_strategy param: {type(auth_strategy)}"
        )
        self._api_client = AireloomClient(
            settings=session_specific_settings,
            auth_strategy=auth_strategy,  # Pass the original auth_strategy parameter
            base_url=_api_base_url,  # Pass Graph API base URL
            scholix_base_url=_scholix_base_url,  # Pass Scholix base URL
        )
        logger.debug(f"AireloomSession initialized for API: {_api_base_url}")
        logger.debug(f"Scholexplorer base URL configured for: {_scholix_base_url}")

    @property
    def queries(self):
        """Access convenience query functions.

        Returns a ``_QueryAccessor`` bound to this session so you can call any
        convenience function without passing the session explicitly::

            papers = await session.queries.publications_by_doi(
                "10.1234/..."
            )
        """
        return _QueryAccessor(queries, self)

    def __getattr__(self, name: str):
        if name in _DELEGATED_CLIENTS:
            return getattr(self._api_client, name)
        raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")

    def __dir__(self):
        return list(super().__dir__()) + list(_DELEGATED_CLIENTS)

    async def close(self) -> None:
        """Closes the underlying HTTP client session."""
        await self._api_client.aclose()

    async def __aenter__(self) -> "AireloomSession":
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        await self.close()

_api_client = AireloomClient(settings=session_specific_settings, auth_strategy=auth_strategy, base_url=_api_base_url, scholix_base_url=_scholix_base_url) instance-attribute

queries property

Access convenience query functions.

Returns a _QueryAccessor bound to this session so you can call any convenience function without passing the session explicitly::

papers = await session.queries.publications_by_doi(
    "10.1234/..."
)

__aenter__() async

Source code in src/aireloom/session.py
152
153
async def __aenter__(self) -> "AireloomSession":
    return self

__aexit__(exc_type, exc_val, exc_tb) async

Source code in src/aireloom/session.py
155
156
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
    await self.close()

__dir__()

Source code in src/aireloom/session.py
145
146
def __dir__(self):
    return list(super().__dir__()) + list(_DELEGATED_CLIENTS)

__getattr__(name)

Source code in src/aireloom/session.py
140
141
142
143
def __getattr__(self, name: str):
    if name in _DELEGATED_CLIENTS:
        return getattr(self._api_client, name)
    raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")

__init__(auth_strategy=None, timeout=None, api_base_url=None, scholix_base_url=None)

Initializes the Aireloom session and its underlying AireloomClient.

The session allows for overriding certain configurations like request timeout and API base URLs. Authentication strategy can also be provided directly. If not provided, the AireloomClient will attempt to determine it based on its own settings (loaded from environment or .env files).

Parameters:

Name Type Description Default
auth_strategy AuthStrategy | None

An optional AuthStrategy instance to be used for all requests made through this session. If None, the AireloomClient will determine authentication based on its settings.

None
timeout int | None

An optional integer to override the default request timeout (in seconds) for all HTTP requests made during this session. If None, the timeout from global or client-specific settings is used.

None
api_base_url str | None

An optional string to override the default base URL for the OpenAIRE Graph API.

None
scholix_base_url str | None

An optional string to override the default base URL for the OpenAIRE Scholix API.

None
Source code in src/aireloom/session.py
 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
def __init__(
    self,
    auth_strategy: AuthStrategy | None = None,
    timeout: int | None = None,
    api_base_url: str | None = None,
    scholix_base_url: str | None = None,
):
    """Initializes the Aireloom session and its underlying `AireloomClient`.

    The session allows for overriding certain configurations like request timeout
    and API base URLs. Authentication strategy can also be provided directly.
    If not provided, the `AireloomClient` will attempt to determine it based
    on its own settings (loaded from environment or .env files).

    Args:
        auth_strategy: An optional `AuthStrategy` instance to be used for
            all requests made through this session. If `None`, the
            `AireloomClient` will determine authentication based on its settings.
        timeout: An optional integer to override the default request timeout
            (in seconds) for all HTTP requests made during this session.
            If `None`, the timeout from global or client-specific settings is used.
        api_base_url: An optional string to override the default base URL for the
            OpenAIRE Graph API.
        scholix_base_url: An optional string to override the default base URL for
            the OpenAIRE Scholix API.
    """
    _api_base_url = api_base_url or OPENAIRE_GRAPH_API_BASE_URL
    _scholix_base_url = scholix_base_url or OPENAIRE_SCHOLIX_API_BASE_URL

    current_settings = get_settings()
    session_specific_settings: ApiSettings
    if timeout is not None:
        logger.debug(f"Overriding request timeout for this session to: {timeout}s")
        session_specific_settings = current_settings.model_copy(
            update={"request_timeout": timeout}
        )
    else:
        session_specific_settings = current_settings

    # Pass the original auth_strategy (which can be None) to the client.
    # The client will then decide its auth based on this and its settings.
    logger.debug(
        f"AireloomSession: Initializing AireloomClient with auth_strategy param: {type(auth_strategy)}"
    )
    self._api_client = AireloomClient(
        settings=session_specific_settings,
        auth_strategy=auth_strategy,  # Pass the original auth_strategy parameter
        base_url=_api_base_url,  # Pass Graph API base URL
        scholix_base_url=_scholix_base_url,  # Pass Scholix base URL
    )
    logger.debug(f"AireloomSession initialized for API: {_api_base_url}")
    logger.debug(f"Scholexplorer base URL configured for: {_scholix_base_url}")

close() async

Closes the underlying HTTP client session.

Source code in src/aireloom/session.py
148
149
150
async def close(self) -> None:
    """Closes the underlying HTTP client session."""
    await self._api_client.aclose()

Resource Clients

Clients for specific OpenAIRE API endpoints.

ResearchProductsClient

For accessing research products (publications, datasets, software, etc.) and relation links.

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]

OrganizationsClient

For accessing organization data.

Bases: StandardResourceClient

Client for the OpenAIRE Organizations API endpoint.

Attributes:

Name Type Description
_entity_path str

The API path for organizations.

_entity_model type[Organization]

Pydantic model for a single organization.

_search_response_model type[OrganizationResponse]

Pydantic model for the search response envelope.

Source code in src/aireloom/resources/organizations_client.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class OrganizationsClient(StandardResourceClient):
    """Client for the OpenAIRE Organizations API endpoint.

    Attributes:
        _entity_path (str): The API path for organizations.
        _entity_model (type[Organization]): Pydantic model for a single organization.
        _search_response_model (type[OrganizationResponse]): Pydantic model for the
                                                              search response envelope.
    """

    _entity_path: str = ORGANIZATIONS
    _entity_model: type[Organization] = Organization
    _search_response_model: type[OrganizationResponse] = OrganizationResponse
    _batch_fields: dict[str, str] = {
        "pid": "pid",
        "openaire_id": "id",
    }

ProjectsClient

For accessing research project data.

Bases: StandardResourceClient

Client for the OpenAIRE Projects API endpoint.

Attributes:

Name Type Description
_entity_path str

The API path for projects.

_entity_model type[Project]

Pydantic model for a single project.

_search_response_model type[ProjectResponse]

Pydantic model for the search response envelope.

Source code in src/aireloom/resources/projects_client.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ProjectsClient(StandardResourceClient):
    """Client for the OpenAIRE Projects API endpoint.

    Attributes:
        _entity_path (str): The API path for projects.
        _entity_model (type[Project]): Pydantic model for a single project.
        _search_response_model (type[ProjectResponse]): Pydantic model for the
                                                        search response envelope.
    """

    _entity_path: str = PROJECTS
    _entity_model: type[Project] = Project
    _search_response_model: type[ProjectResponse] = ProjectResponse
    _batch_fields: dict[str, str] = {
        "code": "code",
        "openaire_id": "id",
    }

DataSourcesClient

For accessing data source information.

Bases: StandardResourceClient

Client for the OpenAIRE Data Sources API endpoint.

Attributes:

Name Type Description
_entity_path str

The API path for data sources.

_entity_model type[DataSource]

Pydantic model for a single data source.

_search_response_model type[DataSourceResponse]

Pydantic model for the search response envelope.

Source code in src/aireloom/resources/data_sources_client.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class DataSourcesClient(StandardResourceClient):
    """Client for the OpenAIRE Data Sources API endpoint.

    Attributes:
        _entity_path (str): The API path for data sources.
        _entity_model (type[DataSource]): Pydantic model for a single data source.
        _search_response_model (type[DataSourceResponse]): Pydantic model for the
                                                            search response envelope.
    """

    _entity_path: str = DATA_SOURCES
    _entity_model: type[DataSource] = DataSource
    _search_response_model: type[DataSourceResponse] = DataSourceResponse
    _batch_fields: dict[str, str] = {
        "pid": "pid",
        "openaire_id": "id",
    }

PersonsClient

For accessing person data.

Bases: StandardResourceClient

Client for the OpenAIRE Persons API endpoint.

Attributes:

Name Type Description
_entity_path str

The API path for persons.

_entity_model type[Person]

Pydantic model for a single person.

_search_response_model type[PersonResponse]

Pydantic model for the search response envelope.

Source code in src/aireloom/resources/persons_client.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PersonsClient(StandardResourceClient):
    """Client for the OpenAIRE Persons API endpoint.

    Attributes:
        _entity_path (str): The API path for persons.
        _entity_model (type[Person]): Pydantic model for a single person.
        _search_response_model (type[PersonResponse]): Pydantic model for the
                                                        search response envelope.
    """

    _entity_path: str = PERSONS
    _entity_model: type[Person] = Person
    _search_response_model: type[PersonResponse] = PersonResponse
    _batch_fields: dict[str, str] = {
        "openaire_id": "id",
        "original_id": "originalId",
    }

ScholixClient

For accessing Scholix link data via the Scholexplorer API.

Bases: BaseResourceClient

Client for the OpenAIRE Scholexplorer API (Scholix links).

This client handles requests to the Scholix API, which provides data on relationships between research artifacts (e.g., citations, supplements). It uses a specific base URL (_base_url_override) and custom methods (search_links, iterate_links) tailored to the Scholix API's structure, including its 0-indexed pagination and specific request parameters.

Attributes:

Name Type Description
_entity_path str

The API path for Scholix links (typically "Links").

_base_url_override str

The base URL for the Scholexplorer API.

_endpoint_def dict

Configuration for this endpoint from ENDPOINT_DEFINITIONS.

Source code in src/aireloom/resources/scholix_client.py
 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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
class ScholixClient(BaseResourceClient):
    """Client for the OpenAIRE Scholexplorer API (Scholix links).

    This client handles requests to the Scholix API, which provides data on
    relationships between research artifacts (e.g., citations, supplements).
    It uses a specific base URL (``_base_url_override``) and custom methods
    (``search_links``, ``iterate_links``) tailored to the Scholix API's structure,
    including its 0-indexed pagination and specific request parameters.

    Attributes:
        _entity_path (str): The API path for Scholix links (typically "Links").
        _base_url_override (str): The base URL for the Scholexplorer API.
        _endpoint_def (dict): Configuration for this endpoint from ``ENDPOINT_DEFINITIONS``.
    """

    _base_url_override: str | None = None
    _entity_model = None
    _search_response_model = ScholixResponse
    _entity_path: str = SCHOLIX  # This is the endpoint path, typically "Links"

    def __init__(
        self, api_client: "AireloomClient", scholix_base_url: str | None = None
    ):
        """Initializes the ScholixClient.

        Args:
            api_client: An instance of `AireloomClient` to be used for making requests.
            scholix_base_url: Optional base URL for the Scholexplorer API. If None,
                the default from `aireloom.constants` is used.
        """
        super().__init__(api_client)
        self._base_url_override = scholix_base_url or OPENAIRE_SCHOLIX_API_BASE_URL
        if self._entity_path not in ENDPOINT_DEFINITIONS:
            raise ValueError(
                f"Missing endpoint definition for Scholix path: {self._entity_path}"
            )
        self._endpoint_def = ENDPOINT_DEFINITIONS[self._entity_path]
        # Scholix does not have sort fields defined in ENDPOINT_DEFINITIONS
        logger.debug(
            f"ScholixClient initialized for base URL: {self._base_url_override}"
        )

    def _build_scholix_params(
        self,
        page: int,
        page_size: int,
        filters: dict[str, Any] | None,
    ) -> dict[str, Any]:
        """Builds the query parameter dictionary specifically for the Scholix API.

        The Scholix v3 API uses 'size' for page size and expects 'page' to be 0-indexed.

        Args:
            page: The 0-indexed page number.
            page_size: The number of results per page (maps to 'size' parameter).
            filters: A dictionary of filter criteria to include in the parameters.

        Returns:
            A dictionary of query parameters suitable for the Scholix API.
        """
        # Scholix v3 uses 'size' for page_size and 0-indexed 'page'
        params: dict[str, Any] = {"page": page, "size": page_size}
        if filters:
            params.update(filters)
        return {k: v for k, v in params.items() if v is not None}

    async def search_links(
        self,
        page: int = 0,  # Scholix default is 0-indexed
        page_size: int = DEFAULT_PAGE_SIZE,
        filters: ScholixFilters | None = None,  # Changed to Pydantic model
    ) -> ScholixResponse:
        """Searches for Scholexplorer relationship links.

        Args:
            page: The page number to retrieve (0-indexed).
            page_size: The number of results per page (max 99; values >=100 are
                silently truncated to 10 by V3).
            filters: An instance of ScholixFilters with filter criteria.
                       `sourcePid` or `targetPid` is typically required within the model.

        Returns:
            A ScholixResponse object containing the results for the requested page.

        Raises:
            ValueError: If neither sourcePid nor targetPid is provided in the filters model.
            BibliofabricError: For API communication errors or unexpected issues.
        """
        filter_dict = (
            filters.model_dump(exclude_none=True, by_alias=True) if filters else {}
        )
        logger.info(
            f"Searching Scholix links: page={page}, size={page_size}, filters={filter_dict}"
        )

        if not filter_dict.get("sourcePid") and not filter_dict.get("targetPid"):
            raise ValueError(
                "Either sourcePid or targetPid must be provided for Scholix search within the filters."
            )
        if page_size > MAX_LINK_PAGE_SIZE:
            logger.warning(
                "Scholix page_size=%d exceeds the V3 maximum of %d "
                "(values >=100 are silently truncated to 10); clamping.",
                page_size,
                MAX_LINK_PAGE_SIZE,
            )
            page_size = MAX_LINK_PAGE_SIZE

        # Pydantic model validation happens at instantiation or via .model_validate()

        params = self._build_scholix_params(
            page=page, page_size=page_size, filters=filter_dict
        )

        try:
            response = await self._api_client.request(
                method="GET",
                path=self._entity_path,  # SCHOLIX constant
                params=params,
                base_url_override=self._base_url_override,
                data=None,
                json_data=None,
            )
            return ScholixResponse.model_validate(response.json())
        except Exception as e:
            if isinstance(
                e, BibliofabricError | ValidationError
            ):  # ValidationError can come from Pydantic
                raise
            logger.exception(
                f"Failed to search {self._entity_path} with params {params} at {self._base_url_override}"
            )
            raise BibliofabricError(
                f"Unexpected error searching {self._entity_path}: {e}"
            ) from e

    async def iterate_links(
        self,
        page_size: int = DEFAULT_PAGE_SIZE,
        filters: ScholixFilters | None = None,  # Changed to Pydantic model
    ) -> AsyncIterator[ScholixRelationship]:
        """Iterates through all Scholexplorer relationship links matching the filters.

        Handles pagination automatically based on 'total_pages'.

        Args:
            page_size: The number of results per page during iteration.
            filters: An instance of ScholixFilters with filter criteria.
                       `sourcePid` or `targetPid` is typically required.

        Yields:
            ScholixRelationship objects matching the query.

        Raises:
            ValueError: If neither sourcePid nor targetPid is provided in the filters.
            BibliofabricError: For API communication errors or unexpected issues.
        """
        # The Pydantic model (ScholixFilters) will be passed to search_links,
        # which now expects the model instance.
        logger.info(
            f"Iterating Scholix links: size={page_size}, filters provided: {filters is not None}"
        )

        current_page = 0
        total_pages = 1  # Assume at least one page initially

        while current_page < total_pages:
            logger.debug(
                f"Iterating Scholix page {current_page + 1}/{total_pages if total_pages > 1 else '?'}"
            )
            try:
                # search_links now takes the ScholixFilters model directly
                response_data = await self.search_links(
                    page=current_page,
                    page_size=page_size,
                    filters=filters,
                )

                if not response_data.result:
                    logger.debug(
                        "No results found on this Scholix page, stopping iteration."
                    )
                    break

                for link in response_data.result:
                    yield link

                if current_page == 0:  # Only update total_pages on the first call
                    total_pages = response_data.total_pages
                    logger.debug(f"Total pages reported by Scholix: {total_pages}")
                    if total_pages == 0:  # No results at all
                        logger.debug(
                            "Scholix reported 0 total pages. Stopping iteration."
                        )
                        break

                if current_page >= total_pages - 1:
                    logger.debug("Last Scholix page processed, stopping iteration.")
                    break

                current_page += 1

            except Exception as e:
                if isinstance(e, BibliofabricError | ValidationError):
                    raise
                logger.exception(
                    f"Failed during iteration of {self._entity_path} on page {current_page}"
                )
                raise BibliofabricError(
                    f"Failed during iteration of {self._entity_path} on page {current_page}: {e}"
                ) from e
        logger.debug("Scholix iteration finished.")

    # ── Standard-name aliases for BaseResourceClient.collect/count/first ──

    async def search(
        self,
        page: int = 0,
        page_size: int = DEFAULT_PAGE_SIZE,
        filters: ScholixFilters | None = None,
        sort_by: str | None = None,
        search: str | None = None,  # noqa: ARG002 — Scholix doesn't support free-text search
    ) -> ScholixResponse:
        """Alias for ``search_links`` so ``collect``/``count`` can find it."""
        return await self.search_links(page=page, page_size=page_size, filters=filters)

    async def iterate(
        self,
        page_size: int = DEFAULT_PAGE_SIZE,
        filters: ScholixFilters | None = None,
        sort_by: str | None = None,
        search: str | None = None,  # noqa: ARG002
    ) -> AsyncIterator[ScholixRelationship]:
        """Alias for ``iterate_links`` so ``collect``/``count`` can find it."""
        async for link in self.iterate_links(page_size=page_size, filters=filters):
            yield link

search(page=0, page_size=DEFAULT_PAGE_SIZE, filters=None, sort_by=None, search=None) async

Alias for search_links so collect/count can find it.

Source code in src/aireloom/resources/scholix_client.py
249
250
251
252
253
254
255
256
257
258
async def search(
    self,
    page: int = 0,
    page_size: int = DEFAULT_PAGE_SIZE,
    filters: ScholixFilters | None = None,
    sort_by: str | None = None,
    search: str | None = None,  # noqa: ARG002 — Scholix doesn't support free-text search
) -> ScholixResponse:
    """Alias for ``search_links`` so ``collect``/``count`` can find it."""
    return await self.search_links(page=page, page_size=page_size, filters=filters)

Searches for Scholexplorer relationship links.

Parameters:

Name Type Description Default
page int

The page number to retrieve (0-indexed).

0
page_size int

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

DEFAULT_PAGE_SIZE
filters ScholixFilters | None

An instance of ScholixFilters with filter criteria. sourcePid or targetPid is typically required within the model.

None

Returns:

Type Description
ScholixResponse

A ScholixResponse object containing the results for the requested page.

Raises:

Type Description
ValueError

If neither sourcePid nor targetPid is provided in the filters model.

BibliofabricError

For API communication errors or unexpected issues.

Source code in src/aireloom/resources/scholix_client.py
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
async def search_links(
    self,
    page: int = 0,  # Scholix default is 0-indexed
    page_size: int = DEFAULT_PAGE_SIZE,
    filters: ScholixFilters | None = None,  # Changed to Pydantic model
) -> ScholixResponse:
    """Searches for Scholexplorer relationship links.

    Args:
        page: The page number to retrieve (0-indexed).
        page_size: The number of results per page (max 99; values >=100 are
            silently truncated to 10 by V3).
        filters: An instance of ScholixFilters with filter criteria.
                   `sourcePid` or `targetPid` is typically required within the model.

    Returns:
        A ScholixResponse object containing the results for the requested page.

    Raises:
        ValueError: If neither sourcePid nor targetPid is provided in the filters model.
        BibliofabricError: For API communication errors or unexpected issues.
    """
    filter_dict = (
        filters.model_dump(exclude_none=True, by_alias=True) if filters else {}
    )
    logger.info(
        f"Searching Scholix links: page={page}, size={page_size}, filters={filter_dict}"
    )

    if not filter_dict.get("sourcePid") and not filter_dict.get("targetPid"):
        raise ValueError(
            "Either sourcePid or targetPid must be provided for Scholix search within the filters."
        )
    if page_size > MAX_LINK_PAGE_SIZE:
        logger.warning(
            "Scholix page_size=%d exceeds the V3 maximum of %d "
            "(values >=100 are silently truncated to 10); clamping.",
            page_size,
            MAX_LINK_PAGE_SIZE,
        )
        page_size = MAX_LINK_PAGE_SIZE

    # Pydantic model validation happens at instantiation or via .model_validate()

    params = self._build_scholix_params(
        page=page, page_size=page_size, filters=filter_dict
    )

    try:
        response = await self._api_client.request(
            method="GET",
            path=self._entity_path,  # SCHOLIX constant
            params=params,
            base_url_override=self._base_url_override,
            data=None,
            json_data=None,
        )
        return ScholixResponse.model_validate(response.json())
    except Exception as e:
        if isinstance(
            e, BibliofabricError | ValidationError
        ):  # ValidationError can come from Pydantic
            raise
        logger.exception(
            f"Failed to search {self._entity_path} with params {params} at {self._base_url_override}"
        )
        raise BibliofabricError(
            f"Unexpected error searching {self._entity_path}: {e}"
        ) from e

Iterates through all Scholexplorer relationship links matching the filters.

Handles pagination automatically based on 'total_pages'.

Parameters:

Name Type Description Default
page_size int

The number of results per page during iteration.

DEFAULT_PAGE_SIZE
filters ScholixFilters | None

An instance of ScholixFilters with filter criteria. sourcePid or targetPid is typically required.

None

Yields:

Type Description
AsyncIterator[ScholixRelationship]

ScholixRelationship objects matching the query.

Raises:

Type Description
ValueError

If neither sourcePid nor targetPid is provided in the filters.

BibliofabricError

For API communication errors or unexpected issues.

Source code in src/aireloom/resources/scholix_client.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
async def iterate_links(
    self,
    page_size: int = DEFAULT_PAGE_SIZE,
    filters: ScholixFilters | None = None,  # Changed to Pydantic model
) -> AsyncIterator[ScholixRelationship]:
    """Iterates through all Scholexplorer relationship links matching the filters.

    Handles pagination automatically based on 'total_pages'.

    Args:
        page_size: The number of results per page during iteration.
        filters: An instance of ScholixFilters with filter criteria.
                   `sourcePid` or `targetPid` is typically required.

    Yields:
        ScholixRelationship objects matching the query.

    Raises:
        ValueError: If neither sourcePid nor targetPid is provided in the filters.
        BibliofabricError: For API communication errors or unexpected issues.
    """
    # The Pydantic model (ScholixFilters) will be passed to search_links,
    # which now expects the model instance.
    logger.info(
        f"Iterating Scholix links: size={page_size}, filters provided: {filters is not None}"
    )

    current_page = 0
    total_pages = 1  # Assume at least one page initially

    while current_page < total_pages:
        logger.debug(
            f"Iterating Scholix page {current_page + 1}/{total_pages if total_pages > 1 else '?'}"
        )
        try:
            # search_links now takes the ScholixFilters model directly
            response_data = await self.search_links(
                page=current_page,
                page_size=page_size,
                filters=filters,
            )

            if not response_data.result:
                logger.debug(
                    "No results found on this Scholix page, stopping iteration."
                )
                break

            for link in response_data.result:
                yield link

            if current_page == 0:  # Only update total_pages on the first call
                total_pages = response_data.total_pages
                logger.debug(f"Total pages reported by Scholix: {total_pages}")
                if total_pages == 0:  # No results at all
                    logger.debug(
                        "Scholix reported 0 total pages. Stopping iteration."
                    )
                    break

            if current_page >= total_pages - 1:
                logger.debug("Last Scholix page processed, stopping iteration.")
                break

            current_page += 1

        except Exception as e:
            if isinstance(e, BibliofabricError | ValidationError):
                raise
            logger.exception(
                f"Failed during iteration of {self._entity_path} on page {current_page}"
            )
            raise BibliofabricError(
                f"Failed during iteration of {self._entity_path} on page {current_page}: {e}"
            ) from e
    logger.debug("Scholix iteration finished.")

iterate(page_size=DEFAULT_PAGE_SIZE, filters=None, sort_by=None, search=None) async

Alias for iterate_links so collect/count can find it.

Source code in src/aireloom/resources/scholix_client.py
260
261
262
263
264
265
266
267
268
269
async def iterate(
    self,
    page_size: int = DEFAULT_PAGE_SIZE,
    filters: ScholixFilters | None = None,
    sort_by: str | None = None,
    search: str | None = None,  # noqa: ARG002
) -> AsyncIterator[ScholixRelationship]:
    """Alias for ``iterate_links`` so ``collect``/``count`` can find it."""
    async for link in self.iterate_links(page_size=page_size, filters=filters):
        yield link