diff --git a/xdk-gen/templates/python/client_macros.j2 b/xdk-gen/templates/python/client_macros.j2 index 77e2a7f5..bf36d03e 100644 --- a/xdk-gen/templates/python/client_macros.j2 +++ b/xdk-gen/templates/python/client_macros.j2 @@ -28,7 +28,10 @@ stream_config: Optional[StreamConfig] = None {# Macro for method return type #} {% macro return_type(operation) -%} -{% if operation.is_streaming %}Generator[{% if operation.responses and "200" in operation.responses %}{{ operation.class_name }}Response{% else %}Dict[str, Any]{% endif %}, None, None]{% elif operation.parameters and (operation.parameters | selectattr('original_name', 'equalto', 'pagination_token') | list | length > 0 or operation.parameters | selectattr('original_name', 'equalto', 'next_token') | list | length > 0) %}Iterator[{% if operation.responses and "200" in operation.responses %}{{ operation.class_name }}Response{% else %}Dict[str, Any]{% endif %}]{% else %}{% if operation.responses and "200" in operation.responses %}{{ operation.class_name }}Response{% else %}Dict[str, Any]{% endif %}{% endif %} +{% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %}bytes{% elif operation.is_streaming %}Generator[{% if operation.responses and "200" in operation.responses %}{{ operation.class_name }}Response{% else %}Dict[str, Any]{% endif %}, None, None]{% elif operation.parameters and (operation.parameters | selectattr('original_name', 'equalto', 'pagination_token') | list | length > 0 or operation.parameters | selectattr('original_name', 'equalto', 'next_token') | list | length > 0) %}Iterator[{% if operation.responses and "200" in operation.responses %}{{ operation.class_name }}Response{% else %}Dict[str, Any]{% endif %}]{% else %}{% if operation.responses and "200" in operation.responses %}{{ operation.class_name }}Response{% else %}Dict[str, Any]{% endif %}{% endif %} {%- endmacro %} {# Macro for method docstring #} @@ -388,7 +391,15 @@ headers = {% if operation.is_streaming %}{ response.raise_for_status() # Parse the response data + {% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %} + # Binary endpoint - return raw bytes + return response.content + {% else %} response_data = response.json() + {% endif %} # Convert to Pydantic model if applicable {% if operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %} @@ -594,7 +605,16 @@ headers = {% if operation.is_streaming %}{ response.raise_for_status() # Parse the response data + {% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %} + # Binary endpoint - return raw bytes (pagination not applicable for binary) + yield response.content + return + {% else %} response_data = response.json() + {% endif %} # Convert to Pydantic model if applicable {% if operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %} diff --git a/xdk-gen/templates/python/test_contracts.j2 b/xdk-gen/templates/python/test_contracts.j2 index 0ed6d6cd..21939a8b 100644 --- a/xdk-gen/templates/python/test_contracts.j2 +++ b/xdk-gen/templates/python/test_contracts.j2 @@ -62,6 +62,7 @@ class Test{{ tag.class_name }}Contracts: {% endfor %} } mock_response.raise_for_status.return_value = None + mock_response.headers = {'content-type': 'application/json'} mock_session.{{ contract_test.method|lower }}.return_value = mock_response # Prepare test parameters @@ -236,6 +237,7 @@ class Test{{ tag.class_name }}Contracts: mock_response.status_code = 200 mock_response.json.return_value = {} mock_response.raise_for_status.return_value = None + mock_response.headers = {'content-type': 'application/json'} mock_session.{{ contract_test.method|lower }}.return_value = mock_response try: @@ -277,6 +279,7 @@ class Test{{ tag.class_name }}Contracts: mock_response.status_code = {{ contract_test.response_schema.status_code }} mock_response.json.return_value = mock_response_data mock_response.raise_for_status.return_value = None + mock_response.headers = {'content-type': 'application/json'} mock_session.{{ contract_test.method|lower }}.return_value = mock_response # Prepare minimal valid parameters diff --git a/xdk-gen/templates/python/test_pagination.j2 b/xdk-gen/templates/python/test_pagination.j2 index cd7668da..8c3603fd 100644 --- a/xdk-gen/templates/python/test_pagination.j2 +++ b/xdk-gen/templates/python/test_pagination.j2 @@ -71,6 +71,7 @@ class Test{{ tag.class_name }}Pagination: } } first_page_response.raise_for_status.return_value = None + first_page_response.headers = {'content-type': 'application/json'} # Mock second page response (no next token = end of pagination) second_page_response = Mock() @@ -84,6 +85,7 @@ class Test{{ tag.class_name }}Pagination: } } second_page_response.raise_for_status.return_value = None + second_page_response.headers = {'content-type': 'application/json'} # Return different responses for consecutive calls mock_session.get.side_effect = [first_page_response, second_page_response] @@ -127,6 +129,7 @@ class Test{{ tag.class_name }}Pagination: } } mock_response.raise_for_status.return_value = None + mock_response.headers = {'content-type': 'application/json'} mock_session.get.return_value = mock_response # Test item iteration @@ -152,6 +155,7 @@ class Test{{ tag.class_name }}Pagination: "meta": {"result_count": 0} } mock_response.raise_for_status.return_value = None + mock_response.headers = {'content-type': 'application/json'} mock_session.get.return_value = mock_response method = getattr(self.{{ tag.property_name }}_client, "{{ pagination_test.method_name }}") @@ -191,6 +195,7 @@ class Test{{ tag.class_name }}Pagination: } } mock_response_with_token.raise_for_status.return_value = None + mock_response_with_token.headers = {'content-type': 'application/json'} second_page_response = Mock() second_page_response.status_code = 200 @@ -199,6 +204,7 @@ class Test{{ tag.class_name }}Pagination: "meta": {"result_count": 0} } second_page_response.raise_for_status.return_value = None + second_page_response.headers = {'content-type': 'application/json'} mock_session.get.side_effect = [mock_response_with_token, second_page_response] @@ -229,6 +235,7 @@ class Test{{ tag.class_name }}Pagination: empty_response.status_code = 200 empty_response.json.return_value = {"data": [], "meta": {"result_count": 0}} empty_response.raise_for_status.return_value = None + empty_response.headers = {'content-type': 'application/json'} mock_session.get.return_value = empty_response # Pick first paginatable method for testing diff --git a/xdk-gen/templates/typescript/client_class.j2 b/xdk-gen/templates/typescript/client_class.j2 index 91740953..655d8a39 100644 --- a/xdk-gen/templates/typescript/client_class.j2 +++ b/xdk-gen/templates/typescript/client_class.j2 @@ -117,7 +117,13 @@ export class {{ tag.class_name }}Client { {% if operation.request_body and operation.request_body.required %} * @param body {% if operation.request_body.content and operation.request_body.content["application/json"] and operation.request_body.content["application/json"].schema and operation.request_body.content["application/json"].schema.description %}{{ operation.request_body.content["application/json"].schema.description }}{% else %}Request body{% endif %} {% endif %} - * @returns {Promise<{% if operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %}{{ operation.class_name }}Response{% else %}any{% endif %}>} Promise resolving to the API response, or raw Response if requestOptions.raw is true + * @returns {Promise<{% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %}ArrayBuffer{% elif operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %}{{ operation.class_name }}Response{% else %}any{% endif %}>} Promise resolving to the API response{% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %} as binary ArrayBuffer{% endif %}, or raw Response if requestOptions.raw is true */ // Overload 1: raw: true returns Response {{ operation.method_name }}( @@ -158,7 +164,10 @@ export class {{ tag.class_name }}Client { {% if operation.parameters | rejectattr('required') | rejectattr('location', 'equalto', 'path') | list | length > 0 or (operation.request_body and not operation.request_body.required) %} options?: {{ operation.class_name }}Options {% endif %} - ): Promise<{% if operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %}{{ operation.class_name }}Response{% else %}any{% endif %}>; + ): Promise<{% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %}ArrayBuffer{% elif operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %}{{ operation.class_name }}Response{% else %}any{% endif %}>; // Implementation async {{ operation.method_name }}( {# Path parameters are always required - use location field #} @@ -181,7 +190,10 @@ export class {{ tag.class_name }}Client { {% if operation.parameters | rejectattr('required') | rejectattr('location', 'equalto', 'path') | list | length > 0 or (operation.request_body and not operation.request_body.required) %} options: {{ operation.class_name }}Options = {} {% endif %} - ): Promise<{% if operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %}{{ operation.class_name }}Response{% else %}any{% endif %} | Response> { + ): Promise<{% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %}ArrayBuffer{% elif operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %}{{ operation.class_name }}Response{% else %}any{% endif %} | Response> { // Normalize options to handle both camelCase and original API parameter names {% if operation.parameters | rejectattr('required') | rejectattr('location', 'equalto', 'path') | list | length > 0 or (operation.request_body and not operation.request_body.required) %} {% if operation.parameters | rejectattr('required') | rejectattr('location', 'equalto', 'path') | list | length > 0 %} @@ -280,10 +292,16 @@ export class {{ tag.class_name }}Client { {% endif %} }; - return this.client.request<{% if operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %}{{ operation.class_name }}Response{% else %}any{% endif %}>( + return this.client.request<{% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %}ArrayBuffer{% elif operation.responses and "200" in operation.responses or operation.responses and "201" in operation.responses %}{{ operation.class_name }}Response{% else %}any{% endif %}>( '{{ operation.method | upper }}', path + (params.toString() ? `?${params.toString()}` : ''), - finalRequestOptions + {% if operation.responses and ( + ("200" in operation.responses and operation.responses["200"].content and "application/octet-stream" in operation.responses["200"].content) or + ("201" in operation.responses and operation.responses["201"].content and "application/octet-stream" in operation.responses["201"].content) +) %}{ ...finalRequestOptions, binary: true }{% else %}finalRequestOptions{% endif %} ); } diff --git a/xdk-gen/templates/typescript/main_client.j2 b/xdk-gen/templates/typescript/main_client.j2 index 9fbb4b49..d45be528 100644 --- a/xdk-gen/templates/typescript/main_client.j2 +++ b/xdk-gen/templates/typescript/main_client.j2 @@ -77,6 +77,8 @@ export interface RequestOptions { raw?: boolean; /** Security requirements for the endpoint (from OpenAPI spec) - used for smart auth selection */ security?: Array>; + /** Whether this endpoint returns binary data (ArrayBuffer) - determined from OpenAPI spec */ + binary?: boolean; } /** @@ -416,13 +418,14 @@ export class Client { } let data: T; - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { + // Check if binary response is expected (from OpenAPI spec) + if (options.binary) { + // Return ArrayBuffer for binary endpoints + data = await response.arrayBuffer() as T; + } else { + // Default: parse as JSON and transform keys const rawData = await response.json(); - // Transform snake_case keys to camelCase to match TypeScript conventions data = transformKeys(rawData); - } else { - data = await response.text() as T; } // Return parsed body for non-streaming requests