diff --git a/codegen/pkg/builder/methods.go b/codegen/pkg/builder/methods.go index 2fc3970..677f81a 100644 --- a/codegen/pkg/builder/methods.go +++ b/codegen/pkg/builder/methods.go @@ -277,7 +277,6 @@ func (b *Builder) buildQueryFields(o *v3.Operation) ([]Property, error) { SerializedName: alias, Type: typeName, Optional: p.Required == nil || !*p.Required, - Nullable: schemaIsNullable(p.Schema.Schema()), Schema: p.Schema, Comment: parameterPropertyDoc(p.Schema.Schema()), }) diff --git a/openapi.json b/openapi.json index f514d26..8f2f661 100644 --- a/openapi.json +++ b/openapi.json @@ -243,7 +243,7 @@ "post": { "operationId": "CreateCheckout", "summary": "Create a checkout", - "description": "Creates a new payment checkout resource. The unique `checkout_reference` created by this request, is used for further manipulation of the checkout.\n\nFor 3DS checkouts, add the `redirect_url` parameter to your request body schema.\nTo use the [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) page, set the `hosted_checkout.enabled` to `true`.\n\nFollow by processing a checkout to charge the provided payment instrument.", + "description": "Creates a payment checkout for the specified merchant, amount, and currency. Supply a `checkout_reference` to identify the payment attempt in your own systems. Creating a checkout does not charge a payment instrument.\n\nSet `hosted_checkout.enabled` to `true` to receive a [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) URL where the customer can complete the payment.\nUse `redirect_url` for redirect-based payment and 3DS flows. If `return_url` is provided, SumUp sends processing updates to that backend callback URL.\n\nComplete the payment through [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) or the [Payment Widget](https://developer.sumup.com/online-payments/checkouts/card-widget).", "tags": [ "Checkouts" ], @@ -746,7 +746,7 @@ "patch": { "operationId": "UpdateCheckout", "summary": "Update a checkout", - "description": "Updates an identified checkout resource.", + "description": "Updates the amount, currency, description, reference, expiration, or customer associated with an existing checkout. Only the supplied fields are updated.\n\nThis request changes the checkout details; it does not charge a payment instrument. Process the checkout separately to attempt a payment.", "tags": [ "Checkouts" ], @@ -3443,28 +3443,30 @@ "type": "string" } }, + { + "name": "resource.id", + "in": "query", + "description": "Filter memberships by the ID of the resource the membership is in.", + "schema": { + "type": "string" + } + }, { "name": "resource.parent.id", "in": "query", - "description": "Filter memberships by the parent of the resource the membership is in.\nWhen filtering by parent both `resource.parent.id` and `resource.parent.type` must be present. Pass explicit null to filter for resources without a parent.", + "allowEmptyValue": true, + "description": "Filter memberships by the parent of the resource the membership is in.\nOmit both `resource.parent.id` and `resource.parent.type` to skip parent filtering. When filtering by parent, both parameters must be present. To select resources without a parent, set each parameter to an empty value. Otherwise, both parameters must identify a parent.", "schema": { - "type": [ - "string", - "null" - ] + "type": "string" } }, { "name": "resource.parent.type", "in": "query", - "description": "Filter memberships by the parent of the resource the membership is in.\nWhen filtering by parent both `resource.parent.id` and `resource.parent.type` must be present. Pass explicit null to filter for resources without a parent.", + "allowEmptyValue": true, + "description": "Filter memberships by the parent of the resource the membership is in.\nOmit both `resource.parent.id` and `resource.parent.type` to skip parent filtering. When filtering by parent, both parameters must be present. To select resources without a parent, set each parameter to an empty value. Otherwise, both parameters must identify a parent.", "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ResourceType" - } - ], - "type": "null" + "$ref": "#/components/schemas/ResourceType" } }, { @@ -3603,19 +3605,6 @@ "example": "245b2ead-85bf-45ff-856f-311a88a5d454" } }, - { - "name": "user.type", - "in": "query", - "description": "Filter the returned members by user type. Repeat this parameter to include multiple user types.", - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserType" - } - }, - "style": "form", - "explode": true - }, { "name": "status", "in": "query", @@ -3730,7 +3719,7 @@ "post": { "operationId": "CreateMerchantMember", "summary": "Create a member", - "description": "Create a merchant member.", + "description": "Adds a member to the merchant account with the specified roles.\n\nBy default, sends an invitation email to the provided address. The recipient must accept the invitation to join the account.\nWhen `is_managed_user` is `true`, creates a managed user with the provided password and optional nickname and assigns the roles directly, without sending an invitation.", "tags": [ "Members" ], @@ -3795,6 +3784,7 @@ "roles": { "type": "array", "description": "List of roles to assign to the new member.", + "minItems": 1, "maxItems": 124, "items": { "type": "string", @@ -3978,7 +3968,7 @@ }, "put": { "summary": "Update a member", - "description": "Update the merchant member.", + "description": "Updates a merchant member and returns the updated member.\n\nProviding `roles` replaces the member's assigned roles and can grant or revoke access. Providing `metadata` replaces the entire metadata object.\nFor managed users, `user.nickname` changes the display name and `user.password` replaces the password. Updating the password also enables the managed user account.", "tags": [ "Members" ], @@ -4017,6 +4007,7 @@ "properties": { "roles": { "type": "array", + "minItems": 1, "maxItems": 124, "items": { "type": "string", @@ -4392,6 +4383,7 @@ "permissions": { "type": "array", "description": "User's permissions.", + "minItems": 1, "maxItems": 100, "items": { "type": "string" @@ -4652,7 +4644,7 @@ "patch": { "operationId": "UpdateMerchantRole", "summary": "Update a role", - "description": "Update a custom role.", + "description": "Updates a custom role's name, description, or permissions and returns the updated role.\n\nProviding `permissions` replaces the role's permission list and changes the access granted to members assigned to that role. Omitted fields remain unchanged.", "tags": [ "Roles" ], @@ -4696,6 +4688,7 @@ "permissions": { "type": "array", "description": "User's permissions.", + "minItems": 1, "maxItems": 100, "items": { "type": "string" @@ -5438,7 +5431,7 @@ }, "patch": { "summary": "Update a Reader", - "description": "Update a Reader.", + "description": "Updates a reader's name or metadata and returns the updated reader.\n\nProviding `metadata` replaces the entire metadata object; include all entries that should be retained. Omitted fields remain unchanged.", "operationId": "UpdateReader", "tags": [ "Readers" @@ -8923,17 +8916,6 @@ } } }, - "UserType": { - "type": "string", - "description": "Type of the user account.", - "enum": [ - "user", - "managed_user", - "service_account", - "system_account" - ], - "example": "user" - }, "Metadata": { "description": "Set of user-defined key-value pairs attached to the object. Partial updates are not supported. When updating, always submit whole metadata. Maximum of 64 parameters are allowed in the object.", "type": "object", @@ -8947,6 +8929,17 @@ "example": {}, "additionalProperties": true }, + "UserType": { + "type": "string", + "description": "Type of the user account.", + "enum": [ + "user", + "managed_user", + "service_account", + "system_account" + ], + "example": "user" + }, "Address": { "externalDocs": { "description": "Address documentation", diff --git a/sumup/_service.py b/sumup/_service.py index b703ab2..d841e08 100644 --- a/sumup/_service.py +++ b/sumup/_service.py @@ -12,7 +12,7 @@ from ._version import __version__ HeaderTypes = typing.Mapping[str, str] -PrimitiveQueryValue = str | int | float | None +PrimitiveQueryValue = str | int | float QueryValue = PrimitiveQueryValue | typing.Sequence[PrimitiveQueryValue] QueryParamTypes = typing.Mapping[str, QueryValue] diff --git a/sumup/checkouts/resource.py b/sumup/checkouts/resource.py index 2e565db..49e209c 100644 --- a/sumup/checkouts/resource.py +++ b/sumup/checkouts/resource.py @@ -642,12 +642,12 @@ def create( """ Create a checkout - Creates a new payment checkout resource. The unique `checkout_reference` created by this request, is used for furthermanipulation of the checkout. + Creates a payment checkout for the specified merchant, amount, and currency. Supply a `checkout_reference` to identifythe payment attempt in your own systems. Creating a checkout does not charge a payment instrument. - For 3DS checkouts, add the `redirect_url` parameter to your request body schema. - To use the [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) page, setthe `hosted_checkout.enabled` to `true`. + Set `hosted_checkout.enabled` to `true` to receive a [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) URLwhere the customer can complete the payment. + Use `redirect_url` for redirect-based payment and 3DS flows. If `return_url` is provided, SumUp sends processing updatesto that backend callback URL. - Follow by processing a checkout to charge the provided payment instrument. + Complete the payment through [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) orthe [Payment Widget](https://developer.sumup.com/online-payments/checkouts/card-widget). Raises: @@ -786,7 +786,9 @@ def update( """ Update a checkout - Updates an identified checkout resource. + Updates the amount, currency, description, reference, expiration, or customer associated with an existing checkout. Onlythe supplied fields are updated. + + This request changes the checkout details; it does not charge a payment instrument. Process the checkout separately toattempt a payment. Raises: @@ -1062,12 +1064,12 @@ async def create( """ Create a checkout - Creates a new payment checkout resource. The unique `checkout_reference` created by this request, is used for furthermanipulation of the checkout. + Creates a payment checkout for the specified merchant, amount, and currency. Supply a `checkout_reference` to identifythe payment attempt in your own systems. Creating a checkout does not charge a payment instrument. - For 3DS checkouts, add the `redirect_url` parameter to your request body schema. - To use the [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) page, setthe `hosted_checkout.enabled` to `true`. + Set `hosted_checkout.enabled` to `true` to receive a [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) URLwhere the customer can complete the payment. + Use `redirect_url` for redirect-based payment and 3DS flows. If `return_url` is provided, SumUp sends processing updatesto that backend callback URL. - Follow by processing a checkout to charge the provided payment instrument. + Complete the payment through [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) orthe [Payment Widget](https://developer.sumup.com/online-payments/checkouts/card-widget). Raises: @@ -1206,7 +1208,9 @@ async def update( """ Update a checkout - Updates an identified checkout resource. + Updates the amount, currency, description, reference, expiration, or customer associated with an existing checkout. Onlythe supplied fields are updated. + + This request changes the checkout details; it does not charge a payment instrument. Process the checkout separately toattempt a payment. Raises: diff --git a/sumup/members/resource.py b/sumup/members/resource.py index a551b5c..ee6a04d 100644 --- a/sumup/members/resource.py +++ b/sumup/members/resource.py @@ -37,7 +37,6 @@ MetadataInput, Problem, UserType, - UserTypeInput, ) @@ -57,7 +56,9 @@ class CreateMerchantMemberBodyInput(typing_extensions.TypedDict, total=False): roles: typing_extensions.Required[ typing_extensions.Annotated[ typing.Sequence[str], - typing_extensions.Doc("List of roles to assign to the new member.\nMax items: 124"), + typing_extensions.Doc( + "List of roles to assign to the new member.\nMin items: 1\nMax items: 124" + ), ] ] attributes: typing_extensions.NotRequired[ @@ -147,7 +148,9 @@ class UpdateMerchantMemberBodyInput(typing_extensions.TypedDict, total=False): ] ] roles: typing_extensions.NotRequired[ - typing_extensions.Annotated[typing.Sequence[str], typing_extensions.Doc("Max items: 124")] + typing_extensions.Annotated[ + typing.Sequence[str], typing_extensions.Doc("Min items: 1\nMax items: 124") + ] ] user: typing_extensions.NotRequired[ typing_extensions.Annotated[ @@ -182,7 +185,6 @@ def list( scroll: bool | NotGivenType = NOT_GIVEN, email: str | NotGivenType = NOT_GIVEN, user_id: str | NotGivenType = NOT_GIVEN, - user_type: typing.Sequence[UserTypeInput] | NotGivenType = NOT_GIVEN, status: MembershipStatusInput | NotGivenType = NOT_GIVEN, roles: typing.Sequence[str] | NotGivenType = NOT_GIVEN, headers: HeaderTypes | None = None, @@ -209,8 +211,6 @@ def list( query_data["email"] = email if not isinstance(user_id, NotGivenType) and user_id is not None: query_data["user.id"] = user_id - if not isinstance(user_type, NotGivenType) and user_type is not None: - query_data["user.type"] = list(user_type) if not isinstance(status, NotGivenType) and status is not None: query_data["status"] = status if not isinstance(roles, NotGivenType) and roles is not None: @@ -244,7 +244,10 @@ def create( """ Create a member - Create a merchant member. + Adds a member to the merchant account with the specified roles. + + By default, sends an invitation email to the provided address. The recipient must accept the invitation tojoin the account. + When `is_managed_user` is `true`, creates a managed user with the provided password and optional nickname andassigns the roles directly, without sending an invitation. Raises: @@ -325,7 +328,10 @@ def update( """ Update a member - Update the merchant member. + Updates a merchant member and returns the updated member. + + Providing `roles` replaces the member's assigned roles and can grant or revoke access. Providing `metadata` replaces theentire metadata object. + For managed users, `user.nickname` changes the display name and `user.password` replaces the password. Updating thepassword also enables the managed user account. Raises: @@ -420,7 +426,6 @@ async def list( scroll: bool | NotGivenType = NOT_GIVEN, email: str | NotGivenType = NOT_GIVEN, user_id: str | NotGivenType = NOT_GIVEN, - user_type: typing.Sequence[UserTypeInput] | NotGivenType = NOT_GIVEN, status: MembershipStatusInput | NotGivenType = NOT_GIVEN, roles: typing.Sequence[str] | NotGivenType = NOT_GIVEN, headers: HeaderTypes | None = None, @@ -447,8 +452,6 @@ async def list( query_data["email"] = email if not isinstance(user_id, NotGivenType) and user_id is not None: query_data["user.id"] = user_id - if not isinstance(user_type, NotGivenType) and user_type is not None: - query_data["user.type"] = list(user_type) if not isinstance(status, NotGivenType) and status is not None: query_data["status"] = status if not isinstance(roles, NotGivenType) and roles is not None: @@ -482,7 +485,10 @@ async def create( """ Create a member - Create a merchant member. + Adds a member to the merchant account with the specified roles. + + By default, sends an invitation email to the provided address. The recipient must accept the invitation tojoin the account. + When `is_managed_user` is `true`, creates a managed user with the provided password and optional nickname andassigns the roles directly, without sending an invitation. Raises: @@ -565,7 +571,10 @@ async def update( """ Update a member - Update the merchant member. + Updates a merchant member and returns the updated member. + + Providing `roles` replaces the member's assigned roles and can grant or revoke access. Providing `metadata` replaces theentire metadata object. + For managed users, `user.nickname` changes the display name and `user.password` replaces the password. Updating thepassword also enables the managed user account. Raises: diff --git a/sumup/memberships/resource.py b/sumup/memberships/resource.py index 754419c..85e96aa 100644 --- a/sumup/memberships/resource.py +++ b/sumup/memberships/resource.py @@ -37,12 +37,6 @@ ) -class ListMembershipsParamsResourceParentTypeInput(typing_extensions.TypedDict, total=False): - """ - ListMembershipsParamsResourceParentType is a schema definition. - """ - - class ListMemberships200Response(pydantic.BaseModel): """ ListMemberships200Response is a schema definition. @@ -69,9 +63,9 @@ def list( resource_type: ResourceTypeInput | NotGivenType = NOT_GIVEN, resource_attributes_sandbox: bool | NotGivenType = NOT_GIVEN, resource_name: str | NotGivenType = NOT_GIVEN, + resource_id: str | NotGivenType = NOT_GIVEN, resource_parent_id: str | NotGivenType = NOT_GIVEN, - resource_parent_type: ListMembershipsParamsResourceParentTypeInput - | NotGivenType = NOT_GIVEN, + resource_parent_type: ResourceTypeInput | NotGivenType = NOT_GIVEN, roles: typing.Sequence[str] | NotGivenType = NOT_GIVEN, headers: HeaderTypes | None = None, ) -> ListMemberships200Response: @@ -105,6 +99,8 @@ def list( query_data["resource.attributes.sandbox"] = resource_attributes_sandbox if not isinstance(resource_name, NotGivenType) and resource_name is not None: query_data["resource.name"] = resource_name + if not isinstance(resource_id, NotGivenType) and resource_id is not None: + query_data["resource.id"] = resource_id if not isinstance(resource_parent_id, NotGivenType) and resource_parent_id is not None: query_data["resource.parent.id"] = resource_parent_id if not isinstance(resource_parent_type, NotGivenType) and resource_parent_type is not None: @@ -149,9 +145,9 @@ async def list( resource_type: ResourceTypeInput | NotGivenType = NOT_GIVEN, resource_attributes_sandbox: bool | NotGivenType = NOT_GIVEN, resource_name: str | NotGivenType = NOT_GIVEN, + resource_id: str | NotGivenType = NOT_GIVEN, resource_parent_id: str | NotGivenType = NOT_GIVEN, - resource_parent_type: ListMembershipsParamsResourceParentTypeInput - | NotGivenType = NOT_GIVEN, + resource_parent_type: ResourceTypeInput | NotGivenType = NOT_GIVEN, roles: typing.Sequence[str] | NotGivenType = NOT_GIVEN, headers: HeaderTypes | None = None, ) -> ListMemberships200Response: @@ -185,6 +181,8 @@ async def list( query_data["resource.attributes.sandbox"] = resource_attributes_sandbox if not isinstance(resource_name, NotGivenType) and resource_name is not None: query_data["resource.name"] = resource_name + if not isinstance(resource_id, NotGivenType) and resource_id is not None: + query_data["resource.id"] = resource_id if not isinstance(resource_parent_id, NotGivenType) and resource_parent_id is not None: query_data["resource.parent.id"] = resource_parent_id if not isinstance(resource_parent_type, NotGivenType) and resource_parent_type is not None: diff --git a/sumup/readers/resource.py b/sumup/readers/resource.py index e675160..af52746 100644 --- a/sumup/readers/resource.py +++ b/sumup/readers/resource.py @@ -541,7 +541,9 @@ def update( """ Update a Reader - Update a Reader. + Updates a reader's name or metadata and returns the updated reader. + + Providing `metadata` replaces the entire metadata object; include all entries that should be retained. Omitted fieldsremain unchanged. Raises: @@ -1042,7 +1044,9 @@ async def update( """ Update a Reader - Update a Reader. + Updates a reader's name or metadata and returns the updated reader. + + Providing `metadata` replaces the entire metadata object; include all entries that should be retained. Omitted fieldsremain unchanged. Raises: diff --git a/sumup/roles/resource.py b/sumup/roles/resource.py index e3e3dd4..fac7086 100644 --- a/sumup/roles/resource.py +++ b/sumup/roles/resource.py @@ -36,7 +36,8 @@ class CreateMerchantRoleBodyInput(typing_extensions.TypedDict, total=False): ] permissions: typing_extensions.Required[ typing_extensions.Annotated[ - typing.Sequence[str], typing_extensions.Doc("User's permissions.\nMax items: 100") + typing.Sequence[str], + typing_extensions.Doc("User's permissions.\nMin items: 1\nMax items: 100"), ] ] description: typing_extensions.NotRequired[ @@ -69,7 +70,8 @@ class UpdateMerchantRoleBodyInput(typing_extensions.TypedDict, total=False): ] permissions: typing_extensions.NotRequired[ typing_extensions.Annotated[ - typing.Sequence[str], typing_extensions.Doc("User's permissions.\nMax items: 100") + typing.Sequence[str], + typing_extensions.Doc("User's permissions.\nMin items: 1\nMax items: 100"), ] ] @@ -219,7 +221,9 @@ def update( """ Update a role - Update a custom role. + Updates a custom role's name, description, or permissions and returns the updated role. + + Providing `permissions` replaces the role's permission list and changes the access granted to members assigned tothat role. Omitted fields remain unchanged. Raises: @@ -390,7 +394,9 @@ async def update( """ Update a role - Update a custom role. + Updates a custom role's name, description, or permissions and returns the updated role. + + Providing `permissions` replaces the role's permission list and changes the access granted to members assigned tothat role. Omitted fields remain unchanged. Raises: diff --git a/sumup/types/__init__.py b/sumup/types/__init__.py index 6012278..f53e939 100644 --- a/sumup/types/__init__.py +++ b/sumup/types/__init__.py @@ -2969,7 +2969,6 @@ class MembershipUserClassic(pydantic.BaseModel): UserType = typing.Literal["managed_user", "service_account", "system_account", "user"] | str -UserTypeInput = UserType class MembershipUser(pydantic.BaseModel): diff --git a/tests/test_query_params.py b/tests/test_query_params.py index 09b9b55..bcb9a78 100644 --- a/tests/test_query_params.py +++ b/tests/test_query_params.py @@ -1,9 +1,64 @@ +import asyncio import typing import httpx import pytest from sumup._service import NotGivenType +from sumup.memberships.resource import AsyncMembershipsResource, MembershipsResource + + +@pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) +@pytest.mark.parametrize( + ("kwargs", "expected_query"), + [ + ({}, b""), + ( + {"resource_parent_id": "", "resource_parent_type": ""}, + b"resource.parent.id=&resource.parent.type=", + ), + ( + {"resource_parent_id": "merchant-123", "resource_parent_type": "merchant"}, + b"resource.parent.id=merchant-123&resource.parent.type=merchant", + ), + ], + ids=["omitted", "empty", "parent"], +) +def test_memberships_parent_filter_query_params(use_async, kwargs, expected_query): + requests: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(200, json={"items": [], "total_count": 0}) + + transport = httpx.MockTransport(handler) + if use_async: + + async def run(): + async with httpx.AsyncClient( + base_url="https://api.sumup.test", transport=transport + ) as client: + return await AsyncMembershipsResource(client).list(**kwargs) + + response = asyncio.run(run()) + else: + with httpx.Client(base_url="https://api.sumup.test", transport=transport) as client: + response = MembershipsResource(client).list(**kwargs) + + assert response.items == [] + assert len(requests) == 1 + assert requests[0].url.path == "/v0.1/memberships" + assert requests[0].url.query == expected_query + + +@pytest.mark.parametrize("resource", [MembershipsResource, AsyncMembershipsResource]) +def test_memberships_parent_filters_accept_strings_without_none(resource): + annotations = typing.get_type_hints(resource.list) + for name in ("resource_parent_id", "resource_parent_type"): + args = typing.get_args(annotations[name]) + assert str in args + assert NotGivenType in args + assert type(None) not in args @pytest.mark.parametrize(