From 5f195d0f4086686ce435d9efde8e9b97e7f4610a Mon Sep 17 00:00:00 2001 From: Jakub Ziebinski Date: Fri, 8 Aug 2025 08:38:02 +0000 Subject: [PATCH 1/4] Add x-responses-header to the RestApiMethod struct --- .../rest_api_tools/create_endpoints_for_all_url_paths.py | 4 +++- .../_private/rest_api_tools/rest_method_model.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/python/api_client_generator/_private/rest_api_tools/create_endpoints_for_all_url_paths.py b/python/api_client_generator/_private/rest_api_tools/create_endpoints_for_all_url_paths.py index 838c066..67b5081 100644 --- a/python/api_client_generator/_private/rest_api_tools/create_endpoints_for_all_url_paths.py +++ b/python/api_client_generator/_private/rest_api_tools/create_endpoints_for_all_url_paths.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING +import msgspec + from api_client_generator._private.common.converters import snake_to_camel from api_client_generator._private.common.models_aliased import SwaggerReadyForExtraction from api_client_generator._private.common.openapi_to_python_type import convert_openapi_type_to_python_type @@ -64,7 +66,7 @@ def create_endpoints_for_all_url_paths( else: response = convert_openapi_type_to_python_type(response_schema["type"]) - method = RestApiMethod(**path[method_type]) + method = msgspec.convert(path[method_type], RestApiMethod) endpoints.append( create_endpoint(method_name, url_path, method, response, method_type, asynchronous=asynchronous) diff --git a/python/api_client_generator/_private/rest_api_tools/rest_method_model.py b/python/api_client_generator/_private/rest_api_tools/rest_method_model.py index fe59e92..b30723d 100644 --- a/python/api_client_generator/_private/rest_api_tools/rest_method_model.py +++ b/python/api_client_generator/_private/rest_api_tools/rest_method_model.py @@ -1,6 +1,6 @@ from __future__ import annotations -from msgspec import Struct +from msgspec import Struct, field from api_client_generator._private.common.models_aliased import AnyJson @@ -12,3 +12,4 @@ class RestApiMethod(Struct): operationId: str # NOQA: N815 responses: AnyJson parameters: list[AnyJson] | None = None + x_response_headers: list[AnyJson] | None = field(name="x-response-headers", default=None) -- GitLab From f5f7b56be943e4689a88545fcecfe215fd94f132 Mon Sep 17 00:00:00 2001 From: Jakub Ziebinski Date: Fri, 8 Aug 2025 08:44:39 +0000 Subject: [PATCH 2/4] Replace type AnyJson with Any since msgspec does not support forward references See: https://github.com/jcrist/msgspec/issues/676 --- .../_private/rest_api_tools/rest_method_model.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/api_client_generator/_private/rest_api_tools/rest_method_model.py b/python/api_client_generator/_private/rest_api_tools/rest_method_model.py index b30723d..63d97b1 100644 --- a/python/api_client_generator/_private/rest_api_tools/rest_method_model.py +++ b/python/api_client_generator/_private/rest_api_tools/rest_method_model.py @@ -1,8 +1,8 @@ from __future__ import annotations -from msgspec import Struct, field +from typing import Any -from api_client_generator._private.common.models_aliased import AnyJson +from msgspec import Struct, field class RestApiMethod(Struct): @@ -10,6 +10,6 @@ class RestApiMethod(Struct): summary: str description: str operationId: str # NOQA: N815 - responses: AnyJson - parameters: list[AnyJson] | None = None - x_response_headers: list[AnyJson] | None = field(name="x-response-headers", default=None) + responses: Any + parameters: Any | None = None + x_response_headers: Any | None = field(name="x-response-headers", default=None) -- GitLab From f69a1140bc5877caf2d363a5120f828041ae41a4 Mon Sep 17 00:00:00 2001 From: Jakub Ziebinski Date: Fri, 8 Aug 2025 10:21:47 +0000 Subject: [PATCH 3/4] Adjust generation functions to situation when swagger does not contain components field --- .../create_client_and_imports.py | 28 +++++++++++-------- .../rest/generate_api_client_from_swagger.py | 16 +++++++---- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/python/api_client_generator/_private/rest_api_tools/create_client_and_imports.py b/python/api_client_generator/_private/rest_api_tools/create_client_and_imports.py index 7ff4c81..a221e8b 100644 --- a/python/api_client_generator/_private/rest_api_tools/create_client_and_imports.py +++ b/python/api_client_generator/_private/rest_api_tools/create_client_and_imports.py @@ -27,12 +27,13 @@ def create_client_and_imports( # NOQA: PLR0913 api_name: str, server_url: str, endpoints: CreatedEndpoints, - types_module_path: str | Path, base_class: type[BaseApiClass] | str, base_class_source: str | None = None, + types_module_path: str | Path | None = None, endpoint_decorator: str = DEFAULT_ENDPOINT_REST_DECORATOR_NAME, additional_items_to_import: Sequence[Importable] | None = None, already_imported: list[str] | None = None, + types_generated: bool = True, ) -> GeneratedClass: """ Create a client class and resolve the needed imports. @@ -40,13 +41,14 @@ def create_client_and_imports( # NOQA: PLR0913 Args: api_name: The name of the API. server_url: The server URL for the API. - types_module_path: The path to the module containing types. endpoints: The endpoints description for the API. base_class: The base class for the API client. base_class_source: The source of the base class. + types_module_path: The path to the module containing types. endpoint_decorator: The name of the endpoint decorator to be used. additional_items_to_import: Additional items to import. already_imported: A list of already imported items. + types_generated: Whether types have been generated from the Swagger definition. """ already_imported = already_imported or [] @@ -61,17 +63,19 @@ def create_client_and_imports( # NOQA: PLR0913 base_class_name = base_class if isinstance(base_class, str) else base_class.__name__ already_imported.append(base_class_name) - types_module_path = Path(types_module_path) - root = find_package_root(types_module_path) - full_module_name = compute_full_module_name(types_module_path, root) - - needed_imports.append( - ast.ImportFrom( - module=full_module_name, - names=[ast.alias(name="*")], - level=DEFAULT_IMPORT_LEVEL, + if types_generated: + assert types_module_path is not None, "types_module_path must be provided when types are generated" + types_module_path = Path(types_module_path) + root = find_package_root(types_module_path) + full_module_name = compute_full_module_name(types_module_path, root) + + needed_imports.append( + ast.ImportFrom( + module=full_module_name, + names=[ast.alias(name="*")], + level=DEFAULT_IMPORT_LEVEL, + ) ) - ) additional_imports = import_classes(additional_items_to_import or [], already_imported) needed_imports.extend(additional_imports) diff --git a/python/api_client_generator/rest/generate_api_client_from_swagger.py b/python/api_client_generator/rest/generate_api_client_from_swagger.py index 92fa4de..3d26bd9 100644 --- a/python/api_client_generator/rest/generate_api_client_from_swagger.py +++ b/python/api_client_generator/rest/generate_api_client_from_swagger.py @@ -51,26 +51,30 @@ def generate_api_client_from_swagger( # NOQA: PLR0913 openapi_file = openapi_api_definition if isinstance(openapi_api_definition, Path) else Path(openapi_api_definition) output_package = output_package if isinstance(output_package, Path) else Path(output_package) - generate_types_from_swagger(openapi_api_definition, output_package) - openapi = json.loads(openapi_file.read_text()) + types_generated = False # Flag to check if types were generated from the Swagger definition + module_path = None + + if openapi.get("components") is not None: # Situation when swagger not contains components is possible + generate_types_from_swagger(openapi_api_definition, output_package) + types_generated = True + types_module_name = get_types_name_from_components(openapi) + module_path = output_package / f"{types_module_name}.py" server_url = get_api_name_from_server_property(openapi) api_name = hyphen_to_snake(server_url) - types_module_name = get_types_name_from_components(openapi) - module_path = output_package / f"{types_module_name}.py" - endpoints = create_endpoints_for_all_url_paths(openapi, asynchronous=asynchronous) class_and_imports = create_client_and_imports( api_name, server_url, endpoints, - module_path, base_class, base_class_source, + module_path, endpoint_decorator, + types_generated=types_generated, ) client_module = ast.Module(body=[*class_and_imports.imports, class_and_imports.class_def], type_ignores=[]) -- GitLab From d620eb11d0d86fdb7f209eeb2df974feb1c40fdd Mon Sep 17 00:00:00 2001 From: Jakub Ziebinski Date: Tue, 26 Aug 2025 07:54:49 +0000 Subject: [PATCH 4/4] Get the real items of the array response if the endpoint response is an array Previously generator assymed that the schema of the response item would be ResponseSchemaItem which was wrong. --- .../json_rpc/generate_api_description.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/python/api_client_generator/json_rpc/generate_api_description.py b/python/api_client_generator/json_rpc/generate_api_description.py index 4616725..9fdb85d 100644 --- a/python/api_client_generator/json_rpc/generate_api_description.py +++ b/python/api_client_generator/json_rpc/generate_api_description.py @@ -43,6 +43,7 @@ from api_client_generator._private.description_tools import ( get_params_name_for_endpoint, get_result_name_for_endpoint, is_result_array, + get_last_part_of_ref, ) from api_client_generator._private.export_client_module_to_file import export_module_to_file from api_client_generator.generate_types_from_swagger import generate_types_from_swagger @@ -107,7 +108,14 @@ def generate_api_description( endpoint_description["response_array"] = True assert isinstance(endpoint_description["result"], str), "Result must be a string at this point." - endpoint_description["result"] += "Item" # It will be typed as list[ClasNameResponseItem] + + potential_ref = components[result_name].get("items", {}).get("$ref") + if potential_ref: # This means that the elements of the array are represented by a custom class/model/components, which are contained in the components section. + endpoint_description["result"] = snake_to_camel( + get_last_part_of_ref(components[result_name]["items"]["$ref"]) + ) + else: + endpoint_description["result"] += "Item" api_description[api_name][endpoint_name] = endpoint_description -- GitLab