Skip to content

Client

AireloomClient

Bases: BaseApiClient

Asynchronous client for interacting with the OpenAIRE Graph and Scholix APIs.

This client provides a high-level interface to various OpenAIRE API endpoints, handling authentication, request retries, caching, and rate limiting. It builds upon the generic bibliofabric.client.BaseApiClient and is configured specifically for OpenAIRE services.

Resource clients for different OpenAIRE entities (e.g., research products, projects, organizations) are available as properties of this client.

Authentication is handled automatically based on provided settings or can be customized by passing an auth_strategy. If no credentials or strategy are provided, requests will be made without authentication.

Typical usage:

async with AireloomClient() as client:
    product = await client.research_products.get(
        "some_product_id"
    )
    async for project in client.projects.iterate(
        filters=ProjectFilters(...)
    ):
        print(project.title)

Attributes:

Name Type Description
research_products ResearchProductsClient

Client for research product endpoints.

organizations OrganizationsClient

Client for organization endpoints.

projects ProjectsClient

Client for project endpoints.

data_sources DataSourcesClient

Client for data source endpoints.

scholix ScholixClient

Client for Scholix (scholarly link exchange) endpoints.

_settings ApiSettings

The resolved API settings for this client instance.

_scholix_base_url str

The base URL for the Scholix API.

