Skip to content

Scholix

Models

Pydantic models for representing Scholix (Scholarly Link Exchange) data.

This module defines Pydantic models for parsing and validating data from the OpenAIRE Scholexplorer API, which follows the Scholix schema. It includes models for entities, relationships, identifiers, and the overall response structure. Reference: https://graph.openaire.eu/docs/apis/scholexplorer/v3/response_schema and DDI-CDI Codi Model: https://ddi-alliance.github.io/DDI-CDI/current/Model/

ScholixEntityTypeName = Literal['publication', 'dataset', 'software', 'other'] module-attribute

Defines the allowed types for a Scholix entity (e.g., publication, dataset).

ScholixCreator

Bases: BaseModel

Represents a creator (e.g., author, contributor) in the Scholix schema.

Attributes:

Name Type Description
name str | None

The name of the creator (aliased from "Name").

identifier list[ScholixIdentifier] | None

An optional list of ScholixIdentifier objects for the creator.

Source code in src/aireloom/models/scholix.py
44
45
46
47
48
49
50
51
52
53
54
55
class ScholixCreator(BaseModel):
    """Represents a creator (e.g., author, contributor) in the Scholix schema.

    Attributes:
        name: The name of the creator (aliased from "Name").
        identifier: An optional list of `ScholixIdentifier` objects for the creator.
    """

    name: str | None = Field(alias="Name", default=None)
    identifier: list[ScholixIdentifier] | None = Field(alias="Identifier", default=None)

    model_config = ConfigDict(populate_by_name=True, extra="allow")

ScholixEntity

Bases: BaseModel

Represents a scholarly entity (source or target) in a Scholix relationship.

Attributes:

Name Type Description
identifier list[ScholixIdentifier]

A list of ScholixIdentifier objects for the entity.

type ScholixEntityTypeName

The ScholixEntityTypeName (e.g., "publication", "dataset").

sub_type str | None

An optional subtype providing more specific classification.

title str | None

The title of the scholarly entity.

creator list[ScholixCreator] | None

A list of ScholixCreator objects.

publication_date str | None

The publication date of the entity (string format).

publisher list[ScholixPublisher] | None

A list of ScholixPublisher objects.

Source code in src/aireloom/models/scholix.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class ScholixEntity(BaseModel):
    """Represents a scholarly entity (source or target) in a Scholix relationship.

    Attributes:
        identifier: A list of `ScholixIdentifier` objects for the entity.
        type: The `ScholixEntityTypeName` (e.g., "publication", "dataset").
        sub_type: An optional subtype providing more specific classification.
        title: The title of the scholarly entity.
        creator: A list of `ScholixCreator` objects.
        publication_date: The publication date of the entity (string format).
        publisher: A list of `ScholixPublisher` objects.
    """

    identifier: list[ScholixIdentifier] = Field(alias="Identifier")
    type: ScholixEntityTypeName = Field(alias="Type")
    sub_type: str | None = Field(alias="SubType", default=None)
    title: str | None = Field(alias="Title", default=None)
    creator: list[ScholixCreator] | None = Field(alias="Creator", default=None)
    publication_date: str | None = Field(alias="PublicationDate", default=None)
    publisher: list[ScholixPublisher] | None = Field(alias="Publisher", default=None)

    model_config = ConfigDict(populate_by_name=True, extra="allow")

ScholixIdentifier

Bases: BaseModel

Represents a persistent identifier within the Scholix schema.

Attributes:

Name Type Description
id_val str

The value of the identifier (aliased from "ID").

id_scheme str

The scheme of the identifier (aliased from "IDScheme", e.g., "doi", "url").

id_url HttpUrl | None

An optional resolvable URL for the identifier (aliased from "IDURL").

Source code in src/aireloom/models/scholix.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class ScholixIdentifier(BaseModel):
    """Represents a persistent identifier within the Scholix schema.

    Attributes:
        id_val: The value of the identifier (aliased from "ID").
        id_scheme: The scheme of the identifier (aliased from "IDScheme", e.g., "doi", "url").
        id_url: An optional resolvable URL for the identifier (aliased from "IDURL").
    """

    id_val: str = Field(alias="ID")
    id_scheme: str = Field(alias="IDScheme")
    id_url: HttpUrl | None = Field(alias="IDURL", default=None)

    model_config = ConfigDict(populate_by_name=True, extra="allow")

ScholixLinkProvider

Bases: BaseModel

Represents the provider of the Scholix link.

Attributes:

Name Type Description
name str

The name of the link provider (aliased from "Name").

identifier list[ScholixIdentifier] | None

An optional list of ScholixIdentifier objects for the provider.

