From 484e9d9ac0af6a5c5f528520cedaa45d6b6980bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Sat, 28 Mar 2026 00:31:32 +0100 Subject: [PATCH] fix(codegen): generate enums for query params Use the inline schema generation path for query parameter models so enum metadata is preserved in generated SDK types. Regenerate the SDK and add a regression test covering enum-typed query params such as transactions order. --- codegen/pkg/builder/transform.go | 29 ++++++++++++++--------------- sumup/customers/resource.py | 6 +----- sumup/members/resource.py | 7 +------ sumup/memberships/__init__.py | 2 ++ sumup/memberships/resource.py | 14 ++++++++------ sumup/merchants/resource.py | 6 +----- sumup/payouts/__init__.py | 8 ++++++++ sumup/payouts/resource.py | 17 +++++++++++++---- sumup/receipts/resource.py | 4 +--- sumup/transactions/__init__.py | 12 ++++++++++++ sumup/transactions/resource.py | 32 ++++++++++++++++++++++++++------ tests/test_query_params.py | 12 ++++++++++++ 12 files changed, 99 insertions(+), 50 deletions(-) diff --git a/codegen/pkg/builder/transform.go b/codegen/pkg/builder/transform.go index 20069c6..bf3e185 100644 --- a/codegen/pkg/builder/transform.go +++ b/codegen/pkg/builder/transform.go @@ -67,37 +67,29 @@ func (b *Builder) pathsToParamTypes(paths *v3.Paths) []Writable { if len(opSpec.Parameters) > 0 { fields := make([]Property, 0) + fieldTypes := make([]Writable, 0) + paramsTypeName := operationName + "Params" for _, p := range opSpec.Parameters { // path parameters are passed as a parameters to the generated method if p.In == "path" || p.In == "header" { continue } - name := p.Name - alias := name - if p.GoLow().IsReference() { - name = strcase.ToCamel(strings.TrimPrefix(p.Schema.GetReference(), "#/components/schemas/")) - } - - // NOTE: we should leave it as-is, but that doesn't work with 'include[]' - name = strings.ReplaceAll(name, "[]", "") - - // NOTE: this also needs to be handled properly - name = strings.ReplaceAll(name, ".", "_") - - typ := b.convertToValidPyType("", p.Schema) + alias := p.Name + name := parameterFieldName(alias) + typeName, types := b.genSchema(p.Schema, paramsTypeName+strcase.ToCamel(name)) + fieldTypes = append(fieldTypes, types...) fields = append(fields, Property{ Name: name, SerializedName: alias, - Type: typ, + Type: typeName, Optional: p.Required == nil || !*p.Required, Comment: parameterPropertyDoc(p.Schema.Schema()), }) } if len(fields) != 0 { - paramsTypeName := operationName + "Params" paramsTpl := ClassDeclaration{ Type: "struct", Name: paramsTypeName, @@ -105,6 +97,7 @@ func (b *Builder) pathsToParamTypes(paths *v3.Paths) []Writable { Fields: fields, } + paramTypes = append(paramTypes, fieldTypes...) paramTypes = append(paramTypes, ¶msTpl) } } @@ -114,6 +107,12 @@ func (b *Builder) pathsToParamTypes(paths *v3.Paths) []Writable { return paramTypes } +func parameterFieldName(name string) string { + name = strings.ReplaceAll(name, "[]", "") + name = strings.ReplaceAll(name, ".", "_") + return name +} + // pathsToResponseTypes generates response types for operations. This is responsible only for inlined // schemas that are specific to the operation itself and are not references. func (b *Builder) pathsToResponseTypes(paths *v3.Paths) []Writable { diff --git a/sumup/customers/resource.py b/sumup/customers/resource.py index bf6802e..1b42978 100755 --- a/sumup/customers/resource.py +++ b/sumup/customers/resource.py @@ -2,11 +2,7 @@ from __future__ import annotations from .._service import Resource, AsyncResource, HeaderTypes from .._exceptions import APIError -from ..types import ( - Customer, - PaymentInstrumentResponse, - PersonalDetails, -) +from ..types import Customer, PaymentInstrumentResponse, PersonalDetails import httpx import typing import pydantic diff --git a/sumup/members/resource.py b/sumup/members/resource.py index f654912..1527cce 100755 --- a/sumup/members/resource.py +++ b/sumup/members/resource.py @@ -3,12 +3,7 @@ from .._service import Resource, AsyncResource, HeaderTypes from .._exceptions import APIError from .._secret import Secret -from ..types import ( - Attributes, - Member, - MembershipStatus, - Metadata, -) +from ..types import Attributes, Member, MembershipStatus, Metadata import httpx import typing import pydantic diff --git a/sumup/memberships/__init__.py b/sumup/memberships/__init__.py index 54ded2b..8cb041e 100755 --- a/sumup/memberships/__init__.py +++ b/sumup/memberships/__init__.py @@ -2,6 +2,7 @@ from .resource import ( MembershipsResource, AsyncMembershipsResource, + ListMembershipsParamsResourceParentType, ListMembershipsParams, ListMemberships200Response, ) @@ -20,6 +21,7 @@ __all__ = [ "MembershipsResource", "AsyncMembershipsResource", + "ListMembershipsParamsResourceParentType", "ListMembershipsParams", "ListMemberships200Response", "Attributes", diff --git a/sumup/memberships/resource.py b/sumup/memberships/resource.py index a49383d..9e85e18 100755 --- a/sumup/memberships/resource.py +++ b/sumup/memberships/resource.py @@ -2,16 +2,18 @@ from __future__ import annotations from .._service import Resource, AsyncResource, HeaderTypes from .._exceptions import APIError -from ..types import ( - Membership, - MembershipStatus, - ResourceType, -) +from ..types import Membership, MembershipStatus, ResourceType import httpx import typing import pydantic +class ListMembershipsParamsResourceParentType(pydantic.BaseModel): + """ + ListMembershipsParamsResourceParentType is a schema definition. + """ + + class ListMembershipsParams(pydantic.BaseModel): """ ListMembershipsParams: query parameters for ListMemberships @@ -49,7 +51,7 @@ class ListMembershipsParams(pydantic.BaseModel): validation_alias=pydantic.AliasChoices("resource.parent.id", "resource_parent_id"), ) - resource_parent_type: typing.Optional[ResourceType] = pydantic.Field( + resource_parent_type: typing.Optional[ListMembershipsParamsResourceParentType] = pydantic.Field( default=None, serialization_alias="resource.parent.type", validation_alias=pydantic.AliasChoices("resource.parent.type", "resource_parent_type"), diff --git a/sumup/merchants/resource.py b/sumup/merchants/resource.py index 2f94e62..2c7d2fc 100755 --- a/sumup/merchants/resource.py +++ b/sumup/merchants/resource.py @@ -2,11 +2,7 @@ from __future__ import annotations from .._service import Resource, AsyncResource, HeaderTypes from .._exceptions import APIError -from ..types import ( - ListPersonsResponseBody, - Merchant, - Person, -) +from ..types import ListPersonsResponseBody, Merchant, Person import httpx import typing import pydantic diff --git a/sumup/payouts/__init__.py b/sumup/payouts/__init__.py index 23716c0..d182de3 100755 --- a/sumup/payouts/__init__.py +++ b/sumup/payouts/__init__.py @@ -2,7 +2,11 @@ from .resource import ( PayoutsResource, AsyncPayoutsResource, + ListPayoutsV1ParamsFormat, + ListPayoutsV1ParamsOrder, ListPayoutsV1Params, + ListPayoutsParamsFormat, + ListPayoutsParamsOrder, ListPayoutsParams, ) from ..types import ( @@ -16,7 +20,11 @@ __all__ = [ "PayoutsResource", "AsyncPayoutsResource", + "ListPayoutsV1ParamsFormat", + "ListPayoutsV1ParamsOrder", "ListPayoutsV1Params", + "ListPayoutsParamsFormat", + "ListPayoutsParamsOrder", "ListPayoutsParams", "Error", "ErrorExtended", diff --git a/sumup/payouts/resource.py b/sumup/payouts/resource.py index 0efe5c9..0c931a2 100755 --- a/sumup/payouts/resource.py +++ b/sumup/payouts/resource.py @@ -9,6 +9,10 @@ import pydantic import typing_extensions +ListPayoutsV1ParamsFormat = typing.Union[typing.Literal["csv", "json"], str] + +ListPayoutsV1ParamsOrder = typing.Union[typing.Literal["asc", "desc"], str] + class ListPayoutsV1Params(pydantic.BaseModel): """ @@ -19,11 +23,16 @@ class ListPayoutsV1Params(pydantic.BaseModel): start_date: datetime.date - format: typing.Optional[str] = None + format: typing.Optional[ListPayoutsV1ParamsFormat] = None limit: typing.Optional[int] = None - order: typing.Optional[str] = None + order: typing.Optional[ListPayoutsV1ParamsOrder] = None + + +ListPayoutsParamsFormat = typing.Union[typing.Literal["csv", "json"], str] + +ListPayoutsParamsOrder = typing.Union[typing.Literal["asc", "desc"], str] class ListPayoutsParams(pydantic.BaseModel): @@ -35,11 +44,11 @@ class ListPayoutsParams(pydantic.BaseModel): start_date: datetime.date - format: typing.Optional[str] = None + format: typing.Optional[ListPayoutsParamsFormat] = None limit: typing.Optional[int] = None - order: typing.Optional[str] = None + order: typing.Optional[ListPayoutsParamsOrder] = None class PayoutsResource(Resource): diff --git a/sumup/receipts/resource.py b/sumup/receipts/resource.py index dfc92e6..085a5b5 100755 --- a/sumup/receipts/resource.py +++ b/sumup/receipts/resource.py @@ -2,9 +2,7 @@ from __future__ import annotations from .._service import Resource, AsyncResource, HeaderTypes from .._exceptions import APIError -from ..types import ( - Receipt, -) +from ..types import Receipt import httpx import typing import pydantic diff --git a/sumup/transactions/__init__.py b/sumup/transactions/__init__.py index f7d26c8..0372c3e 100755 --- a/sumup/transactions/__init__.py +++ b/sumup/transactions/__init__.py @@ -5,7 +5,13 @@ RefundTransactionBody, GetTransactionV21Params, GetTransactionParams, + ListTransactionsV21ParamsOrder, + ListTransactionsV21ParamsStatuse, + ListTransactionsV21ParamsType, ListTransactionsV21Params, + ListTransactionsParamsOrder, + ListTransactionsParamsStatuse, + ListTransactionsParamsType, ListTransactionsParams, ListTransactionsV21200Response, ListTransactions200Response, @@ -46,7 +52,13 @@ "RefundTransactionBody", "GetTransactionV21Params", "GetTransactionParams", + "ListTransactionsV21ParamsOrder", + "ListTransactionsV21ParamsStatuse", + "ListTransactionsV21ParamsType", "ListTransactionsV21Params", + "ListTransactionsParamsOrder", + "ListTransactionsParamsStatuse", + "ListTransactionsParamsType", "ListTransactionsParams", "ListTransactionsV21200Response", "ListTransactions200Response", diff --git a/sumup/transactions/resource.py b/sumup/transactions/resource.py index 55e7a48..c650dc3 100755 --- a/sumup/transactions/resource.py +++ b/sumup/transactions/resource.py @@ -55,6 +55,17 @@ class GetTransactionParams(pydantic.BaseModel): transaction_code: typing.Optional[str] = None +ListTransactionsV21ParamsOrder = typing.Union[typing.Literal["ascending", "descending"], str] + +ListTransactionsV21ParamsStatuse = typing.Union[ + typing.Literal["CANCELLED", "CHARGE_BACK", "FAILED", "REFUNDED", "SUCCESSFUL"], str +] + +ListTransactionsV21ParamsType = typing.Union[ + typing.Literal["CHARGE_BACK", "PAYMENT", "REFUND"], str +] + + class ListTransactionsV21Params(pydantic.BaseModel): """ ListTransactionsV21Params: query parameters for ListTransactionsV2.1 @@ -78,11 +89,11 @@ class ListTransactionsV21Params(pydantic.BaseModel): oldest_time: typing.Optional[datetime.datetime] = None - order: typing.Optional[str] = None + order: typing.Optional[ListTransactionsV21ParamsOrder] = None payment_types: typing.Optional[list[PaymentType]] = None - statuses: typing.Optional[list[str]] = pydantic.Field( + statuses: typing.Optional[list[ListTransactionsV21ParamsStatuse]] = pydantic.Field( default=None, serialization_alias="statuses[]", validation_alias=pydantic.AliasChoices("statuses[]", "statuses"), @@ -90,11 +101,20 @@ class ListTransactionsV21Params(pydantic.BaseModel): transaction_code: typing.Optional[str] = None - types: typing.Optional[list[str]] = None + types: typing.Optional[list[ListTransactionsV21ParamsType]] = None users: typing.Optional[list[str]] = None +ListTransactionsParamsOrder = typing.Union[typing.Literal["ascending", "descending"], str] + +ListTransactionsParamsStatuse = typing.Union[ + typing.Literal["CANCELLED", "CHARGE_BACK", "FAILED", "REFUNDED", "SUCCESSFUL"], str +] + +ListTransactionsParamsType = typing.Union[typing.Literal["CHARGE_BACK", "PAYMENT", "REFUND"], str] + + class ListTransactionsParams(pydantic.BaseModel): """ ListTransactionsParams: query parameters for ListTransactions @@ -112,11 +132,11 @@ class ListTransactionsParams(pydantic.BaseModel): oldest_time: typing.Optional[datetime.datetime] = None - order: typing.Optional[str] = None + order: typing.Optional[ListTransactionsParamsOrder] = None payment_types: typing.Optional[list[PaymentType]] = None - statuses: typing.Optional[list[str]] = pydantic.Field( + statuses: typing.Optional[list[ListTransactionsParamsStatuse]] = pydantic.Field( default=None, serialization_alias="statuses[]", validation_alias=pydantic.AliasChoices("statuses[]", "statuses"), @@ -124,7 +144,7 @@ class ListTransactionsParams(pydantic.BaseModel): transaction_code: typing.Optional[str] = None - types: typing.Optional[list[str]] = None + types: typing.Optional[list[ListTransactionsParamsType]] = None users: typing.Optional[list[str]] = None diff --git a/tests/test_query_params.py b/tests/test_query_params.py index ecf19a4..17f4fab 100644 --- a/tests/test_query_params.py +++ b/tests/test_query_params.py @@ -1,5 +1,6 @@ import httpx import pytest +import typing from sumup.transactions import ListTransactionsV21Params @@ -34,3 +35,14 @@ def handler(request: httpx.Request) -> httpx.Response: request = captured_request["request"] assert request.url.path == "/v2.1/merchants/merchant-123/transactions/history" assert list(request.url.params.multi_items()) == expected_query_items + + +def test_transactions_list_query_param_enums_are_typed(): + order_annotation = ListTransactionsV21Params.model_fields["order"].annotation + + assert typing.get_origin(order_annotation) is typing.Union + assert typing.get_args(order_annotation) == ( + typing.Literal["ascending", "descending"], + str, + type(None), + )