Source code in src/aireloom/client.py
 27
 28
 29
 30
 31
 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
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
class AireloomClient(BaseApiClient):
    """Asynchronous client for interacting with the OpenAIRE Graph and Scholix APIs.

    This client provides a high-level interface to various OpenAIRE API endpoints,
    handling authentication, request retries, caching, and rate limiting.
    It builds upon the generic `bibliofabric.client.BaseApiClient` and is configured
    specifically for OpenAIRE services.

    Resource clients for different OpenAIRE entities (e.g., research products,
    projects, organizations) are available as properties of this client.

    Authentication is handled automatically based on provided settings or can be
    customized by passing an `auth_strategy`. If no credentials or strategy are
    provided, requests will be made without authentication.

    Typical usage:
    ```python
    async with AireloomClient() as client:
        product = await client.research_products.get(
            "some_product_id"
        )
        async for project in client.projects.iterate(
            filters=ProjectFilters(...)
        ):
            print(project.title)
    ```

    Attributes:
        research_products (ResearchProductsClient): Client for research product endpoints.
        organizations (OrganizationsClient): Client for organization endpoints.
        projects (ProjectsClient): Client for project endpoints.
        data_sources (DataSourcesClient): Client for data source endpoints.
        scholix (ScholixClient): Client for Scholix (scholarly link exchange) endpoints.
        _settings (ApiSettings): The resolved API settings for this client instance.
        _scholix_base_url (str): The base URL for the Scholix API.
    """

    def __init__(
        self,
        settings: ApiSettings | None = None,
        auth_strategy: AuthStrategy | None = None,
        *,
        api_token: str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        base_url: str = OPENAIRE_GRAPH_API_BASE_URL,
        scholix_base_url: str = OPENAIRE_SCHOLIX_API_BASE_URL,
    ):
        """Initializes the AireloomClient.

        This constructor sets up the client with necessary configurations,
        determines the authentication strategy, and initializes resource-specific
        sub-clients.

        Authentication Strategy Resolution:
        - If `auth_strategy` is explicitly provided, it is used.
        - Otherwise, credentials (api_token, client_id, client_secret) passed
          directly to this constructor take precedence over those in `settings`.
        - If credentials are not passed directly, they are sourced from `settings`
          (which are loaded from environment variables or .env files).
        - The order of preference for automatic strategy selection is:
            1. Client Credentials (if client_id & client_secret are available)
            2. Static Token (if api_token is available)
            3. No Authentication (if no credentials are found)

        Args:
            settings: An optional `ApiSettings` instance. If `None`, global settings
                are loaded via `aireloom.config.get_settings()`. These settings
                can be a source for authentication credentials and other client behaviors.
            auth_strategy: An optional explicit `AuthStrategy` instance. If provided,
                it overrides automatic authentication resolution.
            api_token: An optional static API token. If provided, it takes precedence
                over `settings.openaire_api_token` for StaticTokenAuth.
            client_id: An optional client ID for ClientCredentialsAuth. Takes
                precedence over `settings.openaire_client_id`.
            client_secret: An optional client secret for ClientCredentialsAuth. Takes
                precedence over `settings.openaire_client_secret`.
            base_url: The base URL for the OpenAIRE Graph API. Defaults to the
                production OpenAIRE Graph API URL.
            scholix_base_url: The base URL for the OpenAIRE Scholix API. Defaults
                to the production OpenAIRE Scholix API URL.
        """
        self._settings: ApiSettings = settings or get_settings()
        self._scholix_base_url: str = scholix_base_url.rstrip("/")

        logger.debug(
            f"AireloomClient.__init__ settings: id={id(self._settings)}, "
            f"client_id={self._settings.openaire_client_id}, "
            f"token={self._settings.openaire_api_token}, "
            f"timeout={self._settings.request_timeout}"
        )

        # Determine authentication strategy if not explicitly provided
        if auth_strategy:
            logger.info(
                f"Using explicitly provided authentication strategy: {type(auth_strategy).__name__}"
            )
            resolved_auth_strategy = auth_strategy
        else:
            logger.info(
                "Determining auth type based on provided parameters or settings."
            )
            # Use overrides if provided, otherwise use settings
            _client_id = client_id or self._settings.openaire_client_id
            _client_secret = client_secret or self._settings.openaire_client_secret
            _api_token = api_token or self._settings.openaire_api_token
            _token_url = self._settings.openaire_token_url

            logger.debug(
                f"Auth decision: client_id_param={client_id}, "
                f"settings_client_id={self._settings.openaire_client_id}, "
                f"api_token_param={api_token}, "
                f"settings_api_token={self._settings.openaire_api_token}"
            )

            if _client_id and _client_secret:
                logger.info("Using Client Credentials authentication.")
                if client_id and client_secret:
                    logger.info(
                        "Client ID and secret were directly passed as parameters."
                    )
                else:
                    logger.info(
                        "Client ID and secret were loaded from settings or environment variables."
                    )
                resolved_auth_strategy = ClientCredentialsAuth(
                    client_id=_client_id,
                    client_secret=_client_secret,
                    token_url=_token_url,
                )
            elif _api_token:
                logger.info("Using Static Token authentication.")
                resolved_auth_strategy = StaticTokenAuth(token=_api_token)
            else:
                logger.info("No authentication credentials found, using NoAuth.")
                resolved_auth_strategy = NoAuth()

        # Create the OpenAIRE response unwrapper
        unwrapper = OpenAireUnwrapper()

        # Initialize the base client with all the generic functionality
        super().__init__(
            base_url=base_url,
            settings=self._settings,
            auth_strategy=resolved_auth_strategy,
            response_unwrapper=unwrapper,
        )

        # Initialize OpenAIRE-specific resource clients
        self._research_products = ResearchProductsClient(api_client=self)
        self._organizations = OrganizationsClient(api_client=self)
        self._projects = ProjectsClient(api_client=self)
        self._data_sources = DataSourcesClient(api_client=self)
        self._scholix = ScholixClient(
            api_client=self, scholix_base_url=self._scholix_base_url
        )

        logger.debug("AireloomClient initialized successfully.")

    @property
    def research_products(self) -> ResearchProductsClient:
        """Provides access to the ResearchProductsClient for OpenAIRE research product APIs."""
        return self._research_products

    @property
    def organizations(self) -> OrganizationsClient:
        """Provides access to the OrganizationsClient for OpenAIRE organization APIs."""
        return self._organizations

    @property
    def projects(self) -> ProjectsClient:
        """Provides access to the ProjectsClient for OpenAIRE project APIs."""
        return self._projects

    @property
    def data_sources(self) -> DataSourcesClient:
        """Provides access to the DataSourcesClient for OpenAIRE data source APIs."""
        return self._data_sources

    @property
    def scholix(self) -> ScholixClient:
        """Provides access to the ScholixClient for OpenAIRE Scholix (scholarly link) APIs."""
        return self._scholix

    async def __aenter__(self) -> Self:
        """Async context manager entry."""
        logger.info(
            f"AireloomClient.__aenter__() called. Client ID: {id(self)}. "
            f"HTTP client closed: {self._http_client.is_closed if self._http_client else 'N/A'}"
        )
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """Async context manager exit."""
        logger.info(
            f"AireloomClient.__aexit__() called. Client ID: {id(self)}. "
            f"HTTP client closed before aclose: {self._http_client.is_closed if self._http_client else 'N/A'}"
        )
        await self.aclose()
        logger.info(
            f"AireloomClient.__aexit__() finished. Client ID: {id(self)}. "
            f"HTTP client closed after aclose: {self._http_client.is_closed if self._http_client else 'N/A'}"
        )