Source code in src/aireloom/models/scholix.py
113
114
115
116
117
118
119
120
121
122
123
124
class ScholixLinkProvider(BaseModel):
    """Represents the provider of the Scholix link.

    Attributes:
        name: The name of the link provider (aliased from "Name").
        identifier: An optional list of `ScholixIdentifier` objects for the provider.
    """

    name: str = Field(alias="Name")
    identifier: list[ScholixIdentifier] | None = Field(alias="Identifier", default=None)

    model_config = ConfigDict(populate_by_name=True, extra="allow")

ScholixPublisher

Bases: BaseModel

Represents a publisher in the Scholix schema.

Attributes:

Name Type Description
name str

The name of the publisher (aliased from "Name").

identifier list[ScholixIdentifier] | None

An optional list of ScholixIdentifier objects for the publisher.

Source code in src/aireloom/models/scholix.py
58
59
60
61
62
63
64
65
66
67
68
69
class ScholixPublisher(BaseModel):
    """Represents a publisher in the Scholix schema.

    Attributes:
        name: The name of the publisher (aliased from "Name").
        identifier: An optional list of `ScholixIdentifier` objects for the publisher.
    """

    name: str = Field(alias="Name")
    identifier: list[ScholixIdentifier] | None = Field(alias="Identifier", default=None)

    model_config = ConfigDict(populate_by_name=True, extra="allow")

ScholixRelationship

Bases: BaseModel

Represents a single Scholix relationship link between two scholarly entities.

This is a core model in the Scholix schema, detailing the link provider, the type of relationship, the source entity, and the target entity.

Attributes:

Name Type Description
link_provider list[ScholixLinkProvider] | None

A list of ScholixLinkProvider objects detailing who provided the link.

relationship_type ScholixRelationshipType

A ScholixRelationshipType object describing the nature of the link.

source ScholixEntity

A ScholixEntity representing the source of the relationship.

target ScholixEntity

A ScholixEntity representing the target of the relationship.

link_publication_date datetime | None

The date when this link was published or made available.

license_url HttpUrl | None

An optional URL pointing to the license governing the use of this link information.

harvest_date str | None

The date when this link information was last harvested or updated.

Source code in src/aireloom/models/scholix.py
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
class ScholixRelationship(BaseModel):
    """Represents a single Scholix relationship link between two scholarly entities.

    This is a core model in the Scholix schema, detailing the link provider,
    the type of relationship, the source entity, and the target entity.

    Attributes:
        link_provider: A list of `ScholixLinkProvider` objects detailing who provided the link.
        relationship_type: A `ScholixRelationshipType` object describing the nature of the link.
        source: A `ScholixEntity` representing the source of the relationship.
        target: A `ScholixEntity` representing the target of the relationship.
        link_publication_date: The date when this link was published or made available.
        license_url: An optional URL pointing to the license governing the use of this link information.
        harvest_date: The date when this link information was last harvested or updated.
    """

    link_provider: list[ScholixLinkProvider] | None = Field(
        alias="LinkProvider", default=None
    )
    relationship_type: ScholixRelationshipType = Field(alias="RelationshipType")
    source: ScholixEntity = Field(alias="Source")
    target: ScholixEntity = Field(alias="Target")
    link_publication_date: datetime | None = Field(
        alias="LinkPublicationDate",
        default=None,
        description="Date the link was published.",
    )
    license_url: HttpUrl | None = Field(alias="LicenseURL", default=None)
    harvest_date: str | None = Field(alias="HarvestDate", default=None)

    model_config = ConfigDict(populate_by_name=True, extra="allow")

ScholixRelationshipType

Bases: BaseModel

Describes the type of relationship between two Scholix entities.

Attributes:

Name Type Description
name ScholixRelationshipNameValue

The primary name of the relationship type (e.g., "References", "IsSupplementTo"). Uses ScholixRelationshipNameValue literal type.

sub_type str | None

An optional subtype for more specific relationship classification.

sub_type_schema HttpUrl | None

An optional URL pointing to the schema defining the subtype.

Source code in src/aireloom/models/scholix.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
class ScholixRelationshipType(BaseModel):
    """Describes the type of relationship between two Scholix entities.

    Attributes:
        name: The primary name of the relationship type (e.g., "References", "IsSupplementTo").
              Uses `ScholixRelationshipNameValue` literal type.
        sub_type: An optional subtype for more specific relationship classification.
        sub_type_schema: An optional URL pointing to the schema defining the subtype.
    """

    name: ScholixRelationshipNameValue = Field(alias="Name")
    sub_type: str | None = Field(alias="SubType", default=None)
    sub_type_schema: HttpUrl | None = Field(alias="SubTypeSchema", default=None)

    model_config = ConfigDict(populate_by_name=True, extra="allow")

ScholixResponse

Bases: BaseModel

Response structure for the Scholexplorer Links endpoint.

