From beb3c88f3794d3f35ca889ba43167dfbb076164a Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Thu, 26 Jun 2025 21:00:26 +0300 Subject: [PATCH 1/9] Raise exceptions instead of returning None --- openapi_python_client/templates/client.py.jinja | 14 -------------- .../templates/endpoint_macros.py.jinja | 2 +- .../templates/endpoint_module.py.jinja | 13 +++++-------- openapi_python_client/templates/errors.py.jinja | 2 +- 4 files changed, 7 insertions(+), 24 deletions(-) diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index cf0301a9a..447545396 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -6,13 +6,6 @@ import httpx {% set attrs_info = { - "raise_on_unexpected_status": namespace( - type="bool", - default="field(default=False, kw_only=True)", - docstring="Whether or not to raise an errors.UnexpectedStatus if the API returns a status code" - " that was not documented in the source OpenAPI document. Can also be provided as a keyword" - " argument to the constructor." - ), "token": namespace(type="str", default="", docstring="The token to use for authentication"), "prefix": namespace(type="str", default='"Bearer"', docstring="The prefix to use for the Authorization header"), "auth_header_name": namespace(type="str", default='"Authorization"', docstring="The name of the Authorization header"), @@ -54,14 +47,8 @@ class Client: ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. {% endmacro %} {{ httpx_args_docstring() }} -{% if not config.docstrings_on_attributes %} - - Attributes: - {{ attr_in_class_docstring("raise_on_unexpected_status") | wordwrap(101) | indent(12) }} -{% endif %} """ {% macro attributes() %} - {{ declare_attr("raise_on_unexpected_status") | indent(4) }} _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") @@ -175,7 +162,6 @@ class AuthenticatedClient: {% if not config.docstrings_on_attributes %} Attributes: - {{ attr_in_class_docstring("raise_on_unexpected_status") | wordwrap(101) | indent(12) }} {{ attr_in_class_docstring("token") | indent(8) }} {{ attr_in_class_docstring("prefix") | indent(8) }} {{ attr_in_class_docstring("auth_header_name") | indent(8) }} diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 1b53becdd..045c6e1f5 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -170,7 +170,7 @@ Args: {% endif %} Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + errors.UnexpectedStatus: If the server returns an undocumented status code. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 802fcc2ea..76720117f 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, Union, cast import httpx @@ -65,7 +65,7 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[{{ return_string }}]: +def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> {{ return_string }}: {% for response in endpoint.responses %} if response.status_code == {{ response.status_code.value }}: {% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %} @@ -81,10 +81,7 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt return None {% endif %} {% endfor %} - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None + raise errors.UnexpectedStatus(response.status_code, response.content) def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[{{ return_string }}]: @@ -114,7 +111,7 @@ def sync_detailed( {% if parsed_responses %} def sync( {{ arguments(endpoint) | indent(4) }} -) -> Optional[{{ return_string }}]: +) -> {{ return_string }}: {{ docstring(endpoint, return_string, is_detailed=false) | indent(4) }} return sync_detailed( @@ -140,7 +137,7 @@ async def asyncio_detailed( {% if parsed_responses %} async def asyncio( {{ arguments(endpoint) | indent(4) }} -) -> Optional[{{ return_string }}]: +) -> {{ return_string }}: {{ docstring(endpoint, return_string, is_detailed=false) | indent(4) }} return (await asyncio_detailed( diff --git a/openapi_python_client/templates/errors.py.jinja b/openapi_python_client/templates/errors.py.jinja index b912123d0..e3c5d2719 100644 --- a/openapi_python_client/templates/errors.py.jinja +++ b/openapi_python_client/templates/errors.py.jinja @@ -1,7 +1,7 @@ """ Contains shared errors types that can be raised from API functions """ class UnexpectedStatus(Exception): - """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + """Raised by api functions when the response status an undocumented status""" def __init__(self, status_code: int, content: bytes): self.status_code = status_code From bd2b0e8b29660b3f188c616765d510ee9305035c Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Thu, 26 Jun 2025 22:23:43 +0300 Subject: [PATCH 2/9] Fix build --- openapi_python_client/templates/model.py.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index d792797c3..6bad31654 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -3,9 +3,9 @@ from typing import Any, TypeVar, Optional, BinaryIO, TextIO, TYPE_CHECKING, Gene from attrs import define as _attrs_define from attrs import field as _attrs_field +from .. import types {% if model.is_multipart_body %} import json -from .. import types {% endif %} from ..types import UNSET, Unset From ae5fce0b923af4966298a30f9232391eebf601a3 Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Thu, 26 Jun 2025 22:40:07 +0300 Subject: [PATCH 3/9] Use Token instead of Bearer as auth prefix --- openapi_python_client/templates/client.py.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index 447545396..01f8e9820 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -7,7 +7,7 @@ import httpx {% set attrs_info = { "token": namespace(type="str", default="", docstring="The token to use for authentication"), - "prefix": namespace(type="str", default='"Bearer"', docstring="The prefix to use for the Authorization header"), + "prefix": namespace(type="str", default='"Token"', docstring="The prefix to use for the Authorization header"), "auth_header_name": namespace(type="str", default='"Authorization"', docstring="The name of the Authorization header"), } %} From bbea3d38f8502dce9a73475f9503fbf35873218b Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Mon, 4 Aug 2025 12:00:55 +0300 Subject: [PATCH 4/9] Parse number of items in the HEAD response --- .../templates/endpoint_module.py.jinja | 17 +++++++++++++++++ openapi_python_client/templates/types.py.jinja | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 76720117f..e5adfb894 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -14,8 +14,13 @@ from ... import errors {% from "endpoint_macros.py.jinja" import header_params, cookie_params, query_params, arguments, client, kwargs, parse_response, docstring, body_to_kwarg %} +{% if endpoint.method.lower() == "head" %} +{% set return_string = "int" %} +{% set parsed_responses = True %} +{% else %} {% set return_string = endpoint.response_type() %} {% set parsed_responses = (endpoint.responses | length > 0) and return_string != "Any" %} +{% endif %} def _get_kwargs( {{ arguments(endpoint, include_client=False) | indent(4) }} @@ -66,6 +71,17 @@ def _get_kwargs( def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> {{ return_string }}: +{% if endpoint.method.lower() == "head" %} + if response.status_code == HTTPStatus.OK: + try: + return int(response.headers["x-result-count"]) + except KeyError: + raise errors.UnexpectedStatus(response.status_code, b"Expected 'X-Result-Count' header for HEAD request, but it was not found.") + except ValueError: + count_val = response.headers.get("x-result-count") + msg = f"Expected 'X-Result-Count' header to be an integer, but got '{count_val}'." + raise errors.UnexpectedStatus(response.status_code, msg.encode()) +{% else %} {% for response in endpoint.responses %} if response.status_code == {{ response.status_code.value }}: {% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %} @@ -81,6 +97,7 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt return None {% endif %} {% endfor %} +{% endif %} raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index 2330892ca..0c961dee0 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -47,7 +47,7 @@ class Response(Generic[T]): status_code: HTTPStatus content: bytes headers: MutableMapping[str, str] - parsed: Optional[T] + parsed: T __all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] From c96bdd7990bdac08a10719c6f33e2eaf3462d260 Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Fri, 26 Sep 2025 11:37:27 +0300 Subject: [PATCH 5/9] Add missing parentheses --- .../templates/property_templates/uuid_property.py.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_python_client/templates/property_templates/uuid_property.py.jinja b/openapi_python_client/templates/property_templates/uuid_property.py.jinja index 3a6ce46bb..ec51a5ef5 100644 --- a/openapi_python_client/templates/property_templates/uuid_property.py.jinja +++ b/openapi_python_client/templates/property_templates/uuid_property.py.jinja @@ -27,5 +27,5 @@ if not isinstance({{ source }}, Unset): {% endmacro %} {% macro multipart(property, source, name) %} -files.append(({{ name }}, (None, str({{ source }}), "text/plain")) +files.append(({{ name }}, (None, str({{ source }}), "text/plain"))) {% endmacro %} From a3815ee12d0313f53078766330b2dde062c2d181 Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Mon, 29 Sep 2025 11:56:33 +0300 Subject: [PATCH 6/9] Fix file upload --- openapi_python_client/templates/endpoint_module.py.jinja | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index e5adfb894..586449099 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -54,7 +54,9 @@ def _get_kwargs( {% for body in endpoint.bodies %} if isinstance(body, {{body.prop.get_type_string() }}): {{ body_to_kwarg(body) | indent(8) }} + {% if body.content_type != "multipart/form-data" %}{# Need httpx to set the boundary automatically #} headers["Content-Type"] = "{{ body.content_type }}" + {% endif %} {% endfor %} {% elif endpoint.bodies | length == 1 %} {% set body = endpoint.bodies[0] %} From bca49d5d1fc64e9f0a88922b4553fe997478b30f Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Mon, 27 Oct 2025 10:47:44 +0200 Subject: [PATCH 7/9] Expose URL path in exception [WAL-9342] --- openapi_python_client/templates/endpoint_module.py.jinja | 8 +++++--- openapi_python_client/templates/errors.py.jinja | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 586449099..66e82a73b 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -78,12 +78,14 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt try: return int(response.headers["x-result-count"]) except KeyError: - raise errors.UnexpectedStatus(response.status_code, b"Expected 'X-Result-Count' header for HEAD request, but it was not found.") + raise errors.UnexpectedStatus(response.status_code, b"Expected 'X-Result-Count' header for HEAD request, but it was not found.", response.url) except ValueError: count_val = response.headers.get("x-result-count") msg = f"Expected 'X-Result-Count' header to be an integer, but got '{count_val}'." - raise errors.UnexpectedStatus(response.status_code, msg.encode()) + raise errors.UnexpectedStatus(response.status_code, msg.encode(), response.url) {% else %} + if response.status_code == 404: + raise errors.UnexpectedStatus(response.status_code, response.content, response.url) {% for response in endpoint.responses %} if response.status_code == {{ response.status_code.value }}: {% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %} @@ -100,7 +102,7 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt {% endif %} {% endfor %} {% endif %} - raise errors.UnexpectedStatus(response.status_code, response.content) + raise errors.UnexpectedStatus(response.status_code, response.content, response.url) def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[{{ return_string }}]: diff --git a/openapi_python_client/templates/errors.py.jinja b/openapi_python_client/templates/errors.py.jinja index e3c5d2719..c49ceb6d6 100644 --- a/openapi_python_client/templates/errors.py.jinja +++ b/openapi_python_client/templates/errors.py.jinja @@ -1,14 +1,18 @@ """ Contains shared errors types that can be raised from API functions """ +from httpx import URL + + class UnexpectedStatus(Exception): """Raised by api functions when the response status an undocumented status""" - def __init__(self, status_code: int, content: bytes): + def __init__(self, status_code: int, content: bytes, url: URL): self.status_code = status_code self.content = content + self.url = url super().__init__( - f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}\n\nURL:\n{url}" ) __all__ = ["UnexpectedStatus"] From 1875cd6c9a1cafd15d1146fab5c2e573d9095f65 Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Sun, 30 Nov 2025 16:15:59 +0200 Subject: [PATCH 8/9] feat: Implement automatic pagination for endpoints by parsing Link headers and abstracting `page`/`page_size` parameters from the generated API. --- .../templates/endpoint_macros.py.jinja | 8 +- .../templates/endpoint_module.py.jinja | 164 ++++++++++++++++++ .../templates/utils.py.jinja | 31 ++++ 3 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 openapi_python_client/templates/utils.py.jinja diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 045c6e1f5..8be111388 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -89,7 +89,7 @@ _kwargs["json"] = {{ property.python_name }} {% endmacro %} {# The all the kwargs passed into an endpoint (and variants thereof)) #} -{% macro arguments(endpoint, include_client=True) %} +{% macro arguments(endpoint, include_client=True, skip_pagination=False) %} {# path parameters #} {% for parameter in endpoint.path_parameters %} {{ parameter.to_string() }}, @@ -117,7 +117,9 @@ body: Union[ {% endif %} {# query parameters #} {% for parameter in endpoint.query_parameters %} +{% if not skip_pagination or parameter.name not in ['page', 'page_size'] %} {{ parameter.to_string() }}, +{% endif %} {% endfor %} {% for parameter in endpoint.header_parameters %} {{ parameter.to_string() }}, @@ -129,7 +131,7 @@ body: Union[ {% endmacro %} {# Just lists all kwargs to endpoints as name=name for passing to other functions #} -{% macro kwargs(endpoint, include_client=True) %} +{% macro kwargs(endpoint, include_client=True, skip_pagination=False) %} {% for parameter in endpoint.path_parameters %} {{ parameter.python_name }}={{ parameter.python_name }}, {% endfor %} @@ -140,7 +142,9 @@ client=client, body=body, {% endif %} {% for parameter in endpoint.query_parameters %} +{% if not skip_pagination or parameter.name not in ['page', 'page_size'] %} {{ parameter.python_name }}={{ parameter.python_name }}, +{% endif %} {% endfor %} {% for parameter in endpoint.header_parameters %} {{ parameter.python_name }}={{ parameter.python_name }}, diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 66e82a73b..c500ab2ee 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -7,6 +7,7 @@ from ...client import AuthenticatedClient, Client from ...types import Response, UNSET from ... import errors + {% for relative in endpoint.relative_imports | sort %} {{ relative }} {% endfor %} @@ -22,6 +23,10 @@ from ... import errors {% set parsed_responses = (endpoint.responses | length > 0) and return_string != "Any" %} {% endif %} +{% if endpoint.name.endswith("_list") and parsed_responses and return_string.startswith("list[") %} +from ...utils import parse_link_header +{% endif %} + def _get_kwargs( {{ arguments(endpoint, include_client=False) | indent(4) }} ) -> dict[str, Any]: @@ -165,3 +170,162 @@ async def asyncio( {{ kwargs(endpoint) }} )).parsed {% endif %} + +{% if endpoint.name.endswith("_list") and parsed_responses and return_string.startswith("list[") %} +def sync_all( + {{ arguments(endpoint, skip_pagination=True) | indent(4) }} +) -> {{ return_string }}: + """Get All Pages + + Fetch all pages of paginated results. This function automatically handles pagination + by following the 'next' link in the Link header until all results are retrieved. + + Note: page_size will be set to 100 (the maximum allowed) automatically. + + Args: +{% set all_parameters = endpoint.list_all_parameters() %} +{% if all_parameters %} +{% for parameter in all_parameters %} +{% if parameter.name not in ['page', 'page_size'] %} + {{ parameter.to_docstring() | wordwrap(90) | indent(8) }} +{% endif %} +{% endfor %} +{% endif %} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + {{ return_string }}: Combined results from all pages + """ + from urllib.parse import urlencode, parse_qs, urlparse, urlunparse + + all_results{{ ":" if return_string.startswith("list[") else " =" }} {{ return_string.replace("list[", "list[") if return_string.startswith("list[") else return_string }} = [] + + # Get initial request kwargs + kwargs = _get_kwargs( + {{ kwargs(endpoint, include_client=False, skip_pagination=True) }} + ) + + # Set page_size to maximum + if "params" not in kwargs: + kwargs["params"] = {} + kwargs["params"]["page_size"] = 100 + + # Make initial request + response = client.get_httpx_client().request(**kwargs) + parsed_response = _parse_response(client=client, response=response) + + if parsed_response: + all_results.extend(parsed_response) + + # Follow pagination links + while True: + link_header = response.headers.get("Link", "") + links = parse_link_header(link_header) + + if "next" not in links: + break + + # Extract page number from next URL + next_url = links["next"] + parsed_url = urlparse(next_url) + next_params = parse_qs(parsed_url.query) + + if "page" not in next_params: + break + + # Update only the page parameter, keep all other params + page_number = next_params["page"][0] + kwargs["params"]["page"] = page_number + + # Fetch next page + response = client.get_httpx_client().request(**kwargs) + parsed_response = _parse_response(client=client, response=response) + + if parsed_response: + all_results.extend(parsed_response) + + return all_results + + +async def asyncio_all( + {{ arguments(endpoint, skip_pagination=True) | indent(4) }} +) -> {{ return_string }}: + """Get All Pages (Async) + + Fetch all pages of paginated results asynchronously. This function automatically handles pagination + by following the 'next' link in the Link header until all results are retrieved. + + Note: page_size will be set to 100 (the maximum allowed) automatically. + + Args: +{% set all_parameters = endpoint.list_all_parameters() %} +{% if all_parameters %} +{% for parameter in all_parameters %} +{% if parameter.name not in ['page', 'page_size'] %} + {{ parameter.to_docstring() | wordwrap(90) | indent(8) }} +{% endif %} +{% endfor %} +{% endif %} + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + {{ return_string }}: Combined results from all pages + """ + from urllib.parse import urlencode, parse_qs, urlparse, urlunparse + + all_results{{ ":" if return_string.startswith("list[") else " =" }} {{ return_string.replace("list[", "list[") if return_string.startswith("list[") else return_string }} = [] + + # Get initial request kwargs + kwargs = _get_kwargs( + {{ kwargs(endpoint, include_client=False, skip_pagination=True) }} + ) + + # Set page_size to maximum + if "params" not in kwargs: + kwargs["params"] = {} + kwargs["params"]["page_size"] = 100 + + # Make initial request + response = await client.get_async_httpx_client().request(**kwargs) + parsed_response = _parse_response(client=client, response=response) + + if parsed_response: + all_results.extend(parsed_response) + + # Follow pagination links + while True: + link_header = response.headers.get("Link", "") + links = parse_link_header(link_header) + + if "next" not in links: + break + + # Extract page number from next URL + next_url = links["next"] + parsed_url = urlparse(next_url) + next_params = parse_qs(parsed_url.query) + + if "page" not in next_params: + break + + # Update only the page parameter, keep all other params + page_number = next_params["page"][0] + kwargs["params"]["page"] = page_number + + # Fetch next page + response = await client.get_async_httpx_client().request(**kwargs) + parsed_response = _parse_response(client=client, response=response) + + if parsed_response: + all_results.extend(parsed_response) + + return all_results +{% endif %} + + diff --git a/openapi_python_client/templates/utils.py.jinja b/openapi_python_client/templates/utils.py.jinja new file mode 100644 index 000000000..67b0aef73 --- /dev/null +++ b/openapi_python_client/templates/utils.py.jinja @@ -0,0 +1,31 @@ +def parse_link_header(link_header: str) -> dict[str, str]: + """ + Parse Link header to extract pagination URLs. + + Args: + link_header: The Link header string (e.g. '; rel="next"') + + Returns: + Dictionary mapping relation types (next, prev, first, last) to URLs + """ + links = {} + if not link_header: + return links + + for link in link_header.split(","): + link = link.strip() + if not link: + continue + parts = link.split(";") + if len(parts) < 2: + continue + url = parts[0].strip() + if url.startswith("<") and url.endswith(">"): + url = url[1:-1] + for part in parts[1:]: + part = part.strip() + if part.startswith("rel="): + rel_type = part[4:].strip().strip('"').strip("'") + links[rel_type] = url + break + return links From 697cbe8f4b0ff5bc484190ce85b33e7393a5ccc6 Mon Sep 17 00:00:00 2001 From: Viktor Mirieiev Date: Sun, 30 Nov 2025 22:05:12 +0200 Subject: [PATCH 9/9] feat: Add pagination support with Link header parsing and page size management, and generate utility functions. --- openapi_python_client/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index ba6380cd5..4b2c97cdb 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -172,6 +172,10 @@ def _create_package(self) -> None: types_path = self.package_dir / "types.py" types_path.write_text(types_template.render(), encoding=self.config.file_encoding) + utils_template = self.env.get_template("utils.py.jinja") + utils_path = self.package_dir / "utils.py" + utils_path.write_text(utils_template.render(), encoding=self.config.file_encoding) + def _build_metadata(self) -> None: if self.config.meta_type == MetaType.NONE: return