data_sources property

Provides access to the DataSourcesClient for OpenAIRE data source APIs.

organizations property

Provides access to the OrganizationsClient for OpenAIRE organization APIs.

projects property

Provides access to the ProjectsClient for OpenAIRE project APIs.

research_products property

Provides access to the ResearchProductsClient for OpenAIRE research product APIs.

scholix property

Provides access to the ScholixClient for OpenAIRE Scholix (scholarly link) APIs.

__aenter__() async

Async context manager entry.

Source code in src/aireloom/client.py
211
212
213
214
215
216
217
async def __aenter__(self) -> Self:
    """Async context manager entry."""
    logger.info(
        f"AireloomClient.__aenter__() called. Client ID: {id(self)}. "
        f"HTTP client closed: {self._http_client.is_closed if self._http_client else 'N/A'}"
    )
    return self

__aexit__(exc_type, exc_val, exc_tb) async

Async context manager exit.

Source code in src/aireloom/client.py
219
220
221
222
223
224
225
226
227
228
229
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
    """Async context manager exit."""
    logger.info(
        f"AireloomClient.__aexit__() called. Client ID: {id(self)}. "
        f"HTTP client closed before aclose: {self._http_client.is_closed if self._http_client else 'N/A'}"
    )
    await self.aclose()
    logger.info(
        f"AireloomClient.__aexit__() finished. Client ID: {id(self)}. "
        f"HTTP client closed after aclose: {self._http_client.is_closed if self._http_client else 'N/A'}"
    )

__init__(settings=None, auth_strategy=None, *, api_token=None, client_id=None, client_secret=None, base_url=OPENAIRE_GRAPH_API_BASE_URL, scholix_base_url=OPENAIRE_SCHOLIX_API_BASE_URL)

Initializes the AireloomClient.

This constructor sets up the client with necessary configurations, determines the authentication strategy, and initializes resource-specific sub-clients.

Authentication Strategy Resolution: - If auth_strategy is explicitly provided, it is used. - Otherwise, credentials (api_token, client_id, client_secret) passed directly to this constructor take precedence over those in settings. - If credentials are not passed directly, they are sourced from settings (which are loaded from environment variables or .env files). - The order of preference for automatic strategy selection is: 1. Client Credentials (if client_id & client_secret are available) 2. Static Token (if api_token is available) 3. No Authentication (if no credentials are found)

Parameters:

Name Type Description Default
settings ApiSettings | None

An optional ApiSettings instance. If None, global settings are loaded via aireloom.config.get_settings(). These settings can be a source for authentication credentials and other client behaviors.

None
auth_strategy AuthStrategy | None

An optional explicit AuthStrategy instance. If provided, it overrides automatic authentication resolution.

None
api_token str | None

An optional static API token. If provided, it takes precedence over settings.openaire_api_token for StaticTokenAuth.

None
client_id str | None

An optional client ID for ClientCredentialsAuth. Takes precedence over settings.openaire_client_id.

None
client_secret str | None

An optional client secret for ClientCredentialsAuth. Takes precedence over settings.openaire_client_secret.

None
base_url str

The base URL for the OpenAIRE Graph API. Defaults to the production OpenAIRE Graph API URL.

OPENAIRE_GRAPH_API_BASE_URL
scholix_base_url str

The base URL for the OpenAIRE Scholix API. Defaults to the production OpenAIRE Scholix API URL.