Source code in src/aireloom/models/scholix.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
class ScholixResponse(BaseModel):
    """Response structure for the Scholexplorer Links endpoint."""

    current_page: int = Field(
        alias="currentPage", description="The current page number (0-indexed)."
    )
    total_links: int = Field(
        alias="totalLinks", description="Total number of links matching the query."
    )
    total_pages: int = Field(
        alias="totalPages", description="Total number of pages available."
    )
    result: list[ScholixRelationship] = Field(
        alias="result", description="List of Scholix relationship links."
    )

    model_config = ConfigDict(populate_by_name=True, extra="allow")

Filters

Bases: BaseModel

Filter model for Scholix API endpoint.

Attributes:

Name Type Description
sourcePid str | None

Persistent identifier of the source entity.

targetPid str | None

Persistent identifier of the target entity.

sourcePublisher str | None

Publisher of the source entity.

targetPublisher str | None

Publisher of the target entity.

sourceType Literal['Publication', 'Dataset', 'Software', 'Other'] | None

Type of the source entity.

targetType Literal['Publication', 'Dataset', 'Software', 'Other'] | None

Type of the target entity.

relation str | None

Type of relation between the source and target entities.

from_date date | None

Start date of the relation (API calls use "from").

to_date date | None

End date of the relation (API calls use "to").

Source code in src/aireloom/endpoints.py
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
class ScholixFilters(BaseModel):
    """Filter model for Scholix API endpoint.

    Attributes:
        sourcePid (str | None): Persistent identifier of the source entity.
        targetPid (str | None): Persistent identifier of the target entity.
        sourcePublisher (str | None): Publisher of the source entity.
        targetPublisher (str | None): Publisher of the target entity.
        sourceType (Literal["Publication", "Dataset", "Software", "Other"] | None): Type of the source entity.
        targetType (Literal["Publication", "Dataset", "Software", "Other"] | None): Type of the target entity.
        relation (str | None): Type of relation between the source and target entities.
        from_date (date | None): Start date of the relation (API calls use "from").
        to_date (date | None): End date of the relation (API calls use "to").
    """

    sourcePid: str | None = None
    targetPid: str | None = None
    sourcePublisher: str | None = None
    targetPublisher: str | None = None
    sourceType: Literal["Publication", "Dataset", "Software", "Other"] | None = None
    targetType: Literal["Publication", "Dataset", "Software", "Other"] | None = None
    relation: str | None = None
    from_date: date | None = Field(default=None, alias="from")  # API uses "from"
    to_date: date | None = Field(default=None, alias="to")  # API uses "to"

    model_config = {"extra": "forbid", "populate_by_name": True}

Client

Client for interacting with the OpenAIRE Scholix (Scholexplorer) API.

This module provides the ScholixClient, which is specialized for querying the OpenAIRE Scholexplorer API to find links (relationships) between scholarly entities (e.g., publications, datasets). It uses a different base URL and has custom methods for searching and iterating through Scholix links, as the Scholix API has a distinct structure and pagination mechanism compared to the main OpenAIRE Graph API.

ScholixClient

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 (_scholix_base_url) 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").

_scholix_base_url 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
 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
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
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 (`_scholix_base_url`) 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").
        _scholix_base_url (str): The base URL for the Scholexplorer API.
        _endpoint_def (dict): Configuration for this endpoint from `ENDPOINT_DEFINITIONS`.
    """

    _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._scholix_base_url: str = 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._scholix_base_url}"
        )

    # _validate_filters and _validate_and_convert_filter_value are removed as Pydantic handles this.
    # Scholix API has specific PID requirements handled in search_links.

    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 API uses 'rows' 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 'rows' parameter).
            filters: A dictionary of filter criteria to include in the parameters.

        Returns:
            A dictionary of query parameters suitable for the Scholix API.
        """
        # Scholix uses 'rows' for page_size and 0-indexed 'page'
        params: dict[str, Any] = {"page": page, "rows": 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.
            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."
            )

        # Pydantic model validation happens at instantiation or via .model_validate()
        # No need for self._validate_filters(filter_dict) here.

        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._scholix_base_url,
                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._scholix_base_url}"
            )
            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.")

__init__(api_client, scholix_base_url=None)

Initializes the ScholixClient.

Parameters:

Name Type Description Default
api_client AireloomClient

An instance of AireloomClient to be used for making requests.

required
scholix_base_url str | None

Optional base URL for the Scholexplorer API. If None, the default from aireloom.constants is used.

None
Source code in src/aireloom/resources/scholix_client.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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._scholix_base_url: str = 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._scholix_base_url}"
    )

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
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
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.")

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.

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
 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
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.
        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."
        )

    # Pydantic model validation happens at instantiation or via .model_validate()
    # No need for self._validate_filters(filter_dict) here.

    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._scholix_base_url,
            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._scholix_base_url}"
        )
        raise BibliofabricError(
            f"Unexpected error searching {self._entity_path}: {e}"
        ) from e