OPENAIRE_SCHOLIX_API_BASE_URL
Source code in src/aireloom/client.py
 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
def __init__(
    self,
    settings: ApiSettings | None = None,
    auth_strategy: AuthStrategy | None = None,
    *,
    api_token: str | None = None,
    client_id: str | None = None,
    client_secret: str | None = None,
    base_url: str = OPENAIRE_GRAPH_API_BASE_URL,
    scholix_base_url: str = OPENAIRE_SCHOLIX_API_BASE_URL,
):
    """Initializes the AireloomClient.

    This constructor sets up the client with necessary configurations,
    determines the authentication strategy, and initializes resource-specific
    sub-clients.

    Authentication Strategy Resolution:
    - If `auth_strategy` is explicitly provided, it is used.
    - Otherwise, credentials (api_token, client_id, client_secret) passed
      directly to this constructor take precedence over those in `settings`.
    - If credentials are not passed directly, they are sourced from `settings`
      (which are loaded from environment variables or .env files).
    - The order of preference for automatic strategy selection is:
        1. Client Credentials (if client_id & client_secret are available)
        2. Static Token (if api_token is available)
        3. No Authentication (if no credentials are found)

    Args:
        settings: An optional `ApiSettings` instance. If `None`, global settings
            are loaded via `aireloom.config.get_settings()`. These settings
            can be a source for authentication credentials and other client behaviors.
        auth_strategy: An optional explicit `AuthStrategy` instance. If provided,
            it overrides automatic authentication resolution.
        api_token: An optional static API token. If provided, it takes precedence
            over `settings.openaire_api_token` for StaticTokenAuth.
        client_id: An optional client ID for ClientCredentialsAuth. Takes
            precedence over `settings.openaire_client_id`.
        client_secret: An optional client secret for ClientCredentialsAuth. Takes
            precedence over `settings.openaire_client_secret`.
        base_url: The base URL for the OpenAIRE Graph API. Defaults to the
            production OpenAIRE Graph API URL.
        scholix_base_url: The base URL for the OpenAIRE Scholix API. Defaults
            to the production OpenAIRE Scholix API URL.
    """
    self._settings: ApiSettings = settings or get_settings()
    self._scholix_base_url: str = scholix_base_url.rstrip("/")

    logger.debug(
        f"AireloomClient.__init__ settings: id={id(self._settings)}, "
        f"client_id={self._settings.openaire_client_id}, "
        f"token={self._settings.openaire_api_token}, "
        f"timeout={self._settings.request_timeout}"
    )

    # Determine authentication strategy if not explicitly provided
    if auth_strategy:
        logger.info(
            f"Using explicitly provided authentication strategy: {type(auth_strategy).__name__}"
        )
        resolved_auth_strategy = auth_strategy
    else:
        logger.info(
            "Determining auth type based on provided parameters or settings."
        )
        # Use overrides if provided, otherwise use settings
        _client_id = client_id or self._settings.openaire_client_id
        _client_secret = client_secret or self._settings.openaire_client_secret
        _api_token = api_token or self._settings.openaire_api_token
        _token_url = self._settings.openaire_token_url

        logger.debug(
            f"Auth decision: client_id_param={client_id}, "
            f"settings_client_id={self._settings.openaire_client_id}, "
            f"api_token_param={api_token}, "
            f"settings_api_token={self._settings.openaire_api_token}"
        )

        if _client_id and _client_secret:
            logger.info("Using Client Credentials authentication.")
            if client_id and client_secret:
                logger.info(
                    "Client ID and secret were directly passed as parameters."
                )
            else:
                logger.info(
                    "Client ID and secret were loaded from settings or environment variables."
                )
            resolved_auth_strategy = ClientCredentialsAuth(
                client_id=_client_id,
                client_secret=_client_secret,
                token_url=_token_url,
            )
        elif _api_token:
            logger.info("Using Static Token authentication.")
            resolved_auth_strategy = StaticTokenAuth(token=_api_token)
        else:
            logger.info("No authentication credentials found, using NoAuth.")
            resolved_auth_strategy = NoAuth()

    # Create the OpenAIRE response unwrapper
    unwrapper = OpenAireUnwrapper()

    # Initialize the base client with all the generic functionality
    super().__init__(
        base_url=base_url,
        settings=self._settings,
        auth_strategy=resolved_auth_strategy,
        response_unwrapper=unwrapper,
    )

    # Initialize OpenAIRE-specific resource clients
    self._research_products = ResearchProductsClient(api_client=self)
    self._organizations = OrganizationsClient(api_client=self)
    self._projects = ProjectsClient(api_client=self)
    self._data_sources = DataSourcesClient(api_client=self)
    self._scholix = ScholixClient(
        api_client=self, scholix_base_url=self._scholix_base_url
    )

    logger.debug("AireloomClient initialized successfully.")

Base model

Base Pydantic models for OpenAIRE API entities and responses.

This module defines foundational Pydantic models used across the aireloom library to represent common structures in OpenAIRE API responses, such as response headers, base entity identifiers, and generic API response envelopes. These models provide data validation and a clear structure for API data.

ApiResponse

Bases: BaseModel

Generic Pydantic model for standard OpenAIRE API list responses.

This model represents the common envelope structure for API responses that return a list of entities. It includes a header (metadata) and a results field containing the list of entities. It is generic over EntityType to allow specific entity types to be used in the results list.

Attributes:

Name Type Description
header Header

A Header object containing metadata about the response.

results list[EntityType] | None

An optional list of entities of type EntityType. A validator ensures this field is a list or None, handling potential API inconsistencies gracefully.

Source code in src/aireloom/models/base.py
 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
class ApiResponse[EntityType: "BaseEntity"](BaseModel):
    """Generic Pydantic model for standard OpenAIRE API list responses.

    This model represents the common envelope structure for API responses that
    return a list of entities. It includes a `header` (metadata) and a `results`
    field containing the list of entities. It is generic over `EntityType` to
    allow specific entity types to be used in the `results` list.

    Attributes:
        header: A `Header` object containing metadata about the response.
        results: An optional list of entities of type `EntityType`. A validator
                 ensures this field is a list or None, handling potential API
                 inconsistencies gracefully.
    """

    header: Header
    # Results can sometimes be null/absent, sometimes an empty list
    results: list[EntityType] | None = None

    @field_validator("results", mode="before")
    @classmethod
    def handle_null_results(cls, v: Any) -> list[EntityType] | None:
        """Ensure 'results' is a list or None.

        Handles potential None or unexpected formats from the API.
        Logs a warning and returns an empty list for unexpected types.
        """
        if v is None:
            return None  # Explicitly return None if API sends null
        if isinstance(v, list):
            return v  # Already a list

        # Handle unexpected formats (e.g., dict wrappers like {'result': [...]})
        # or other non-list types by logging and returning an empty list.
        logger.warning(
            f"Unexpected format for 'results' field: {type(v)}. "
            f"Expected list or None, got {v!r}. Returning empty list."
        )
        return []

    model_config = ConfigDict(extra="allow")

handle_null_results(v) classmethod

Ensure 'results' is a list or None.

Handles potential None or unexpected formats from the API. Logs a warning and returns an empty list for unexpected types.

Source code in src/aireloom/models/base.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
@field_validator("results", mode="before")
@classmethod
def handle_null_results(cls, v: Any) -> list[EntityType] | None:
    """Ensure 'results' is a list or None.

    Handles potential None or unexpected formats from the API.
    Logs a warning and returns an empty list for unexpected types.
    """
    if v is None:
        return None  # Explicitly return None if API sends null
    if isinstance(v, list):
        return v  # Already a list

    # Handle unexpected formats (e.g., dict wrappers like {'result': [...]})
    # or other non-list types by logging and returning an empty list.
    logger.warning(
        f"Unexpected format for 'results' field: {type(v)}. "
        f"Expected list or None, got {v!r}. Returning empty list."
    )
    return []

BaseEntity

Bases: BaseModel

A base Pydantic model for OpenAIRE entities (e.g., publication, project).

This model provides a common foundation for all specific entity types, primarily by ensuring an id field is present, which is a common identifier across most OpenAIRE entities. It allows extra fields from the API to be captured without causing validation errors.

Attributes:

Name Type Description
id str

The unique identifier for the entity.

Source code in src/aireloom/models/base.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class BaseEntity(BaseModel):
    """A base Pydantic model for OpenAIRE entities (e.g., publication, project).

    This model provides a common foundation for all specific entity types,
    primarily by ensuring an `id` field is present, which is a common
    identifier across most OpenAIRE entities. It allows extra fields from the
    API to be captured without causing validation errors.

    Attributes:
        id: The unique identifier for the entity.
    """

    # Common identifier across most entities
    id: str

    model_config = ConfigDict(extra="allow")

Header

Bases: BaseModel

Represents the 'header' section commonly found in OpenAIRE API responses.

This model captures metadata about the API response, such as status, query time, total number of results found (numFound), pagination details like nextCursor, and page size. It includes validators to coerce numeric fields that might be returned as strings by the API.

Attributes:

Name Type Description
status str | None

Optional status message from the API.

code str | None

Optional status code from the API.

message str | None

Optional descriptive message from the API.

queryTime int | None

Time taken by the API to process the query, in milliseconds.

numFound int | None

Total number of results found matching the query criteria.

nextCursor str | HttpUrl | None

The cursor string to use for fetching the next page of results. Can be a string or an HttpUrl.

pageSize int | None

The number of results included in the current page.

Source code in src/aireloom/models/base.py
20
21
22
23
24
25
26
27
28
29
30
31
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
class Header(BaseModel):
    """Represents the 'header' section commonly found in OpenAIRE API responses.

    This model captures metadata about the API response, such as status,
    query time, total number of results found (`numFound`), pagination details
    like `nextCursor`, and page size. It includes validators to coerce
    numeric fields that might be returned as strings by the API.

    Attributes:
        status: Optional status message from the API.
        code: Optional status code from the API.
        message: Optional descriptive message from the API.
        queryTime: Time taken by the API to process the query, in milliseconds.
        numFound: Total number of results found matching the query criteria.
        nextCursor: The cursor string to use for fetching the next page of results.
                    Can be a string or an HttpUrl.
        pageSize: The number of results included in the current page.
    """

    # Note: status, code, message are typically expected, but optional for robustness.
    status: str | None = None
    code: str | None = None
    message: str | None = None
    # total and count are often strings in the API response, needs validation/coercion
    queryTime: int | None = None
    numFound: int | None = None  # next/prev can be full URLs or just the cursor string
    nextCursor: str | HttpUrl | None = Field(default=None)  # API returns "nextCursor"
    pageSize: int | None = None

    @field_validator("queryTime", "numFound", "pageSize", mode="before")
    @classmethod
    def coerce_str_to_int(cls, v: Any) -> int | None:
        """Coerce string representations of numbers to integers, logging on failure."""
        if isinstance(v, str):
            try:
                return int(v)
            except (ValueError, TypeError):
                logger.warning(f"Could not coerce header value '{v}' to int.")
                return None
        # Allow integers through if they somehow bypass 'before' validation or API changes
        if isinstance(v, int):
            return v
        # Handle other unexpected types if necessary
        logger.warning(f"Unexpected type {type(v)} for header numeric value '{v}'.")
        return None

    model_config = ConfigDict(extra="allow")

coerce_str_to_int(v) classmethod

Coerce string representations of numbers to integers, logging on failure.

Source code in src/aireloom/models/base.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@field_validator("queryTime", "numFound", "pageSize", mode="before")
@classmethod
def coerce_str_to_int(cls, v: Any) -> int | None:
    """Coerce string representations of numbers to integers, logging on failure."""
    if isinstance(v, str):
        try:
            return int(v)
        except (ValueError, TypeError):
            logger.warning(f"Could not coerce header value '{v}' to int.")
            return None
    # Allow integers through if they somehow bypass 'before' validation or API changes
    if isinstance(v, int):
        return v
    # Handle other unexpected types if necessary
    logger.warning(f"Unexpected type {type(v)} for header numeric value '{v}'.")
    return None